Source code for pyluach.dates

"""The dates module implements classes for representing and
manipulating several date types.

Classes
-------
* BaseDate
* CalendarDateMixin
* JulianDay
* GregorianDate
* HebrewDate

Note
----
All instances of the classes in this module should be treated as read
only. No attributes should be changed once they're created.
"""

from __future__ import division

from datetime import date
from numbers import Number

from pyluach.utils import memoize


[docs]class BaseDate(object): """BaseDate is a base class for all date types. It provides the following arithmetic and comparison operators common to all child date types. =================== ============================================= Operation Result =================== ============================================= d2 = date1 + int New date ``int`` days after date1 d2 = date1 - int New date ``int`` days before date1 int = date1 - date2 Integer equal to the absolute value of the difference between date1 and date2 date1 > date2 True if date1 occurs later than date2 date1 < date2 True if date1 occurs earlier than date2 date1 == date2 True if date1 occurs on the same day as date2 date1 != date2 True if ``date1 == date2`` is False date1 >=, <= date2 True if both are True =================== ============================================= Any child of BaseDate that implements a ``jd`` attribute representing the Julian Day of that date can be compared to and diffed with any other valid date type. """ def __hash__(self): return hash(self.jd) def __add__(self, other): try: return JulianDay(self.jd + other)._to_x(self) except AttributeError: raise TypeError('You can only add a number to a date.') def __sub__(self, other): if isinstance(other, Number): return JulianDay(self.jd - other)._to_x(self) try: return abs(self.jd - other.jd) except AttributeError: raise TypeError("""You can only subtract a number or another date that has a "jd" attribute from a date""") def __eq__(self, other): try: if self.jd == other.jd: return True return False except AttributeError: raise TypeError(self._error_string) def __ne__(self, other): try: if self.jd != other.jd: return True return False except AttributeError: raise TypeError(self._error_string) def __lt__(self, other): try: if self.jd < other.jd: return True return False except AttributeError: raise TypeError(self._error_string) def __gt__(self, other): try: if self.jd > other.jd: return True return False except AttributeError: raise TypeError(self._error_string) def __le__(self, other): try: if self.jd <= other.jd: return True return False except AttributeError: raise TypeError(self._error_string) def __ge__(self, other): try: if self.jd >= other.jd: return True return False except AttributeError: raise TypeError(self._error_string)
[docs] def shabbos(self): """Return the Shabbos on or following the date. Returns ------- Date Self if it's Shabbos or else the following Shabbos as the same date type as operated on. """ return self + (7 - self.weekday())
[docs]class CalendarDateMixin(object): """CalendarDateMixin is a mixin for Hebrew and Gregorian dates. Parameters ---------- Year : int Month : int day : int Attributes ---------- year : int month : int day : int jd : float The equivelant Julian day at midnight. """ def __init__(self, year, month, day, jd=None): """Initialize a calendar date.""" self.year = year self.month = month self.day = day self._jd = jd self. _error_string = ('''Only a date with a "jd" attribute can be compared to a {0}'''.format( self.__class__.__name__) ) def __repr__(self): return '{0}({1}, {2}, {3})'.format(self.__class__.__name__, self.year, self.month, self.day) def __str__(self): return '{0:04d}-{1:02d}-{2:02d}'.format(self.year, self.month, self.day) def __iter__(self): yield self.year yield self.month yield self.day
[docs] def weekday(self): """Return day of week as an integer. Returns ------- int An integer representing the day of the week with Sunday as 1 through Saturday as 7. """ return int(self.jd+.5+1) % 7 + 1
[docs] def tuple(self): """Return date as tuple. Returns ------- tuple of ints A tuple of ints in the form ``(year, month, day)``. """ return (self.year, self.month, self.day)
[docs] def dict(self): """Return the date as a dictionary. Returns ------- Dict A dictionary in the form ``{'year': int, 'month': int, 'day': int}``. """ return {'year': self.year, 'month': self.month, 'day': self.day}
[docs]class JulianDay(BaseDate): """A JulianDay object represents a Julian Day at midnight. Parameters ---------- day : float or int The julian day. Note that Julian days start at noon so day number 10 is represented as 9.5 which is day 10 at midnight. Attributes ---------- day : float The Julian Day Number at midnight (as *n*.5) jd : float Alias for day. """ def __init__(self, day): """Initialize a JulianDay instance.""" if day-int(day) < .5: self.day = int(day) - .5 else: self.day = int(day) + .5 self.jd = self.day self._error_string = """Only a date with a "jd" attribute can be compared to a Julian Day instance.""" def __repr__(self): return 'JulianDay({0})'.format(self.day) def __str__(self): return str(self.day)
[docs] def weekday(self): """Return weekday of date. Returns ------- int The weekday with Sunday as 1 through Saturday as 7. """ return (int(self.day+.5) + 1) % 7 + 1
[docs] @staticmethod def from_pydate(pydate): """Return a `JulianDay` from a python date object. Parameters ---------- pydate : datetime.date A python standard library ``datetime.date`` instance Returns ------- JulianDay """ return GregorianDate.from_pydate(pydate).to_jd()
[docs] @staticmethod def today(): """Return instance of current Julian day from timestamp. Extends the built-in ``datetime.date.today()``. Returns ------- JulianDay A JulianDay instance representing the current Julian day from the timestamp. """ return GregorianDate.today().to_jd()
[docs] def to_greg(self): """Convert JulianDay to a Gregorian Date. Returns ------- GregorianDate The equivalent Gregorian date instance. Notes ----- This method uses the Fliegel-Van Flandern algorithm. """ jd = int(self.day + .5) L = jd + 68569 n = 4*L // 146097 L = L - (146097*n + 3) // 4 i = (4000 * (L+1)) // 1461001 L = L - ((1461*i) // 4) + 31 j = (80*L) // 2447 day = L - 2447*j // 80 L = j // 11 month = j + 2 - 12*L year = 100 * (n-49) + i + L if year < 1: year -= 1 return GregorianDate(year, month, day, self.day)
[docs] def to_heb(self): """ Convert to a Hebrew date. Returns ------- HebrewDate The equivalent Hebrew date instance. """ if self.day <= 347997: raise ValueError('Date is before creation') jd = int(self.day + .5) # Try to account for half day jd -= 347997 year = int(jd//365) + 2 ## try that to debug early years first_day = HebrewDate._elapsed_days(year) while first_day > jd: year -= 1 first_day = HebrewDate._elapsed_days(year) months = [7, 8, 9, 10, 11, 12, 13, 1, 2, 3, 4, 5, 6] if not HebrewDate._is_leap(year): months.remove(13) days_remaining = jd - first_day for month in months: if days_remaining >= HebrewDate._month_length(year, month): days_remaining -= HebrewDate._month_length(year, month) else: return HebrewDate(year, month, days_remaining + 1, self.day)
def _to_x(self, type_): """Return a date object of the given type.""" if isinstance(type_, GregorianDate): return self.to_greg() elif isinstance(type_, HebrewDate): return self.to_heb() elif isinstance(type_, JulianDay): return self
[docs] def to_pydate(self): """Convert to a datetime.date object. Returns ------- datetime.date A standard library ``datetime.date`` instance. """ return self.to_greg().to_pydate()
[docs]class GregorianDate(BaseDate, CalendarDateMixin): """A GregorianDate object represents a Gregorian date (year, month, day). This is an idealized date with the current Gregorian calendar infinitely extended in both directions. Parameters ---------- year : int month : int day : int jd : float, optional This parameter should not be assigned manually. Attributes ---------- year : int month : int day : int jd : float(property) The corresponding Julian Day Number at midnight (as *n*.5). Warnings -------- Although B.C.E. dates are allowed, they should be treated as approximations as they may return inconsistent results when converting between date types and using arithmetic and comparison operators! """ def __init__(self, year, month, day, jd=None): """Initialize a GregorianDate. This initializer extends the CalendarDateMixin initializer adding in date validation specific to Gregorian dates. """ if month < 1 or month > 12: raise ValueError('{0} is an invalid month.'.format(str(month))) monthlength = self._monthlength(year, month) if day < 1 or day > monthlength: raise ValueError('Given month has {0} days.'.format(monthlength)) super(GregorianDate, self).__init__(year, month, day, jd) @property def jd(self): """Return the corresponding Julian day number. This property retrieves the corresponding Julian Day as a float if it was passed into the init method or already calculated, and if it wasn't, it calculates it and saves it for later retrievals and returns it. Returns ------- float The Julian day number at midnight. """ if self._jd is None: year = self.year month = self.month day = self.day if year < 0: year += 1 if month < 3: year -= 1 month += 12 month += 1 a = year // 100 b = 2 - a + a//4 self._jd = (int(365.25*year) + int(30.6001*month) + b + day + 1720994.5) return self._jd
[docs] @staticmethod def from_pydate(pydate): """Return a `GregorianDate` instance from a python date object. Parameters ---------- pydate : datetime.date A python standard library ``datetime.date`` instance. Returns ------- GregorianDate """ return GregorianDate(*pydate.timetuple()[:3])
[docs] @staticmethod def today(): """Return a GregorianDate instance for the current day. This static method wraps the Python standard library's date.today() method to get the date from the timestamp. Returns ------- GregorianDate The current Gregorian date from the computer's timestamp. """ return GregorianDate.from_pydate(date.today())
@staticmethod def _is_leap(year): """Return True if year of date is a leap year, otherwise False.""" if year < 0: year += 1 if( (year % 4 == 0) and not (year % 100 == 0 and year % 400 != 0) ): return True return False
[docs] def is_leap(self): """Return if the date is in a leap year Returns ------- bool True if the date is in a leap year, False otherwise. """ return self._is_leap(self.year)
@classmethod def _monthlength(cls, year, month): if month in [1, 3, 5, 7, 8, 10, 12]: return 31 elif month != 2: return 30 else: return 29 if cls._is_leap(year) else 28
[docs] def to_jd(self): """Convert to a Julian day. Returns ------- JulianDay The equivalent JulianDay instance. """ return JulianDay(self.jd)
[docs] def to_heb(self): """Convert to Hebrew date. Returns ------- HebrewDate The equivalent HebrewDate instance. """ return self.to_jd().to_heb()
[docs] def to_pydate(self): """Convert to a standard library date. Returns ------- datetime.date The equivalent datetime.date instance. """ return date(*self.tuple())
[docs]class HebrewDate(BaseDate, CalendarDateMixin): """A class for manipulating Hebrew dates. Parameters ---------- year : int The Hebrew year. If the year is less than 1 it will raise a ValueError. month : int The Hebrew month starting with Nissan as 1 (and Tishrei as 7). If there is a second Adar in the year it is represented as 13. A month below 1 or above the last month will raise a ValueError. day : int The Hebrew day of the month. An invalid day will raise a ValueError. jd : float, optional This parameter should not be assigned manually. Attributes ---------- year : int month : int The Hebrew month starting with Nissan as 1 (and Tishrei as 7). If there is a second Adar it is represented as 13. day : int The day of the month. """ def __init__(self, year, month, day, jd=None): """Initialize a HebrewDate instance. This initializer extends the CalendarDateMixin adding validation specific to hebrew dates. """ if year < 1: raise ValueError('Date supplied is before creation.') if month < 1 or month > 13: raise ValueError('{0} is an invalid month.'.format(str(month))) if (not self._is_leap(year)) and month == 13: raise ValueError('{0} is not a leap year'.format(year)) monthlength = self._month_length(year, month) if day < 1 or day > monthlength: raise ValueError('Given month has {0} days.'.format(monthlength)) super(HebrewDate, self).__init__(year, month, day, jd) @property def jd(self): """Return the corresponding Julian day number. This property retrieves the corresponding Julian Day as a float if it was passed into the init method or already calculated, and if it wasn't, it calculates it, saves it for later retrievals, and returns it. Returns ------- float The Julian day number at midnight. """ if self._jd is None: months = [7, 8, 9, 10, 11, 12, 13, 1, 2, 3, 4, 5, 6] if not HebrewDate._is_leap(self.year): months.remove(13) jd = HebrewDate._elapsed_days(self.year) for m in months: if m != self.month: jd += HebrewDate._month_length(self.year, m) else: self._jd = jd + (self.day-1) + 347996.5 return self._jd
[docs] @staticmethod def from_pydate(pydate): """Return a `HebrewDate` from a python date object. Parameters ---------- pydate : datetime.date A python standard library ``datetime.date`` instance Returns ------- HebrewDate """ return GregorianDate.from_pydate(pydate).to_heb()
[docs] @staticmethod def today(): """Return HebrewDate instance for the current day. This static method wraps the Python standard library's ``date.today()`` method to get the date from the timestamp. Returns ------- HebrewDate The current Hebrew date from the computer's timestamp. Note ---- This method coverts the Gregorian date from the time stamp to a Hebrew date, so if it is after nightfall but before midnight you will have to add one day, ie. ``today = HebrewDate.today() + 1``. """ return GregorianDate.today().to_heb()
[docs] def to_jd(self): """Convert to a Julian day. Returns ------- JulianDay The equivalent JulianDay instance. """ return JulianDay(self.jd)
[docs] def to_greg(self): """Convert to a Gregorian date. Returns ------- GregorianDate The equivalent GregorianDate instance. """ return self.to_jd().to_greg()
[docs] def to_pydate(self): """Convert to a standard library date. Returns ------- datetime.date The equivalent datetime.date instance. """ return self.to_greg().to_pydate()
def to_heb(self): return self @staticmethod def _is_leap(year): if (((7*year) + 1) % 19) < 7: return True return False @classmethod @memoize(maxlen=100) def _elapsed_days(cls, year): months_elapsed = ( (235 * ((year-1) // 19)) + (12 * ((year-1) % 19)) + (7 * ((year-1) % 19) + 1) // 19 ) parts_elapsed = 204 + 793*(months_elapsed%1080) hours_elapsed = (5 + 12*months_elapsed + 793*(months_elapsed//1080) + parts_elapsed//1080) conjunction_day = 1 + 29*months_elapsed + hours_elapsed//24 conjunction_parts = 1080 * (hours_elapsed%24) + parts_elapsed%1080 if ( (conjunction_parts >= 19440) or ( (conjunction_day % 7 == 2) and (conjunction_parts >= 9924) and (not cls._is_leap(year)) ) or ( (conjunction_day % 7 == 1) and conjunction_parts >= 16789 and cls._is_leap(year - 1))): # if all that alt_day = conjunction_day + 1 else: alt_day = conjunction_day if (alt_day % 7) in (0, 3, 5): alt_day += 1 return alt_day @classmethod def _days_in_year(cls, year): return cls._elapsed_days(year + 1) - cls._elapsed_days(year) @classmethod def _long_cheshvan(cls, year): """Returns True if Cheshvan has 30 days""" return cls._days_in_year(year) % 10 == 5 @classmethod def _short_kislev(cls, year): """Returns True if Kislev has 29 days""" return cls._days_in_year(year) % 10 == 3 @classmethod def _month_length(cls, year, month): """Months start with Nissan (Nissan is 1 and Tishrei is 7)""" if month in [1, 3, 5, 7, 11]: return 30 elif month in [2, 4, 6, 10, 13]: return 29 elif month == 12: return 30 if cls._is_leap(year) else 29 elif month == 8: # if long Cheshvan return 30, else return 29 return 30 if cls._long_cheshvan(year) else 29 elif month == 9: # if short Kislev return 29, else return 30 return 29 if cls._short_kislev(year) else 30