"""transdate -- Python implementation of Asian lunisolar calendar Copyright (c) 2004-2006, Kang Seonghoon aka Tokigun. This module declares lunardate class which represents a day of Asian lunisolar calendar. lunardate class is compatible with datetime.date class, so you can use both lunardate and date interchangeably. lunardate class can handle date between 1881-01-30 (lunar 1881-01-01) and 2051-02-10 (lunar 2050-12-29). Since lunisolar calendar table is based on Korea Astronomy & Space Science Institute, it can be different with calendars used by other countries. In order to reduce size of bytecode, all of numeric table is stored as Unicode string (capable for range between 0 and 65535). If your Python is not compiled with Unicode, use transdate_nounicode.py instead. """ __author__ = 'Kang Seonghoon aka Tokigun' __version__ = '1.1.1 (2006-06-25)' __copyright__ = 'Copyright (c) 2004-2006 Kang Seonghoon aka Tokigun' __license__ = 'LGPL' __all__ = ['sol2lun', 'lun2sol', 'date', 'timedelta', 'solardate', 'lunardate', 'getganzistr', 'strftime'] from datetime import date, timedelta import locale, time ################################################################################### ## Lunisolar Calendar Table _BASEYEAR = 1881 _MINDATE = 686686 # 1881.1.30 (lunar 1881.1.1) _MAXDATE = 748788 # 2051.2.10 (lunar 2050.12.29) _DEFAULTLOCALE = locale.getdefaultlocale()[0].split('_')[0] try: import re; _STRFTIMEREGEXP = re.compile('(? (year, month, day, leap) Returns corresponding date in lunar calendar. leap will be ignored.""" days = date(year, month, day).toordinal() if not _MINDATE <= days <= _MAXDATE: raise ValueError, "year is out of range" days -= _MINDATE month = _bisect(_MONTHTABLE, days) year = _bisect(_YEARTABLE, month) month, day = month - ord(_YEARTABLE[year]) + 1, days - ord(_MONTHTABLE[month]) + 1 if (ord(_LEAPTABLE[year]) or 13) < month: month -= 1 leap = (ord(_LEAPTABLE[year]) == month) else: leap = False return (year + _BASEYEAR, month, day, leap) def lun2sol(year, month, day, leap=False): """lun2sol(year, month, day, leap=False) -> (year, month, day, leap) Returns corresponding date in solar calendar.""" year -= _BASEYEAR if not 0 <= year < len(_YEARTABLE): raise ValueError, "year is out of range" if not 1 <= month <= 12: raise ValueError, "wrong month" if leap and ord(_LEAPTABLE[year]) != month: raise ValueError, "wrong leap month" months = ord(_YEARTABLE[year]) + month - 1 if leap or (ord(_LEAPTABLE[year]) or 13) < month: months += 1 days = ord(_MONTHTABLE[months]) + day - 1 if day < 1 or days >= ord(_MONTHTABLE[months + 1]): raise ValueError, "wrong day" return date.fromordinal(days + _MINDATE).timetuple()[:3] + (False,) def getganzistr(index, locale=None): """getganzistr(index, locale=None) -> unicode string Returns corresponding unicode string of ganzi. locale can be "ko", "ja", "zh". Uses default locale when locale is ignored.""" locale = locale or _DEFAULTLOCALE return _GANZIMAP[locale][index%10] + _GANZIMAP[locale][10+index%12] def strftime(format, t=None): """strftime(format, t=None) -> string Returns formatted string of given timestamp. If timestamp is omitted, current timestamp (return value of time.localtime()) is used. Similar to time.strftime, but has the following extensions: %LC - (year / 100) as a decimal number (at least 2 digits) %Ld - lunar day of the month as a decimal number [01,30] %Le - same as %Ld, but preceding blank instead of zero %LF - same as "%LY-%Lm-%Ld" %Lj - day of the lunar year as a decimal number [001,390] %Ll - 0 for non-leap month, 1 for leap month %Lm - lunar month as a decimal number [01,12] %Ly - lunar year without century as a decimal number [00,99] %LY - lunar year with century as a decimal number """ if t is None: t = time.localtime() if _STRFTIMEREGEXP is not None: lt = sol2lun(*t[:3]) lord = date(t[0], t[1], t[2]).toordinal() - _MINDATE ldoy = lord - ord(_MONTHTABLE[ord(_YEARTABLE[lt[0] - _BASEYEAR])]) + 1 lmap = {'Y': '%04d' % lt[0], 'm': '%02d' % lt[1], 'd': '%02d' % lt[2], 'y': '%02d' % (lt[0] % 100), 'C': '%02d' % (lt[0] // 100), 'F': '%04d-%02d-%02d' % lt[:3], 'e': str(lt[2]), 'l': '%d' % lt[3], 'j': '%03d' % ldoy} format = _STRFTIMEREGEXP.sub(lambda m: '%' * (len(m.group(1)) / 2) + lmap.get(m.group(2), ''), format) return time.strftime(format, t) ################################################################################### ## Class Declaration # just alias. we have lunardate, so why not we have solardate? solardate = date class lunardate(date): """lunardate(year, month, day, leap=False) -> new lunardate object""" def __new__(cls, year, month, day, leap=False): obj = date.__new__(cls, *lun2sol(year, month, day, leap)[:3]) object.__setattr__(obj, 'lunaryear', year) object.__setattr__(obj, 'lunarmonth', month) object.__setattr__(obj, 'lunarday', day) object.__setattr__(obj, 'lunarleap', leap) return obj def __repr__(self): return '%s.%s(%d, %d, %d, %s)' % \ (self.__class__.__module__, self.__class__.__name__, self.lunaryear, self.lunarmonth, self.lunarday, self.lunarleap) min = type('propertyproxy', (object,), { '__doc__': 'lunardate.min -> The earliest representable date', '__get__': lambda self, inst, cls: cls.fromordinal(_MINDATE)})() max = type('propertyproxy', (object,), { '__doc__': 'lunardate.max -> The latest representable date', '__get__': lambda self, inst, cls: cls.fromordinal(_MAXDATE)})() def __setattr__(self, name, value): raise AttributeError, "can't set attribute." def __add__(self, other): return self.fromsolardate(date.__add__(self, other)) def __radd__(self, other): return self.fromsolardate(date.__radd__(self, other)) def __sub__(self, other): result = date.__sub__(self, other) if not isinstance(result, timedelta): result = self.fromsolardate(result) return result def replace(self, year=None, month=None, day=None, leap=None): """lunardate.replace(year, month, day, leap) -> new lunardate object Same as date.replace, but returns lunardate object instead of date object.""" if leap is None: leap = self.lunarleap return self.__class__(year or self.lunaryear, month or self.lunarmonth, day or self.month, leap) def tosolardate(self): """lunardate.tosolardate() -> date object Returns corresponding date object.""" return date(self.year, self.month, self.day) def today(self): """lunardate.today() -> new lunardate object Returns lunardate object which represents today.""" return self.fromsolardate(date.today()) def fromsolardate(self, solardate): """lunardate.fromsolardate(solardate) -> new lunardate object Returns corresponding lunardate object from date object.""" return self(*sol2lun(*solardate.timetuple()[:3])) def fromtimestamp(self, timestamp): """lunardate.fromtimestamp(timestamp) -> new lunardate object Returns corresponding lunardate object from UNIX timestamp.""" return self.fromsolardate(date.fromtimestamp(timestamp)) def fromordinal(self, ordinal): """lunardate.fromordinal(ordinal) -> new lunardate object Returns corresponding lunardate object from Gregorian ordinal.""" return self.fromsolardate(date.fromordinal(ordinal)) def getganzi(self): """lunardate.getganzi() -> (year_ganzi, month_ganzi, day_ganzi) Returns ganzi index between 0..59 from lunardate object. Ganzi index can be converted using getganzistr function.""" return ((self.lunaryear + 56) % 60, (self.lunaryear * 12 + self.lunarmonth + 13) % 60, (self.toordinal() + 14) % 60) def getganzistr(self, locale=None): """lunardate.getganzistr(locale=None) -> 3-tuple of unicode string Returns unicode string of ganzi from lunardate object. See getganzistr global function for detail.""" return tuple([getganzistr(i, locale) for i in self.getganzi()]) def strftime(self, format): """lunardate.strftime(format) -> string Returns formatted string of lunardate object. See strftime global function for detail.""" return strftime(format, self.timetuple()) today = classmethod(today) fromsolardate = classmethod(fromsolardate) fromtimestamp = classmethod(fromtimestamp) fromordinal = classmethod(fromordinal) # we create new lunardate class from old lunardate class using typeproxy, # because default type class always allows setting class variable. # __slots__ is added later to forbid descriptor initialization by type. class typeproxy(type): def __setattr__(self, name, value): raise AttributeError, "can't set attribute." clsdict = dict(lunardate.__dict__) clsdict['__slots__'] = ['lunaryear', 'lunarmonth', 'lunarday', 'lunarleap'] lunardate = typeproxy(lunardate.__name__, lunardate.__bases__, clsdict) del typeproxy ################################################################################### ## Command Line Interface if __name__ == '__main__': import sys try: mode = sys.argv[1].lower() if mode == 'today': if len(sys.argv) != 2: raise RuntimeError today = lunardate.today() isleap = today.lunarleap and ' (leap)' or '' print today.strftime('Today: solar %Y-%m-%d %a = lunar %LY-%Lm-%Ld' + isleap) elif mode == 'solar': if len(sys.argv) != 5: raise RuntimeError solar = lunardate.fromsolardate(date(*map(int, sys.argv[2:]))) isleap = solar.lunarleap and ' (leap)' or '' print solar.strftime('solar %Y-%m-%d %a -> lunar %LY-%Lm-%Ld' + isleap) elif mode == 'lunar': if len(sys.argv) not in (5, 6): raise RuntimeError leap = (len(sys.argv) == 6 and sys.argv[5].lower() == 'leap') solar = lunardate(*(map(int, sys.argv[2:5]) + [leap])) isleap = leap and ' (leap)' or '' print solar.strftime('lunar %LY-%Lm-%Ld' + isleap + ' -> solar %Y-%m-%d %a') else: raise RuntimeError except (IndexError, RuntimeError): app = sys.argv[0] print 'Usage:' print ' for today - python %s today' % app print ' for solar to lunar - python %s solar ' % app print ' for lunar to solar - python %s lunar [leap]' % app except: print 'Error: %s' % sys.exc_info()[1]