#
# xcal.py
#	Manipulation for calendar values from year 0 to 100000 and
#	beyond.
#
# Based on a library in C:
#
# Copyright (c) 1997 Wolfgang Helbig
# All rights reserved.
#
# Converted to Python by Andy Valencia 11/2008
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
import time

#
# For each month tabulate the number of days elapsed in a year before the
# month. This assumes the internal date representation, where a year
# starts on March 1st. So we don't need a special table for leap years.
# But we do need a special table for the year 1582, since 10 days are
# deleted in October. This is month1s for the switch from Julian to
# Gregorian calendar.
#
month1 = \
    (0, 31, 61, 92, 122, 153, 184, 214, 245, 275, 306, 337)
#       M   A   M   J    J    A    S    O    N    D    J
month1s = \
    (0, 31, 61, 92, 122, 153, 184, 214, 235, 265, 296, 327)

# The last day of Julian calendar, in internal and ndays representation
# Dates in this format are stored in a tuple as (year, month, day)
nswitch = None
jiswitch = (1582, 7, 3)

# Names of days of week
daynames = ("Monday", "Tuesday", "Wednesday", "Thursday", \
    "Friday", "Saturday", "Sunday")

#
# Class wrapper around a (y, m, d) tuple
#
# y, m, d - Year month and day for this Date
#
class Date():
    def __init__(self, year, month, day):
	self.y = year
	self.m = month
	self.d = day
	self.wday = None
	self.nmonth = None

    # Sring rep
    def __str__(self):
	return "%s/%s/%s" % (self.m, self.d, self.y)

    # Comparisons
    def __eq__(self, other):
	if other == None:
	    return False
	return (self.y == other.y) and (self.m == other.m) \
	    and (self.d == other.d)
    def __ne__(self, other):
	if other == None:
	    return True
	return not (self == other)
    def __lt__(self, other):
	if self.y > other.y: return False
	if self.y < other.y: return True
	if self.m > other.m: return False
	if self.m < other.m: return True
	return self.d < other.d
    def __le__(self, other):
	return (self == other) or (self < other)
    def __gt__(self, other):
	return (not (self == other)) and (not (self < other))
    def __ge__(self, other):
	return (self == other) or (self > other)

    # Return the number of days since March 1st of the year zero.
    # The date is given according to Julian calendar.
    def ndaysj(self):
	    idt = self.date2idt()
	    return idt.ndaysji()

    # Same as above, where the Julian date is given in internal notation.
    # This formula shows the beauty of this notation.
    def ndaysji(self):
	return self.d + month1[self.m] + (self.y * 365) + (self.y / 4)

    # Return the number of days since March 1st of the year zero. The date is
    #  assumed Gregorian if younger than 1582-10-04 and Julian otherwise. This
    #  is the reverse of gdate.
    def ndaysg(self):
	idt = self.date2idt()
	return idt.ndaysgi()

    # Same as above, but with the Gregorian date given in internal
    #  representation.
    def ndaysgi(self):
	global nswitch

	# Cache nswitch if not already done
	if nswitch == None:
	    nswitch = Date(*jiswitch).ndaysji()

	# Assume Julian calendar and adapt to Gregorian if necessary, i. e.
	# younger than nswitch. Gregori deleted
	# the ten days from Oct 5th to Oct 14th 1582.
	# Thereafter years which are multiples of 100 and not multiples
	# of 400 were not leap years anymore.
	# This makes the average length of a year
	# 365d +.25d - .01d + .0025d = 365.2425d. But the tropical
	# year measures 365.2422d. So in 10000/3 years we are
	# again one day ahead of the earth. Sigh :-)
	# (d is the average length of a day and tropical year is the
	# time from one spring point to the next.)
	#
	# "nd" is number of days--return value
	nd = self.ndaysji()
	if self.y >= 1600:
	    nd = (nd - 10 - (self.y - 1600) / 100 + (self.y - 1600) / 400)
	elif nd > nswitch:
	    nd -= 10
	return (nd);

    # Convert a date to internal date representation: The year starts on
    #  March 1st, month and day numbering start at zero. E. g. March 1st of
    #  year zero is written as y=0, m=0, d=0.
    def date2idt(self):
	d = self.d - 1
	if self.m > 2:
	    m = self.m - 3
	    y = self.y
	else:
	    m = self.m + 9
	    y = self.y - 1
	assert not ((m < 0) or (m > 11) or (y < 0))
	return Date(y, m, d)

    # Reverse of date2idt
    def idt2date(self):
	d = self.d + 1;
	if self.m < 10:
	    m = self.m + 3
	    y = self.y
	else:
	    m = self.m - 9
	    y = self.y + 1
	assert m >= 1
	return Date(y, m, d)

    # Tell our weekday
    def weekday(self):
	# Cache it on first reference
	if self.wday == None:
	    self.wday = weekday(self.ndaysg())
	return self.wday

    # Tell name of weekday
    def dayname(self):
	return daynames[self.weekday()]

    # Tell number of days to reach 1st of next month
    def days2next(self):
	# (The answer is cached)
	if self.nmonth == None:
	    y = self.y
	    m = self.m+1
	    if m > 12:
		y += 1
		m = 1
	    nd = Date(y, m, 1)
	    self.nmonth = nd.ndaysj() - self.ndaysj()
	return self.nmonth

# Compute the Julian date from the number of days elapsed since
#  March 1st of year zero.
def jdate(ndays):
    #
    # Compute the year by starting with an approximation not smaller
    #  than the answer and using linear search for the greatest
    #  year which does not begin after ndays.
    #
    # "idt" is our internal date representation,
    # "r" holds the rest of days
    #
    idt = Date(ndays / 365, 0, 0)
    while True:
	r = idt.ndaysji()
	if r <= ndays:
	    break
	idt.y -= 1
    
    # Set r to the days left in the year and compute the month by
    #  linear search as the largest month that does not begin after r
    #  days.
    r = ndays - r
    idt.m = 11
    while month1[idt.m] > r:
	idt.m -= 1

    # Compute the days left in the month
    idt.d = r - month1[idt.m]

    # Return external representation of the date
    return idt.idt2date()

# Compute the date according to the Gregorian calendar from the number of
#  days since March 1st, year zero. The date computed will be Julian if it
#  is older than 1582-10-05. This is the reverse of the function ndaysg().
def gdate(ndays):
    # Compute the year by starting with an approximation not smaller
    #  than the answer and search linearly for the greatest year not
    #  starting after ndays.
    # "idt" is our internal date representation,
    # "r" holds the rest of days
    idt = Date(ndays / 365, 0, 0)
    while True:
	r = idt.ndaysgi()
	if r <= ndays:
	    break
	idt.y -= 1

    # Set ndays to the number of days left and compute by linear
    #  search the greatest month which does not start after ndays. We
    #  use the table month1 which provides for each month the number
    #  of days that elapsed in the year before that month. Here the
    #  year 1582 is special, as 10 days are left out in October to
    #  resynchronize the calendar with the earth's orbit. October 4th
    #  1582 is followed by October 15th 1582. We use the "switch"
    #  table month1s for this year.
    ndays = ndays - r
    if idt.y == 1582: montht = month1s
    else: montht = month1

    idt.m = 11
    while montht[idt.m] > ndays:
	idt.m -= 1

    # The rest is the day in month
    idt.d = ndays - montht[idt.m]

    # Advance ten days deleted from October if after switch in Oct 1582
    if (idt.y == jiswitch[0]) and (idt.m == jiswitch[1]) and \
	    (jiswitch[2] < idt.d):
	idt.d += 10;

    # Return external representation of found date
    return idt.idt2date()

# Compute the week number from the number of days since March 1st year 0.
# The weeks are numbered per year starting with 1. If the first
#  week of a year includes at least four days of that year it is week 1,
#  otherwise it gets the number of the last week of the previous year.
# Return value is (week #, Y)
#  Where Y is the year that contains the greater part of the week
def week(nd):
    dt = gdate(nd)
    y = dt.y + 1
    while True:
	fw = firstweek(y)
	if nd >= fw:
	    break
	y -= 1
    return ((nd - fw) / 7 + 1, y)

# Return the first day of week 1 of year y
def firstweek(y):
    # Internal representation of y-1-1
    idt = Date(y - 1, 10, 0)

    # If more than 3 days of this week are in the preceding year, the
    #  next week is week 1 (and the next monday is the answer),
    #  otherwise this week is week 1 and the last monday is the
    #  answer.
    nd = idt.ndaysgi()
    wd = weekday(nd)
    if wd > 3:
	return nd - wd + 7
    return nd - wd

# Cache daynumber of one Monday
nmonday = None

# Return the weekday (Mo = 0 .. Su = 6)
def weekday(nd):
    global nmonday

    # Cache the daynumber of one monday
    if nmonday == None:
	# Internal repr. of 1997-11-17
	dmondaygi = Date(1997, 8, 16)
	nmonday = dmondaygi.ndaysgi()

    # Return (nd - nmonday) modulo 7 which is the weekday
    nd = (nd - nmonday) % 7
    if nd < 0:
	return nd + 7
    return nd

# Return a Date() for today
def today():
    return Date(*time.localtime()[0:3])

# Parse M/D/Y into a Date
def parse(str):
    # TBD is use a stronger parser than time.strptime()
    tm = time.strptime(str, "%m/%d/%Y")
    return Date(*tm[0:3])
