import datetime
import math
import doctest
import typing 
from typing import Union
try:
    from .durations import Duration
except:
    pass

# ========================================================
# ========================================================
# === DATE
# ========================================================
# ========================================================

class Date:
    """ Class to convert dates for astronomy

    Date formats are:
    -----------------
    
    now = Now. e.g. "now"    
    jd = Julian day. e.g. 24504527.45678
    iso = ISO 8601. e.g. 2018-02-28T12:34:55.23
    sql = ISO 8601. e.g. 2018-02-28 12:34:55.23
    ymdhms = Calendar. e.g. 2018 2 28 12 34 55.23
    equinox = Equinox. e.g. J2000,0
    digits = Pure digits e.g. 20180228123455.23
    
    Usage:
    ------

    First, instanciate an object from the class:    
    date = Date()

    Second, assign a date in any date format:
    date.date("2018-02-28T12:34:55")

    Third, get the converted date:    
    jd = date.jd()
    date = date.date()
    iso = date.iso()
    ymdhms = date.ymdhms()
    equinox = date.equinox()

    Informations:
    -------------
    All dates are in UTC.
    
    help(Date)
    Date().infos("doctest")
    Date().infos("doc_methods")
    """
# ========================================================
# === attributs
# ========================================================

    _B1850 = 2396758.203
    _B1900 = 2415020.3135
    _B1950 = 2433282.4235
    _B1975 = 2442413.478
    _B2000 = 2451544.533
    _B2025 = 2460675.588
    _B2050 = 2469806.643
    _B2100 = 2488068.753
    _J1900 = 2415020.0000
    _J1950 = 2433282.5000
    _J2000 = 2451545.0000
    _J2050 = 2469807.5000
    _J2100 = 2488070.0000

# ========================================================
# === internal methods
# ========================================================

    def _init(self,date: Union[str, float] = "") -> None:
        """ Initialize internal attributes.

        :param date: date is a date in any supported format (cf. help(Date))
        :type date: any 
        :return: None
        :rtype: None
 
        :Example:
            
        >>> objdate = Date()
        >>> objdate._init()
        
        """
        self._init_date = date
        self._init_dateformat = 0
        self._computed_jd = 0
        self._computed_mjd = 0
        self._computed_iso = 0
        self._computed_iso_nb_subdigit = 3
        self._computed_iso_letter = 'T'
        self._computed_ymdhms = 0
        self._computed_digits = 0
        self._computed_digits_nb_subdigit = 3
        self._computed_equinox = 0
        self._computed_equinox_year_type = "J"
        self._computed_equinox_nb_subdigit = 1
        self._jd = 0
        self._mjd = 0
        self._iso = 0
        self._ymdhms = 0
        self._equinox = 0

    def _is_number(self,s) -> bool:
        """ Return True if the string is a number else return False.

        :param s: A string to test
        :type s: string
        :return: True is the string can be concerted into a float
        :rtype: bool
 
        :Example:

        >>> objdate = Date()
        >>> objdate._is_number("3e5")
        True
        >>> objdate._is_number("3a5")
        False
        
        """
        try:
            float(s)
            return True
        except ValueError:
            pass 
        try:
            import unicodedata
            unicodedata.numeric(s)
            return True
        except (TypeError, ValueError):
            pass 
        return False

    def _duration2day(self, duration) -> float:
        """ Return a duration in day unit from a duration in dhms format.

        :param duration: A string formated as "2d7h23m12.5s"
        :type duration: string
        :return: duration expressed in day and fraction of day
        :rtype: float
 
        :Example:

        >>> objdate = Date()
        >>> objdate._duration2day("2d7h23m12.5s")
        2.3077835648148146
        
        """
        duration = str(duration)
        duration = duration.upper()
        cars = "+-.E0123456789"
        units = "DHMS"
        dur = 0
        k1 = -1
        k2 = -1
        k = 0
        div = 0
        for car in duration:
            if car in cars:
                if k1 == -1:
                    k1 = k
                k2 = k
            elif car in units:                
                if (k1>=0 and k2>=0):
                    div = 0
                    if car=='D':
                        div = 1.
                    elif car=='H':
                        div = 24.
                    elif car=='M':
                        div = 1440.
                    elif car=='S':
                        div = 86400.
                    if div>0:
                        n = float(duration[k1:k2+1])
                        dur += n/div
                    k1 = -1
                    k2 = -1
            k += 1
        if (div==0 and dur==0 and k1>=0 and k2>=k1):
            n = float(duration[k1:k2+1])
            dur += n
        return dur
    
    def _date_compare(self, date, operator):
        """ Comparaison of dates for various operators.

        :param date: A date in any supported format (cf. help(Date))
        :type date: Date()
        :param operator : Operator such as == != > >= < <=
        :type operator : string
        :return: The logic result of the comparison.
        :rtype: bool
        
        :Example:

        >>> objdate1 = Date()
        >>> objdate2 = Date()
        >>> objdate1.date("2018 02 28"); objdate2.date("2018 02 27"); objdate1._date_compare(objdate2,">")
        '2018 02 28'
        '2018 02 27'
        True
        
        .. note:: Does not account for the modulo.
        """
        if self._computed_jd == 0:
            self.jd()
        if date._computed_jd == 0:
            date.jd()
        res = False
        if (self._computed_jd == 1) and (date._computed_jd == 1):
            toeval = str(self._jd)+" "+operator+" "+str(date._jd)
            res = eval(toeval)
        return res
    
# ========================================================
# === date methods
# ========================================================

    def date_date2jd(self,date) -> typing.Tuple[int, float]:
        """ Compute a julian day from any date format

        :param date: A string formated as "2d7h23m12.5s"
        :type date: string
        :return : A tuple of init_dateformat, julian day.
        init_dateformat : The identified format of the input:
        0 = Error, format not known
        1 = Now
        2 = Equinox
        3 = Sql
        4 = Calendar
        5 = ISO 8601
        6 = Julian day
        7 = Modified Julian day
        8 = Digits
        :rtype: tuple(int, float)
 
        :Example:

        >>> objdate = Date()
        >>> objdate.date_date2jd("2018-02-28T12:34:55.234")
        (5, 2458178.0242503937)
                
        .. note:: Prefer using objdate.date() followed by objdate.jd().            
        """
        # --- First we do not process if date is ever a Date object
        if (isinstance(date, Date)):
            return date
        # --- Decode the date
        jd = 0
        init_dateformat = 0
        str_date = str(date).upper()
        str_split = str_date.split(" ")
        str_nsplit = len(str_split)
        if str_date == "NOW":
            # style NOW
            utc_datetime = datetime.datetime.utcnow()
            day = utc_datetime.day + ((((utc_datetime.microsecond*1e-6) + utc_datetime.second)/60. + utc_datetime.minute)/60. + utc_datetime.hour)/24.
            error, jd = self.date_ymd2jd(utc_datetime.year, utc_datetime.month, day)
            init_dateformat = 1
        elif (str_date[0] is "B") or (str_date[0] is "J"):
            # style J2000,0
            error, jd = self.date_equinox2jd(str_date)
            init_dateformat = 2
        elif (str_nsplit is 2):
            # style SQL 2018-01-02 23:02:45.456
            iso_date = str_split[0] + "T" + str_split[1]
            error, jd = self.date_iso2jd(iso_date)
            init_dateformat = 3
        elif (str_nsplit is 3):
            # style calenday 2018 01 02.4567
            str_split = [ float(x) for x in str_split]
            error, jd = self.date_ymd2jd(str_split[0],str_split[1],str_split[2])
            init_dateformat = 4
        elif (str_nsplit is 4):
            # style calenday 2018 01 02 05.324
            str_split = [ float(x) for x in str_split]
            error, jd = self.date_ymdhms2jd(str_split[0],str_split[1],str_split[2],str_split[3])
            init_dateformat = 4
        elif (str_nsplit is 5):
            # style calenday 2018 01 02 05 12.1809
            str_split = [ float(x) for x in str_split]
            error, jd = self.date_ymdhms2jd(str_split[0],str_split[1],str_split[2],str_split[3],str_split[4])
            init_dateformat = 4
        elif (str_nsplit is 6):
            # style calenday 2018 01 02 05 12 56.789
            str_split = [ float(x) for x in str_split]
            error, jd = self.date_ymdhms2jd(str_split[0],str_split[1],str_split[2],str_split[3],str_split[4],str_split[5])
            init_dateformat = 4
        elif (str_nsplit is 1) and (str_date.find("T") > 6):
            # style ISO 2018-01-02T23:02:45.456
            error, jd = self.date_iso2jd(str_date)
            init_dateformat = 5
        elif (self._is_number(str_date) is True):
            # style is julian day or MJD   
            # MJD = JD - 2400000.5 (SAO in 1957)
            # digits as 20180316
            jd = float(str_date)
            init_dateformat = 6
            if (math.log10(jd) > 7.2):
                # --- digits
                error, jd = self.date_digits2jd(str_date)
                init_dateformat = 8
            elif (math.log10(jd) < 5):
                # --- MJD
                jd += 2400000.5
                init_dateformat = 7
        return init_dateformat, jd
        
    def date_iso2jd(self,string):
        """ Compute a julian day from a ISO8601 date

        :param string: A string formated in ISO 8601
        :type string: string
        :return : A tuple of error, julian day.
        error = 0 means no error
        :rtype: tuple(int, float)
 
        :Example:

        >>> objdate = Date()
        >>> objdate.date_iso2jd("2018-02-28T12:34:55.234")
        (0, 2458178.0242503937)
        
        First integer is an error code. 0 = no problem.
            
        .. note:: Prefer using objdate.date() followed by objdate.jd().            
        """
        error = 0
        k1 = string.find("-",1)
        k2 = string.find("-",k1+1)
        kt = string.find("T",k2+1)
        if (kt==-1):
            kt = string.find("T",k2+1)
        k3 = string.find(":",kt+1)
        k4 = string.find(":",k3+1)
        d=1
        hh=0
        mm=0
        ss=0.0
        if (k2 > -1): 
            y = int(string[0:k1])
            m = int(string[k1+1:k2])
            if (kt == -1):
                d = float(string[k2+1:])
            else:
                d = int(string[k2+1:kt])
            if (k3 > -1):
                hh = int(string[kt+1:k3])
                d += hh/24.
            if (k4 == -1):
                mm = float(string[k3+1:])
                d += mm/1440.
            else:
                mm = int(string[k3+1:k4])
                d += mm/1440.
                ss = float(string[k4+1:])
                d += ss/86400.                
            error, jd = self.date_ymd2jd(y,m,d)
        else:
            # pb format
            error = 1
        return error, jd

    def date_jd2mjd(self, jd):
        """ Compute modified julian day from julian day

        :param jd: A julian day
        :return : A tuple of error, string of a date formatted into ISO8601.
        error = 0 means no error.
        :rtype: tuple(int, float)
 
        :Example:

        >>> objdate = Date()
        >>> objdate.date_jd2mjd(2458178.0242503937)
        
        .. note:: Prefer using objdate.date() followed by objdate.mjd()            
        """
        error = 0
        res = jd - 2400000.5
        return error, res

    def date_jd2iso(self, jd, nb_subdigit=3, letter='T'):
        """ Compute a ISO8601 date from a julian day

        :param jd: A julian day
        :type jd: float
        :param nb_subdigit : The number of digit returned after the seconds.
        :type nb_subdigit: int
        :param letter : The letter to separe date end time. If letter is ""
        then the output format sticks all digits without any characters :-T.
        So the format is no longer ISO but useful for a pure digit code.
        :type letter: int
        :return : A tuple of error, string of a date formatted into ISO8601.
        error = 0 means no error.
        :rtype: tuple(int, float)
 
        :Example:

        >>> objdate = Date()
        >>> objdate.date_jd2iso(2458178.0242503937)
        (0, '2018-02-28T12:34:55.234')
        
        .. note:: Prefer using objdate.date() followed by objdate.iso()            
        """
        error, y, m, d, hh, mm, ss = self.date_jd2ymdhms(jd)
        nb_prefixdigit = nb_subdigit+3
        if (nb_subdigit < 0):
            nb_subdigit = 0
        if (nb_subdigit == 0):
            nb_prefixdigit -= 1
        if (letter==''):
            fstring  = "{:04d}{:02d}{:02d}{}{:02d}{:02d}{:0"+str(nb_prefixdigit)+"."+str(nb_subdigit)+"f}"
        else:
            fstring  = "{:04d}-{:02d}-{:02d}{}{:02d}:{:02d}:{:0"+str(nb_prefixdigit)+"."+str(nb_subdigit)+"f}"
        res = fstring.format(y, m, d, letter, hh, mm, ss)
        return error, res
                
    def date_equinox2jd(self,string):
        """ Compute a julian day from a equinoxal date
        
        :param string: date string is a string in Equinox format (cf. help(Date))
        :type string: string 
        :return : A tuple of error, julian day.
        error = 0 means no error
        :rtype: tuple(int, float)
 
        :Example:
        >>> objdate = Date()
        >>> objdate.date_equinox2jd("J2025,0")
        (0, 2460676.25)
        
        .. note:: Prefer using objdate.date() followed by objdate.jd()            
        """
        error = 0
        t = string[0]
        s = string[1:].replace(',','.')
        a = float(s)
        jd = 0
        if t is "J":
            if (a == 1900):
                jd = self._J1900
            elif (a == 1950):
                jd = self._J1950
            elif (a == 2000):
                jd = self._J2000
            elif (a == 2050):
                jd = self._J2050
            elif (a == 2100):
                jd = self._J2100
            else:
                jd = 2451545.0+(a-2000.0)*365.25;
        elif t is "B":
            if (a == 1850):
                jd = self._B1850
            elif (a == 1900):
                jd = self._B1900
            elif (a == 1950):
                jd = self._B1950
            elif (a == 1975):
                jd = self._B1975
            elif (a == 2000):
                jd = self._B2000
            elif (a == 2025):
                jd = self._B2025
            elif (a == 2050):
                jd = self._B2050
            elif (a == 2100):
                jd = self._B2100
            else:
                error = 2
        return error,jd

    def date_digits2jd(self,string):
        """ Compute a julian day from a date with only digits

        :param string: A string formated in digits
        :type string: string
        :return : A tuple of error, julian day.
        error = 0 means no error
        :rtype: tuple(int, float)
 
        :Example:

        >>> objdate = Date()
        >>> objdate.date_iso2jd("2018-02-28T12:34:55.234")
        (0, 2458178.0242503937)
        
        First integer is an error code. 0 = no problem.
            
        .. note:: Prefer using objdate.date() followed by objdate.digits().            
        """
        error = 0
        d=1
        hh=0
        mm=0
        ss=0.0
        if len(string)>=4: 
            y = int(string[0:4])
        else:
            y = int(string)
        if len(string)>=6: 
            m = int(string[4:6])
        else:
            m = 1
        if len(string)>=8: 
            d = int(string[6:8])
        else:
            d = 1
        if len(string)>=10: 
            hh = int(string[8:10])
        else:
            hh = 0
        if len(string)>=12: 
            mm = int(string[10:12])
        else:
            mm = 0
        if len(string)>12:
            ss = float(string[12:])
        else:
            ss = 0
        d += hh/24.
        d += mm/1440.
        d += ss/86400.
        error, jd = self.date_ymd2jd(y,m,d)
        return error, jd

    def date_jd2digits(self, jd, nb_subdigit=3):
        """ Compute a date only with digits from a julian day

        :param jd: A julian day
        :type jd: float
        :param nb_subdigit : The number of digit returned after the seconds.
        :type nb_subdigit: int
        :return : A tuple of error, string of a date formatted into ISO8601.
        error = 0 means no error.
        :rtype: tuple(int, float)
 
        :Example:

        >>> objdate = Date()
        >>> objdate.date_jd2iso(2458178.0242503937)
        (0, '2018-02-28T12:34:55.234')
        
        .. note:: Prefer using objdate.date() followed by objdate.digits()            
        """
        return self.date_jd2iso(jd,nb_subdigit,"")
    
    def date_jd2equinox(self,jd,year_type="",nb_subdigit=1):
        """ Compute an equinoxal date from a julian day

        :param jd: A julian day
        :type jd: float
        :param year_type: "B" (Bessel) or "J" (Julian) or "" for automatic choice
        :type year_type: string
        :param nb_subdigit : The number of digits returned after the year.
        :type nb_subdigit: int
        :return : A tuple of error, string of a date formatted into ISO8601.
        error = 0 means no error.
        :rtype: tuple(int, float)
 
        :Example:

        >>> objdate = Date()
        >>> objdate.date_jd2equinox(2458178.0242503937)
        (0, 'J2018.2')
        
        .. note:: Prefer using objdate.date() followed by objdate.equinox()            
        """
        error = 0
        eps=1e-3
        if (year_type==""):
            year_type="J"
            if ((math.fabs(jd-2415020.3135)<eps) or (math.fabs(jd-2433282.4235)<eps)):
                year_type="B"
        if (year_type=="J"):
            # julian
            a=(jd-2451545.0)/365.25+2000.0
            chaine0="J"
        if (year_type=="B"):
            # besselian
            a = 1900.0 + (jd - 2415020.31352) / 365.242198781
            chaine0 = "B"
        if (nb_subdigit>0):
            fstring = "{:."+str(nb_subdigit)+"f}"
            equinox = chaine0 + fstring.format(a)
        else:
	         equinox = chaine0 + "{:.0f}".format(a)
        return error, equinox

    def date_ymdhms2jd(self, y, m, d, hh=0, mm=0, ss=0):
        """ Compute a julian day from a calendar date
        
        Inputs:
        -------
        y = year
        m = month
        d = day
        hh = hour
        mm = minutes
        ss = seconds
        N.B. d can include fraction of day of hh, mm, ss are not used.
        
        Usage:
        ------
        >>> objdate = Date()
        >>> objdate.date_ymdhms2jd(2017,3,12,0,23,12.34)
        (0, 2457824.516115046)
        
        First integer is an error code. 0 = no problem.
            
        Related topics:
        ---------------
        Prefer using objdate.date() followed by objdate.jd()            
        """
        d += ( hh + ( mm + ss/60.) /60.) /24.;
        error, jd = self.date_ymd2jd(y, m, d)
        return error, jd
             
    def date_ymd2jd(self,year, month, day):
        """ Compute a julian day from a calendar date
        
        Inputs:
        -------
        year = year
        month = month
        day = day and fraction of day
        
        Usage:
        ------
        >>> objdate = Date()
        >>> objdate.date_ymd2jd(2017,3,12.64232)
        (0, 2457825.14232)
        
        First integer is an error code. 0 = no problem.
            
        Related topics:
        ---------------
        Prefer using objdate.date() followed by objdate.jd()            
        """
        error = 0
        a=year;
        m=month;
        j=day;
        if m <= 2:
            a=a-1
            m=m+12
        aa=math.floor(a/100)
        bb=2-aa+math.floor(aa/4)
        jd=math.floor(365.25*(a+4716))+math.floor(30.6001*(m+1))+bb-1524.5
        jd=jd+j;
        if (jd<2299160.5) :
            jd=math.floor(365.25*(a+4716))+math.floor(30.6001*(m+1))-1524.5;
            jd=jd+j
        return error,jd

    def date_jd2ymd(self,jd):
        """ Compute a calendar date from a julian day
        
        Inputs:
        -------
        jd is a julian day 
        
        Usage:
        ------
        >>> objdate = Date()
        >>> objdate.date_jd2ymd(2457825.14232)
        (0, 2017, 3, 12.64232000010088)
        
        First integer is an error code. 0 = no problem.
        Following items are:
        year = year
        month = month
        day = day and fraction of day
            
        Related topics:
        ---------------
        Prefer using objdate.date() followed by objdate.ymdhms()            
        """
        error=0;
        jd+=.5
        z=math.floor(jd)
        f=jd-z
        if (z<2299161.):
            a=z
        else:
            alpha=math.floor((z-1867216.25)/36524.25)
            a=z+1+alpha-math.floor(alpha/4)
        b=a+1524
        c=math.floor(((b-122.1)/365.25))
        d=math.floor(365.25*c)
        e=math.floor((b-d)/30.6001)
        d=b-d-math.floor(30.6001*e)+f
        if e<14:
            m = (int)(e-1)
        else:
            m = (int)(e-13)
        if m>2:
            y = (int)(c-4716)
        else:
            y = (int)(c-4715)
        return error, y, m, d

    def date_jd2ymdhms(self,jd):
        """ Compute a calendar date from a julian day
        
        Inputs:
        -------
        jd is a julian day 
        
        Usage:
        ------
        >>> objdate = Date()
        >>> objdate.date_jd2ymdhms(2457824.516115046)
        (0, 2017, 3, 12, 0, 23, 12.339983582496643)
        
        First integer is an error code. 0 = no problem.
        Following items are:
        y = year
        m = month
        d = day
        hh = hour
        mm = minutes
        ss = seconds
            
        Related topics:
        ---------------
        Prefer using objdate.date() followed by objdate.ymdhms()            
        """
        error, y, m, day = self.date_jd2ymd(jd)
        d = int(math.floor(day))
        hh = 0
        mm = 0
        ss = 0
        if (error==0):
            r = (day-d)*24
            hh = int(math.floor(r))
            r = (r-hh)*60
            mm = int(math.floor(r))
            ss = (r-mm)*60
        return error, y, m, d, hh, mm, ss
                 

# ========================================================
# === get/set methods
# ========================================================

    def date(self, date=""):
        """ Set the input date in any format

        :param date: date is a date in any supported format (cf. help(Date))
        :type date: any
        :return : The input date.
        :rtype: string
 
        :Example:

        >>> objdate = Date()
        >>> objdate.date("2018-02-28T12:34:55.234")
        '2018-02-28T12:34:55.234'
        
        .. note:: After using objdate.date() get conversions with methods as objdate.jd() or objdate.iso().            
        """
        if isinstance(date, Date) == False:
            if date != "":
                if isinstance(date, str) == True:
                    dt = date.upper()
                else:
                    dt = ""
                if (date != self._init_date) or (dt == "NOW"):
                    self._init(date)
        else:
            self = date
        return self._init_date

    def jd(self):
        """ Get the date in julian day format

        :return : The julian day.
        :rtype: float
 
        :Example:

        >>> objdate = Date()
        >>> objdate.date("2018-02-28T12:34:55.234")
        '2018-02-28T12:34:55.234'
        >>> objdate.jd()
        2458178.0242503937
        
        .. note:: Before use objdate.date() to set the input date.
        """
        if (self._computed_jd == 0):
            init_dateformat, jd = self.date_date2jd(self._init_date)
            if (init_dateformat > 0):
                self._init_dateformat = init_dateformat
                self._computed_jd = 1        
                self._jd = jd
                return self._jd
            return -1            
        return self._jd

    def iso(self, nb_subdigit=3,letter='T'):
        """ Get the date in ISO 8601 format
        
        Inputs:
        -------
        nb_subdigit is the number of digit returned after the seconds.
        
        Usage:
        ------
        >>> objdate = Date()
        >>> objdate.date("2018-02-28T12:34:55.234")
        '2018-02-28T12:34:55.234'
        >>> objdate.iso(2)
        '2018-02-28T12:34:55.23'
        
        Related topics:
        ---------------
        Before use objdate.date() to set the input date.
        """
        if (self._computed_iso == 0) or (self._computed_iso_nb_subdigit != nb_subdigit)  or (self._computed_iso_letter != letter):
            if (self._computed_jd == 0):
                self.jd()
            if (self._init_dateformat > 0):                    
                error, iso = self.date_jd2iso(self._jd, nb_subdigit, letter)
                if error==0:
                    self._computed_iso = 1
                    self._iso = iso
                    self._computed_iso_nb_subdigit = nb_subdigit
                    self._computed_iso_letter = letter
                    return self._iso
            return -1            
        return self._iso    
        
    def mjd(self):
        """ Get the date in modified julian day format

        :return : The modified julian day.
        :rtype: float
 
        :Example:

        >>> objdate = Date()
        >>> objdate.date("2018-02-28T12:34:55.234")
        '2018-02-28T12:34:55.234'
        >>> objdate.mjd()
        2458178.0242503937
        
        .. note:: Before use objdate.date() to set the input date.
        """
        if (self._computed_mjd == 0):
            if (self._computed_jd == 0):
                self.jd()
            error, mjd = self.date_jd2mjd(self._jd)
            if error==0:
                self._computed_mjd = 1
                self._mjd = mjd
                return self._mjd
            return -1            
        return self._mjd
    
    def ymdhms(self):
        """ Get the date in ymdhms format
        
        Usage:
        ------
        >>> objdate = Date()
        >>> objdate.date("2018-02-28T12:34:55.234")
        '2018-02-28T12:34:55.234'
        >>> objdate.ymdhms()
        [2018, 2, 28, 12, 34, 55.23401856422424]
        
        Related topics:
        ---------------
        Before use objdate.date() to set the input date.
        """
        if (self._computed_ymdhms == 0):
            if (self._computed_jd == 0):
                self.jd()
            if (self._init_dateformat > 0):                    
                error, *ymdhms = self.date_jd2ymdhms(self._jd)
                if error==0:
                    self._computed_ymdhms = 1
                    self._ymdhms = ymdhms
                    return self._ymdhms
            return -1            
        return self._ymdhms
    
    def digits(self, nb_subdigit=3):
        """ Get the date in digits format
        
        Inputs:
        -------
        nb_subdigit is the number of digit returned after the seconds.
        
        Usage:
        ------
        >>> objdate = Date()
        >>> objdate.date("2018-02-28T12:34:55.234")
        '2018-02-28T12:34:55.234'
        >>> objdate.iso(2)
        '2018-02-28T12:34:55.23'
        
        Related topics:
        ---------------
        Before use objdate.date() to set the input date.
        """
        if (self._computed_digits == 0) or (self._computed_digits_nb_subdigit != nb_subdigit):
            if (self._computed_jd == 0):
                self.jd()
            if (self._init_dateformat > 0):                    
                error, digits = self.date_jd2digits(self._jd, nb_subdigit)
                if error==0:
                    self._computed_digits = 1
                    self._digits = digits
                    self._computed_digits_nb_subdigit = nb_subdigit
                    return self._digits
            return -1            
        return self._digits
    
    def equinox(self, year_type="J", nb_subdigit=1):
        """ Get the date in equinox format

        Inputs:
        -------
        year_type = "B" (Bessel) or "J" (Julian) or "" for automatic choice
        nb_subdigit is the number of digit returned after the year.
        
        Usage:
        ------
        >>> objdate = Date()
        >>> objdate.date("2018-02-28T12:34:55.234")
        '2018-02-28T12:34:55.234'
        >>> objdate.equinox()
        'J2018.2'
        
        Related topics:
        ---------------
        Before use objdate.date() to set the input date.
        """
        if (self._computed_equinox == 0) or (self._computed_equinox_year_type != year_type) or (self._computed_equinox_nb_subdigit != nb_subdigit):
            if (self._computed_jd == 0):
                self.jd()
            if (self._init_dateformat > 0):                    
                error, equinox = self.date_jd2equinox(self._jd,year_type,nb_subdigit)
                if error==0:
                    self._computed_equinox = 1
                    self._equinox = equinox
                    self._computed_equinox_year_type = year_type
                    self._computed_equinox_nb_subdigit = nb_subdigit                    
                    return self._equinox
            return -1            
        return self._equinox

# ========================================================
# === debug methods
# ========================================================
    
    def infos(self, action):
        """ To get informations about this class
        
        :param action: A command to run a debug action (see examples).
        :type action: string
        
        :Example:
            
        Date().infos("doctest")
        Date().infos("doc_methods")
        Date().infos("internal_attributes")
        Date().infos("public_methods")        
        """
        if (action == "doc_methods"):
            publics = [x for x in dir(self) if x[0]!="_"]
            for public in publics:
                varname = "{}".format(public)
                if (callable(getattr(self,varname))==True):
                    print("\n{:=^40}".format(" method "+varname+" "))
                    t = "Date()."+varname+".__doc__"
                    tt =eval(t)
                    print(tt)
        if (action == "doctest"):
            if __name__ == "__main__":
                print("\n{:~^40}".format("doctest"))
                #doctest.testmod(verbose=True, extraglobs={'objdate': Date()})
                doctest.testmod(verbose=True)
        if (action == "internal_attributes"):
            internals = [x for x in dir(self) if x[0]=="_" and x[1]!="_"]
            for internal in internals:
                varname = "{}".format(internal)
                #if (hasattr(self,varname)==True):
                if (callable(getattr(self,varname))==False):
                    print(varname + "=" + str(getattr(self,varname)))
        if (action == "public_methods"):
            publics = [x for x in dir(self) if x[0]!="_"]
            for public in publics:
                varname = "{}".format(public)
                if (callable(getattr(self,varname))==True):
                    print(varname)


# ========================================================
# === special methods
# ========================================================
        
    def __init__(self, date=""):
        """ Object initialization where date is the input in any format

        Inputs:
        -------
        date is a date in any supported format (cf. help(Date))
        """
        self._init(date)
        
    def __add__(self, duration):
        """ Add a duration to a date

        Inputs:
        -------
        duration in dhms format (e.g 3d20h5m3s)
        
        Usage:
        ------
        >>> objdate = Date()
        >>> objdate.date("2018-02-28T12:34:55.234") ; date = objdate + "12m45s" ; date.iso()
        '2018-02-28T12:34:55.234'
        '2018-02-28T12:47:40.234'
        """
        if self._computed_jd == 0:
            self.jd()
        if self._computed_jd == 1:
            jd = self._jd
        else:
            return Date()
        duration = Duration(duration)
        day = duration.day()
        jd += day
        return Date(jd)

    def __radd__(self, duration):
        """ Right addition a duration to a date
        """
        return self + duration

    def __iadd__(self, duration):
        """ Add a duration to a date
        """
        return self + duration
    
    def __sub__(self, object_or_duration: Union["Date", float]) -> "Date":
        """ Subtract a duration to a date (returns an object date) or Compute the duration between two date objects.

        Inputs:
        -------
        object_or_duration: Duration in dhms format (e.g 3d20h5m3s)
        or 
        object_or_duration: Date object
        
        Usage:
        ------
        >>> objdate = Date()
        >>> objdate.date("2018-02-28T12:34:55.234") ; date = objdate - "12m45s" ; date.iso()
        '2018-02-28T12:34:55.234'
        '2018-02-28T12:22:10.234'
        >>> objdate.date("2018-02-28T12:34:55.234") ; objdate2 = Date("2018-02-25T12:34:55.234") ; days = objdate - objdate2 ; print(days)
        '2018-02-28T12:34:55.234'
        3.0
        """
        res = 0
        if self._computed_jd == 0:
            self.jd()
        if isinstance(object_or_duration, Date) == True:
            date = object_or_duration
            if date._computed_jd == 0:
                date.jd()
            if (self._computed_jd == 1) and (date._computed_jd == 1):
                res = self._jd - date._jd
        else:
            duration = object_or_duration
            if self._computed_jd == 1:
                jd = self._jd
            else:
                return Date()
            duration = Duration(duration)
            day = duration.day()
            jd -= day
            return Date(jd)
        return res

    def __rsub__(self, date):
        """ Right subtraction only for a date to another date
        """
        if isinstance(date, Date) == True:
            return self - date    
        else:
            return date
    
    def __isub__(self, duration):
        """ Subtract a duration to a date
        Inputs:
        -------
        duration in dhms format (e.g 3d20h5m3s)
        
        Usage:
        ------
        >>> objdate = Date()
        >>> objdate.date("2018-02-28T12:34:55.234") ; objdate -= "12m45s" ; objdate.iso()
        '2018-02-28T12:34:55.234'
        '2018-02-28T12:22:10.234'
        """
        return self - duration    

    def __eq__(self, date):
        """ Comparaison of dates. Return True if dates are defined and equals.

        Inputs:
        -------
        date: An object instancied on Date
        
        Usage:
        ------
        >>> objdate = Date()
        >>> objdate.date("2018 02 28"); objdate2 = Date("2018 02 28"); objdate == objdate2
        '2018 02 28'
        True
        """
        return self._date_compare( date, "==")

    def __ne__(self, date):
        """ Comparaison of dates. Return True if dates are defined and not equals.

        Inputs:
        -------
        date: An object instancied on Date
        
        Usage:
        ------
        >>> objdate = Date()
        >>> objdate.date("2018 02 28"); objdate2 = Date("2018 02 28"); objdate != objdate2
        '2018 02 28'
        False
        """
        return self._date_compare( date, "!=")

    def __gt__(self, date):
        """ Comparaison of dates: date1 > date 2. Return True if dates are defined and date1 > date 2.

        Inputs:
        -------
        date: An object instancied on Date
        
        Usage:
        ------
        >>> objdate = Date()
        >>> objdate.date("2018 02 28"); objdate2 = Date("2018 02 27"); objdate > objdate2
        '2018 02 28'
        True
        """
        return self._date_compare( date, ">")

    def __ge__(self, date):
        """ Comparaison of dates: date1 >= date 2. Return True if dates are defined and date1 >= date 2.

        Inputs:
        -------
        date: An object instancied on Date
        
        Usage:
        ------
        >>> objdate = Date()
        >>> objdate.date("2018 02 28"); objdate2 = Date("2018 02 27"); objdate >= objdate2
        '2018 02 28'
        True
        """
        return self._date_compare( date, ">=")

    def __lt__(self, date):
        """ Comparaison of dates: date1 < date 2. Return True if dates are defined and date1 < date 2.

        Inputs:
        -------
        date: An object instancied on Date
        
        Usage:
        ------
        >>> objdate = Date()
        >>> objdate.date("2018 02 26"); objdate2 = Date("2018 02 27"); objdate < objdate2
        '2018 02 26'
        True
        """
        return self._date_compare( date, "<")

    def __le__(self, date):
        """ Comparaison of dates: date1 <= date 2. Return True if dates are defined and date1 <= date 2.

        Inputs:
        -------
        date: An object instancied on Date
        
        Usage:
        ------
        >>> objdate = Date()
        >>> objdate.date("2018 02 26"); objdate2 = Date("2018 02 27"); objdate <= objdate2
        '2018 02 26'
        True
        """
        return self._date_compare( date, "<=")
    
# ========================================================
# ========================================================
# ========================================================

# Examples of execution

if __name__ == "__main__":

    kex = 0
    print("==========================")
    print("=== Self documentation ===")
    print("==========================")
    
    kex+=1;
    print("\n=== Example {}: Simple conversion ISO 8601".format(kex))
    date_inp = "now"
    date = Date(date_inp)
    print("{} = {} (ISO8601 UTC at milliseconds)".format(date_inp,date.iso()))
    print("{} = {} (ISO8601 UTC at seconds)".format(date_inp,date.iso(0)))
    
    kex+=1;
    print("\n=== Example {}: Simple conversion to Julian day".format(kex))
    date_inp = "now"
    date = Date(date_inp)
    print("{} = {}".format(date_inp,date.jd()))
    print("{} = {} (Modfied Julian day)".format(date_inp,date.mjd()))

    kex+=1;
    print("\n=== Example {}: Addition of date + duration".format(kex))
    date_inp1 = "now"
    duration_inp2 = "2d28m12.4s"
    a = Date(date_inp1)
    b = Duration(duration_inp2)
    c = a + b
    print("{} + {} = {}".format(date_inp1,duration_inp2,c.iso(2)))
  
    cl = "Date"
    print("\n=== To perform unitary tests: {}().infos(\"doctest\")".format(cl))
    print("=== To list docs of all methods: {}().infos(\"doc_methods\")".format(cl))
    print("\n=== List of public methods using {}().infos(\"public_methods\"):".format(cl))
    Date().infos("public_methods")