#!/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 <http://www.gnu.org/licenses/>.

'''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()
			
