Source code for badidatetime.gregorian_calendar

# -*- coding: utf-8 -*-
#
# badidatetime/gregorian_calendar.py
#
__docformat__ = "restructuredtext en"

import math

from badidatetime.base_calendar import BaseCalendar


[docs] class GregorianCalendar(BaseCalendar): """ Implementation of the Gregorian Calendar. """ # Julian date for the Gregorian epoch # https://www.grc.nasa.gov/www/k-12/Numbers/Math/Mathematical_Thinking/calendar_calculations.htm _GREGORIAN_EPOCH = 1721423.5 _MONTHS = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) def __init__(self) -> None: super().__init__() # [year, month, day] self._gregorian_date = None
[docs] def jd_from_gregorian_date(self, g_date: tuple, *, exact: bool=False, alt: bool=False) -> float: """ Convert Gregorian dates to Julian day count with the 1582 10, 15 correction. :param tuple g_date: A Gregorian date in the (year, month, day) format. :param bool exact: Julian days as if the Gregorian calendar started on year 1. This is astronomically correct but not historically correct. :param bool alt: Use a more accurate leap year calculation, only valid when the `exact` keyword is used, there is no effect otherwise. :returns: A Julian day in UT time. :rtype: float .. note:: 1. See Astronomical Formulae for Calculators Enlarged & Revised, by Jean Meeus ch3 p24-25 2. See: https://www.fourmilab.ch/documents/calendar/ https://core2.gsfc.nasa.gov/time/julian.html 3. Caution when using the `exact=True` keyword the Julian Period days returned will not always be the same when `exact=False` is used. This means date comparisons will be in error if both dates do not use the same True or False `exact` keyword. """ year, month, day = self.date_from_ymdhms(g_date) if exact: # Astronomically correct algorithm td = self._days_in_years(year-1, alt=alt) days = td + (self._GREGORIAN_EPOCH - 1) month_days = list(self._MONTHS) month_days[1] += self._is_leap_year(year, alt=alt) days += sum(month_days[:month-1]) + day jd = round(days, self._ROUNDING_PLACES) else: # Meeus historically correct algorithm if (year, month) == (1582, 10): assert day not in (5, 6, 7, 8, 9, 10, 11, 12, 13, 14), ( f"The days 5-14 in 1582-10 are invalid, found day '{day}'." ) if month <= 2: year -= 1 month += 12 if (year, month, day) >= (1582, 10, 15): a = math.floor(year / 100) b = 2 - a + math.floor(a / 4) else: b = 0 jd = round(math.floor(self._JULIAN_YEAR * year) + math.floor( 30.6001 * (month + 1)) + day + b + 1720994.5, self._ROUNDING_PLACES) return jd
[docs] def gregorian_date_from_jd(self, jd: float, *, hms: bool=False, us: bool=False, exact: bool=False, alt: bool=False) -> tuple: """ Convert Julian day to Gregorian date. :param float jd: A Julian period day. This value should be an historical JD if exact is False and an astronomical JD if exact is True. :param bool hms: If `True` convert a fractional day to hours, minutes, seconds, and microseconds else if `False` (default) do not convert. :param bool exact: If `False` (default) Meeus' historically algorithm is used, else if `True` the more astronomically correct algorithm is used. :param bool alt: If `False` (default) the more common 4|100|400 algorithm for leap years is used else if `True` the less common 4|128 algorithm for leap years is used. This argument only has an effect if the `exact` argument is also used. :returns: A Gregorian date in the (year, month, day) format. :rtype: tuple .. note:: See Astronomical Formulae for Calculators Enlarged & Revised, by Jean Meeus ch3 p26-29 """ if exact: # An astronomically correct algorithm. # Get the number of days since the Gregorian epoch. md = jd - (self._GREGORIAN_EPOCH - 1) year = math.floor(abs(md / self._MEAN_TROPICAL_YEAR)) + 1 year *= -1 if md < (self._GREGORIAN_EPOCH - 1) else 1 # Refine the number of days since the epoch for the date. td = self._days_in_years(year, alt=alt) days = md - td while days > 365: year += 1 td = self._days_in_years(year, alt=alt) days = md - td if days == 0: days = 365 if year < 0 and year % 4 != 0 else 366 else: year += 1 month_days = list(self._MONTHS) month_days[1] += self._is_leap_year(year, alt=alt) d = day = 0 for month, ds in enumerate(month_days, start=1): d += ds if days > d: continue day = math.ceil(days - (d - ds)) break f = jd % 1 day += f - (1.5 if f > 0.5 else 0.5) if day < 1: month -= 1 if month > 1 else -11 day = month_days[month-1] + day year -= 1 if month == 12 else 0 date = (year, month, round(day, self._ROUNDING_PLACES)) else: # Meeus' historically correct algorithm. j_day = jd + 0.5 z = math.floor(j_day) f = j_day % 1 if z >= 2299161: # 1582-10-15 Julian and Gregorian crossover. alpha = math.floor((z - 1867216.25) / 36524.25) a = z + 1 + alpha - math.floor(alpha / 4) else: a = z b = a + 1524 c = math.floor((b - 122.1) / 365.25) d = math.floor(365.25 * c) e = math.floor((b - d) / 30.6001) day = b - d - math.floor(30.6001 * e) + f month = 0 year = 0 if e > 13: month = e - 13 else: month = e - 1 if month > 2: year = c - 4716 else: year = c - 4715 date = (year, month, round(day, self._ROUNDING_PLACES)) if hms: date = self.ymdhms_from_date(date, us=us) return date
[docs] def posix_timestamp(self, t: float, *, zone: float=0, us: bool=False) -> tuple: """ Find the year, month, day, hours, minutes, and seconds from a POSIX timestamp updated for provided time zone. .. warning:: DO NOT USE THIS METHOD -- It's experimental and is not used in any other code. *** TODO *** Needs to be fixed for years before 1970 Gregorian. :param float t: POSIX timestamp :param float zone: Timezone :param bool us: If True the seconds are split to seconds and microseconds else if False the seconds has a fractional day as a decimal. :returns: The time of the day corrected for the timezone. :rtype: tuple """ t += zone * 3600 days = math.floor(t / self._SECONDS_PER_DAY) year = 1970 leap = False days = abs(days) + 1 # Find the year and remaining number of days. while True: leap = self._is_leap_year(year) diy = 365 + leap if days < diy: break days -= diy year += 1 month_days = list(self._MONTHS) month_days[1] += leap days_before_month = 0 for month, days_in_month in enumerate(month_days, start=1): days_before_month += days_in_month if days > days_before_month: continue day = days - (days_before_month - days_in_month) # if day > days_in_month: # month += 1 # day = day - days_in_month break seconds = t % self._SECONDS_PER_DAY minutes = math.floor(seconds / 60) minute = minutes % 60 hour = math.floor(minutes / 60) second = round(seconds % 60, 6) microsecond = 0 if us: microsecond = self._PARTIAL_SECOND_TO_MICROSECOND(second) second = math.floor(second) # print(f"{t:18.6f} {zone:>+2.2f} {year:02} {month:02} {day:02} " # f"{hour:02} {int(minute):02} {second:02} {microsecond}") date = (year, month, day, hour, minute, second ) + ((microsecond,) if us else ()) self._check_valid_gregorian_month_day(date, historical=True) return date
[docs] def gregorian_year_from_jd(self, jd: float) -> int: """ Find the Gregorian year from a Julian Period day. :param float jd: The Julian Period day. :returns: The year portion of the Julian day. :rtype: int """ return self.gregorian_date_from_jd(jd)[0]
[docs] def date_from_ymdhms(self, date: tuple) -> tuple: """ Convert (year, month, day, hour, minute, second) into a (year, month, day.fractional) date. :param tuple date: A six part date (y, m, d, hh, mm, ss). :returns: The three part (y, m, d.nnn). :rtype: tuple """ self._check_valid_gregorian_month_day(date) t_len = len(date) year = date[0] month = date[1] day = date[2] hour = date[3] if t_len > 3 and date[3] is not None else 0 minute = date[4] if t_len > 4 and date[4] is not None else 0 second = date[5] if t_len > 5 and date[5] is not None else 0 day += round(self._HR(hour) + self._MN(minute) + self._SEC(second), self._ROUNDING_PLACES) return year, month, day
[docs] def ymdhms_from_date(self, date: tuple, us: bool=False) -> tuple: """ Convert (year, month, day.fractional) into a (year, month, day, hour, minute, second, microseconds). :param tuple date: A three part date (y, m, d.nnn). :param bool us: If `True` return microseconds as separate field from seconds else if `False` (default) return seconds with fractional seconds. :returns: A 6 or 7 part date (y, m, d, hh, mm, ss, us). :rtype: tuple """ self._check_valid_gregorian_month_day(date) date_len = len(date) year = date[0] month = date[1] day = date[2] hour = date[3] if date_len > 3 else 0 minute = date[4] if date_len > 4 else 0 second = date[5] if date_len > 5 else 0 microsec = date[6] if date_len > 6 else 0 total_seconds = ((hour * 3600) + (minute * 60) + second + (microsec / 1e6)) day += total_seconds / self._SECONDS_PER_DAY hhmmssus = self._hms_from_decimal_day(day, us=us) return (year, month, math.floor(day)) + hhmmssus
[docs] def _is_leap_year(self, year: int, *, alt: bool=False) -> bool: """ Determine if a year is a leap year. This method supports two different algorithms the 4|100|400 and the 4|128. :param int year: The year to determine leap year for. :param bool alt: If `False` (default) use the 4|100|400 algorithm else if `True` use the 4|128 algorithm. :returns: `True` or `False`. :rtype: bool """ if alt: result = (year % 4 == 0) * (year % 128 != 0) == 1 else: result = ((year % 4 == 0) * ((year % 100 != 0) + (year % 400 == 0)) == 1) return result
[docs] def _check_valid_gregorian_month_day(self, g_date: tuple, historical: bool=False) -> None: """ Check that the month and day values are valid. :param tuple g_date: The date to check. :param bool historical: If True use the Julian leap year before 1883. :returns: Nothing :rtype: None """ t_len = len(g_date) year = g_date[0] month = abs(g_date[1]) day = abs(g_date[2]) LY = (self._JULIAN_LEAP_YEAR if historical and year < 1583 else self._is_leap_year) hour = g_date[3] if t_len > 3 and g_date[3] is not None else 0 minute = g_date[4] if t_len > 4 and g_date[4] is not None else 0 second = g_date[5] if t_len > 5 and g_date[5] is not None else 0 assert 1 <= month <= 12, f"Invalid month '{month}', should be 1 - 12." days = self._MONTHS[month - 1] if month == 2: # Subtract 0 or 1 from February if leap year. days += LY(year) assert 1 <= math.floor(day) <= days, ( f"Invalid day '{day}' for month '{month}' and year '{year}' " f"should be 1 - {days}.") assert hour < 24, f"Invalid hour '{hour}' it must be < 24" assert minute < 60, f"Invalid minute '{minute}' should be < 60." if any((hour, minute, second)): assert not day % 1, ("If there is a part day then there can be no " "hours, minutes, or seconds.") if any((minute, second)): assert not hour % 1, ( "If there is a part hour then there can be no minutes or " "seconds.") if second: assert not minute % 1, ( "If there is a part minute then there can be no seconds.")