# -*- coding: utf-8 -*-
#
# badidatetime/_timedateutils.py
#
__docformat__ = "restructuredtext en"
import time
import locale
import math
from typing import NamedTuple
from collections import OrderedDict
from ._structures import struct_time, ShortFormStruct, LongFormStruct
from .badi_calendar import BahaiCalendar
[docs]
class TimeDateUtils(BahaiCalendar):
"""
This class provides utility functionality to the datetime package. Its
use is through a pre-instantiated object _td_utils.
"""
# Badi additions are %:K for Kull-i-Shay and %:V for Váḥid.
_VALID_FORMAT_CHRS = 'aAbBcCdDefGhHIjkKlmMnprSTuUVWxXyYzZ%'
"""
str: A list of all the format characters.
"""
DAYNAMES = ('Jalál', 'Jamál', 'Kamál', 'Fiḍāl', '`Idāl',
'Istijlāl', 'Istiqlāl')
"""
tuple: The full day names.
"""
DAYNAMES_ABV = ('Jal', 'Jam', 'Kam', 'Fiḍ', 'Idā', 'Isj', 'Isq')
"""
tuple: The abbreviated day names.
"""
MONTHNAMES = OrderedDict((
(1, 'Bahá'), (2, 'Jalál'), (3, 'Jamál'), (4, "'Aẓamat"), (5, 'Núr'),
(6, 'Raḥmat'), (7, 'Kalimát'), (8, 'Kamál'), (9, "Asmá'"),
(10, "'Izzat"), (11, 'Mashíyyat'), (12, "'Ilm"), (13, 'Qudrat'),
(14, 'Qawl'), (15, 'Masá’il'), (16, 'Sharaf'), (17, 'Sulṭán'),
(18, 'Mulk'), (0, 'Ayyám-i-Há'), (19, "'Alá'")))
"""
dict: The full month names.
"""
MONTHNAMES_ABV = OrderedDict((
(1, 'Bah'), (2, 'Jal'), (3, 'Jam'), (4, 'Aẓa'), (5, 'Núr'), (6, 'Raḥ'),
(7, 'Kal'), (8, 'Kam'), (9, 'Asm'), (10, 'Izz'), (11, 'Mas'),
(12, 'Ilm'), (13, 'Qud'), (14, 'Qaw'), (15, 'Mas'), (16, 'Sha'),
(17, 'Sul'), (18, 'Mul'), (0, 'Ayy'), (19, 'Alá')))
"""
dict: The abbreviated month names.
"""
ORDINAL_1ST_YEAR_EPOCH = 1721501.260417
"""
float: The first day that we start our count, Julian year 1, March 19th.
"""
DAYS_BEFORE_1ST_YEAR = 77
"""
int: Keeps the Badí' day count in par with the Gregorian day count
for ordinals.
"""
_SHORT_STRUCT_TM_ITEMS = 6
"""
int: Length of the short form Badí' date and time portion of the
struct time.
"""
_LONG_STRUCT_TM_ITEMS = 8
"""
int: Length of the long form Badí' date and time portion of the
struct time.
"""
_BADI_MONTH_NUM_DAYS = [
(1, 19), (2, 19), (3, 19), (4, 19), (5, 19), (6, 19), (7, 19),
(8, 19), (9, 19), (10, 19), (11, 19), (12, 19), (13, 19), (14, 19),
(15, 19), (16, 19), (17, 19), (18, 19), (0, 0), (19, 19)
]
"""
list: Provides a list of month number and the number of days in that
month. Month 0 (zero) is in the 19th position and is modified depending
on if a leap year.
"""
def __init__(self):
"""
We need to set the locale. However if an unsupported locale is
required it may nee to be installed. This is the Linux method.
1. Find the supported locales:
$ locale -a
2. Set a new locale:
$ sudo locale-gen fr_FR.UTF-8 # Use the the locale you need.
$ sudo update-locale
"""
super().__init__(self)
self._locale_data = {}
self._date_and_time_locale()
[docs]
def _date_and_time_locale(self):
"""
Set the `_locale_data` dictionary instance object to locale date
and time information.
"""
self._locale_data['locale'] = locale.setlocale(locale.LC_TIME, '')
self._locale_data['am'] = locale.nl_langinfo(locale.AM_STR)
self._locale_data['pm'] = locale.nl_langinfo(locale.PM_STR)
try:
# Get the date format for the current locale
date_format = locale.nl_langinfo(locale.D_FMT)
except AttributeError: # pragma: no cover
date_format = '%m/%d/%y'
self._locale_data['d_format'] = self._order_format(
date_format, '%m/%d/%y')
self._locale_data['t_format'] = self._order_format(
self._find_time_order(), '%H:%M:%S')
[docs]
def _find_time_order(self):
"""
Find the locale time delimiter and id 24 or 12 hour time is used.
:returns: The time delimiter and 24 or 12 hour time.
:rtype: str
"""
delim = time.strftime('%X')[2]
h = 'I' if time.strftime('%p') != "" else 'H'
return f"%{h}{delim}%M{delim}%S"
@property
def locale(self): # pragma: no cover
"""
Returns the current locale.
"""
return self._locale_data['locale']
@property
def am(self):
"""
Returns the AM designator.
"""
return self._locale_data['am']
@property
def pm(self):
"""
Returns the PM designator.
"""
return self._locale_data['pm']
@property
def date_format(self):
"""
Returns the locale's date format.
"""
return self._locale_data['d_format']
@property
def time_format(self):
"""
Returns the locale's time format.
"""
return self._locale_data['t_format']
[docs]
def _checktm(self, ttup: tuple) -> None:
"""
Check that the fields in the tuple are of the correct type. This
check on date information is different than what is found inn the
badi_calendar.py module as it needs to conform with ISO standards.
:param ttup: A time tuple.
:type ttup: ShortFormStruct or LongFormStruct
"""
if not issubclass(ttup.__class__, tuple):
raise TypeError(
f"The ttup argument {ttup.__class__} is not a proper tuple.")
def process_long_form(ttup: tuple):
assert (self.KULLISHAY_MIN <= ttup[0] <= self.KULLISHAY_MAX), (
f"Invalid kull-i-shay {ttup[0]}, it must be in the range of "
f"[{self.KULLISHAY_MIN}, {self.KULLISHAY_MAX}].")
assert 1 <= ttup[1] < 20, (
f"Invalid Váḥids '{ttup[1]}' in a Kull-i-Shay’, it must be in "
"the range of [1, 19].")
assert 1 <= ttup[2] < 20, (
f"Invalid year '{ttup[2]}' in a Váḥid, it must be in the "
"range of [1, 19].")
return (ttup[0] - 1) * 361 + (ttup[1] - 1) * 19 + ttup[2]
def process_short_form(ttup: tuple):
assert self.MINYEAR <= ttup[0] <= self.MAXYEAR, (
f"Invalid year '{ttup[0]}' it must be in the range of ["
f"{self.MINYEAR}, {self.MAXYEAR}].")
return ttup[0]
t_len = len(ttup)
named_tuple = hasattr(ttup, '_asdict') # _make also can work
if named_tuple: # Both long and short NamedTuple
if t_len == 13: # Long form
year = process_long_form(ttup)
idx = 3
elif t_len == 11: # Short form
year = process_short_form(ttup)
idx = 1
else: # pragma: no cover
raise TypeError(f"Invalid timetuple, found length {t_len}.")
else: # A Tuple or class derived from a standard tuple
if t_len == 11: # Long form
year = process_long_form(ttup)
idx = 3
elif t_len == 9: # Short form
year = process_short_form(ttup)
idx = 1
else:
raise TypeError(f"Invalid timetuple, found length {t_len}.")
month = ttup[idx]
day = ttup[idx+1]
hour = ttup[idx+2]
minute = ttup[idx+3]
second = ttup[idx+4]
wday = ttup[idx+5]
yday = ttup[idx+6]
isdst = ttup[idx+7]
assert 0 <= month < 20, (
f"Invalid month '{month}', it must be in the range of [0, 19].")
cycle = (4 + self._is_leap_year(year)) if month == 0 else 19
assert 1 <= day <= cycle, (
f"Invalid day '{day}' for month '{month}', it must be in the "
f"range of [1, {cycle}].")
assert 0 <= hour <= 24, (
f"Invalid hour '{hour}', it must be in the range of [0, 24].")
assert 0 <= minute < 60, (
f"Invalid minute '{minute}', it must be in the range of [0, 60].")
assert 0 <= second <= 61, (
f"Invalid second '{second}', it must be in the range of [0, 61].")
assert 0 <= wday <= 6, (
f"Invalid week day '{wday}', it must be in the range of [0, 6].")
assert 1 <= yday <= 366, (
f"Invalid day '{yday}' in year, it must be in the range of "
"[1, 366].")
assert -1 <= isdst <= 1, (
f"Invalid isdst '{isdst}', it must be in the range of [-1, 1].")
[docs]
def a(self, ttup, org, mod):
"""
Abbreviated weekday.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The abbreviated weekday.
:rtype: str
"""
return self.DAYNAMES_ABV[ttup.tm_wday]
[docs]
def A(self, ttup, org, mod):
"""
Full weekday name.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The full weekday.
:rtype: str
"""
return self.DAYNAMES[ttup.tm_wday]
[docs]
def b(self, ttup, org, mod):
"""
Abbreviated month name.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The abbreviated month name.
:rtype: str
"""
return self.MONTHNAMES_ABV[ttup.tm_mon]
[docs]
def B(self, ttup, org, mod):
"""
Full month name.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The full month name.
:rtype: str
"""
return self.MONTHNAMES[ttup.tm_mon]
[docs]
def c(self, ttup, org, mod):
"""
Locale specific date and time. Equivalent to "%a %b %d %H:%M:%S %Y".
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The locale specific date and time.
:rtype: str
"""
st = f"{self.DAYNAMES_ABV[ttup.tm_wday]} "
st += f"{self.MONTHNAMES_ABV[ttup.tm_mon]} "
st += f"{ttup.tm_mday:02} "
st += f"{ttup.tm_hour:02}:"
st += f"{ttup.tm_min:02}:"
st += f"{ttup.tm_sec:02} "
if not ttup.short:
st += f"{ttup.tm_kull_i_shay} "
st += f"{ttup.tm_vahid:02} "
st += f"{ttup.tm_year:02}"
else:
st += f"{ttup.tm_year:04}"
return st
[docs]
def C(self, ttup, org, mod):
"""
Century as a space-padded decimal number. (The year divided by 100
then truncated to an integer.)
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The century as a space-padded decimal number.
:rtype: str
"""
year = self._get_year(ttup)
n = '-' if year < 0 else ''
return f"{n}{abs(math.floor(year / 100)):02}"
[docs]
def d(self, ttup, org, mod):
"""
Day of month as a zero-padded decimal number.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The day of month as a zero-padded decimal number.
:rtype: str
"""
if mod == '-':
st = f"{ttup.tm_mday}"
else:
pad = ' ' if org == 'e' else 0
st = f"{ttup.tm_mday:{pad}2}"
return st
[docs]
def D(self, ttup, org, mod):
"""
Date where year is without century. Equivalent to a localized %m/%d/%y.
.. note::
Return a locale dependent Badí' short date. Badí' long dates are
converted to short dates first.
This method does not take into account format extenders, in other
words the - or : after the %. They should never show up in the
locale.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The date where year is without century.
:rtype: str
"""
year = self._get_year(ttup)
century = int(year / 100) * 100
year -= 0 if year < century else century
sep = self.date_format[0]
data = []
for p in range(1, 4):
fmt = self.date_format[p]
if fmt[-1] in 'yY':
data.append(f"{year:02}")
else:
org = fmt[0]
mod = ''
data.append(f"{getattr(self, org)(ttup, org, mod):02}")
return sep.join(data)
[docs]
def f(self, ttup, org, mod):
"""
Microseconds as a decimal number.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The Microseconds as a decimal number.
:rtype: str
"""
s, m = self._sec_microsec_from_seconds(ttup.tm_sec)
return f"{round(m, 6):06}"
[docs]
def G(self, ttup, org, mod):
"""
ISO 8601 year with century as a zero-padded decimal number.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The ISO 8601 year with century as a zero-padded decimal
number.
:rtype: str
"""
year = self._get_year(ttup)
n = '-' if year < 0 else ''
return f"{n}{abs(year):04}"
[docs]
def H(self, ttup, org, mod):
"""
Hour (24-hour clock) as a decimal number. Either zero-padded if %H
or not zero-padded if %-H.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The hour (24-hour clock) as a decimal number.
:rtype: str
"""
if mod == '-': # %-H
st = f"{ttup.tm_hour}"
else: # %H
st = f"{ttup.tm_hour:02}"
return st
[docs]
def I(self, ttup, org, mod):
"""
Hour (12-hour clock) as a zero-padded decimal number.
.. note::
1. If we assume that sunset was at 1800 hrs UTC then the Badí' noon
would be about 0600 hrs UTC the next morning. This changes on a
daily bases because sunset changes and there is seldom exactly 24
hours between two sunsets.
2. 1st we need to find sunset for the provided date and the day
after. Subtract these two times and divide the results by 2 to
determine noon. Then determine which side of noon the current
Badí' time is on.
3. Does a 12-hour clock make sense in a Badí' time?
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The hour (12-hour clock) as a zero-padded decimal number.
:rtype: str
"""
midday_frac = self._find_midday(ttup)
time_frac = self._decimal_day_from_hms(ttup.tm_hour, ttup.tm_min,
ttup.tm_sec)
if midday_frac <= time_frac:
hour = ttup.tm_hour - 12
else:
hour = ttup.tm_hour
if org == 'l' and mod == '-':
st = f"{hour}"
elif org == 'l':
st = f"{hour: 2}"
else: # %I
st = f"{hour:02}"
return st
[docs]
def j(self, ttup, org, mod):
"""
Day of the year as a decimal number. Either zero-padded if %j or
not zero-padded if %-j.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The day of the year as a decimal number.
:rtype: str
"""
return f"{ttup.tm_yday}" if mod == '-' else f"{ttup.tm_yday:03}"
[docs]
def K(self, ttup, org, mod):
"""
Kull-i-Shay as a negative or positive decimal number.
.. note::
If the mod is not a : (colon) them return an empty string.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The Kull-i-Shay as a decimal number.
:rtype: str
"""
st = ""
if mod == ':':
if ttup.short:
k = ttup.tm_year / 361
kull_i_shay = 0 if ttup.tm_year == 0 else math.ceil(k)
else:
kull_i_shay = ttup.tm_kull_i_shay
n = '-' if kull_i_shay < 0 else ''
st += f"{n}{kull_i_shay}"
return st
[docs]
def m(self, ttup, org, mod):
"""
Month as a decimal number. Either zero-padded if %m or not
zero-padded if %-m.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The month as a decimal number.
:rtype: str
"""
return f"{ttup.tm_mon}" if mod == '-' else f"{ttup.tm_mon:02}"
[docs]
def M(self, ttup, org, mod):
"""
Minute as a zero-padded decimal number. Either zero-padded if %M or not
zero-padded if %-M.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The minute as a zero-padded decimal number.
:rtype: str
"""
return f"{ttup.tm_min}" if mod == '-' else f"{ttup.tm_min:02}"
[docs]
def n(self, ttup, org, mod):
"""
Newline character.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The newline character.
:rtype: str
"""
return "\n"
[docs]
def p(self, ttup, org, mod):
"""
Locale defined AM and PM.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The locale defined AM and PM.
:rtype: str
"""
midday_frac = self._find_midday(ttup)
time_frac = self._decimal_day_from_hms(ttup.tm_hour, ttup.tm_min,
ttup.tm_sec)
if midday_frac <= time_frac:
st = self.pm
else:
st = self.am
return st
[docs]
def r(self, ttup, org, mod):
"""
Locale defined 12-hour clock time (am/pm).
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The locale defined 12-hour clock time.
:rtype: str
"""
sec = math.floor(ttup.tm_sec)
if org == 'T':
st = f"{ttup.tm_hour:02}:{ttup.tm_min:02}:{sec:02}"
else:
hour = self.I(ttup, '', '')
p = self.p(ttup, '', '')
st = hour + f":{ttup.tm_min:02}:{sec:02} " + p
return st
[docs]
def S(self, ttup, org, mod):
"""
Second as a decimal number. Either zero-padded if %S or not
zero-padded if %-S.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The second as a decimal number.
:rtype: str
"""
return f"{ttup.tm_sec}" if mod == '-' else f"{ttup.tm_sec:02}"
[docs]
def u(self, ttup, org, mod):
"""
Weekday as a decimal number. With Jalál as 1.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The weekday as a decimal number.
:rtype: str
"""
wday = ttup.tm_wday + (0 if org == 'w' else 1)
return f"{wday}"
[docs]
def U(self, ttup, org, mod):
r"""
Week number of the year (Jalál as the first day of the week) as a
zero-padded decimal number. All days in a new year preceding the
first \`Idāl are considered to be in week 0.
.. note::
It makes no sense to start a week on different day in the Badí'
Calendar. So the %W format is the same as %U.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The week number of the year.
:rtype: str
"""
year = self._get_year(ttup)
year, week, day = self._year_week_day(year, ttup.tm_mon,
ttup.tm_mday, week0=True)
return f"{week:02}"
[docs]
def V(self, ttup, org, mod):
"""
ISO 8601 week as a decimal number with Jalál as the first day of the
week. Week 01 is the week containing the 4th of Bahá. Either
zero-padded if %V or not zero-padded if %-V.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The ISO 8601 week as a decimal number.
:rtype: str
"""
if mod == ':':
if ttup.short:
date = self.long_date_from_short_date(
(ttup.tm_year, ttup.tm_mon, ttup.tm_mday))
st = f"{date[1]:02}"
else:
st = f"{ttup.tm_vahid:02}"
else:
year = self._get_year(ttup)
year, week, day = self._year_week_day(year, ttup.tm_mon,
ttup.tm_mday)
st = f"{week:02}"
return st
[docs]
def x(self, ttup, org, mod):
"""
Locale defined date representation.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The locale defined date representation.
:rtype: str
"""
delim = self.date_format[0]
data = []
for fmt in self.date_format[1:]:
data.append(getattr(self, fmt)(ttup, '', ''))
return f"{delim}".join(data)
[docs]
def X(self, ttup, org, mod):
"""
Locale defined time representation.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The locale defined time representation.
:rtype: str
"""
delim = self.time_format[0]
data = []
for fmt in self.time_format[1:]:
data.append(getattr(self, fmt)(ttup, '', ''))
return f"{delim}".join(data)
[docs]
def y(self, ttup, org, mod):
"""
Year without century as a decimal number. Either zero-padded if %y
or not zero-padded if %-y.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The year without century as a decimal number.
:rtype: str
"""
year = self._get_year(ttup)
century = int(year / 100) * 100
year = year - century
return f"{year}" if mod == '-' else f"{year:02}"
[docs]
def Y(self, ttup, org, mod):
"""
Year with century as a zero-padded decimal number.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The year with century as a decimal number.
:rtype: str
"""
year = self._get_year(ttup)
n = '-' if year < 0 else ''
return f"{n}{abs(year):04}"
[docs]
def z(self, ttup, org, mod):
"""
UTC offset in the form ±HHMM[SS[.ffffff]] (empty string if the
object is naive).
.. note::
Some equivalents from offset to ISO standard.
* -14400.0 == -0400
* 37080 == +1030
* 22829.4 == +063415
* 11056.44427776 == +030712.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The UTC offset in the form ±HHMM[SS[.ffffff]].
:rtype: str
"""
st = ""
if ttup.tm_gmtoff:
n = '-' if ttup.tm_gmtoff < 0 else '+'
h = abs(ttup.tm_gmtoff / 3600)
hh = int(h)
m = h - hh
m0 = m * 100
mm = math.floor(m0)
s = m0 - mm
ss = int(s * 100)
ms = int((s * 100 - ss) * 1000000)
delim = ':' if mod == ':' else ""
st += f"{n}{hh:02}{delim}{mm:02}"
st += f"{delim}{ss:02}" if ss > 0 else ""
st += f".{ms:06}" if ms > 0 else ""
return st
[docs]
def Z(self, ttup, org, mod):
"""
Time zone name (empty string if the object is naive).
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The time zone name.
:rtype: str
"""
return f"{ttup.tm_zone}" if ttup.tm_zone else ""
[docs]
def percent(self, ttup, org, mod):
"""
The literal '%' character.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param str, org: The original text code.
:param str mod: Code modifier.
:returns: The literal '%' character.
:rtype: str
"""
return "%"
__METHOD_LOOKUP = {'a': a, 'A': A, 'b': b, 'B': B, 'c': c, 'C': C, 'd': d,
'D': D, 'e': d, 'f': f, 'G': G, 'h': b, 'H': H, 'I': I,
'j': j, 'k': H, 'K': K, 'l': I, 'm': m, 'M': M, 'm': m,
'M': M, 'n': n, 'p': p, 'r': r, 'S': S, 'T': r, 'u': u,
'U': U, 'V': V, 'w': u, 'W': U, 'x': x, 'X': X, 'y': y,
'Y': Y, 'z': z, 'Z': Z, '%': percent
}
"""
dict: An internal list of the format methods.
"""
[docs]
def strftime(self, format: str, ttup: tuple, tzinfo=None) -> str:
"""
Convert a struct_time object into a string according to a specified
format.
:param str format: A string format.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:param tzinfo tzinfo: A tzinfo object.
:returns: A struck_time object converted to a formatted string.
:rtype: str
"""
self._check_format(format)
self._checktm(ttup)
if not isinstance(ttup, (ShortFormStruct, LongFormStruct)):
ttup = struct_time(ttup, tzinfo=tzinfo)
idx, fmtlen = 0, len(format)
strf = ""
while idx < fmtlen:
ch = format[idx]
if ch == '%' and idx+1 < fmtlen:
ch0 = format[idx+1]
i = 2 if ch0 in '-:' else 1
ch1 = format[idx+i]
strf += self.__METHOD_LOOKUP[ch1](
self, ttup, ch1, ch0 if i == 2 else '')
elif format[idx-1] not in '%-:':
strf += ch
idx += 1
return strf
[docs]
def _find_midday(self, ttup: tuple) -> tuple:
"""
Midday time in hours, minutes, and seconds representing the Badí'
midday.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:returns: The Badí' midday.
:rtype: tuple
"""
if ttup.short:
date = (ttup.tm_year, ttup.tm_mon, ttup.tm_mday, ttup.tm_hour,
ttup.tm_min, ttup.tm_sec)
else:
date = (ttup.tm_kull_i_shay, ttup.tm_vahid, ttup.tm_year,
ttup.tm_mon, ttup.tm_mday, ttup.tm_hour, ttup.tm_min,
ttup.tm_sec)
return self.midday(date, _short=ttup.short)
[docs]
def _get_year(self, ttup: tuple) -> int:
"""
Convert The Kull-i-Shay', Váḥid, year to a short mode year.
:param ttup: A struct_time object.
:type ttup: ShortFormStruct or LongFormStruct
:returns: The short form year converted if necessary.
:rtype: int
"""
return (ttup.tm_year if ttup.short else
((ttup.tm_kull_i_shay - 1) * 361 + (ttup.tm_vahid - 1) * 19 +
ttup.tm_year))
[docs]
def _year_week_day(self, year: int, month: int, day: int,
week0: bool=False) -> tuple:
"""
Return the year, week, and day of the week from a short form
Badí' date.
:param int, year: The year.
:param int month: The month.
:param int day: The day.
:returns: The year, week, and day of the week.
:rtype: tuple
"""
week1jalal = self._isoweek1jalal(year)
today = self._ymd2ord(year, month, day)
# Internally, week and day have origin 0
week, day = divmod(today - week1jalal, 7)
if not week0:
if week < 0:
year -= 1
week1jalal = self._isoweek1jalal(year)
week, day = divmod(today - week1jalal, 7)
elif week >= 52:
if today >= self._isoweek1jalal(year+1):
year += 1
week = 0
return year, week+1, day + 1
[docs]
def _days_before_year(self, year: int) -> float:
"""
Get the number of days before the 1st of Bahá of the year.
:param int year: Badí' year
:returns: The number of days since (-1841, 19, 19) of the Badí'
calendar.
:rtype: int
"""
jd0 = self.jd_from_badi_date((self.MINYEAR-1, 19, 19))
jd1 = self.jd_from_badi_date((year, 1, 1))
return math.floor(jd1 - jd0) - 1
[docs]
def _days_before_month(self, year: int, month: int) -> int:
"""
The number of days in the year preceding the first day of month.
:param int year: Badí' year
:param int month: Badí' month (0..19)
:returns: The number in the year preceding the first day of month.
:rtype: int
"""
month -= -18 if month < 2 else 1 if 1 < month < 19 else 19
dbm = 0
if 0 < month < 19:
dbm += month * 19
elif month == 0:
dbm += 18 * 19 + 4 + self._is_leap_year(year)
return dbm
[docs]
def _day_of_week(self, year: int, month: int, day: int) -> int:
"""
Find the day of the week where 0 == Jalál (Saturday) and
6 == Istiqlāl (Friday).
:param int year: Badí' year
:param int month: Badí' month (0..19)
:param int day: Badí' day
:returns: The numerical day of the week.
:rtype: int
"""
# Since the usual start day is Monday (Kamál) a properly aligned
# day number to the day name we need to add 1 to the ordinal.
return ((self._ymd2ord(year, month, day) + 1) % 7 + 7) % 7
[docs]
def _ymd2ord(self, year: int, month: int, day: int) -> int:
"""
Get the number of days since Badí' year -1842 (Julian 0001-03-19)
including the current day.
year, month, day -> ordinal, considering -1842-01-01 as day 1
.. note::
We add 77 days to the total so that the ordinal number can be
compared to the ordinals in the standard datetime package.
:param int year: Badí' year
:param int month: Badí' month [0, 19]
:param int day: Badí' day
:returns: The number of days since Badí' year -1842 including the
current day.
:rtype: int
"""
# For some reason out of the 3004 years that are provided only
# these three years are off by 1.
if year in (-1796, -1792, -1788):
fudge = 1
else:
fudge = 0
return (self._days_before_year(year) + self._days_before_month(
year, month) + day + fudge + self.DAYS_BEFORE_1ST_YEAR)
[docs]
def _ord2ymd(self, n: int, *, short: bool=False) -> tuple:
"""
It is difficult to do this in the Badí' Calendar because a Badí' day
can be more or less than 24 hours depending on when sunset is and the
time of the year. From the summer Solstice to the winter Solstice the
days get shorter. The day slowly comes down to 24 hours around the
Fall Equinox and then below 24 hours. The inverse happens between the
Winter Solstice and the Summer Solstice. We just use the BadiCalendar
API.
:param int n: The ordinal number of days from the MINYEAR.
:param bool short: If True then parse for a short date else if False
parse for a long date.
:returns: The Badí' date.
:rtype: tuple
"""
# We subtract 77 days from the total then add the value of n so that
# the Badí' date will be the same as the date value passed into
# _ymd2ord and give the same date as Python standard datetime package.
# The reason we need to do this is that the first date that this
# package can provide is equivalent to Julian year 1, March, 19th.
jd = self.ORDINAL_1ST_YEAR_EPOCH - 1 - self.DAYS_BEFORE_1ST_YEAR + n
jd0 = math.floor(jd) + 0.5
return self.badi_date_from_jd(jd0, short=short, trim=True, rtd=True)
[docs]
def _build_struct_time(self, date: tuple, dstflag: int, *, tzinfo=None,
short_in: bool=False) -> NamedTuple:
"""
Build either a ShortFormStruct or LongFormStruct struct_time.
:param tuple date: A tuple date and time object.
:param int dstflag: A flag indicating daylight savings time. May be
set to 1 when daylight savings time is in effect,
and 0 when it is not. A value of -1 indicates
that this is not known.
:param tzinfo tzinfo: If provided a tzinfo object.
:returns: A struct_time object.
:rtype: ShortFormStruct or LongFormStruct
"""
if short_in:
y, m, d, hh, mm, ss = date
else:
# Microsecond (ms) is not used.
y, m, d, hh, mm, ss, ms = self.short_date_from_long_date(date)
wday = self._day_of_week(y, m, d)
dnum = self._days_before_month(y, m) + d
return struct_time(date + (wday, dnum, dstflag), tzinfo=tzinfo)
[docs]
def _isoweek_to_badi(self, year: int, week: int, day: int, *,
short: bool=False) -> tuple:
"""
The week counts from Jalál (Saturday) as the first day and Istiqlāl
(Friday) the last day of the week. This is different from the usual
way ISO weeks are counted in the Gregorian Calendar which is Monday
to Sunday.
:param int year: Badí' year.
:param int month: Badí' month (0..19)
:param int day: Badí' day in week.
:param bool short: If True then parse for a short date else if False
parse for a long date.
:returns: A Badí' date.
:rtype: tuple
:raises AssertionError: If the week or weekday is out of range.
"""
p_offset = 0
if not 0 < week < 52: # We're looking for only the 53rd week.
day_one = _td_utils._day_of_week(year, 1, 1) + 1
if day_one in (3, 4):
out_of_range = True
if week == 52:
# In Badí' years that have 52 weeks and start on the 3rd
# day (Kamál) or the 4th day (Fiḍāl).
# Badí' weeks start on Jalal (Saturday).
p_offset = 7
out_of_range = False
assert not out_of_range, f"Invalid week: {week}"
assert 0 < day < 8, f"Invalid weekday: {day} (range is [1, 7])"
# Now compute the offset from (Y, 1, 1) in days:
day_offset = (week - 1) * 7 + (day - 1) + p_offset
# Calculate the ordinal day for Jalal, week 1
day_1 = self._isoweek1jalal(year)
ord_day = day_1 + day_offset
return self._ord2ymd(ord_day, short=short)
[docs]
def _isoweek1jalal(self, year: int) -> int:
"""
Calculate the day number of Jalál (Saturday) starting week 1. It
would be the first week with 4 or more days in the year in question.
:param int year: Badí' year
:returns: The number of the first Jalál in the Badí' year.
:rtype: int
"""
firstday = self._ymd2ord(year, 1, 1) # 1st day of year
# We subtract 6 instead of add 6 as is done in _isoweek1Monday.
firstweekday = (firstday - 6) % 7
week1jalal = firstday - firstweekday
if firstweekday > 3: # First week day >= Fidal
week1jalal += 7
return week1jalal
[docs]
def _check_date_fields(self, a: int, b: int, c: int, d: int=None,
e: int=None, *, short_in: bool=False) -> None:
"""
Check the validity of the date.
:param int a: The long form Kull-i-Shay or short form year.
:param int b: The long form Váḥid or short form month.
:param int c: The long form year or short form day.
:param int d: The long form month.
:param int e: The long form day.
:param bool short_in: If True then parse for a short date else if False
parse for a long date. This is for incoming dates
not outgoing dates as in most other uses of
'short'.
:returns: Nothing
:rtype: None
:raises AssertionError: If any of the date values are out of range.
"""
if short_in:
b_date = (a, b, c)
else:
b_date = (a, b, c, d, e)
self._check_valid_badi_date(b_date, short_in=short_in)
[docs]
def _check_time_fields(self, hour: int, minute: int, second: int,
microsecond: int, fold: int) -> None:
"""
Check the validity of the time.
:param int hour: The hour.
:param int minute: The minute.
:param int second: The second.
:param int microsecond: The microsecond.
:param int fold: The value of 1 if the time is in the time fold when
the time falls back one hour in the Autumn or 0 any
other time of the year.
"""
self._check_valid_badi_time(hour, minute, second, microsecond,
maxsec=61)
assert fold in (0, 1), (
f"The fold argument '{fold}' must be either 0 or 1.")
[docs]
def _wrap_strftime(self, obj, format: str, timetuple: tuple,
tzinfo=None) -> str:
"""
Correctly substitute for %z and %Z escapes in strftime formats.
:param class obj: A class instance that is calling this method.
:param str format: The formatted string.
:param timetuple:
:type ttup: ShortFormStruct or LongFormStruct
:returns: A correctly formatted string.
:rtype: str
"""
# Don't call utcoffset() or tzname() unless actually needed.
freplace = None # the string to use for %f
# Scan format for %z and %Z escapes, replacing as needed.
newformat = []
push = newformat.append
i, n = 0, len(format)
tzinfo = obj.tzinfo if hasattr(obj, 'tzinfo') else None
while i < n:
ch = format[i]
i += 1
if ch == '%':
if i < n:
ch = format[i]
i += 1
if ch == 'f':
if freplace is None:
freplace = f'{getattr(obj, "microsecond", 0):06d}'
push(freplace)
else:
push('%')
push(ch)
else:
push('%')
else:
push(ch)
newformat = "".join(newformat)
return self.strftime(newformat, timetuple, tzinfo=tzinfo)
_td_utils = TimeDateUtils()