import math import doctest import typing # ======================================================== # ======================================================== # === DURATION # ======================================================== # ======================================================== class Duration: """ Class to convert dates for astronomy Date formats are: ----------------- day = days. e.g. 12.3457 dhms = day, hour, minutes seconds. e.g. 2d12h34m55.23s """ # ======================================================== # === internal methods # ======================================================== def _init(self,duration="") -> None: """ Initialize internal attributes. :param duration: Duration in any supported format (cf. help(Duration)) :type date: any :return: None :rtype: None :Example: >>> objduration = Duration() >>> objduration._init() """ self._init_duration = duration self._init_durationformat = 0 self._computed_day = 0 self._day = 0 self._computed_dhms = 0 self._dhms = "0d00h00m00s" self._dhms_format = "0.3" 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 _day2dhms(self, day, dhms_format) -> str: if (self._computed_day == 0): return "" symbols = str(dhms_format) digits = "" separator=0; # 0=dhms 1=space 2=: sign=0; # zeros=0; # 0=spaces 1=leading zeros k=0 for car in symbols: if (car==' '): separator=1 if (car=='_'): separator=1 if (car==':'): separator=2 if (car=='+'): sign=1 if (self._is_number(car)) or car=='.': digits = symbols[k:] break k += 1 # int unit=0; // 0=deg 1=hours # int modulo=0; // 0=360 1=180 # int separator=0; // 0=hdms 1=space 2=: # int sign=0; // 0=[0:modulo] 1=[-modulo/2:modulo/2] # === trailing format (digits) nb_decimalsec = 2 ld = len(digits) if (ld>0): # e.g. digits = ".3" car = digits[0] if car=='0': zeros=1 digits = digits[1:] # now digits = ".3" and zeros=1 kd = digits.find(".") if kd >= 0: if (self._is_number(digits[kd+1:])==True): nb_decimalsec = int(digits[kd+1:]) # now digits_nb_decimal="3" #print("ld="+str(ld)) #print("symbols="+symbols) #print("digits="+digits) #print("separator="+str(separator)) #print("sign="+str(sign)) #print("zeros="+str(zeros)) #print("nb_decimal="+str(nb_decimalsec)) # --- sign of the input angle s = 1 if day<0: s = -1 day = -day # --- compute the trheee components of dd hh mm ss r = day dd = int(math.floor(r)) hh = 0 mm = 0 ss = 0 r = (r-dd)*24 hh = int(math.floor(r)) r = (r-hh)*60 mm = int(math.floor(r)) ss = (r-mm)*60 #print("hh ="+str(hh)) #print("mm ="+str(mm)) #print("ss ="+str(ss)) # === Compute the result result = "" # --- sign if sign==1 and s>=0: result += "+" elif s<0: result += "-" # --- dd fstring = "{:d}" result += fstring.format(dd) # --- separator dd hh if separator==1: result += " " elif separator==2: result += ":" else: result += "d" # --- hh if zeros==0: fstring = "{:d}" else: fstring = "{:02d}" result += fstring.format(hh) # --- separator hh mm if separator==1: result += " " elif separator==2: result += ":" else: result += "h" # --- mm if zeros==0: fstring = "{:d}" else: fstring = "{:02d}" result += fstring.format(mm) # --- separator mm ss if separator==1: result += " " elif separator==2: result += ":" else: result += "m" # --- ss if zeros==0: fstring = "{:."+str(nb_decimalsec)+"f}" else: if nb_decimalsec==0: fstring = "{:0"+str(nb_decimalsec+2)+".0f}" else: fstring = "{:0"+str(nb_decimalsec+3)+"."+str(nb_decimalsec)+"f}" #print("fstring="+fstring) result += fstring.format(ss) # --- separator ss if separator==0: result += "s" # -- end return 0, result def _duration2day(self, duration) -> float: """ Return a duration in day unit from a duration in ymdhms format. :param duration: A string formated as "2d7h23m12.5s" :type duration: string :return: duration expressed in day and fraction of day :rtype: float :Example: >>> objduration = Date() >>> objduration._duration2day("-0d7h23m12.5s") 0.30778356481481484 """ str_duration = str(duration) lst_durations = str_duration.split() str_duration = " ".join(lst_durations) numbs = "+-.E0123456789" kd = -1 kh = -1 km = -1 ks = -1 da = 0 ho = 0 mi = 0 se = 0 sign = 1 k = 0 knum = -1 dur = 0 #print("km={} ks={}".format(km,ks)) for car in str_duration: if (car == '-') and (knum == -1): sign = -1 if (car in numbs) and (knum == -1): knum = k #print("k={} sign={} car={} knum=={} kd={} kh={} km={} ks={}".format(k,sign,car,knum,kd,kh,km,ks)) if (car=='d' or car=='D') and (kd == -1) and (knum >= 0) : k1 = knum k2 = k str_val = str_duration[k1:k2] if (self._is_number(str_val) == True): kd = k1 da = math.fabs(float(str_val)) knum = -1 dur += da if (car=='h' or car=='H') and (kh == -1) and (knum >= 0) : k1 = knum k2 = k str_val = str_duration[k1:k2] #print("str_val={}".format(str_val)) if (self._is_number(str_val) == True): kh = k1 ho = math.fabs(float(str_val)) knum = -1 dur += ho/24. if ( (car=='m' or car=='M') and (km == -1) ) and (knum >= 0) : k1 = knum k2 = k str_val = str_duration[k1:k2] #print("str_val={}".format(str_val)) if (self._is_number(str_val) == True): km = k1 mi = math.fabs(float(str_val)) knum = -1 dur += mi/1440. if (car=='s' or car=='S') and (ks == -1) and (knum >= 0) : k1 = knum k2 = k str_val = str_duration[k1:k2] #print("str_val={}".format(str_val)) if (self._is_number(str_val) == True): ks = k1 se = math.fabs(float(str_val)) knum = -1 dur += se/86400. k += 1 if (car not in numbs): knum = -1 #print("END k={} car={} knum=={} numbs={}".format(k,car,knum,numbs)) if (dur==0): lst_durations = str_duration.split() #print("(1) lst_durations={}".format(lst_durations)) lst_durations = [ math.fabs(float(x)) for x in lst_durations if (self._is_number(x) == True) ] #print("(2) lst_durations={}".format(lst_durations)) n_duration = len(lst_durations) if (n_duration==1): da = lst_durations[0] dur = da elif (n_duration>=2): se = lst_durations[-1] dur += se/86400. mi = lst_durations[-2] dur += mi/1440. if (n_duration>=3): ho = lst_durations[-3] dur += ho/24. if (n_duration>=3): da = lst_durations[-4] dur += da dur *= sign decode = (sign, da, ho, mi, se) return decode, dur def _duration_compare(self, duration, operator): """ Comparaison of durations for various operators. :param duration: An duration in any supported format (cf. help(Duration)) :type duration: Duration() :param operator : Operator such as == != > >= < <= :type operator : string :return: The logic result of the comparison. :rtype: bool :Example: >>> objduration1 = Duration() >>> objduration2 = Duration() >>> objduration1.duration(6.12) ; objduration2.duration("5d2m") ; objduration1._duration_compare(objduration2,">") 6.12 '5d2m' True .. note:: Does not account for the modulo. """ if isinstance(duration, Duration) == False: duration = Duration(duration) if self._computed_day == 0: self.day() if duration._computed_day == 0: duration.day() res = False if (self._computed_day == 1) and (duration._computed_day == 1): toeval = str(self._day)+" "+operator+" "+str(duration._day) res = eval(toeval) return res # ======================================================== # === duration methods # ======================================================== def duration_duration2day(self,duration) -> typing.Tuple[int, float]: """ Compute day number from any duration format :param date: A string formated as "2d7h23m12.5s" or "2 7 23 12.5" :type date: string :return : A tuple of decode, julian day. decode : (sign, da, ho, mi, se) :rtype: tuple(tuple, float) :Example: >>> objduration = Duration() >>> objduration.duration_duration2day("2d7h23m12.5s") ((1, 2.0, 7.0, 23.0, 12.5), 2.3077835648148146) .. note:: Prefer using objduration.duration() followed by objduration.day(). """ decode, day = self._duration2day(duration) self._computed_day = 1 return decode, day # ======================================================== # === get/set methods # ======================================================== def duration(self, duration=""): """ Set the input duration in any format :param duration: duration is a duration in any supported format (cf. help(Duration)) :type duration: any :return : The input duration. :rtype: string :Example: >>> objduration = Duration() >>> objduration.duration("2d7h23m12.5s") '2d7h23m12.5s' .. note:: After using objdate.duration() get conversions with methods as objdate.day() or objdate.dhms(). """ if duration != "": if (duration != self._init_duration): self._init(duration) return self._init_duration def day(self): """ Get the date in julian day format :return : The julian day. :rtype: float :Example: >>> objduration = Duration() >>> objduration.duration("2d7h23m12.5s") '2d7h23m12.5s' >>> objduration.day() 2.3077835648148146 .. note:: After using objdate.duration() get conversion with objdate.day(). """ if (self._computed_day == 0): init_durationformat, day = self.duration_duration2day(self._init_duration) self._init_durationformat = init_durationformat self._computed_day = 1 self._day = day return self._day def dhms(self, dhms_format): """ Get the date in dhms format :param dhms_format: dhms format (cf. help(Duration)) :type dhms_format: string :return : The formated string. :rtype: string :Example: >>> objduration = Duration() >>> objduration.duration("2d7h3m12.5s") '2d7h3m12.5s' >>> objduration.dhms("0") '2d07h03m12.50s' .. note:: After using objdate.duration() get conversion with objdate.dhms(). """ if (self._computed_day == 0): self.day() if (self._computed_dhms == 0) or (self._dhms_format != dhms_format) : error, dhms = self._day2dhms(self._day,dhms_format) self._dhms_format = dhms_format self._dhms = dhms self._computed_dhms = 1 return self._dhms # ======================================================== # === debug methods # ======================================================== def infos(self, action) -> None: """ To get informations about this class :param action: A command to run a debug action (see examples). :type action: string :Example: Duration().infos("doctest") Duration().infos("doc_methods") Duration().infos("internal_attributes") Duration().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 = "Duration()."+varname+".__doc__" tt =eval(t) print(tt) if (action == "doctest"): if __name__ == "__main__": print("\n{:~^40}".format("doctest")) #doctest.testmod(verbose=True, extraglobs={'objangle': Duration()}) 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, duration=""): """ Object initialization where duration is the input in any format. :param duration: A duration in any supported format (cf. help(Duration)) :type duration: any """ self._init(duration) def __add__(self, duration): """ Add a duration to a duration. :param duration: A duration in any supported format (cf. help(Duration)) :type duration: Duration() :return: The result of the addition :rtype: Duration() :Example: >>> objduration1 = Duration() >>> objduration2 = Duration() >>> objduration1.duration(12.25) ; objduration2.duration("56d28m") ; objduration = objduration1 + objduration2 ; objduration.dhms("+0.3") 12.25 '56d28m' '+68d06h28m00.000s' """ if isinstance(duration, Duration) == False: duration = Duration(duration) res = Duration() if self._computed_day == 0: self.day() if duration._computed_day == 0: duration.day() if (self._computed_day == 1) and (duration._computed_day == 1): day = self._day + duration._day res = Duration(day) return res def __radd__(self, duration): """ Right addition a duration to a duration. """ return self + duration def __iadd__(self, duration): """ Add a duration to a duration. """ return self + duration def __sub__(self, duration): """ Subtract a duration to a duration. :param duration: A duration in any supported format (cf. help(Duration)) :type duration: Duration() :return: The result of the addition :rtype: Duration() :Example: >>> objduration1 = Duration() >>> objduration2 = Duration() >>> objduration1.duration(12.25) ; objduration2.duration("56d28m") ; objduration = objduration1 - objduration2 ; objduration.dhms("+0.3") 12.25 '56d28m' '-43d18h28m00.000s' """ if isinstance(duration, Duration) == False: duration = Duration(duration) res = Duration() if self._computed_day == 0: self.day() if duration._computed_day == 0: duration.day() if (self._computed_day == 1) and (duration._computed_day == 1): day = self._day - duration._day res = Duration(day) return res def __rsub__(self, duration): """ Right subtraction a duration to a duration. """ if isinstance(duration, Duration) == True: return self - duration else: return duration def __isub__(self, duration): """ Subtract a duration to a duration. :param duration: A duration in any supported format (cf. help(Duration)) :type duration: Duration() :return: The result of the addition :rtype: Duration() :Example: >>> objduration1 = Duration() >>> objduration2 = Duration() >>> objduration1.duration(12.25) ; objduration2.duration("56d28m") ; objduration1 -= objduration2 ; objduration1.dhms("+0.3") 12.25 '56d28m' '-43d18h28m00.000s' """ return self - duration def __eq__(self, duration): """ Comparaison of durations. Return True if durations are defined and equals. :param duration: A duration in any supported format (cf. help(Duration)) :type duration: Duration() :return: The logic result of the comparison. :rtype: bool :Example: >>> objduration1 = Duration() >>> objduration2 = Duration() >>> objduration1.duration(12.345) ; objduration2.duration("56d28m") ; objduration1 == objduration2 12.345 '56d28m' False """ return self._duration_compare(duration, "==") def __ne__(self, duration): """ Comparaison of durations. Return True if durations are defined and not equals. :param duration: A duration in any supported format (cf. help(Duration)) :type duration: Duration() :return: The logic result of the comparison. :rtype: bool :Example: >>> objduration1 = Duration() >>> objduration2 = Duration() >>> objduration1.duration(12.345) ; objduration2.duration("56d28m") ; objduration1 != objduration2 12.345 '56d28m' True """ return self._duration_compare(duration, "!=") def __gt__(self, duration): """ Comparaison of durations. Return True if self > duration :param duration: A duration in any supported format (cf. help(Duration)) :type duration: Duration() :return: The logic result of the comparison. :rtype: bool :Example: >>> objduration1 = Duration() >>> objduration2 = Duration() >>> objduration1.duration(12.345) ; objduration2.duration("56d28m") ; objduration1 > objduration2 12.345 '56d28m' False """ return self._duration_compare(duration, ">") def __ge__(self, duration): """ Comparaison of durations. Return True if self >= duration :param duration: A duration in any supported format (cf. help(Duration)) :type duration: Duration() :return: The logic result of the comparison. :rtype: bool :Example: >>> objduration1 = Duration() >>> objduration2 = Duration() >>> objduration1.duration(12.345) ; objduration2.duration("56d28m") ; objduration1 >= objduration2 12.345 '56d28m' False """ return self._duration_compare(duration, ">=") def __lt__(self, duration): """ Comparaison of durations. Return True if self < duration :param duration: A duration in any supported format (cf. help(Duration)) :type duration: Duration() :return: The logic result of the comparison. :rtype: bool :Example: >>> objduration1 = Duration() >>> objduration2 = Duration() >>> objduration1.duration(12.345) ; objduration2.duration("56d28m") ; objduration1 < objduration2 12.345 '56d28m' True """ return self._duration_compare(duration, "<") def __le__(self, duration): """ Comparaison of durations. Return True if self <= duration :param duration: A duration in any supported format (cf. help(Duration)) :type duration: Duration() :return: The logic result of the comparison. :rtype: bool :Example: >>> objduration1 = Duration() >>> objduration2 = Duration() >>> objduration1.duration(12.345) ; objduration2.duration("56d28m") ; objduration1 <= objduration2 12.345 '56d28m' True """ return self._duration_compare(duration, "<=") def __mul__(self, multiplier): """ multiplication of duration by a float or int. Return a duration :param multiplier: A real number :type multiplier: float :return: A duration :rtype: Duration() :Example: >>> objduration = Duration() >>> objduration.duration(30.43) ; (objduration*2).day() 30.43 60.86 """ if isinstance(multiplier, (int, float)) == False: raise TypeError return "" if self._computed_day == 0: self.day() durationmult = Duration() if (self._computed_day == 1): day = self._day * multiplier durationmult.duration(day) return durationmult def __rmul__(self, multiplier): """ Right multiplication of a duration by a float or int. """ return self * multiplier def __truediv__(self, divisor): """ division of an angle by a float or int. Return a duration :param divisor: A real number :type divisor: float :return: A duration :rtype: Duration() :Example: >>> objduration = Duration() >>> objduration.duration(30.43) ; (objduration/2).day() 30.43 15.215 """ if isinstance(divisor, (int, float)) == False: raise TypeError return "" if (divisor==0): raise ZeroDivisionError if self._computed_day == 0: self.day() durationdiv = Duration() if (self._computed_day == 1): day = self._day / divisor durationdiv.duration(day) return durationdiv