summaryrefslogtreecommitdiffstats
path: root/pendulum/_extensions/helpers.py
diff options
context:
space:
mode:
Diffstat (limited to 'pendulum/_extensions/helpers.py')
-rw-r--r--pendulum/_extensions/helpers.py358
1 files changed, 358 insertions, 0 deletions
diff --git a/pendulum/_extensions/helpers.py b/pendulum/_extensions/helpers.py
new file mode 100644
index 0000000..16d078c
--- /dev/null
+++ b/pendulum/_extensions/helpers.py
@@ -0,0 +1,358 @@
+import datetime
+import math
+import typing
+
+from collections import namedtuple
+
+from ..constants import DAY_OF_WEEK_TABLE
+from ..constants import DAYS_PER_L_YEAR
+from ..constants import DAYS_PER_MONTHS
+from ..constants import DAYS_PER_N_YEAR
+from ..constants import EPOCH_YEAR
+from ..constants import MONTHS_OFFSETS
+from ..constants import SECS_PER_4_YEARS
+from ..constants import SECS_PER_100_YEARS
+from ..constants import SECS_PER_400_YEARS
+from ..constants import SECS_PER_DAY
+from ..constants import SECS_PER_HOUR
+from ..constants import SECS_PER_MIN
+from ..constants import SECS_PER_YEAR
+from ..constants import TM_DECEMBER
+from ..constants import TM_JANUARY
+
+
+class PreciseDiff(
+ namedtuple(
+ "PreciseDiff",
+ "years months days " "hours minutes seconds microseconds " "total_days",
+ )
+):
+ def __repr__(self):
+ return (
+ "{years} years "
+ "{months} months "
+ "{days} days "
+ "{hours} hours "
+ "{minutes} minutes "
+ "{seconds} seconds "
+ "{microseconds} microseconds"
+ ).format(
+ years=self.years,
+ months=self.months,
+ days=self.days,
+ hours=self.hours,
+ minutes=self.minutes,
+ seconds=self.seconds,
+ microseconds=self.microseconds,
+ )
+
+
+def is_leap(year): # type: (int) -> bool
+ return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
+
+
+def is_long_year(year): # type: (int) -> bool
+ def p(y):
+ return y + y // 4 - y // 100 + y // 400
+
+ return p(year) % 7 == 4 or p(year - 1) % 7 == 3
+
+
+def week_day(year, month, day): # type: (int, int, int) -> int
+ if month < 3:
+ year -= 1
+
+ w = (
+ year
+ + year // 4
+ - year // 100
+ + year // 400
+ + DAY_OF_WEEK_TABLE[month - 1]
+ + day
+ ) % 7
+
+ if not w:
+ w = 7
+
+ return w
+
+
+def days_in_year(year): # type: (int) -> int
+ if is_leap(year):
+ return DAYS_PER_L_YEAR
+
+ return DAYS_PER_N_YEAR
+
+
+def timestamp(dt): # type: (datetime.datetime) -> int
+ year = dt.year
+
+ result = (year - 1970) * 365 + MONTHS_OFFSETS[0][dt.month]
+ result += (year - 1968) // 4
+ result -= (year - 1900) // 100
+ result += (year - 1600) // 400
+
+ if is_leap(year) and dt.month < 3:
+ result -= 1
+
+ result += dt.day - 1
+ result *= 24
+ result += dt.hour
+ result *= 60
+ result += dt.minute
+ result *= 60
+ result += dt.second
+
+ return result
+
+
+def local_time(
+ unix_time, utc_offset, microseconds
+): # type: (int, int, int) -> typing.Tuple[int, int, int, int, int, int, int]
+ """
+ Returns a UNIX time as a broken down time
+ for a particular transition type.
+
+ :type unix_time: int
+ :type utc_offset: int
+ :type microseconds: int
+
+ :rtype: tuple
+ """
+ year = EPOCH_YEAR
+ seconds = int(math.floor(unix_time))
+
+ # Shift to a base year that is 400-year aligned.
+ if seconds >= 0:
+ seconds -= 10957 * SECS_PER_DAY
+ year += 30 # == 2000
+ else:
+ seconds += (146097 - 10957) * SECS_PER_DAY
+ year -= 370 # == 1600
+
+ seconds += utc_offset
+
+ # Handle years in chunks of 400/100/4/1
+ year += 400 * (seconds // SECS_PER_400_YEARS)
+ seconds %= SECS_PER_400_YEARS
+ if seconds < 0:
+ seconds += SECS_PER_400_YEARS
+ year -= 400
+
+ leap_year = 1 # 4-century aligned
+
+ sec_per_100years = SECS_PER_100_YEARS[leap_year]
+ while seconds >= sec_per_100years:
+ seconds -= sec_per_100years
+ year += 100
+ leap_year = 0 # 1-century, non 4-century aligned
+ sec_per_100years = SECS_PER_100_YEARS[leap_year]
+
+ sec_per_4years = SECS_PER_4_YEARS[leap_year]
+ while seconds >= sec_per_4years:
+ seconds -= sec_per_4years
+ year += 4
+ leap_year = 1 # 4-year, non century aligned
+ sec_per_4years = SECS_PER_4_YEARS[leap_year]
+
+ sec_per_year = SECS_PER_YEAR[leap_year]
+ while seconds >= sec_per_year:
+ seconds -= sec_per_year
+ year += 1
+ leap_year = 0 # non 4-year aligned
+ sec_per_year = SECS_PER_YEAR[leap_year]
+
+ # Handle months and days
+ month = TM_DECEMBER + 1
+ day = seconds // SECS_PER_DAY + 1
+ seconds %= SECS_PER_DAY
+ while month != TM_JANUARY + 1:
+ month_offset = MONTHS_OFFSETS[leap_year][month]
+ if day > month_offset:
+ day -= month_offset
+ break
+
+ month -= 1
+
+ # Handle hours, minutes, seconds and microseconds
+ hour = seconds // SECS_PER_HOUR
+ seconds %= SECS_PER_HOUR
+ minute = seconds // SECS_PER_MIN
+ second = seconds % SECS_PER_MIN
+
+ return (year, month, day, hour, minute, second, microseconds)
+
+
+def precise_diff(
+ d1, d2
+): # type: (typing.Union[datetime.datetime, datetime.date], typing.Union[datetime.datetime, datetime.date]) -> PreciseDiff
+ """
+ Calculate a precise difference between two datetimes.
+
+ :param d1: The first datetime
+ :type d1: datetime.datetime or datetime.date
+
+ :param d2: The second datetime
+ :type d2: datetime.datetime or datetime.date
+
+ :rtype: PreciseDiff
+ """
+ sign = 1
+
+ if d1 == d2:
+ return PreciseDiff(0, 0, 0, 0, 0, 0, 0, 0)
+
+ tzinfo1 = d1.tzinfo if isinstance(d1, datetime.datetime) else None
+ tzinfo2 = d2.tzinfo if isinstance(d2, datetime.datetime) else None
+
+ if (
+ tzinfo1 is None
+ and tzinfo2 is not None
+ or tzinfo2 is None
+ and tzinfo1 is not None
+ ):
+ raise ValueError(
+ "Comparison between naive and aware datetimes is not supported"
+ )
+
+ if d1 > d2:
+ d1, d2 = d2, d1
+ sign = -1
+
+ d_diff = 0
+ hour_diff = 0
+ min_diff = 0
+ sec_diff = 0
+ mic_diff = 0
+ total_days = _day_number(d2.year, d2.month, d2.day) - _day_number(
+ d1.year, d1.month, d1.day
+ )
+ in_same_tz = False
+ tz1 = None
+ tz2 = None
+
+ # Trying to figure out the timezone names
+ # If we can't find them, we assume different timezones
+ if tzinfo1 and tzinfo2:
+ if hasattr(tzinfo1, "name"):
+ # Pendulum timezone
+ tz1 = tzinfo1.name
+ elif hasattr(tzinfo1, "zone"):
+ # pytz timezone
+ tz1 = tzinfo1.zone
+
+ if hasattr(tzinfo2, "name"):
+ tz2 = tzinfo2.name
+ elif hasattr(tzinfo2, "zone"):
+ tz2 = tzinfo2.zone
+
+ in_same_tz = tz1 == tz2 and tz1 is not None
+
+ if isinstance(d2, datetime.datetime):
+ if isinstance(d1, datetime.datetime):
+ # If we are not in the same timezone
+ # we need to adjust
+ #
+ # We also need to adjust if we do not
+ # have variable-length units
+ if not in_same_tz or total_days == 0:
+ offset1 = d1.utcoffset()
+ offset2 = d2.utcoffset()
+
+ if offset1:
+ d1 = d1 - offset1
+
+ if offset2:
+ d2 = d2 - offset2
+
+ hour_diff = d2.hour - d1.hour
+ min_diff = d2.minute - d1.minute
+ sec_diff = d2.second - d1.second
+ mic_diff = d2.microsecond - d1.microsecond
+ else:
+ hour_diff = d2.hour
+ min_diff = d2.minute
+ sec_diff = d2.second
+ mic_diff = d2.microsecond
+
+ if mic_diff < 0:
+ mic_diff += 1000000
+ sec_diff -= 1
+
+ if sec_diff < 0:
+ sec_diff += 60
+ min_diff -= 1
+
+ if min_diff < 0:
+ min_diff += 60
+ hour_diff -= 1
+
+ if hour_diff < 0:
+ hour_diff += 24
+ d_diff -= 1
+
+ y_diff = d2.year - d1.year
+ m_diff = d2.month - d1.month
+ d_diff += d2.day - d1.day
+
+ if d_diff < 0:
+ year = d2.year
+ month = d2.month
+
+ if month == 1:
+ month = 12
+ year -= 1
+ else:
+ month -= 1
+
+ leap = int(is_leap(year))
+
+ days_in_last_month = DAYS_PER_MONTHS[leap][month]
+ days_in_month = DAYS_PER_MONTHS[int(is_leap(d2.year))][d2.month]
+
+ if d_diff < days_in_month - days_in_last_month:
+ # We don't have a full month, we calculate days
+ if days_in_last_month < d1.day:
+ d_diff += d1.day
+ else:
+ d_diff += days_in_last_month
+ elif d_diff == days_in_month - days_in_last_month:
+ # We have exactly a full month
+ # We remove the days difference
+ # and add one to the months difference
+ d_diff = 0
+ m_diff += 1
+ else:
+ # We have a full month
+ d_diff += days_in_last_month
+
+ m_diff -= 1
+
+ if m_diff < 0:
+ m_diff += 12
+ y_diff -= 1
+
+ return PreciseDiff(
+ sign * y_diff,
+ sign * m_diff,
+ sign * d_diff,
+ sign * hour_diff,
+ sign * min_diff,
+ sign * sec_diff,
+ sign * mic_diff,
+ sign * total_days,
+ )
+
+
+def _day_number(year, month, day): # type: (int, int, int) -> int
+ month = (month + 9) % 12
+ year = year - month // 10
+
+ return (
+ 365 * year
+ + year // 4
+ - year // 100
+ + year // 400
+ + (month * 306 + 5) // 10
+ + (day - 1)
+ )