# -*- coding: utf-8 -*-
#
# badidatetime/datetime.py
#
__docformat__ = "restructuredtext en"
__all__ = ('date', 'datetime', 'time', 'timezone', 'timedelta', 'tzinfo',
'TZWithCoords', 'MINYEAR', 'MAXYEAR', 'BADI_IANA', 'BADI_COORD',
'GMT_COORD', 'UTC', 'BADI', 'LOCAL_COORD', 'LOCAL', 'MONTHNAMES',
'MONTHNAMES_ABV', 'DAYNAMES', 'DAYNAMES_ABV')
import time as _time
import math as _math
from datetime import timedelta, tzinfo
from types import NoneType
from .badi_calendar import BahaiCalendar
from ._timedateutils import _td_utils
_MAXORDINAL = 1097267 # date.max.toordinal()
MINYEAR = BahaiCalendar.MINYEAR
MAXYEAR = BahaiCalendar.MAXYEAR
BADI_IANA = BahaiCalendar._BAHAI_LOCATION[3] # Asia/Tehran
BADI_COORD = BahaiCalendar._BAHAI_LOCATION[:3]
GMT_COORD = (51.477928, -0.001545, 0)
# LOCAL_COORD and LOCAL is lazily configured to the local coordinates
# if enables or the default is BADI_COORD, and BADI.
MONTHNAMES = [v for k, v in _td_utils.MONTHNAMES.items()]
MONTHNAMES_ABV = [v for k, v in _td_utils.MONTHNAMES_ABV.items()]
DAYNAMES = _td_utils.DAYNAMES
DAYNAMES_ABV = _td_utils.DAYNAMES_ABV
[docs]
def _cmp(x, y):
"""
Compare two items.
:param object x: Item one.
:param object y: Item two.
:returns: If 0 then x == y, else if 1 then x > y else if -1 then x < y.
"""
return 0 if x == y else 1 if x > y else -1
[docs]
def _divide_and_round(a: int, b: int) -> int:
"""
Divide a by b and round result to the nearest integer.
When the ratio is exactly half-way between two integers,
the even integer is returned.
:param int a: numerator
:param int b: denominator
:returns: Resultant value.
:rtype: int
"""
# Based on the reference implementation for divmod_near
# in Objects/longobject.c.
q, r = divmod(a, b)
# Round up if either r / b > 0.5, or r / b == 0.5 and q is odd.
# The expression r / b > 0.5 is equivalent to 2 * r > b if b is
# positive, 2 * r < b if b negative.
r *= 2
greater_than_half = r > b if b > 0 else r < b
return q + (1 if greater_than_half or r == b and q % 2 == 1 else 0)
[docs]
def _check_offset(name: str, offset: timedelta) -> None:
"""
Check that the arguments are valid. If offset is None, returns None else
offset is checked for being in range.
:param str name: Name is the offset-producing method, `utcoffset`,
`badioffset`, or `dst`.
:param timedelta offset: The timezone offset.
:raises assert: If the name is not in a list of constants.
:raises TypeError: If `offset` is not a `timedelta` instance.
:raises ValueError: If `offset` not greater than -1 and less than 1.
"""
assert name in ('utcoffset', 'badioffset', 'dst'), (
f"Invalid name argument '{name}' must be one of "
"('utcoffset', 'badioffset', 'dst').")
if offset is not None:
if offset is not None:
if not isinstance(offset, timedelta):
raise TypeError(f"tzinfo.{name}() must return None "
f"or timedelta, not {type(offset)}")
if not -timedelta(1) < offset < timedelta(1):
raise ValueError(
f"{name}()={offset}, must be strictly between "
"-timedelta(hours=24) and timedelta(hours=24), "
f"not {offset!r}")
[docs]
def _check_tzinfo_arg(tz: tzinfo) -> None:
"""
Check that the `tz` argument is either `None` or a `tzinfo` subclass.
:param tzinfo tz: A `tzinfo` instance.
:raises TypeError: If `tz is not `None` or a `tzinfo` subclass.
"""
if tz is not None and not isinstance(tz, tzinfo):
raise TypeError("tzinfo argument must be None or of a tzinfo "
f"subclass, not {type(tz).__name__!r}")
[docs]
def _cmperror(x, y) -> None:
"""
Test that `x` and `y` are the correct types to be compared.
:param x: Item one.
:param y: Item two.
:raises TypeError: Argument `a` and `b` cannot be compared.
"""
raise TypeError(f"Cannot compare {type(x).__name__!r} to "
f"{type(y).__name__!r}")
[docs]
def _check_tzname(name: str) -> None:
"""
Check that the `name` argument is either `None` or a `str`.
:param str name: The name of the timezone.
:raises TypeError: If `name` is not `None` or a `str`.
"""
if name is not None and not isinstance(name, str):
raise TypeError("tzinfo.tzname() must return None or string, "
f"not {type(name).__name__!r}")
[docs]
def _get_class_module(self) -> str:
"""
Gets the module name.
:returns: An updated datetime module name or the module of the class.
:rtype: str
"""
module_name = self.__class__.__module__
if module_name == 'badidatetime.datetime':
return 'badidatetime'
else:
return module_name
[docs]
def _module_name(module: str) -> str:
"""
Find the package name without the first directory.
:param str module: The module name including it path information.
:returns: The name of the module.
"""
idx = module.find('.')
dt_true = True if module[:idx] == 'datetime' else False
return f"badidatetime{module[idx:]}" if idx != -1 and dt_true else module
[docs]
class date(BahaiCalendar):
"""
Implements the date object for the Badí' datetime package.
"""
__slots__ = ('_kull_i_shay', '_vahid', '_year', '_month', '_day',
'_hashcode', '__date', '__short')
[docs]
def __new__(cls, a: int, b: int=None, c: int=None, d: int=None,
e: int=None):
"""
Instantiate the class.
:param date cls: The class object.
:param int a: Long form this value is the Kill-i-Shay and short form
it's the year. If **b** and **c** are None then **a**
becomes the pickle value that is parsed to the remaining
values below.
:param int b: Long form this value is the Váḥid and short form it's
the month.
:param int c: Long form this is the year and short form it's the day.
:param int d: Long for this value is the month and in the short form
it's not used.
:param int e: Long form this value is the day and in the short form
it's not used.
:returns: The instantiated class.
:rtype: date
"""
# Pickle support
if (short := date._is_pickle_data(a, b)) is not None:
self = object.__new__(cls)
self.__short = short
self.__setstate(a)
else:
b_date = tuple([x for x in (a, b, c, d, e) if x is not None])
date_len = len(b_date)
assert date_len in (3, 5), (
"A full short or long form Badí' date must be used, found "
f"{date_len} fields.")
self = object.__new__(cls)
if date_len == 5:
self._kull_i_shay = a
self._vahid = b
self._year = c
self._month = d
self._day = e
self.__date = b_date
self.__short = False
else:
self._year = a
self._month = b
self._day = c
self.__date = b_date
self.__short = True
super().__init__(self)
_td_utils._check_date_fields(*self.__date, short_in=self.__short)
self._hashcode = -1
return self
# Additional constructors
[docs]
@classmethod
def fromtimestamp(cls, t: float, *, short: bool=True) -> object:
"""
Construct a date from a POSIX timestamp--like time.time().
:param date cls: The class object.
:param float t: The POSIX timestamp.
:param bool short: If True (default) the short form date is returned
else False the long form date is returned.
:returns: The instantiated class.
:rtype: date
"""
bc = BahaiCalendar()
date = bc.badi_date_from_timestamp(t, *LOCAL_COORD, short=short,
trim=True)
date = date[:3] if short else date[:5] # We do not want time values.
return cls(*date)
[docs]
@classmethod
def today(cls, *, short: bool=True) -> object:
"""
Construct a date from time.time().
:param date cls: The class object.
:param bool short: If True (default) the short form date is returned
else False the long form date is returned.
:returns: The instantiated class.
:rtype: date
"""
return cls.fromtimestamp(_time.time(), short=short)
[docs]
@classmethod
def fromordinal(cls, n: int, *, short: bool=True) -> object:
"""
Construct a date from a proleptic Badí' ordinal.
Bahá 1 of year 1 is day 1. Only the year, month and day are
non-zero in the result.
:param date cls: The class object.
:param int n: The ordinal value.
:param bool short: If True (default) the short form date is returned
else False the long form date is returned.
:returns: The instantiated class.
:rtype: date
"""
date = _td_utils._ord2ymd(n, short=short)
return cls(*date)
[docs]
@classmethod
def fromisocalendar(cls, year: int, week: int, day: int, *,
short: bool=True) -> object:
"""
Construct a date from the ISO year, week number and weekday.
This is the inverse of the date.isocalendar() function.
:param int year: The Badí' year.
:param int week: The number of the week in the year.
:param int day: Badí' day in week.
:param bool short: If True (default) the short form date is returned
else False the long form date is returned.
:returns: The date instance.
:rtype: date
"""
date = _td_utils._isoweek_to_badi(year, week, day, short=short)
b_date = date[:3] if short else date[:5]
return cls(*b_date)
# Conversions to string
[docs]
def __repr__(self):
"""
Convert to formal a string, for repr().
:returns: A string representing the date.
:rtype: str
.. note::
>>> d = date(181, 1, 1)
>>> repr(d)
'datetime.date(181, 1, 1)'
"""
msg = (f"{_get_class_module(self)}."
f"{self.__class__.__qualname__}")
if hasattr(self, '_kull_i_shay'):
msg += (f"({self._kull_i_shay}, {self._vahid}, {self._year}, "
f"{self._month}, {self._day})")
else:
msg += f"({self._year}, {self._month}, {self._day})"
return msg
[docs]
def ctime(self) -> str:
"""
Return ctime() style string in the short form Badí' date.
:returns: A string representing the weekday, month name, and year.
:rtype: str
"""
date = self._short_from_long_form()
weekday = _td_utils._day_of_week(*date[:3])
wd_name = _td_utils.DAYNAMES[weekday]
year, month, day = date[:3]
m_name = _td_utils.MONTHNAMES[month]
y_shim = 4 if year > -1 else 5
return f"{wd_name} {m_name} {day:2d} 00:00:00 {year:0{y_shim}d}"
[docs]
def strftime(self, fmt: str) -> str:
"""
Returns a string representing the date from a format string.
:param str fmt:The format string.
:returns: A string representing the date.
:rtype: str
.. note::
Example: '%d/%m/%Y'
"""
return _td_utils._wrap_strftime(self, fmt, self.timetuple())
[docs]
def _str_convertion(self) -> str:
"""
Return a string representation of the date. In the case of a short
form date the returned Badí' date is in ISO format. There is no ISO
standard for the long form Badí' date.
:returns: A string representation of the short or long form Badí' date.
:rtype: str
"""
if self.is_short:
ret = self.isoformat()
else:
ind = 3 if self._kull_i_shay < 0 else 2
ret = (f"{self._kull_i_shay:0{ind}d}-{self._vahid:02d}-"
f"{self._year:02d}-{self._month:02d}-{self._day:02d}")
return ret
__str__ = _str_convertion
# Read-only field accessors
@property
def kull_i_shay(self) -> int:
"""
Get the Kull-i-Shay’.
:returns: The value associated with the Kull-i-Shay’.
:rtype: int
"""
return self._kull_i_shay
@property
def vahid(self) -> int:
"""
Get the Váḥid.
:returns: The value associated with the Váḥid.
:rtype: int
"""
return self._vahid
@property
def year(self) -> int:
"""
Get the year.
.. note::
This value has a different meaning depending on if the date
instance represents a long or short form date.
:returns: The value associated with the year.
:rtype: int
"""
return self._year
@property
def month(self) -> int:
"""
Get the month where 1 - 19 represents the normal Badí' month and 0
represents Ayyám-i-Há.
:returns: The value associated with the Ayyám-i-Há.
:rtype: int
"""
return self._month
@property
def day(self) -> int:
"""
Get the day of the month where 1 - 19 represents the normal Badí'
month and 1 - 4 or 5 represents Ayyám-i-Há.
:returns: The value associated with the day of the month.
:rtype: int
"""
return self._day
@property
def is_short(self) -> bool:
"""
Get `True` if the current instance represents a short form Badí'
date or `False` if a long form Badí' date.
:return `True` if short form date or `False` is long form date.
:rtype: bool
"""
return self.__short
@property
def b_date(self) -> tuple:
"""
Get the Badí' date as a tuple.
:returns: The Badí' date.
:rtype: tuple
"""
return self.__date
# Standard conversions, __eq__, __le__, __lt__, __ge__, __gt__,
# __hash__ (and helpers)
[docs]
def timetuple(self) -> tuple:
"""
Return local time tuple compatible with time.localtime().
:returns: The short or long form date.
:rtype: NamedTuple
"""
return _td_utils._build_struct_time(self.b_date + (0, 0, 0), -1,
short_in=self.is_short)
[docs]
def toordinal(self) -> int:
"""
Return proleptic Badí' ordinal for the year, month and day.
Bahá 1 of year -1842 is day 1. Only the year, month and day values
contribute to the result. If this class provides the long form
Badí' date it is converted to the short form before processing.
:returns: The ordinal representing the year, month, and day.
:rtype: int
"""
return _td_utils._ymd2ord(*self._short_from_long_form()[:3])
[docs]
def replace(self, kull_i_shay: int=None, vahid: int=None, year: int=None,
month: int=None, day: int=None) -> object:
"""
Return a new date with new values for the specified fields.
:param int kull_i_shay: A value representing the Kull-i-Shay’.
:param int vahid: A value representing the Váḥid.
:param int year: A value representing the year.
:param int month: A value representing the month.
:param int day: A value representing the day.
:returns: A date instance with replaced items.
:rtype: date
"""
if self.is_short and (kull_i_shay or vahid):
msg = "Cannot convert from a short to a long form date."
raise ValueError(msg)
elif (not self.is_short and year is not None and
(year < 1 or year > 19)):
msg = ("Cannot convert from a long to a short form date. The "
f"value {year} is not valid for long form dates.")
raise ValueError(msg)
elif self.is_short:
obj = self._replace_short(year=year, month=month, day=day)
else:
obj = self._replace_long(kull_i_shay=kull_i_shay, vahid=vahid,
year=year, month=month, day=day)
return obj
[docs]
def _replace_short(self, *, year: int=None, month: int=None, day: int=None,
hour: int=None, minute: int=None, second: int=None,
microsecond: int=None, tzinfo: tzinfo=True,
fold: int=None) -> object:
"""
Replace any of the year, month, or day values.
:param int year: A value representing the year.
:param int month: A value representing the month.
:param int day: A value representing the day.
:param int hour: A value representing the hour.
:param int minute: A value representing the minute.
:param int second: A value representing the second.
:param int microsecond: A value representing the microsecond.
:param tzinfo tzinfo: A `tzinfo` instance representing the timezone.
:param int fold: Either a 0 meaning not fold or a 1 indicating the
date in in the fold.
:returns: The short form date instance with replaced items.
:rtype: date
"""
if year is None:
year = self.year
if month is None:
month = self.month
if day is None:
day = self.day
if isinstance(self, datetime):
obj = type(self)(year, month, day, None, None, hour, minute,
second, microsecond, tzinfo, fold=fold)
else:
obj = type(self)(year, month, day, None, None)
return obj
[docs]
def _replace_long(self, *, kull_i_shay: int=None, vahid: int=None,
year: int=None, month: int=None, day: int=None,
hour: int=None, minute: int=None, second: int=None,
microsecond: int=None, tzinfo=True,
fold: int=None) -> object:
"""
Replace any of the kull_i_shay, vahid, year, month, or day values.
:param int kull_i_shay: A value representing the Kull-i-Shay’.
:param int vahid: A value representing the Váḥid.
:param int year: A value representing the year.
:param int month: A value representing the month.
:param int day: A value representing the day.
:param int hour: A value representing the hour.
:param int minute: A value representing the minute.
:param int second: A value representing the second.
:param int microsecond: A value representing the microsecond.
:param tzinfo tzinfo: A `tzinfo` instance representing the timezone.
:param int fold: Either a 0 meaning not fold or a 1 indicating the
date in in the fold.
:returns: The long form date instance with replaced items.
:rtype: date
"""
if kull_i_shay is None:
kull_i_shay = self.kull_i_shay
if vahid is None:
vahid = self.vahid
if year is None:
year = self.year
if month is None:
month = self.month
if day is None:
day = self.day
if isinstance(self, datetime):
obj = type(self)(kull_i_shay, vahid, year, month, day, hour,
minute, second, microsecond, tzinfo, fold=fold)
else:
obj = type(self)(kull_i_shay, vahid, year, month, day)
return obj
# Comparisons of date objects with other.
[docs]
def __eq__(self, other) -> bool:
return (self._cmp(other) == 0 if isinstance(other, date)
else NotImplemented)
[docs]
def __le__(self, other) -> bool:
return (self._cmp(other) <= 0 if isinstance(other, date)
else NotImplemented)
[docs]
def __lt__(self, other) -> bool:
return (self._cmp(other) < 0 if isinstance(other, date)
else NotImplemented)
[docs]
def __ge__(self, other) -> bool:
return (self._cmp(other) >= 0 if isinstance(other, date)
else NotImplemented)
[docs]
def __gt__(self, other) -> bool:
return (self._cmp(other) > 0 if isinstance(other, date)
else NotImplemented)
[docs]
def _cmp(self, other) -> int:
"""
Returns an integer representation of >, ==, or < of two date instances.
:param date other: The other date instance to compare to.
:returns: If 0 then x == y, else if 1 then x > y else if -1 then x < y,
where x is the `self` instance and y is the `other` instance.
:rtype: int
"""
assert isinstance(other, date)
if self.is_short:
d0 = self._year, self._month, self._day
d1 = other._year, other._month, other._day
else:
d0 = (self._kull_i_shay, self._vahid, self._year,
self._month, self._day)
d1 = (other._kull_i_shay, other._vahid, other._year,
other._month, other._day)
return _cmp(d0, d1)
[docs]
def __hash__(self) -> int:
"""
Get the hash of the `self` instance.
:returns: The hash.
:rtype: int
"""
if self._hashcode == -1:
self._hashcode = hash(self._getstate())
return self._hashcode
# Computations
[docs]
def __add__(self, other):
"""
Add a date to a timedelta.
:param date other: The other `date` instance which is added to the
`self` instance.
:rtype: date
"""
if isinstance(other, timedelta):
od = self.toordinal() + other.days
if _td_utils.DAYS_BEFORE_1ST_YEAR < od <= _MAXORDINAL:
ret = type(self).fromordinal(od, short=self.is_short)
else:
raise OverflowError("Result out of range.")
else:
ret = NotImplemented
return ret
__radd__ = __add__
[docs]
def __sub__(self, other):
"""
Subtract two dates, or a date and a timedelta.
:param date other: The other date instance which is subtracted from
the `self` instance.
:returns: Subtracted date instances.
:rtype: date
"""
if isinstance(other, timedelta):
ret = self + timedelta(-other.days)
elif isinstance(other, date):
days1 = self.toordinal()
days2 = other.toordinal()
ret = timedelta(days1 - days2)
else:
ret = NotImplemented
return ret
[docs]
def weekday(self) -> int:
"""
Return day of the week, where Jalál (Saturday) == 0 ...
Istiqlāl (Friday) == 6.
:returns: The numerical (0 - 6) day-of-the-week.
:rtype: int
"""
date = self._short_from_long_form()
return _td_utils._day_of_week(*date[:3])
[docs]
def isoweekday(self):
"""
Return day of the week, where Jalál (Saturday) == 1 ...
Istiqlāl (Friday) == 7. This is the ISO standard.
:returns: The numerical (1 - 7) day-of-the-week.
:rtype: int
"""
date = self._short_from_long_form()[:3]
return _td_utils._day_of_week(*date) + 1
[docs]
def isocalendar(self):
"""
Return a `NamedTuple` containing ISO year, week number, and weekday.
The first ISO week of the year is the (Jalál-Istiqlāl) week
containing the year's first Fiḍāl; everything else derives
from that.
The first week is 1; Jalál is 1 ... Istiqlāl is 7.
:returns: An ISO year, week, and day.
:rtype: _IsoCalendarDate
.. note::
ISO calendar algorithm taken from
http://www.phys.uu.nl/~vgent/calendar/isocalendar.htm
modified for the Badí' Calendar.
"""
y, m, d = self._short_from_long_form()[:3]
year, week, day = _td_utils._year_week_day(y, m, d)
return _IsoCalendarDate(year, week, day)
# Pickle support.
[docs]
@classmethod
def _is_pickle_data(cls, a, b) -> bool:
"""
Check if the incoming date is pickle data or actual date information.
:param a: Pickle data, the Kull-i-Shay’, or year.
:type a: int, str, or bytes
:param b: None, Váḥid, or month
:type b: NoneType or int
:returns: A Boolean if a short or long Badí' date derived from pickle
data. A None can be returned if a and b are real date
information.
:rtype: bool or NoneType
"""
if isinstance(b, NoneType) and isinstance(a, (bytes, str)):
a_len = len(a)
assert a_len in (4, 5), (
f"Invalid string {a} had length of {a_len} for pickle.")
short = True if a_len == 4 else False
if ((short and 1 <= ord(a[2:3]) <= 19)
or not short and 1 <= ord(a[3:4]) <= 19):
if isinstance(a, str):
try:
a = a.encode('latin1')
except UnicodeEncodeError:
raise ValueError(
"Failed to encode latin1 string when unpickling "
"a date or datetime instance. "
"pickle.load(data, encoding='latin1') is assumed.")
else:
short = None
else:
short = None
return short
[docs]
def _getstate(self) -> bytes:
"""
Get the current state.
:returns: The state of the current `self` instance.
:rtype: bytes
"""
if self.is_short:
yhi, ylo = divmod(self._year - MINYEAR, 256)
state = (yhi, ylo, self._month, self._day)
else:
# We need to add an arbitrarily number (19) larger that any
# Kull-i-Shay that I support so we don't get a negative number
# making bytes puke.
kull_i_shay = self._kull_i_shay + 19
vahid = self._vahid
year = self._year
month = self._month
day = self._day
state = (kull_i_shay, vahid, year, month, day)
return bytes(state),
def __setstate(self, bytes_str: bytes) -> None:
"""
Set the current state.
:param bytes bytes_str: The bytes string.
"""
if self.is_short:
yhi, ylo, self._month, self._day = bytes_str
self._year = yhi * 256 + MINYEAR + ylo
self.__date = (self._year, self._month, self._day)
else:
k, v, y, m, d = bytes_str
self._kull_i_shay = k - 19
self._vahid = v
self._year = y
self._month = m
self._day = d
self.__date = (self._kull_i_shay, v, y, m, d)
[docs]
def __reduce__(self) -> tuple:
"""
A tuple of the class name and current state.
:returns: The class name and current state.
:rtype: tuple
"""
return (self.__class__, self._getstate())
_date_class = date # So functions w/ args named "date" can get at the class.
date.min = date(MINYEAR, 1, 1)
date.max = date(MAXYEAR, 19, 19)
date.longmin = date(-5, 18, 1, 1, 1)
date.longmax = date(4, 5, 2, 19, 19)
date.resolution = timedelta(days=1)
_tzinfo_class = tzinfo
class _IsoCalendarDate(tuple):
def __new__(cls, year: int, week: int, weekday: int, /) -> object:
"""
This is a wrapper class around ISO year, week, and weekday.
:param int, year: The year of the date.
:param int, week: The week of the year.
:param int weekday: The day of the week.
:returns: The ISO representation of the year, week, and weekday.
:rtype: _IsoCalendarDate
"""
return super().__new__(cls, (year, week, weekday))
@property
def year(self) -> int:
"""
Get the year.
:returns: The year.
:rtype: int
"""
return self[0]
@property
def week(self) -> int:
"""
Get the week.
:returns: The week.
:rtype: int
"""
return self[1]
@property
def weekday(self) -> int:
"""
Get the weekday.
:returns: The weekday.
:rtype: int
"""
return self[2]
def __reduce__(self) -> tuple:
"""
A tuple of the class name and current state.
:returns: The class name and current state.
:rtype: tuple
.. note::
This code is intended to pickle the object without making the
class public. See https://bugs.python.org/msg352381
"""
return (tuple, (tuple(self),))
def __repr__(self) -> str:
"""
Convert to a formal string, for repr().
:returns: A string representing the date.
:rtype: str
"""
return (f'{self.__class__.__name__}'
f'(year={self[0]}, week={self[1]}, weekday={self[2]})')
[docs]
class time:
"""
Time with time zone.
Constructors:
__new__()
Operators:
__repr__, __str__
__eq__, __le__, __lt__, __ge__, __gt__, __hash__
Methods:
strftime()
isoformat()
utcoffset()
tzname()
dst()
Properties (readonly):
hour, minute, second, microsecond, tzinfo, fold
"""
__slots__ = ('_hour', '_minute', '_second', '_microsecond', '_tzinfo',
'_hashcode', '_fold')
[docs]
def __new__(cls, hour: float=0, minute: float=0, second: float=0,
microsecond: int=0, tzinfo: tzinfo=None, *,
fold: int=0) -> object:
"""
Constructor.
:param float hour: Hours (required)
:param float minute: Minutes (required)
:param float second: Seconds (default to zero)
:param int microsecond: Microseconds (default to zero)
:param tzinfo tzinfo: Timezone information (default to None)
:param int fold: (keyword only, default to zero)
:returns: The `self` instance.
:rtype: time
"""
if (isinstance(hour, (bytes, str)) and len(hour) == 6 and
ord(hour[0:1]) & 0x7F < 24):
# Pickle support
if isinstance(hour, str):
try:
hour = hour.encode('latin1')
except UnicodeEncodeError:
# More informative error message.
raise ValueError(
"Failed to encode latin1 string when unpickling "
"a time instance. "
"pickle.load(data, encoding='latin1') is assumed.")
self = object.__new__(cls)
self.__setstate(hour, minute or None)
self._hashcode = -1
else:
_td_utils._check_time_fields(hour, minute, second,
microsecond, fold)
_check_tzinfo_arg(tzinfo)
self = object.__new__(cls)
self._hour = hour
self._minute = minute
self._second = second
self._microsecond = microsecond
self._tzinfo = tzinfo
self._hashcode = -1
self._fold = fold
return self
# Read-only field accessors
@property
def hour(self) -> float:
"""
Get the hour.
:returns: The hour.
:rtype: float
"""
return self._hour
@property
def minute(self) -> float:
"""
Get the minute.
:returns: The minute.
:rtype: float
"""
return self._minute
@property
def second(self) -> float:
"""
Get the second.
:returns: The second.
:rtype: float
"""
return self._second
@property
def microsecond(self) -> int:
"""
Get the microsecond.
:returns: The microsecond.
:rtype: int
"""
return self._microsecond
@property
def tzinfo(self) -> tzinfo:
"""
Get the timezone info instance.
:returns: The timezone info instance.
:rtype: tzinfo
"""
return self._tzinfo
@property
def fold(self) -> int:
"""
Get the time fold. This is in the Autumn when the time is set back
from daylight savings time to standard time and the same hour is
repeated.
:returns: The time fold.
:rtype: int
"""
return self._fold
# Comparisons of time objects with other.
[docs]
def __eq__(self, other):
return (self._cmp(other, allow_mixed=True) == 0
if isinstance(other, time) else NotImplemented)
[docs]
def __le__(self, other):
return (self._cmp(other, time) <= 0 if isinstance(other, time)
else NotImplemented)
[docs]
def __lt__(self, other):
return (self._cmp(other) < 0 if isinstance(other, time)
else NotImplemented)
[docs]
def __ge__(self, other):
return (self._cmp(other) >= 0 if isinstance(other, time)
else NotImplemented)
[docs]
def __gt__(self, other):
return (self._cmp(other) > 0 if isinstance(other, time)
else NotImplemented)
[docs]
def _cmp(self, other, allow_mixed=False):
"""
A low level time compare method.
:param time other: Another time instance.
:param bool allow_mixed: True if a naive and aware time objects are
allowed else if False they are not allowed.
Only the __eq__ method sets this to True.
:returns: 0 if self and other are equal, 1 if self > other, and -1
self < other.
:rtype: int
"""
assert isinstance(other, time), f"Invalid time module, found {other}."
mytz = self.tzinfo
ottz = other.tzinfo
myoff = otoff = None
if mytz is ottz:
base_compare = True
else:
myoff = self.utcoffset()
otoff = other.utcoffset()
base_compare = myoff == otoff
if base_compare:
return _cmp((self._hour, self._minute, self._second,
self._microsecond),
(other._hour, other._minute, other._second,
other._microsecond))
if myoff is None or otoff is None:
if allow_mixed:
return 2 # arbitrary non-zero value
else:
raise TypeError("Cannot compare naive and aware times.")
myhhmm = self._hour * 60 + self._minute - myoff//timedelta(minutes=1)
othhmm = other._hour * 60 + other._minute - otoff//timedelta(minutes=1)
return _cmp((myhhmm, self._second, self._microsecond),
(othhmm, other._second, other._microsecond))
# Standard conversions, __hash__ (and helpers)
[docs]
def __hash__(self) -> int:
"""
Get the hash of the `self` instance.
:returns: The hash.
:rtype: int
"""
if self._hashcode == -1:
if self.fold:
t = self.replace(fold=0)
else:
t = self
tzoff = t.utcoffset()
if not tzoff: # zero or None
self._hashcode = hash(t._getstate()[0])
else:
h, m = divmod(timedelta(
hours=self.hour, minutes=self.minute) - tzoff,
timedelta(hours=1))
assert not m % timedelta(minutes=1), "Must be a whole minute."
m //= timedelta(minutes=1)
if 0 <= h < 24:
self._hashcode = hash(time(h, m, self.second,
self.microsecond))
else:
self._hashcode = hash((h, m, self.second,
self.microsecond))
return self._hashcode
# Conversion to string
[docs]
def _tzstr(self) -> str:
"""
Return a formatted timezone offset (+xx:xx) or an empty string.
:returns: The formatted timezone offset.
:rtype: str
"""
off = self.utcoffset()
return _format_offset(off)
[docs]
def __repr__(self) -> str:
"""
Convert to formal string, for repr().
:returns: A string representing the current `self` instance.
:rtype: str
"""
if self._microsecond != 0:
s = f", {self._second:d}, {self._microsecond:d}"
elif self._second != 0:
s = f", {self._second:d}"
else:
s = ""
s = (f"{_get_class_module(self)}."
f"{self.__class__.__qualname__}"
f"({self._hour:d}, {self._minute:d}{s})")
if self.tzinfo is not None:
assert s[-1:] == ")"
s = s[:-1] + f", tzinfo={self.tzinfo})"
if self._fold:
assert s[-1:] == ")"
s = s[:-1] + ", fold=1)"
return s
__str__ = isoformat
[docs]
def strftime(self, format: str) -> str:
"""
Format using strftime(). The date part of the timestamp passed to
underlying strftime should not be used.
:param str format: The string format.
:returns: An updated format string.
:rtype: str
"""
# We use the Badí' epoch for the year, month and day.
timetuple = (1, 1, 1, self._hour, self._minute, self._second, 0, 1, -1)
return _td_utils._wrap_strftime(self, format, timetuple)
# Timezone functions
[docs]
def utcoffset(self) -> timedelta:
"""
Return the timezone offset as timedelta, positive east of UTC
(negative west of UTC).
:returns: The offset from UTC.
:rtype: timedelta
"""
if self.tzinfo is not None:
offset = self.tzinfo.utcoffset(None)
_check_offset("utcoffset", offset)
return offset
[docs]
def badioffset(self) -> timedelta:
"""
Return the timezone offset as timedelta, positive east of Asia/Tehran
(negative west of UTC).
:returns: The offset from UTC.
:rtype: timedelta
"""
if self.tzinfo is not None:
offset = self.utcoffset()
if offset is not None:
offset -= timedelta(hours=BADI_COORD[2])
return offset
[docs]
def tzname(self) -> str:
"""
Return the timezone name.
:returns: A name representing the time zone.
:rtype: str
.. note::
The name is 100% informational--there's no requirement that
it mean anything in particular. For example, *GMT*, *UTC*, *-500*,
*-5:00*, *EDT*, *US/Eastern*, *America/New York* are all valid
responses.
"""
if self.tzinfo is not None:
name = self.tzinfo.tzname(None)
_check_tzname(name)
return name
[docs]
def dst(self) -> timedelta:
"""
Return 0 if DST is not in effect, or the DST offset (as timedelta
positive eastward) if DST is in effect.
:returns: The DST offset from UTC.
:rtype: timedelta
.. note::
This is purely informational; the DST offset has already been
added to the UTC offset returned by utcoffset() if applicable,
so there's no need to consult dst() unless you're interested in
displaying the DST info.
"""
if self.tzinfo is not None:
offset = self.tzinfo.dst(None)
_check_offset("dst", offset)
return offset
[docs]
def replace(self, hour: float=None, minute: float=None, second: float=None,
microsecond: int=None, tzinfo: tzinfo=True, *, fold: int=None):
"""
Return a new time with new values for the specified fields.
:param float hour: Hours (required)
:param float minute: Minutes (required)
:param float second: Seconds (default to zero)
:param int microsecond: Microseconds (default to zero)
:param tzinfo tzinfo: Timezone information (default to None)
:param int fold: (keyword only, default to zero)
:returns: A new time instance updated with the provided information.
:rtype: time
"""
if hour is None:
hour = self.hour
if minute is None:
minute = self.minute
if second is None:
second = self.second
if microsecond is None:
microsecond = self.microsecond
if tzinfo is True:
tzinfo = self.tzinfo
if fold is None:
fold = self._fold
return type(self)(hour, minute, second, microsecond, tzinfo, fold=fold)
# Pickle support.
[docs]
def _getstate(self, protocol: int=3) -> tuple:
"""
Get the state of the current time instance.
:param int protocol: The protocol used to derive the state
(default is 3).
:returns: The current state of the `self` instance.
:rtype: tuple
"""
us2, us3 = divmod(self._microsecond, 256)
us1, us2 = divmod(us2, 256)
h = self._hour
h += 128 if self._fold and protocol > 3 else 0
basestate = bytes([h, self._minute, self._second, us1, us2, us3])
if self.tzinfo is None:
return (basestate,)
else:
return (basestate, self.tzinfo)
def __setstate(self, string: str, tzinfo: tzinfo) -> None:
"""
Set the current state of the `self` instance.
:param str string: A byte string.
:param tzinfo, tzinfo: Time zone information.
:raises TypeError: If the `tzinfo` argument is not a `tzinfo` instance.
"""
_check_tzinfo_arg(tzinfo)
h, self._minute, self._second, us1, us2, us3 = string
if h > 127:
self._fold = 1
self._hour = h - 128
else:
self._fold = 0
self._hour = h
self._microsecond = (((us1 << 8) | us2) << 8) | us3
self._tzinfo = tzinfo
[docs]
def __reduce_ex__(self, protocol: int) -> tuple:
"""
Get the class name and the default protocol state.
:param int protocol: The protocol used to derive the state.
:returns: The class name and current state.
:rtype: tuple
"""
return (self.__class__, self._getstate(protocol))
[docs]
def __reduce__(self) -> tuple: # pragma: no cover
"""
Get the class name and current state using protocol 2.
:returns: The class name and current state.
:rtype: tuple
"""
return self.__reduce_ex__(2)
_time_class = time # so functions w/ args named "time" can get at the class
time.min = time(0, 0, 0)
time.max = time(24, 0, 3) # See contrib/misc/badi_jd_tests.py --day
time.resolution = timedelta(microseconds=1)
[docs]
class datetime(date):
"""
datetime(year, month, day[, hour[, minute[, second[,
microsecond[,tzinfo]]]]])
The year, month and day arguments are required. tzinfo may be None, or an
instance of a tzinfo subclass. The remaining arguments may be ints.
"""
__slots__ = date.__slots__ + time.__slots__
[docs]
def __new__(cls, a: int, b: int=None, c: int=None, d: int=None,
e: int=None, hour: float=0, minute: float=0, second: float=0,
microsecond: int=0, tzinfo: tzinfo=None, *,
fold: int=0) -> object:
"""
Check if there is pickle data. If so parse and create the object. If
not pickle data create the instance from the incoming date data.
:param int a: If pickle data this is the bytes string. If not pickle
data **a** could be the Kull-i-Shay’ if a long form date
or if a short form date **a** is the year.
:param int b: If pickle data this is the `tzinfo` instance. If not
pickle data **b** could be the Váḥid if a long form date
or if a short form date **b** is the month.
:param int c: If a long form date **c** is the year or if a short form
date, **c** is the day.
:param int d: If a long form date **d** is the month, if a short form
date **d** is None and not used.
:param int e: If a long form date **e** is the day, if a short form
date **e** is None and not used.
:param float hour: The hour.
:param float minute: The minute.
:param float second: The second.
:param int microsecond: The microsecond.
:param tzinfo tzinfo: The time zone information.
:param int fold: If *0* there is no fold in time, this is the more
common situation, however, if it is *1* there is a
fold in time at the DST switch to standard time.
:returns: The instance of the datetime class.
:rtype: datetime
"""
if (short := datetime._is_pickle_data(a, b)) is not None:
self = object.__new__(cls)
super().__init__(self)
self.__short = short
self.__setstate(a, b)
else:
b_date = tuple([x for x in (a, b, c, d, e) if x is not None])
date_len = len(b_date)
assert date_len in (3, 5), (
"A full short or long form Badí' date must be used, found "
f"{date_len} fields.")
self = object.__new__(cls)
super().__init__(self)
if date_len == 5:
self._kull_i_shay = a
self._vahid = b
self._year = c
self._month = d
self._day = e
self.__date = b_date
self.__short = False
else:
self._year = a
self._month = b
self._day = c
self.__date = b_date
self.__short = True
self._tzinfo = tzinfo
self._fold = fold
self._create_time(hour, minute, second, microsecond)
_td_utils._check_date_fields(*self.__date, short_in=self.__short)
_td_utils._check_time_fields(hour, minute, second, microsecond, fold)
_check_tzinfo_arg(tzinfo)
self._hashcode = -1
return self
[docs]
def _create_time(self, hour: float, minute: float, second: float,
microsecond: int) -> None:
"""
Create the time portion of the `datetime` instance.
:param float hour: The hour of the dat.
:param float minute: The minute of the hour.
:param float second: The second of the minute.
:param int microsecond: The microsecond.
"""
def fractionals(value, items):
if value % 1 and any(items):
raise ValueError("A fractional value cannot be followed by "
"a least significant value.")
fractionals(hour, (minute, second, microsecond))
fractionals(minute, (second, microsecond))
fractionals(second, (microsecond,))
if hour % 1:
mm = self._PARTIAL_HOUR_TO_MINUTE(hour)
ss = self._PARTIAL_MINUTE_TO_SECOND(mm)
self._hour = _math.floor(hour)
self._minute = _math.floor(mm)
self._second = _math.floor(ss)
self._microsecond = self._PARTIAL_SECOND_TO_MICROSECOND(ss)
elif minute % 1:
self._hour = hour
ss = self._PARTIAL_MINUTE_TO_SECOND(minute)
self._minute = _math.floor(minute)
self._second = _math.floor(ss)
self._microsecond = self._PARTIAL_SECOND_TO_MICROSECOND(ss)
elif second % 1:
self._hour = hour
self._minute = minute
self._second = _math.floor(second)
self._microsecond = self._PARTIAL_SECOND_TO_MICROSECOND(second)
else:
self._hour = hour
self._minute = minute
self._second = second
self._microsecond = round(microsecond, 6)
self.__time = (self._hour, self._minute,
self._second, self._microsecond)
# Read-only field accessors
@property
def hour(self) -> float:
"""
Get the hour.
:returns: The hour.
:rtype: float
"""
return self._hour
@property
def minute(self) -> float:
"""
Get the minute.
:returns: The minute.
:rtype: float
"""
return self._minute
@property
def second(self) -> float:
"""
Get the second.
:returns: The second.
:rtype: float
"""
return self._second
@property
def microsecond(self) -> int:
"""
Get the microsecond.
:returns: The microsecond.
:rtype: int
"""
return self._microsecond
@property
def tzinfo(self) -> tzinfo:
"""
Get the timezone info instance.
:returns: The timezone info instance.
:rtype: tzinfo
"""
return self._tzinfo
@property
def fold(self) -> int:
"""
Get the time fold. This is in the Autumn when the time is set back
from daylight savings time to standard time and the same hour is
repeated.
:returns: The time fold.
:rtype: int
"""
return self._fold
@property
def b_date(self) -> tuple:
"""
Get the Badí' date as a tuple.
:returns: The Badí' date.
:rtype: tuple
"""
return self.__date
@property
def b_time(self) -> tuple:
"""
Get the time as a tuple.
:returns: The time.
:rtype: tuple
"""
return self.__time
@property
def is_short(self) -> bool:
"""
Get `True` if the current instance represents a short form Badí'
date or `False` if a long form Badí' date.
:return `True` if short form date or `False` is long form date.
:rtype: bool
"""
return self.__short
[docs]
@classmethod
def _fromtimestamp(cls, t, tz, *, short=True):
"""
Construct a datetime from a POSIX timestamp (like time.time()).
.. note::
An IANA key and timezone information alone cannot be reliably mapped
to geographic location. Without an internet connection, the local
latitude and longitude cannot be looked up. Without the latitude and
longitude the calendar will compute sunset for Badíʿ dates using the
configured default coordinates (e.g., Tehran). This fallback will
cause different local Badíʿ days than expected for other regions.
:param float t: POSIX timestamp.
:param bool utc: If True then the result is relative to UTC time else
if False it is relative to local time.
:param tzinfo tz: A tzinfo instance.
:param bool short: If True (default) the short form date is returned
else False the long form date is returned.
:returns: A datetime instance set to the date derived from the
POSIX timestamp.
:rtype: datetime.datetime
"""
def _fix_short_date(date, short=True):
return date[:3] + (None, None) + date[3:] if short else date
bc = BahaiCalendar()
# 1. Determine coordinates
if tz is None:
lat, lon, zone = LOCAL_COORD
elif hasattr(tz, "coordinates"):
lat, lon, zone = tz.coordinates
else:
# Fallback
lat, lon, zone = LOCAL_COORD
# 2. Compute Badíʿ date directly for that location
b_date = bc.badi_date_from_timestamp(
t, lat, lon, zone, us=True, short=short)
date = _fix_short_date(b_date, short=short)
# Clamp out leap seconds if the platform has them.
date = date[:7] + (min(date[7], 59),) + date[8:]
# 3. Construct result
return cls(*date, tzinfo=tz)
[docs]
@classmethod
def fromtimestamp(cls, t: float, tz: tzinfo=None, *, short: bool=True):
"""
Construct a datetime from a POSIX timestamp representing local time
(like time.time()).
:param float t: The timestamp.
:param tzinfo tz: The `tzinfo` instance.
:param bool short: If True (default) the short form date is returned
else False the long form date is returned.
:returns: A `datetime` instance.
:rtype: datetime
"""
_check_tzinfo_arg(tz)
return cls._fromtimestamp(t, tz, short=short)
# Both the ustfromtimestamp() and utcnow() methods have been deprecated.
# https://docs.python.org/3/deprecations/index.html
[docs]
@classmethod
def now(cls, tz: tzinfo=None, short: bool=True):
"""
Construct a datetime from time.time() and optional time zone info.
:param tzinfo tz: The `timezone` instance.
:param bool short: If True (default) the short form date is returned
else False the long form date is returned.
:returns: A `datetime` instance.
:rtype: datetime
"""
return cls.fromtimestamp(_time.time(), tz, short=short)
[docs]
@classmethod
def combine(cls, date: date, time: time, tzinfo: tzinfo=True):
"""
Construct a datetime from a given date and a given time.
:param date date: A date instance.
:param time time: A time instance.
:param tzinfo tzinfo: A tzinfo instance.
:returns: A `datetime` instance.
:rtype: datetime
"""
if not isinstance(date, _date_class):
raise TypeError("The date argument must be a date instance, "
f"found {date!r}.")
if not isinstance(time, _time_class):
raise TypeError("The time argument must be a time instance, "
f"found {time!r}.")
if tzinfo is True:
tzinfo = time.tzinfo
if date.is_short:
d = (date.year, date.month, date.day)
else:
d = (date.kull_i_shay, date.vahid, date.year, date.month, date.day)
return cls(*d, hour=time.hour, minute=time.minute, second=time.second,
microsecond=time.microsecond, tzinfo=tzinfo, fold=time.fold)
[docs]
def timetuple(self):
"""
Return local time tuple compatible with time.localtime().
:returns: A tuple of the date and time values.
:rtype: NamedTuple
"""
date = self.b_date + self.b_time[:3]
return _td_utils._build_struct_time(date, -1, tzinfo=self.tzinfo,
short_in=self.is_short)
[docs]
def _mktime(self) -> float:
"""
Return integer POSIX timestamp.
:returns: The POSIX timestamp.
:rtype: float
"""
return self.timestamp_from_badi_date(self.b_date + self.b_time,
*LOCAL_COORD)
[docs]
def timestamp(self) -> float:
"""
Return POSIX timestamp for the current datetime instance.
:returns: The POSIX timestamp.
:rtype: float
"""
if self.tzinfo is None:
return self._mktime() + self.microsecond / 1e6
else:
return (self - _EPOCH).total_seconds()
[docs]
def utctimetuple(self) -> tuple:
"""
Return UTC time tuple compatible with time.gmtime().
:returns: The UTC time tuple.
:rtype: NamedTuple
"""
return self._timetuple(self.utcoffset())
[docs]
def _timetuple(self, offset) -> tuple:
"""
Return UTC or BADI time tuple compatible with time.gmtime().
:returns: The UTC time tuple.
:rtype: NamedTuple
"""
if offset:
self -= offset
date = self.b_date + (self.hour, self.minute, self.second)
return _td_utils._build_struct_time(date, 0, short_in=self.is_short)
[docs]
def date(self) -> date:
"""
Return the date part of the `datetime` instance.
:returns: The date part of the `datetime` instance.
:rtype: date
"""
return date(self._year, self._month, self._day)
[docs]
def time(self):
"""
Return the time part, with tzinfo None of the `datetime` instance.
:returns: The time part of the `datetime` instance.
:rtype: time
"""
return time(self.hour, self.minute, self.second, self.microsecond,
fold=self.fold)
[docs]
def timetz(self) -> time:
"""
Return the time part, with same tzinfo of the `datetime` instance.
:returns: The time part of the `datetime` instance.
:rtype: time
"""
return time(self.hour, self.minute, self.second, self.microsecond,
self.tzinfo, fold=self.fold)
[docs]
def replace(self, kull_i_shay: int=None, vahid: int =None, year: int =None,
month: int =None, day: int=None, hour: float=None,
minute: float=None, second: float=None, microsecond: int=None,
tzinfo: tzinfo=True, *, fold: int=None):
"""
Return a new datetime with new values for the specified fields.
:param int kull_i_shay: The Kull-i-Shay’.
:param int vahid: The Váḥid.
:param int year: The year.
:param int month: The month.
:param int day: The day.
:param float hour: The hour.
:param float minute: The minute.
:param float second: The second.
:param int microsecond: The microsecond.
:param tzinfo tzinfo: A `tzinfo` instance.
:param int fold: The time fold.
:returns: The updated `datetime` instance.
:rtype: datetime
"""
if hour is None:
hour = self.hour
if minute is None:
minute = self.minute
if second is None:
second = self.second
if microsecond is None:
microsecond = self.microsecond
if tzinfo is True:
tzinfo = self.tzinfo
if fold is None: # pragma: no cover
fold = self.fold
if self.is_short and (kull_i_shay or vahid):
msg = "Cannot convert from a short to a long form date."
raise ValueError(msg)
elif (not self.is_short and year is not None and
(year < 1 or year > 19)):
msg = ("Cannot convert from a long to a short form date. The "
f"value {year} is not valid for long form dates.")
raise ValueError(msg)
elif self.is_short:
obj = self._replace_short(
year=year, month=month, day=day, hour=hour, minute=minute,
second=second, microsecond=microsecond, tzinfo=tzinfo,
fold=fold)
else:
obj = self._replace_long(
kull_i_shay=kull_i_shay, vahid=vahid, year=year, month=month,
day=day, hour=hour, minute=minute, second=second,
microsecond=microsecond, tzinfo=tzinfo, fold=fold)
return obj
[docs]
def _local_timezone(self):
"""
Always return the local time offset in a timezone instance.
:returns: The local time zone.
:rtype: timezone
"""
if self.tzinfo is None:
ts = self._mktime()
else:
ts = (self - _EPOCH) // timedelta(seconds=1)
ts = self.badi_date_from_timestamp(ts, *LOCAL_COORD,
short=self.is_short, trim=True)
ts_len = len(ts)
if self.is_short:
need = 6 - ts_len
ts += (0,) * need if need > 0 else ()
localtm = _td_utils._build_struct_time(ts, -1, tzinfo=LOCAL,
short_in=True)
else:
need = 8 - ts_len
ts += (0,) * need if need > 0 else ()
localtm = _td_utils._build_struct_time(ts, -1, tzinfo=LOCAL,
short_in=False)
# Extract TZ data
gmtoff = localtm.tm_gmtoff
zone = localtm.tm_zone
return timezone(timedelta(seconds=gmtoff), zone)
[docs]
def astimezone(self, tz: tzinfo=None):
"""
Returns a datetime instance with the provided tzinfo instance attached.
:param tzinfo tz: A timezone instance.
:returns: A `datetime` instance with a tzinfo instance attached.
:rtype: datetime
"""
if tz is None:
tz = self._local_timezone()
elif not isinstance(tz, tzinfo):
raise TypeError("tz argument must be an instance of tzinfo, "
f"found {type(tz)}.")
mytz = self.tzinfo
if mytz is None:
mytz = self._local_timezone()
myoffset = mytz.utcoffset(self)
else:
myoffset = mytz.utcoffset(self)
if myoffset is None:
mytz = self.replace(tzinfo=None)._local_timezone()
myoffset = mytz.utcoffset(self)
if tz is not mytz:
# Convert self to UTC, and attach the new time zone instance.
utc = (self - myoffset).replace(tzinfo=tz)
# Convert from UTC to tz's local time.
ret = tz.fromutc(utc)
else:
ret = self
return ret
# Ways to produce a string.
[docs]
def ctime(self) -> str:
"""
Return a `ctime` formatted string.
:returns: A string with weekday, month name, day, hour, minute, second,
and year.
:rtype: str
"""
if self.is_short:
date = self.b_date + self.b_time
else:
d = self._short_from_long_form(time=self.b_time)
date = (*d[:3], *d[5:])
weekday = _td_utils._day_of_week(*date[:3])
wd_name = _td_utils.DAYNAMES[weekday]
year, month, day, hour, minute, second, us = date
m_name = _td_utils.MONTHNAMES[month]
y_shim = 4 if year > -1 else 5
return (f"{wd_name} {m_name} {day:2d} "
f"{hour:02d}:{minute:02d}:{second:02d} {year:0{y_shim}d}")
[docs]
def __repr__(self) -> str:
"""
Convert to formal string, for repr().
:returns: A string representation of the `datetime` instance.
:rtype: str
"""
L = [self._hour, self._minute, self._second, self._microsecond]
for i in range(2):
if L[-1] == 0:
del L[-1]
s = (f"{_get_class_module(self)}."
f"{self.__class__.__qualname__}")
if hasattr(self, '_kull_i_shay'):
s += (f"({self._kull_i_shay}, {self._vahid}, {self._year}, "
f"{self._month}, {self._day}")
else:
s += f"({self._year}, {self._month}, {self._day}"
s += ", None, None" if self.is_short else ""
s += f", {', '.join(map(str, L))})"
if self.tzinfo is not None:
assert s[-1:] == ")"
s = s[:-1] + f", tzinfo={self.tzinfo!r})"
if self._fold:
assert s[-1:] == ")"
s = s[:-1] + ", fold=1)"
return s
[docs]
def _dt_str_conversion(self, sep='T') -> str:
"""
A representation of the `datetime` instance.
:param str sep: The ISO date and time separator. The standard is to
use *T*.
:returns: A representation of the `datetime` instance.
:rtype: str
"""
if self.is_short:
ret = self.isoformat(sep=sep)
else:
ret = self._str_convertion()
ret += f"{sep}{self.hour:02}:{self.minute:02}:{self.second:02}"
ret += f".{self.microsecond}" if self.microsecond else ""
off = self.utcoffset()
tz = _format_offset(off)
if tz:
ret += tz
return ret
__str__ = _dt_str_conversion
[docs]
@classmethod
def strptime(cls, date_string: str, format: str) -> str:
"""
String, format -> new datetime parsed from a string
(like time.strptime()).
:param str date_string: A string representing the date.
:param str format: A format string
:returns: A date and time string representing the `format` string.
:rtype: str
"""
from badidatetime import _strptime
return _strptime._strptime_datetime(cls, date_string, format)
[docs]
def utcoffset(self):
"""
Return the timezone offset as timedelta positive east of UTC
(negative west of UTC).
"""
if self.tzinfo is not None:
offset = self.tzinfo.utcoffset(self)
_check_offset("utcoffset", offset)
return offset
[docs]
def tzname(self) -> str:
"""
Return the timezone name.
Note that the name is 100% informational -- there's no requirement that
it mean anything in particular. For example, 'GMT', 'UTC', '-500',
'-5:00', 'EDT', 'US/Eastern', 'America/New_York' are all valid replies.
:returns: The name of the time zone or `None` if not `tzinfo`
instance was found.
:rtype: str
"""
if self.tzinfo is not None:
name = self.tzinfo.tzname(self)
_check_tzname(name)
return name
[docs]
def dst(self) -> int:
"""
Return 0 if DST is not in effect, or the DST offset (as timedelta
positive eastward) if DST is in effect.
This is purely informational; the DST offset has already been added to
the UTC offset returned by utcoffset() if applicable, so there's no
need to consult dst() unless you're interested in displaying the DST
info.
:returns: The value as described above.
:rtype: int
"""
if self.tzinfo is not None:
offset = self.tzinfo.dst(self)
_check_offset("dst", offset)
return offset
# Comparisons of datetime objects with other.
[docs]
def __eq__(self, other) -> bool:
if isinstance(other, datetime):
return self._cmp(other, allow_mixed=True) == 0
elif not isinstance(other, date):
return NotImplemented
else:
return False
[docs]
def __le__(self, other) -> bool:
if isinstance(other, datetime):
return self._cmp(other) <= 0
elif not isinstance(other, date):
return NotImplemented
else:
_cmperror(self, other)
[docs]
def __lt__(self, other) -> bool:
if isinstance(other, datetime):
return self._cmp(other) < 0
elif not isinstance(other, date):
return NotImplemented
else:
_cmperror(self, other)
[docs]
def __ge__(self, other) -> bool:
if isinstance(other, datetime):
return self._cmp(other) >= 0
elif not isinstance(other, date):
return NotImplemented
else:
_cmperror(self, other)
[docs]
def __gt__(self, other) -> bool:
if isinstance(other, datetime):
return self._cmp(other) > 0
elif not isinstance(other, date):
return NotImplemented
else:
_cmperror(self, other)
[docs]
def _cmp(self, other, allow_mixed: bool=False) -> int:
"""
Returns an integer representation of >, ==, or < of two date instances.
:param date other: The other date instance to compare to.
:param bool allow_mixed: An integer representation of two date
instances.
:returns: If 0 then x == y, else if 1 then x > y else if -1 then x < y,
where x is the `self` instance and y is the `other` instance.
:rtype: int
"""
assert isinstance(other, datetime), (
f"The other must be a datetime instance, found '{type(other)}'.")
mytz = self.tzinfo
ottz = other.tzinfo
myoff = otoff = None
if mytz is ottz:
base_compare = True
else:
myoff = self.utcoffset()
otoff = other.utcoffset()
# Assume that allow_mixed means that we are called from __eq__
if allow_mixed: # pragma: no cover
if myoff != self.replace(fold=not self.fold).utcoffset():
return 2 # arbitrary non-zero value
if otoff != other.replace(fold=not other.fold).utcoffset():
return 2 # arbitrary non-zero value
base_compare = myoff == otoff
if base_compare:
return _cmp((self._year, self._month, self._day, self._hour,
self._minute, self._second, self._microsecond),
(other._year, other._month, other._day, other._hour,
other._minute, other._second, other._microsecond))
if myoff is None or otoff is None:
if allow_mixed:
return 2 # arbitrary non-zero value
else:
raise TypeError("Cannot compare naive and aware datetimes.")
# XXX What follows could be done more efficiently...
diff = self - other # this will take offsets into account
if diff.days < 0:
return -1
return diff and 1 or 0
[docs]
def __add__(self, other):
"""
Add a datetime and a timedelta.
:param datetime other: The other `datetime` instance which is added
to the `self` instance.
:rtype: datetime
"""
if isinstance(other, timedelta):
delta = timedelta(self.toordinal(),
hours=self._hour,
minutes=self._minute,
seconds=self._second,
microseconds=self._microsecond)
delta += other
hour, rem = divmod(delta.seconds, 3600)
minute, second = divmod(rem, 60)
if _td_utils.DAYS_BEFORE_1ST_YEAR < delta.days <= _MAXORDINAL:
return type(self).combine(
date.fromordinal(delta.days, short=self.is_short),
time(hour, minute, second, delta.microseconds,
tzinfo=self.tzinfo))
else:
return NotImplemented
raise OverflowError("Result out of range.")
__radd__ = __add__
[docs]
def __sub__(self, other) -> object:
"""
Subtract two datetimes, or a datetime and a timedelta.
:param date other: The other datetime instance which is subtracted
from the `self` instance.
:returns: Subtracted datetime instances.
:rtype: date
"""
if not isinstance(other, datetime):
if isinstance(other, timedelta):
return self + -other
return NotImplemented
days1 = self.toordinal()
days2 = other.toordinal()
secs1 = self._second + self._minute * 60 + self._hour * 3600
secs2 = other._second + other._minute * 60 + other._hour * 3600
base = timedelta(days1 - days2,
secs1 - secs2,
self._microsecond - other._microsecond)
if self.tzinfo is other.tzinfo:
return base
myoff = self.utcoffset()
otoff = other.utcoffset()
if myoff == otoff:
return base
if myoff is None or otoff is None:
raise TypeError("Cannot mix naive and timezone-aware time.")
return base + otoff - myoff
[docs]
def __hash__(self) -> int:
"""
Get the hash of the `self` instance.
:returns: The hash.
:rtype: int
"""
if self._hashcode == -1:
if self.fold:
t = self.replace(fold=0)
else:
t = self
tzoff = t.utcoffset()
if tzoff is None:
self._hashcode = hash(t._getstate()[0])
else:
days = _td_utils._ymd2ord(self.year, self.month, self.day)
seconds = self.hour * 3600 + self.minute * 60 + self.second
self._hashcode = hash(timedelta(days, seconds,
self.microsecond) - tzoff)
return self._hashcode
# Pickle support.
[docs]
@classmethod
def _is_pickle_data(cls, a, b) -> int:
"""
Check if the incoming date is pickle data or actual date information.
:param a: Pickle data, the kull_i_shay, or year.
:type a: int, str, or bytes
:param b: None, vahid, or month
:type b: NoneType or int
:returns: A Boolean if a short or long Badí' date derived from pickle
data. A None can be returned if a and b are real date
information.
:rtype: bool or NoneType
"""
if isinstance(b, (NoneType, tzinfo)) and isinstance(a, (bytes, str)):
a_len = len(a)
assert a_len in (10, 11), (
f"Invalid string {a} had length of {a_len} for pickle.")
short = True if a_len == 10 else False
if ((short and 1 <= ord(a[2:3]) & 0x7F <= 19)
or not short and 1 <= ord(a[3:4]) & 0x7F <= 19):
if isinstance(a, str):
try:
a = a.encode('latin1')
except UnicodeEncodeError:
raise ValueError(
"Failed to encode latin1 string when unpickling "
"a date or datetime instance. "
"pickle.load(data, encoding='latin1') is assumed.")
else:
short = None
else:
short = None
return short
[docs]
def _getstate(self, protocol: int=3)-> tuple:
"""
Get the current state.
:param int protocol: The pickle protocol to use defaults to 3.
:returns: The state of the current `self` instance.
:rtype: tuple
"""
if self.is_short:
yhi, ylo = divmod(self._year - MINYEAR, 256)
state = (yhi, ylo)
else:
# We need to add an arbitrarily number (19) larger that any
# Kull-i-Shay that I support so we don't get a negative number
# making bytes puke.
kull_i_shay = self._kull_i_shay + 19
vahid = self._vahid
year = self._year
state = (kull_i_shay, vahid, year)
m = self._month
if self._fold and protocol > 3:
m += 128
us2, us3 = divmod(self._microsecond, 256)
us1, us2 = divmod(us2, 256)
basestate = bytes(state + (m, self._day, self._hour, self._minute,
self._second, us1, us2, us3))
if self.tzinfo is None:
return (basestate,)
else:
return (basestate, self.tzinfo)
def __setstate(self, bytes_str, tzinfo) -> None:
"""
Set the current state.
:param bytes bytes_str: The bytes string.
:param tzinfo tzinfo: A `tzinfo` instance.
"""
if tzinfo is not None and not isinstance(tzinfo, _tzinfo_class):
raise TypeError("Bad tzinfo state arg.")
if self.is_short:
(yhi, ylo, m, self._day, self._hour,
self._minute, self._second, us1, us2, us3) = bytes_str
self._year = yhi * 256 + MINYEAR + ylo
self.__date = (self.year,)
else:
(k, v, y, m, d, self._hour, self._minute,
self._second, us1, us2, us3) = bytes_str
self._kull_i_shay = k - 19
self._vahid = v
self._year = y
self._day = d
self.__date = (self.kull_i_shay, v, y)
if m > 127:
self._fold = 1
self._month = m - 128
else:
self._fold = 0
self._month = m
self._microsecond = (((us1 << 8) | us2) << 8) | us3
self.__date += (self._month, self._day)
self.__time = (self._hour, self._minute,
self._second, self._microsecond)
self._tzinfo = tzinfo
[docs]
def __reduce_ex__(self, protocol: int) -> tuple:
"""
A tuple of the class name and current state.
:param int protocol: The protocol used to derive the state.
:returns: The class name and current state.
:rtype: tuple
"""
return (self.__class__, self._getstate(protocol))
[docs]
def __reduce__(self) -> tuple: # pragma: no cover
"""
A tuple of the class name and current state using protocol 2.
:returns: The class name and current state.
:rtype: tuple
"""
return self.__reduce_ex__(2)
datetime.min = datetime(-1842, 1, 1)
datetime.max = datetime(1161, 19, 19)
datetime.resolution = timedelta(microseconds=1)
[docs]
class timezone(tzinfo):
__slots__ = '_offset', '_name'
# Sentinel value to disallow None
_Omitted = object()
[docs]
def __new__(cls, offset: timedelta, name: str=_Omitted) -> object:
"""
Constructor
:param timedelta offset: A timedelta instance representing the
difference between the local time and UTC.
:param str name: A string that will be used as the value returned by
the datetime.tzname() method.
:returns: An instance of timezone.
:rtype: timezone
"""
if not isinstance(offset, timedelta):
raise TypeError("offset must be a timedelta")
if name is cls._Omitted:
if not offset: # pragma: no cover
return cls.utc
name = None
elif not isinstance(name, str):
raise TypeError("name must be a string")
if not cls._minoffset <= offset <= cls._maxoffset:
raise ValueError("offset must be a timedelta strictly between "
"-timedelta(hours=24) and timedelta(hours=24), "
f"not {offset!r}.")
return cls._create(offset, name)
[docs]
@classmethod
def _create(cls, offset: timedelta, name: str=None) -> object:
"""
Create an instance of `tzinfo`.
:param timedelta offset: A `timedelta` instance representing the
offset from UTC.
:param str name: An optional name indicating the time zone as an
IANA name, however, it could just be UTC or UTC+0330.
:returns: The `timezone` instance.
:rtype: timezone
"""
self = tzinfo.__new__(cls)
self._offset = offset
self._name = name
return self
[docs]
def __eq__(self, other):
if isinstance(other, timezone):
return self._offset == other._offset
return NotImplemented
[docs]
def __repr__(self) -> str:
"""
Convert to formal string, for repr().
:returns: A string representing the date.
:rtype: str
.. note::
>>> tz = timezone.utc
>>> repr(tz)
'datetime.timezone.utc'
>>> tz = timezone(timedelta(hours=-5), 'EST')
>>> repr(tz)
'datetime.timezone(datetime.timedelta(-1, 68400), 'EST')'
"""
if self is self.utc:
return 'badidatetime.timezone.utc'
if self is self.badi:
return 'badidatetime.BADI'
offset_name = f"{self._offset!r}"
if self._name is None:
return (f"{_get_class_module(self)}.{self.__class__.__qualname__}"
f"({_module_name(offset_name)})")
return (f"{_get_class_module(self)}.{self.__class__.__qualname__}"
f"({_module_name(offset_name)}, {self._name!r})")
[docs]
def __str__(self) -> str:
"""
A string representation of the current `timezone` instance.
:returns: A string representation of the current `timezone` instance.
:rtype: str
"""
return self.tzname(None)
[docs]
def utcoffset(self, dt: datetime) -> timedelta:
"""
Return the fixed value specified when the `timezone` instance is
constructed.
:returns: The fixed value specified when the `timezone` instance is
constructed.
:rtype: timedelta
"""
if isinstance(dt, datetime) or dt is None:
return self._offset
raise TypeError("utcoffset() argument must be a datetime instance "
"or None")
[docs]
def badioffset(self, dt: datetime) -> timedelta:
if isinstance(dt, datetime) or dt is None:
return self._offset - timedelta(hours=BADI_COORD[2])
raise TypeError("badioffset() argument must be a datetime instance "
"or None")
[docs]
def tzname(self, dt: datetime):
if isinstance(dt, datetime) or dt is None:
if self._name is None:
return self._name_from_offset(self._offset)
return self._name
raise TypeError("tzname() argument must be a datetime instance "
"or None")
[docs]
def dst(self, dt: datetime):
if isinstance(dt, datetime) or dt is None:
return None
raise TypeError("dst() argument must be a datetime instance or None")
[docs]
def fromutc(self, dt: datetime):
if isinstance(dt, datetime):
if dt.tzinfo is not self:
raise ValueError("fromutc: dt.tzinfo is not self")
return dt + self._offset
raise TypeError("fromutc() argument must be a datetime instance "
"or None")
_maxoffset = timedelta(hours=24, microseconds=-1)
_minoffset = -_maxoffset
[docs]
@staticmethod
def _name_from_offset(delta: timedelta) -> str:
if not delta:
return 'UTC'
if delta < timedelta(0):
sign = '-'
delta = -delta
else:
sign = '+'
hours, mins = divmod(delta, timedelta(hours=1))
minutes, ss_us = divmod(mins, timedelta(minutes=1))
seconds = ss_us.seconds
microseconds = ss_us.microseconds
if microseconds:
return (f"UTC{sign}{hours:02d}:{minutes:02d}:{seconds:02d}"
f".{microseconds:06d}")
if seconds:
return f'UTC{sign}{hours:02d}:{minutes:02d}:{seconds:02d}'
return f'UTC{sign}{hours:02d}:{minutes:02d}'
# pickle support
def __getinitargs__(self):
return (self._offset,) + ((self._name,) if self._name else ())
[docs]
def __hash__(self):
return hash(self._offset)
[docs]
class TZWithCoords(timezone):
"""
Correct Badí' dates must have the latitude, longitude, and time zone. None
of the publicly available packages can use coordinates, hence the need for
this class.
"""
__slots__ = 'lat', 'lon', 'zone', 'key', '_name', '_coords', '_zi'
[docs]
def __new__(cls, lat: float, lon: float, zone: float=None, *, key: str=None
) -> object:
"""
Constructor
:param float lat: The latitude.
:param float lon: The longitude.
:param float zone: The time zone.
:param str key: The IANA key.
:returns: The instantiated object.
:rtype: TZWithCoords
"""
if zone is None:
offset = timedelta(0)
else:
offset = timedelta(hours=zone)
name = "" if key is None else key
self = super().__new__(cls, offset, name)
self.lat = lat
self.lon = lon
self.zone = zone
self.key = name
self._name = None if name == "" else name
self._coords = (lat, lon, zone)
return self
@property
def coordinates(self) -> tuple:
"""
Returns the coordinates as a tuple.
:returns: The coordinates.
:rtype: tuple
"""
return self._coords
[docs]
def dst(self, dt: datetime=None) -> timedelta:
"""
Returns a timedelta set to 0 (default) for no dst (Daylight Savings
Time) or set to 1 for dst.
:param datetime dt: A datetime object.
:returns: An indication if the date is in daylight savings time or not.
:rtype: timedelta
"""
dst = None
if hasattr(self, '_zi'):
dst = self._zi.dst(dt)
if not dst:
dst = timedelta(0)
return dst
[docs]
@classmethod
def fromzoneinfo(cls, zoneinfo, lat: float, lon: float, zone: float
) -> object:
"""
Converts a ZoneInfo object into a TZWithCoords object. Not all
functionality of the ZoneInfo object is implemented in TZWithCoords.
:param ZoneInfo zoneinfo: The ZoneInfo object.
:param float lat: The latitude.
:param float lon: The longitude.
:param float zone: The time zone.
:returns: A TZWithCoords object.
:rtype: TZWithCoords
"""
cls._zi = zoneinfo
return cls(lat, lon, zone, key=zoneinfo.key)
[docs]
def __str__(self) -> str:
"""
Returns a string representation of the current TZWithCoords object.
:returns: String representation of the current TZWithCoords object.
:rtype: str
"""
ret = f"{self.lat}, {self.lon}, {self.zone}"
if self.key != '':
ret += f', {self.key}'
return ret
[docs]
def __reduce__(self) -> tuple:
"""
Gather the class data for pickling.
:returns: The class data.
:rtype: tuple
"""
return (self.__class__, (self.lat, self.lon),
{"zone": self._coords[2], "key": self.key})
[docs]
def __setstate__(self, state: dict) -> None:
"""
Set the pickled data on the class.
:param dict state: Pickled state of a TZWithCoords class.
"""
# Called after __new__
zone = state.get("zone")
key = state.get("key")
if zone is not None:
self._offset = timedelta(hours=zone)
self.key = key
self._coords = (self.lat, self.lon, zone)
UTC = timezone.utc = TZWithCoords(*GMT_COORD, key='UTC')
BADI = timezone.badi = TZWithCoords(*BADI_COORD, key=BADI_IANA)
timezone.min = timezone._create(-timedelta(hours=23, minutes=59))
timezone.max = timezone._create(timedelta(hours=23, minutes=59))
_EPOCH = datetime(126, 16, 2, None, None, 7, 59, 32, 492400,
tzinfo=timezone.utc)