#!/usr/local/bin/pythonw # coding: iso-8859-1 # Copyright © 2008 by Amos Newcombe # # This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along with this program. If not, see . '''These classes translate between date tuples and Julian Day numbers. There are two types of applications for Julian Days. In the first, each day in history is assigned a unique integer, such that the days follow each other in the same order the integers do. A day is described by a triple (yr, mo, da). Times of day are not an issue; neither are time zones. For this kind of date, use the JulianDay class (tag jd). Other applications need Julian Days to be floating point numbers, with fractions of a day. On the date side, the tuple extends into hours, minutes and seconds. For this kind of date, use the JulianDayTime class (tag jdt). Instances of both of these classes are called "jd objects". The zero point of all jd objects is adopted from astronomical convention: January 1, -4712; specifically noon of that day. Astronomers wanted a positive date number for all reasonable dates, and they didn't want that number changing at midnight, in the middle of prime observing time. >>> JulianDay(0).getDt() (-4712, 1, 1) >>> JulianDayTime(0.).getDt() (-4712, 1, 1, 12, 0, 0, 0.0) Note how the tuple returned by JulianDateTime.getDt() has 7 components: the 6 standard ones plus a floating point number representing fractions of a second. >>> JulianDayTime(2451545).getDt() (2000, 1, 1, 12, 0, 0, 0.0) >>> JulianDayTime(2451545+2.33e-10).getDt() # resolution is 2.33e-10 days (2000, 1, 1, 12, 0, 0, 4.0233135223388672e-05) >>> (JulianDayTime((2000,1,1,12,0,2.02e-5))-2451545).getJd() # or 2.02e-5 sec 4.6566128730773926e-10 You can add a number to a jd object (or a jd object to a number) to get a new jd object, you can subtract two jd objects to get a number, and subtract a number from a jd object to get another jd object. These numbers' units are all days. You can, given one type of jd object, get an equivalent one of the other type using self.ToJd() or self.ToJdt() as appropriate. US Presidential inauguration plus 100 days -- the end of the "honeymoon": >>> (100 + JulianDay((2009,1,20))).getDt() (2009, 4, 30) Length of John F. Kennedy administration (sometimes called "1000 days"): >>> JulianDay((1963,11,22)) - JulianDay((1961,1,20)) 1036 Note the absence of a 12 hour offset if you don't use JulianDay(): >>> (JulianDayTime((2008,3,1)) - 1).getDt() (2008, 2, 29, 0, 0, 0, 0.0) The algorithms are from Jan Meeus, _Astronomical Algorithms_, chapter 7. Willmann-Bell: Richmond, VA. ISBN 0-943396-35-2. ''' # ¤ Definitions from math import floor, modf from time import struct_time, localtime, mktime, timezone, altzone class JulianDay: # tag jd '''The class for integer dates, without time of day. ''' # ¤ Creation def __init__(self, D, isGregorian=None): '''D is either a numeric Julian Day, or a date tuple or list.''' #print 'JulianDay(%s)' % (D,) if isinstance(D, list): D = tuple(D) if isinstance(D, (tuple, struct_time)): self.setDt(D) self.setJd(self.TupleToJd(D, isGregorian)) else: self.setDt(self.JdToTuple(D, isGregorian)) self.setJd(D) def setJd(self, D): self.nJd = int(D) def getJd(self): return self.nJd def setDt(self, D): self.dt = D def getDt(self): return self.dt def TupleToJd(self, dt, isGregorian=None): dt = (dt + (1,) * (3 - len(dt)))[0:3] if isGregorian == None: isGregorian = ((1582, 10, 15) <= dt) nYr, nMo, nDa = dt if nMo <= 2: nYr, nMo = nYr-1, nMo+12 B = 0 if isGregorian: A = nYr/100 B = 2 - A + A/4 xJd = floor(365.25*(nYr+4712)) + floor(30.609375*(nMo+1)) + nDa + B - 63 return int(xJd) def JdToTuple(self, nJd, isGregorian=None): #print 'JdToTuple(%s, %s)' % (nJd, isGregorian) Z = nJd if isGregorian == None: isGregorian = (2299161 <= Z) if isGregorian: alpha = floor((Z-1867216.25)/36524.25) # 1867216.25 = 400.2.28@18:00 A = Z + 1 + alpha - floor(alpha/4) else: A = Z B = A + 63 C = floor((B - 122.4375)/365.25) D = floor(365.25*C) E = floor((B-D)/30.609375) nYr = int(C) - 4712 nMo = int(E) - 1 nDa = int(B - D - floor(30.609375*E)) if 12 < nMo: nYr, nMo = nYr + 1, nMo - 12 return (nYr, nMo, nDa) # ¤ Description def __str__(self): return '%s = %s' % (self.getDt(), self.getJd()) # ¤ Operation def ToDateTuple(self): return self.getDt() + (0, 0, 0, self.Weekday(), 1, -1) def ToJd (self): return self def ToJdt(self): return JulianDayTime(float(self.getJd())) def Weekday(self, nDay=None): if nDay == None: nDay = self.getJd() return nDay % 7 # ¤ Numeric interface def __add__(self, other): return self.__class__(self.getJd() + other) def __radd__(self, other): return self + other def __sub__(self, other): if isinstance(other, JulianDay): return self.getJd() - other.getJd() else: return self + (-other) def __float__(self): return self.ToJdt().getJd() def __int__(self): return self.getJd() def __cmp__(self, other): xJdSelf, xJdOther = self.getJd(), other.getJd() # Sort integer days as if they were at the leading midnight if self .__class__ == JulianDay: xJdSelf -= 0.5 if other.__class__ == JulianDay: xJdOther -= 0.5 return cmp(xJdSelf, xJdOther) class JulianDayTime(JulianDay): # tag jdt, or jd '''The class for fractional dates, and times of day. It's subclassed because it starts with the same integer calculations, then grafts the time of day onto that. ''' # ¤ Creation #def __init__(self, D, isGregorian=None, isLocal=False): def setJd(self, D): self.xJd = float(D) def getJd(self): return self.xJd def TupleToJd(self, dt, isGregorian=None): '''Given a tuple which is a prefix of (yr, mo, da, hr, min, sec, F), produce a (float) Julian Day number. ''' dtDate, dtTime = dt[0:3], dt[3:] nJd = JulianDay.TupleToJd(self, dtDate, isGregorian) dtTime = (dtTime + (0,) * (4 - len(dtTime)))[0:4] nHr, nMin, nSec, xF = dtTime return nJd - 0.5 + ((nHr*60 + nMin)*60 + nSec + xF)/86400. def JdToTuple(self, xJd, isGregorian=None): '''Produce a 7-tuple: (yr, mo, da, hr, min, sec, F), where sec is made an integer and F stores its fractional part. noon - (.001 day = 86.4 sec = 1:26.4) = 11:58:33.6 >>> '%d.%d.%d %d:%02d:%02d+%.5f' % JulianDayTime(-0.001).getDt() '-4712.1.1 11:58:33+0.60000' ''' xF, nJd = modf(xJd+0.5) (nYr, nMo, nDa) = JulianDay.JdToTuple(self, int(nJd), isGregorian) xF, xHr = modf(xF * 24) xF, xMin = modf(xF * 60) xF, xSec = modf(xF * 60) return (nYr, nMo, nDa, int(xHr), int(xMin), int(xSec), xF) # ¤ Operation def ToLocal(self): dt = self.ToDateTuple(isLocalized=True) secTz = -timezone if dt[8] == 1: secTz = -altzone return self + secTz/86400. def ToDateTuple(self, isLocalized=True): dt = self.getDt()[0:6] + (self.Weekday(), 1, -1) if isLocalized: dt = Localize(dt) return dt def JdToTz(self, secTz): return self.__class__(self.getJd() + secTz/86400.) def ToJd (self): return JulianDay(self.getDt()[0:3]) def ToJdt(self): return self def Weekday(self, xDay=None): if xDay == None: xDay = self.getJd() return JulianDay.Weekday(self, int(floor(xDay+0.5))) # ¤ Numeric interface def __float__(self): return self.getJd() def __int__(self): return self.ToJd().getJd() def Localize(dt): '''Try to get daylight savings information from the operating system clock, safely. Only works in the Unix date range: 1901.12.13T20:45:52Z ... 2038.01.19T3:14:07Z; other date tuples are returned unchanged. ''' # print 'Localize(%s)' % (dt,) try: dtT = localtime(mktime(dt)) # Get complete tuple, if dt in Unix date range. except OverflowError: dtT = dt # Otherwise, ignore daylight savings. except ValueError : dtT = dt # I said, OTHERWISE, ... oh, forget it. if dt[0] != dtT[0] : dtT = dt # If the year is munged, forget it too. return dtT if __name__ == '__main__': # Unit tests import doctest print '%d tests failed out of %d' % doctest.testmod() # Command line tool from commander.commander import evaluator import re, sys class jdt(evaluator): # ¤ Metadata sUsage = '''\ usage: %prog [options] number|date ... Translate Julian Day numbers to dates and vice versa. Dates are strings, with numerical values delimited by any non-digit. Use "~" instead of "-" for negative years or negative Julian Day numbers.\ ''' sVersion = '%prog 2008.06.22.0' # ¤ evaluator interface reSplit = re.compile('\D') # split date strings on any nondigit def function(self, sArg): 'process a single command line argument' if sArg[0] == '~': sArg = '-' + sArg[1:] # is it an integer? try: nJd = int(sArg) except ValueError: pass # on to the next possibility: a float else: # found an integer return JulianDay(nJd).getDt() # is it a float? try: xJd = float(sArg) except ValueError: pass # on to the next possibility: a tuple else: # found a float return JulianDayTime(xJd).getDt() # gotta be a tuple, as represented in a string, split by self.reSplit try: lnArg = [int(s) for s in self.reSplit.split(sArg)] except ValueError, ex: # fail return sArg if len(lnArg) <= 3: return JulianDay(lnArg).getJd() else: return JulianDayTime(lnArg).getJd() jdt(sys.argv).main()