fa45d8aa5f
- health_checklist.json: 192.168.1.122→node122
- ocr_client.py: docstring IP→node122
- docs/market-data-requirements.md: IP→node122
- 所有API调用通过ProxyHandler({})绕过系统代理
Privoxy对node122:18003返回500,直连正常
1209 lines
36 KiB
Python
1209 lines
36 KiB
Python
"""The dates module implements classes for representing and
|
||
manipulating several date types.
|
||
|
||
Contents
|
||
--------
|
||
* :class:`Rounding`
|
||
* :class:`BaseDate`
|
||
* :class:`CalendarDateMixin`
|
||
* :class:`JulianDay`
|
||
* :class:`GregorianDate`
|
||
* :class:`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.
|
||
"""
|
||
|
||
import abc
|
||
from datetime import date
|
||
from numbers import Number
|
||
from enum import Enum, auto
|
||
|
||
from pyluach import utils
|
||
from pyluach import gematria
|
||
|
||
|
||
class Rounding(Enum):
|
||
"""Enumerator to provide options for rounding Hebrew dates.
|
||
|
||
This provides constants to use as arguments for functions. It
|
||
should not be instantiated.
|
||
|
||
Attributes
|
||
----------
|
||
PREVIOUS_DAY
|
||
If the day is the 30th and the month only has 29 days, round to
|
||
the 29th of the month.
|
||
NEXT_DAY
|
||
If the day is the 30th and the month only has 29 days, round to
|
||
the 1st of the next month.
|
||
EXCEPTION
|
||
If the day is the 30th and the month only has 29 days, raise a
|
||
ValueError.
|
||
"""
|
||
PREVIOUS_DAY = auto()
|
||
NEXT_DAY = auto()
|
||
EXCEPTION = auto()
|
||
|
||
|
||
class BaseDate(abc.ABC):
|
||
"""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 Positive integer equal to the duration from date1
|
||
to 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
|
||
=================== =================================================
|
||
|
||
Any subclass of ``BaseDate`` can be compared to and diffed with any other
|
||
subclass date.
|
||
"""
|
||
|
||
@property
|
||
@abc.abstractmethod
|
||
def jd(self):
|
||
"""Return julian day number.
|
||
|
||
Returns
|
||
-------
|
||
float
|
||
The Julian day number at midnight (as ``n.5``).
|
||
"""
|
||
|
||
@abc.abstractmethod
|
||
def to_heb(self):
|
||
"""Return Hebrew Date.
|
||
|
||
Returns
|
||
-------
|
||
HebrewDate
|
||
"""
|
||
|
||
def __hash__(self):
|
||
return hash(repr(self))
|
||
|
||
def __add__(self, other):
|
||
try:
|
||
return JulianDay(self.jd + other)._to_x(self)
|
||
except TypeError:
|
||
return NotImplemented
|
||
|
||
def __sub__(self, other):
|
||
try:
|
||
if isinstance(other, Number):
|
||
return JulianDay(self.jd - other)._to_x(self)
|
||
return int(abs(self.jd - other.jd))
|
||
except (AttributeError, TypeError):
|
||
return NotImplemented
|
||
|
||
def __eq__(self, other):
|
||
try:
|
||
return self.jd == other.jd
|
||
except AttributeError:
|
||
return NotImplemented
|
||
|
||
def __ne__(self, other):
|
||
try:
|
||
return self.jd != other.jd
|
||
except AttributeError:
|
||
return NotImplemented
|
||
|
||
def __lt__(self, other):
|
||
try:
|
||
return self.jd < other.jd
|
||
except AttributeError:
|
||
return NotImplemented
|
||
|
||
def __gt__(self, other):
|
||
try:
|
||
return self.jd > other.jd
|
||
except AttributeError:
|
||
return NotImplemented
|
||
|
||
def __le__(self, other):
|
||
try:
|
||
return self.jd <= other.jd
|
||
except AttributeError:
|
||
return NotImplemented
|
||
|
||
def __ge__(self, other):
|
||
try:
|
||
return self.jd >= other.jd
|
||
except AttributeError:
|
||
return NotImplemented
|
||
|
||
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
|
||
|
||
def isoweekday(self):
|
||
"""Return the day of the week corresponding to the iso standard.
|
||
|
||
Returns
|
||
-------
|
||
int
|
||
An integer representing the day of the week where Monday
|
||
is 1 and and Sunday is 7.
|
||
"""
|
||
weekday = self.weekday()
|
||
if weekday == 1:
|
||
return 7
|
||
return weekday - 1
|
||
|
||
def shabbos(self):
|
||
"""Return the Shabbos on or following the date.
|
||
|
||
Returns
|
||
-------
|
||
JulianDay, GregorianDate, or HebrewDate
|
||
`self` if the date is Shabbos or else the following Shabbos as
|
||
the same date type as called from.
|
||
|
||
Examples
|
||
--------
|
||
>>> heb_date = HebrewDate(5781, 3, 29)
|
||
>>> greg_date = heb_date.to_greg()
|
||
>>> heb_date.shabbos()
|
||
HebrewDate(5781, 4, 2)
|
||
>>> greg_date.shabbos()
|
||
GregorianDate(2021, 6, 12)
|
||
"""
|
||
return self + (7 - self.weekday())
|
||
|
||
def _day_of_holiday(self, israel, hebrew=False):
|
||
"""Return the day of the holiday.
|
||
|
||
Parameters
|
||
----------
|
||
israel : bool, optional
|
||
hebrew : bool, optional
|
||
|
||
Returns
|
||
-------
|
||
str
|
||
"""
|
||
name = utils._festival_string(self, israel)
|
||
if name is not None:
|
||
holiday = utils._Days(name)
|
||
if holiday is utils._Days.SHAVUOS and israel:
|
||
return ''
|
||
first_day = utils._first_day_of_holiday(holiday)
|
||
if first_day:
|
||
year = self.to_heb().year
|
||
day = HebrewDate(year, *first_day) - self + 1
|
||
if hebrew:
|
||
day = gematria._num_to_str(day)
|
||
return str(day)
|
||
return ''
|
||
|
||
def fast_day(self, hebrew=False):
|
||
"""Return name of fast day of date.
|
||
|
||
Parameters
|
||
----------
|
||
hebrew : bool, optional
|
||
``True`` if you want the fast day name in Hebrew letters. Default
|
||
is ``False``, which returns the name transliterated into English.
|
||
|
||
Returns
|
||
-------
|
||
str or None
|
||
The name of the fast day or ``None`` if the date is not
|
||
a fast day.
|
||
"""
|
||
return utils._fast_day_string(self, hebrew)
|
||
|
||
def festival(
|
||
self,
|
||
israel=False,
|
||
hebrew=False,
|
||
include_working_days=True,
|
||
prefix_day=False
|
||
):
|
||
"""Return name of Jewish festival of date.
|
||
|
||
This method will return all major and minor religous
|
||
Jewish holidays not including fast days.
|
||
|
||
Parameters
|
||
----------
|
||
israel : bool, optional
|
||
``True`` if you want the holidays according to the Israel
|
||
schedule. Defaults to ``False``.
|
||
hebrew : bool, optional
|
||
``True`` if you want the festival name in Hebrew letters. Default
|
||
is ``False``, which returns the name transliterated into English.
|
||
include_working_days : bool, optional
|
||
``True`` to include festival days on which melacha (work) is
|
||
allowed; ie. Pesach Sheni, Chol Hamoed, etc.
|
||
Default is ``True``.
|
||
prefix_day : bool, optional
|
||
``True`` to prefix multi day festivals with the day of the
|
||
festival. Default is ``False``.
|
||
|
||
Returns
|
||
-------
|
||
str or None
|
||
The name of the festival or ``None`` if the given date is not
|
||
a Jewish festival.
|
||
|
||
Examples
|
||
--------
|
||
>>> pesach = HebrewDate(2023, 1, 15)
|
||
>>> pesach.festival(prefix_day=True)
|
||
'1 Pesach'
|
||
>>> pesach.festival(hebrew=True, prefix_day=True)
|
||
'א׳ פסח'
|
||
>>> shavuos = HebrewDate(5783, 3, 6)
|
||
>>> shavuos.festival(israel=True, prefix_day=True)
|
||
'Shavuos'
|
||
"""
|
||
name = utils._festival_string(
|
||
self, israel, hebrew, include_working_days
|
||
)
|
||
if prefix_day and name is not None:
|
||
day = self._day_of_holiday(israel=israel, hebrew=hebrew)
|
||
if day:
|
||
return f'{day} {name}'
|
||
return name
|
||
|
||
def holiday(self, israel=False, hebrew=False, prefix_day=False):
|
||
"""Return name of Jewish holiday of the date.
|
||
|
||
The holidays include the major and minor religious Jewish
|
||
holidays including fast days.
|
||
|
||
Parameters
|
||
----------
|
||
israel : bool, optional
|
||
``True`` if you want the holidays according to the Israel
|
||
schedule. Defaults to ``False``.
|
||
hebrew : bool, optional
|
||
``True`` if you want the holiday name in Hebrew letters. Default is
|
||
``False``, which returns the name transliterated into English.
|
||
prefix_day : bool, optional
|
||
``True`` to prefix multi day holidays with the day of the
|
||
holiday. Default is ``False``.
|
||
|
||
Returns
|
||
-------
|
||
str or None
|
||
The name of the holiday or ``None`` if the given date is not
|
||
a Jewish holiday.
|
||
|
||
Examples
|
||
--------
|
||
>>> pesach = HebrewDate(2023, 1, 15)
|
||
>>> pesach.holiday(prefix_day=True)
|
||
'1 Pesach'
|
||
>>> pesach.holiday(hebrew=True, prefix_day=True)
|
||
'א׳ פסח'
|
||
>>> taanis_esther = HebrewDate(5783, 12, 13)
|
||
>>> taanis_esther.holiday(prefix_day=True)
|
||
'Taanis Esther'
|
||
"""
|
||
return (
|
||
self.fast_day(hebrew=hebrew)
|
||
or self.festival(israel, hebrew, prefix_day=prefix_day)
|
||
)
|
||
|
||
|
||
class CalendarDateMixin:
|
||
"""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 equivalent Julian day at midnight.
|
||
"""
|
||
|
||
def __init__(self, year, month, day, jd=None):
|
||
self.year = year
|
||
self.month = month
|
||
self.day = day
|
||
self._jd = jd
|
||
|
||
def __repr__(self):
|
||
class_name = self.__class__.__name__
|
||
return f'{class_name}({self.year}, {self.month}, {self.day})'
|
||
|
||
def __str__(self):
|
||
return f'{self.year:04d}-{self.month:02d}-{self.day:02d}'
|
||
|
||
def __iter__(self):
|
||
yield self.year
|
||
yield self.month
|
||
yield self.day
|
||
|
||
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)
|
||
|
||
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}
|
||
|
||
def replace(self, year=None, month=None, day=None):
|
||
"""Return new date with new values for the specified field.
|
||
|
||
Parameters
|
||
----------
|
||
year : int, optional
|
||
month: int, optional
|
||
day : int, optional
|
||
|
||
Returns
|
||
-------
|
||
CalendarDateMixin
|
||
Any date that inherits from CalendarDateMixin
|
||
(``GregorianDate``, ````HebrewDate``).
|
||
|
||
Raises
|
||
ValueError
|
||
Raises a ``ValueError`` if the new date does not exist.
|
||
"""
|
||
if year is None:
|
||
year = self.year
|
||
if month is None:
|
||
month = self.month
|
||
if day is None:
|
||
day = self.day
|
||
return type(self)(year, month, day)
|
||
|
||
|
||
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)
|
||
"""
|
||
|
||
def __init__(self, day):
|
||
if day-int(day) < .5:
|
||
self.day = int(day) - .5
|
||
else:
|
||
self.day = int(day) + .5
|
||
|
||
def __repr__(self):
|
||
return f'JulianDay({self.day})'
|
||
|
||
def __str__(self):
|
||
return str(self.day)
|
||
|
||
@property
|
||
def jd(self):
|
||
"""Return julian day.
|
||
|
||
Returns
|
||
-------
|
||
float
|
||
"""
|
||
return self.day
|
||
|
||
@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()
|
||
|
||
@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.
|
||
|
||
Warning
|
||
-------
|
||
Julian Days change at noon, but pyluach treats them as if they
|
||
change at midnight, so at midnight this method will return
|
||
``JulianDay(n.5)`` until the following midnight when it will
|
||
return ``JulianDay(n.5 + 1)``.
|
||
"""
|
||
return GregorianDate.today().to_jd()
|
||
|
||
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)
|
||
|
||
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 = utils._elapsed_days(year)
|
||
|
||
while first_day > jd:
|
||
year -= 1
|
||
first_day = utils._elapsed_days(year)
|
||
|
||
months = utils._monthslist(year)
|
||
days_remaining = jd - first_day
|
||
for month in months:
|
||
if days_remaining >= utils._month_length(year, month):
|
||
days_remaining -= utils._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()
|
||
if isinstance(type_, HebrewDate):
|
||
return self.to_heb()
|
||
if isinstance(type_, JulianDay):
|
||
return self
|
||
raise TypeError(
|
||
'This method has not been implemented with that type.'
|
||
)
|
||
|
||
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()
|
||
|
||
|
||
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
|
||
|
||
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(f'{str(month)} is an invalid month.')
|
||
monthlength = self._monthlength(year, month)
|
||
if day < 1 or day > monthlength:
|
||
raise ValueError(f'Given month has {monthlength} days.')
|
||
super().__init__(year, month, day, jd)
|
||
|
||
def __format__(self, fmt):
|
||
return self.strftime(fmt)
|
||
|
||
def strftime(self, fmt):
|
||
"""Return formatted date.
|
||
|
||
Wraps :py:meth:`datetime.date.strftime` method and uses the same
|
||
format options.
|
||
|
||
Parameters
|
||
----------
|
||
fmt : str
|
||
The format string.
|
||
|
||
Returns
|
||
-------
|
||
str
|
||
"""
|
||
return self.to_pydate().strftime(fmt)
|
||
|
||
@property
|
||
def jd(self):
|
||
"""Return the corresponding Julian day number.
|
||
|
||
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
|
||
|
||
@classmethod
|
||
def from_pydate(cls, pydate):
|
||
"""Return a `GregorianDate` instance from a python date object.
|
||
|
||
Parameters
|
||
----------
|
||
pydate : datetime.date
|
||
A python standard library ``datetime.date`` instance.
|
||
|
||
Returns
|
||
-------
|
||
GregorianDate
|
||
"""
|
||
return cls(*pydate.timetuple()[:3])
|
||
|
||
@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
|
||
|
||
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
|
||
if month == 2:
|
||
if cls._is_leap(year):
|
||
return 29
|
||
return 28
|
||
return 30
|
||
|
||
def to_jd(self):
|
||
"""Convert to a Julian day.
|
||
|
||
Returns
|
||
-------
|
||
JulianDay
|
||
The equivalent JulianDay instance.
|
||
"""
|
||
return JulianDay(self.jd)
|
||
|
||
def to_heb(self):
|
||
"""Convert to Hebrew date.
|
||
|
||
Returns
|
||
-------
|
||
HebrewDate
|
||
The equivalent HebrewDate instance.
|
||
"""
|
||
return self.to_jd().to_heb()
|
||
|
||
def to_pydate(self):
|
||
"""Convert to a standard library date.
|
||
|
||
Returns
|
||
-------
|
||
datetime.date
|
||
The equivalent datetime.date instance.
|
||
"""
|
||
return date(*self.tuple())
|
||
|
||
|
||
class HebrewDate(BaseDate, CalendarDateMixin):
|
||
"""A class for manipulating Hebrew dates.
|
||
|
||
The following format options are available similar to strftime:
|
||
|
||
====== ======= ===========================================================
|
||
Format Example Meaning
|
||
====== ======= ===========================================================
|
||
%a Sun Weekday as locale's abbreviated name
|
||
%A Sunday Weekday as locale's full name
|
||
%w 1 Weekday as decimal number 1-7 Sunday-Shabbos
|
||
%d 07 Day of the month as a 0-padded 2 digit decimal number
|
||
%-d 7 Day of the month as a decimal number
|
||
%B Iyar Month name transliterated into English
|
||
%m 02 Month as a 0-padded 2 digit decimal number
|
||
%-m 2 Month as a decimal number
|
||
%y 82, 01 Year without century as a zero-padded decimal number
|
||
%Y 5782 Year as a decimal number
|
||
%*a א׳ Weekday as a Hebrew numeral
|
||
%*A ראשון Weekday name in Hebrew
|
||
%*d ז׳, ט״ז Day of month as Hebrew numeral
|
||
%*-d א, טו Day of month without gershayim
|
||
%*B אייר Name of month in Hebrew
|
||
%*y תשפ״ב Year in Hebrew numerals without the thousands place
|
||
%*Y ה'תשפ״ב Year in Hebrew numerals with the thousands place
|
||
%% % A literal '%' character
|
||
====== ======= ===========================================================
|
||
|
||
Example
|
||
-------
|
||
>>> date = HebrewDate(5783, 1, 15)
|
||
>>> f'Today is {date:%a - %*-d %*B, %*y}'
|
||
'Today is Thu - טו אייר, תשפ"ג'
|
||
|
||
Parameters
|
||
----------
|
||
year : int
|
||
The Hebrew year.
|
||
|
||
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 has a value of 13.
|
||
|
||
day : int
|
||
The Hebrew day of the month.
|
||
|
||
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 has a value of 13.
|
||
day : int
|
||
The day of the month.
|
||
|
||
Raises
|
||
------
|
||
ValueError
|
||
If the year is less than 1, if the month is less than 1 or greater
|
||
than the last month, or if the day does not exist in the month a
|
||
``ValueError`` will be raised.
|
||
"""
|
||
|
||
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('Year must be >= 1.')
|
||
if month < 1 or month > 13:
|
||
raise ValueError(f'{month} is an invalid month.')
|
||
if (not utils._is_leap(year)) and month == 13:
|
||
raise ValueError(f'{year} is not a leap year')
|
||
monthlength = utils._month_length(year, month)
|
||
if day < 1 or day > monthlength:
|
||
raise ValueError(f'Given month has {monthlength} days.')
|
||
super().__init__(year, month, day, jd)
|
||
|
||
def __format__(self, fmt):
|
||
new = []
|
||
i = 0
|
||
while i < len(fmt):
|
||
if fmt[i] != '%':
|
||
new.append(fmt[i])
|
||
else:
|
||
i += 1
|
||
try:
|
||
curr = fmt[i]
|
||
except IndexError as e:
|
||
raise ValueError(
|
||
'Format string cannot end with single "%".'
|
||
) from e
|
||
if curr == '%':
|
||
new.append('%')
|
||
elif curr == '*':
|
||
i += 1
|
||
try:
|
||
curr = fmt[i]
|
||
except IndexError as e:
|
||
raise ValueError(
|
||
'Format string cannot end with "%*".'
|
||
) from e
|
||
if curr == '-':
|
||
i += 1
|
||
try:
|
||
curr = fmt[i]
|
||
except IndexError as e:
|
||
raise ValueError(
|
||
'Format string cannot end with "%*-"'
|
||
) from e
|
||
if curr == 'd':
|
||
new.append(self.hebrew_day(False))
|
||
else:
|
||
raise ValueError('Invalid format string.')
|
||
elif curr == 'a':
|
||
new.append(gematria._num_to_str(self.weekday()))
|
||
elif curr == 'A':
|
||
new.append(utils.WEEKDAYS[self.weekday()])
|
||
elif curr == 'd':
|
||
new.append(self.hebrew_day())
|
||
elif curr == 'B':
|
||
new.append(self.month_name(True))
|
||
elif curr.casefold() == 'y':
|
||
new.append(self.hebrew_year(curr == 'Y'))
|
||
else:
|
||
raise ValueError('Invalid format string.')
|
||
elif curr == '-':
|
||
i += 1
|
||
try:
|
||
curr = fmt[i]
|
||
except IndexError as e:
|
||
raise ValueError(
|
||
'Format string cannot end with "%-"'
|
||
) from e
|
||
if curr == 'd':
|
||
new.append(str(self.day))
|
||
elif curr == 'm':
|
||
new.append(str(self.month))
|
||
else:
|
||
raise ValueError('Invalid format string.')
|
||
else:
|
||
if curr.casefold() == 'a':
|
||
new.append(self.to_pydate().strftime(f'%{curr}'))
|
||
elif curr == 'w':
|
||
new.append(str(self.weekday()))
|
||
elif curr == 'd':
|
||
new.append(format(self.day, '02d'))
|
||
elif curr == 'B':
|
||
new.append(self.month_name(False))
|
||
elif curr == 'm':
|
||
new.append(format(self.month, '02d'))
|
||
elif curr.casefold() == 'y':
|
||
new.append(date(self.year, 1, 1).strftime(f'%{curr}'))
|
||
else:
|
||
raise ValueError('Invalid format string.')
|
||
i += 1
|
||
return ''.join(new)
|
||
|
||
@property
|
||
def jd(self):
|
||
"""Return the corresponding Julian day number.
|
||
|
||
Returns
|
||
-------
|
||
float
|
||
The Julian day number at midnight.
|
||
|
||
"""
|
||
if self._jd is None:
|
||
months = utils._monthslist(self.year)
|
||
jd = utils._elapsed_days(self.year)
|
||
for m in months:
|
||
if m != self.month:
|
||
jd += utils._month_length(self.year, m)
|
||
else:
|
||
self._jd = jd + (self.day-1) + 347996.5
|
||
|
||
return self._jd
|
||
|
||
@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()
|
||
|
||
@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.
|
||
|
||
Warning
|
||
-------
|
||
Pyluach treats Hebrew dates as if they change at midnight. If it's
|
||
after nightfall but before midnight, to get the true Hebrew date do
|
||
``HebrewDate.today() + 1``.
|
||
"""
|
||
return GregorianDate.today().to_heb()
|
||
|
||
def to_jd(self):
|
||
"""Convert to a Julian day.
|
||
|
||
Returns
|
||
-------
|
||
JulianDay
|
||
The equivalent JulianDay instance.
|
||
"""
|
||
return JulianDay(self.jd)
|
||
|
||
def to_greg(self):
|
||
"""Convert to a Gregorian date.
|
||
|
||
Returns
|
||
-------
|
||
GregorianDate
|
||
The equivalent GregorianDate instance.
|
||
"""
|
||
return self.to_jd().to_greg()
|
||
|
||
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
|
||
|
||
def month_name(self, hebrew=False):
|
||
"""Return the name of the month.
|
||
|
||
Parameters
|
||
----------
|
||
hebrew : bool, optional
|
||
``True`` if the month name should be in Hebrew characters.
|
||
Default is ``False`` which returns the month name
|
||
transliterated into English.
|
||
|
||
Returns
|
||
-------
|
||
str
|
||
"""
|
||
return utils._month_name(self.year, self.month, hebrew)
|
||
|
||
def hebrew_day(self, withgershayim=True):
|
||
"""Return the day of the month in Hebrew letters.
|
||
|
||
Parameters
|
||
----------
|
||
withgershayim : bool, optional
|
||
Default is ``True`` which includes a geresh with a single
|
||
character and gershayim between two characters.
|
||
|
||
Returns
|
||
-------
|
||
str
|
||
The day of the month in Hebrew letters.
|
||
|
||
Examples
|
||
--------
|
||
>>> date = HebrewDate(5782, 3, 6)
|
||
>>> date.hebrew_day()
|
||
'ו׳'
|
||
>>> date.hebrew_day(False)
|
||
'ו'
|
||
>>> HebrewDate(5783, 12, 14).hebrew_day()
|
||
'י״ד'
|
||
"""
|
||
return gematria._num_to_str(self.day, withgershayim=withgershayim)
|
||
|
||
def hebrew_year(self, thousands=False, withgershayim=True):
|
||
"""Return the year in Hebrew letters.
|
||
|
||
Parameters
|
||
----------
|
||
thousands : bool
|
||
``True`` to prefix the year with a letter for the
|
||
thousands place, ie. 'ה׳תשפ״א'. Default is ``False``.
|
||
withgershayim : bool, optional
|
||
Default is ``True`` which includes a geresh after the thousands
|
||
place if applicable and a gershayim before the last character
|
||
of the year.
|
||
|
||
Returns
|
||
-------
|
||
str
|
||
"""
|
||
return gematria._num_to_str(self.year, thousands, withgershayim)
|
||
|
||
def hebrew_date_string(self, thousands=False):
|
||
"""Return a Hebrew string representation of the date.
|
||
|
||
The date is in the form ``f'{day} {month} {year}'``.
|
||
|
||
Parameters
|
||
----------
|
||
thousands : bool
|
||
``True`` to have the thousands include in the year.
|
||
Default is ``False``.
|
||
|
||
Returns
|
||
-------
|
||
str
|
||
|
||
Examples
|
||
--------
|
||
>>> date = HebrewDate(5781, 9, 25)
|
||
>>> date.hebrew_date_string()
|
||
'כ״ה כסלו תשפ״א'
|
||
>>> date.hebrew_date_string(True)
|
||
'כ״ה כסלו ה׳תשפ״א'
|
||
"""
|
||
day = self.hebrew_day()
|
||
month = self.month_name(True)
|
||
year = self.hebrew_year(thousands)
|
||
return f'{day} {month} {year}'
|
||
|
||
def add(
|
||
self,
|
||
years=0,
|
||
months=0,
|
||
days=0,
|
||
adar1=False,
|
||
rounding=Rounding.NEXT_DAY
|
||
):
|
||
"""Add years, months, and days to date.
|
||
|
||
Parameters
|
||
----------
|
||
years : int, optional
|
||
The number of years to add. Default is 0.
|
||
months : int, optional
|
||
The number of months to add. Default is 0.
|
||
days : int, optional
|
||
The number of days to add. Default is 0.
|
||
adar1 : bool, optional
|
||
True to return a date in Adar Aleph if `self` is in a regular
|
||
Adar and after adding the years it's leap year. Default is
|
||
``False`` which will return the date in Adar Beis.
|
||
rounding : Rounding, optional
|
||
Choose what to do if self is the 30th day of the month, and
|
||
there are only 29 days in the destination month.
|
||
:obj:`Rounding.NEXT_DAY` to return the first day of the next
|
||
month. :obj:`Rounding.PREVIOUS_DAY` to return the last day of
|
||
the month. :obj:`Rounding.EXCEPTION` to raise a ValueError.
|
||
Default is :obj:`Rounding.NEXT_DAY`.
|
||
|
||
Returns
|
||
-------
|
||
HebrewDate
|
||
|
||
Note
|
||
----
|
||
This method first adds the `years`. If the starting month is
|
||
Adar and the destination year has two Adars, it chooses which
|
||
one based on the `adar1` argument, then it adds the `months`. If
|
||
the starting day doesn't exist in that month it adjusts it based
|
||
on the `rounding` argument, then it adds the `days`.
|
||
|
||
Examples
|
||
--------
|
||
>>> date = HebrewDate(5783, 11, 30)
|
||
>>> date.add(months=1)
|
||
HebrewDate(5783, 1, 1)
|
||
>>> date.add(months=1, rounding=Rounding.PREVIOUS_DAY)
|
||
HebrewDate(5783, 12, 29)
|
||
"""
|
||
year = self.year + years
|
||
month = self.month
|
||
if self.month == 13 and not utils._is_leap(year):
|
||
month = 12
|
||
elif (
|
||
self.month == 12
|
||
and not utils._is_leap(self.year)
|
||
and utils._is_leap(year)
|
||
and not adar1
|
||
):
|
||
month = 13
|
||
if months > 0:
|
||
year, month = utils._add_months(year, month, months)
|
||
elif months < 0:
|
||
year, month = utils._subtract_months(year, month, -months)
|
||
if utils._month_length(year, month) < self.day:
|
||
date = HebrewDate(year, month, 29)
|
||
if rounding is Rounding.EXCEPTION:
|
||
raise ValueError(f'{date:%B %Y} has only 29 days.')
|
||
if rounding is Rounding.NEXT_DAY:
|
||
date += 1
|
||
elif not isinstance(rounding, Rounding):
|
||
raise TypeError(
|
||
'The rounding argument can only be a member of the'
|
||
' dates.Rounding enum.'
|
||
)
|
||
else:
|
||
date = HebrewDate(year, month, self.day)
|
||
return date + days
|
||
|
||
def subtract(
|
||
self,
|
||
years=0,
|
||
months=0,
|
||
days=0,
|
||
adar1=False,
|
||
rounding=Rounding.NEXT_DAY
|
||
):
|
||
"""Subtract years, months, and days from date.
|
||
|
||
Parameters
|
||
----------
|
||
years : int, optional
|
||
The number of years to subtract. Default is 0.
|
||
months : int, optional
|
||
The number of months to subtract. Default is 0.
|
||
days : int, optional
|
||
The number of days to subtract. Default is 0.
|
||
adar1 : bool, optional
|
||
True to return a date in Adar Aleph if `self` is in a regular
|
||
Adar and the destination year is leap year. Default is
|
||
``False`` which will return the date in Adar Beis.
|
||
rounding : Rounding, optional
|
||
Choose what to do if self is the 30th day of the month, and
|
||
there are only 29 days in the destination month.
|
||
:obj:`Rounding.NEXT_DAY` to return the first day of the next
|
||
month. :obj:`Rounding.PREVIOUS_DAY` to return the last day of
|
||
the month. :obj:`Rounding.EXCEPTION` to raise a ValueError.
|
||
Default is :obj:`Rounding.NEXT_DAY`.
|
||
|
||
Returns
|
||
-------
|
||
HebrewDate
|
||
|
||
Note
|
||
----
|
||
This method first subtracts the `years`. If the starting month
|
||
is Adar and the destination year has two Adars, it chooses which
|
||
one based on the `adar1` argument, then it subtracts the
|
||
`months`. If the starting day doesn't exist in that month it
|
||
adjusts it based on the `rounding` argument, then it subtracts
|
||
the `days`.
|
||
"""
|
||
return self.add(-years, -months, -days, adar1, rounding)
|