Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pendulum/_helpers.py: 71%
188 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-30 06:11 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-30 06:11 +0000
1from __future__ import annotations
3import datetime
4import math
6from typing import NamedTuple
7from typing import cast
9from pendulum.constants import DAY_OF_WEEK_TABLE
10from pendulum.constants import DAYS_PER_L_YEAR
11from pendulum.constants import DAYS_PER_MONTHS
12from pendulum.constants import DAYS_PER_N_YEAR
13from pendulum.constants import EPOCH_YEAR
14from pendulum.constants import MONTHS_OFFSETS
15from pendulum.constants import SECS_PER_4_YEARS
16from pendulum.constants import SECS_PER_100_YEARS
17from pendulum.constants import SECS_PER_400_YEARS
18from pendulum.constants import SECS_PER_DAY
19from pendulum.constants import SECS_PER_HOUR
20from pendulum.constants import SECS_PER_MIN
21from pendulum.constants import SECS_PER_YEAR
22from pendulum.constants import TM_DECEMBER
23from pendulum.constants import TM_JANUARY
24from pendulum.tz.timezone import Timezone
25from pendulum.utils._compat import zoneinfo
28class PreciseDiff(NamedTuple):
29 years: int
30 months: int
31 days: int
32 hours: int
33 minutes: int
34 seconds: int
35 microseconds: int
36 total_days: int
38 def __repr__(self) -> str:
39 return (
40 f"{self.years} years "
41 f"{self.months} months "
42 f"{self.days} days "
43 f"{self.hours} hours "
44 f"{self.minutes} minutes "
45 f"{self.seconds} seconds "
46 f"{self.microseconds} microseconds"
47 )
50def is_leap(year: int) -> bool:
51 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
54def is_long_year(year: int) -> bool:
55 def p(y: int) -> int:
56 return y + y // 4 - y // 100 + y // 400
58 return p(year) % 7 == 4 or p(year - 1) % 7 == 3
61def week_day(year: int, month: int, day: int) -> int:
62 if month < 3:
63 year -= 1
65 w = (
66 year
67 + year // 4
68 - year // 100
69 + year // 400
70 + DAY_OF_WEEK_TABLE[month - 1]
71 + day
72 ) % 7
74 if not w:
75 w = 7
77 return w
80def days_in_year(year: int) -> int:
81 if is_leap(year):
82 return DAYS_PER_L_YEAR
84 return DAYS_PER_N_YEAR
87def local_time(
88 unix_time: int, utc_offset: int, microseconds: int
89) -> tuple[int, int, int, int, int, int, int]:
90 """
91 Returns a UNIX time as a broken-down time
92 for a particular transition type.
93 """
94 year = EPOCH_YEAR
95 seconds = math.floor(unix_time)
97 # Shift to a base year that is 400-year aligned.
98 if seconds >= 0:
99 seconds -= 10957 * SECS_PER_DAY
100 year += 30 # == 2000
101 else:
102 seconds += (146097 - 10957) * SECS_PER_DAY
103 year -= 370 # == 1600
105 seconds += utc_offset
107 # Handle years in chunks of 400/100/4/1
108 year += 400 * (seconds // SECS_PER_400_YEARS)
109 seconds %= SECS_PER_400_YEARS
110 if seconds < 0:
111 seconds += SECS_PER_400_YEARS
112 year -= 400
114 leap_year = 1 # 4-century aligned
116 sec_per_100years = SECS_PER_100_YEARS[leap_year]
117 while seconds >= sec_per_100years:
118 seconds -= sec_per_100years
119 year += 100
120 leap_year = 0 # 1-century, non 4-century aligned
121 sec_per_100years = SECS_PER_100_YEARS[leap_year]
123 sec_per_4years = SECS_PER_4_YEARS[leap_year]
124 while seconds >= sec_per_4years:
125 seconds -= sec_per_4years
126 year += 4
127 leap_year = 1 # 4-year, non century aligned
128 sec_per_4years = SECS_PER_4_YEARS[leap_year]
130 sec_per_year = SECS_PER_YEAR[leap_year]
131 while seconds >= sec_per_year:
132 seconds -= sec_per_year
133 year += 1
134 leap_year = 0 # non 4-year aligned
135 sec_per_year = SECS_PER_YEAR[leap_year]
137 # Handle months and days
138 month = TM_DECEMBER + 1
139 day = seconds // SECS_PER_DAY + 1
140 seconds %= SECS_PER_DAY
141 while month != TM_JANUARY + 1:
142 month_offset = MONTHS_OFFSETS[leap_year][month]
143 if day > month_offset:
144 day -= month_offset
145 break
147 month -= 1
149 # Handle hours, minutes, seconds and microseconds
150 hour, seconds = divmod(seconds, SECS_PER_HOUR)
151 minute, second = divmod(seconds, SECS_PER_MIN)
153 return year, month, day, hour, minute, second, microseconds
156def precise_diff(
157 d1: datetime.datetime | datetime.date, d2: datetime.datetime | datetime.date
158) -> PreciseDiff:
159 """
160 Calculate a precise difference between two datetimes.
162 :param d1: The first datetime
163 :param d2: The second datetime
164 """
165 sign = 1
167 if d1 == d2:
168 return PreciseDiff(0, 0, 0, 0, 0, 0, 0, 0)
170 tzinfo1: datetime.tzinfo | None = (
171 d1.tzinfo if isinstance(d1, datetime.datetime) else None
172 )
173 tzinfo2: datetime.tzinfo | None = (
174 d2.tzinfo if isinstance(d2, datetime.datetime) else None
175 )
177 if (
178 tzinfo1 is None
179 and tzinfo2 is not None
180 or tzinfo2 is None
181 and tzinfo1 is not None
182 ):
183 raise ValueError(
184 "Comparison between naive and aware datetimes is not supported"
185 )
187 if d1 > d2:
188 d1, d2 = d2, d1
189 sign = -1
191 d_diff = 0
192 hour_diff = 0
193 min_diff = 0
194 sec_diff = 0
195 mic_diff = 0
196 total_days = _day_number(d2.year, d2.month, d2.day) - _day_number(
197 d1.year, d1.month, d1.day
198 )
199 in_same_tz = False
200 tz1 = None
201 tz2 = None
203 # Trying to figure out the timezone names
204 # If we can't find them, we assume different timezones
205 if tzinfo1 and tzinfo2:
206 tz1 = _get_tzinfo_name(tzinfo1)
207 tz2 = _get_tzinfo_name(tzinfo2)
209 in_same_tz = tz1 == tz2 and tz1 is not None
211 if isinstance(d2, datetime.datetime):
212 if isinstance(d1, datetime.datetime):
213 # If we are not in the same timezone
214 # we need to adjust
215 #
216 # We also need to adjust if we do not
217 # have variable-length units
218 if not in_same_tz or total_days == 0:
219 offset1 = d1.utcoffset()
220 offset2 = d2.utcoffset()
222 if offset1:
223 d1 = d1 - offset1
225 if offset2:
226 d2 = d2 - offset2
228 hour_diff = d2.hour - d1.hour
229 min_diff = d2.minute - d1.minute
230 sec_diff = d2.second - d1.second
231 mic_diff = d2.microsecond - d1.microsecond
232 else:
233 hour_diff = d2.hour
234 min_diff = d2.minute
235 sec_diff = d2.second
236 mic_diff = d2.microsecond
238 if mic_diff < 0:
239 mic_diff += 1000000
240 sec_diff -= 1
242 if sec_diff < 0:
243 sec_diff += 60
244 min_diff -= 1
246 if min_diff < 0:
247 min_diff += 60
248 hour_diff -= 1
250 if hour_diff < 0:
251 hour_diff += 24
252 d_diff -= 1
254 y_diff = d2.year - d1.year
255 m_diff = d2.month - d1.month
256 d_diff += d2.day - d1.day
258 if d_diff < 0:
259 year = d2.year
260 month = d2.month
262 if month == 1:
263 month = 12
264 year -= 1
265 else:
266 month -= 1
268 leap = int(is_leap(year))
270 days_in_last_month = DAYS_PER_MONTHS[leap][month]
271 days_in_month = DAYS_PER_MONTHS[int(is_leap(d2.year))][d2.month]
273 if d_diff < days_in_month - days_in_last_month:
274 # We don't have a full month, we calculate days
275 if days_in_last_month < d1.day:
276 d_diff += d1.day
277 else:
278 d_diff += days_in_last_month
279 elif d_diff == days_in_month - days_in_last_month:
280 # We have exactly a full month
281 # We remove the days difference
282 # and add one to the months difference
283 d_diff = 0
284 m_diff += 1
285 else:
286 # We have a full month
287 d_diff += days_in_last_month
289 m_diff -= 1
291 if m_diff < 0:
292 m_diff += 12
293 y_diff -= 1
295 return PreciseDiff(
296 sign * y_diff,
297 sign * m_diff,
298 sign * d_diff,
299 sign * hour_diff,
300 sign * min_diff,
301 sign * sec_diff,
302 sign * mic_diff,
303 sign * total_days,
304 )
307def _day_number(year: int, month: int, day: int) -> int:
308 month = (month + 9) % 12
309 year = year - month // 10
311 return (
312 365 * year
313 + year // 4
314 - year // 100
315 + year // 400
316 + (month * 306 + 5) // 10
317 + (day - 1)
318 )
321def _get_tzinfo_name(tzinfo: datetime.tzinfo | None) -> str | None:
322 if tzinfo is None:
323 return None
325 if hasattr(tzinfo, "key"):
326 # zoneinfo timezone
327 return cast(zoneinfo.ZoneInfo, tzinfo).key
328 elif hasattr(tzinfo, "name"):
329 # Pendulum timezone
330 return cast(Timezone, tzinfo).name
331 elif hasattr(tzinfo, "zone"):
332 # pytz timezone
333 return tzinfo.zone # type: ignore[no-any-return]
335 return None