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

1from __future__ import annotations 

2 

3import datetime 

4import math 

5 

6from typing import NamedTuple 

7from typing import cast 

8 

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 

26 

27 

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 

37 

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 ) 

48 

49 

50def is_leap(year: int) -> bool: 

51 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) 

52 

53 

54def is_long_year(year: int) -> bool: 

55 def p(y: int) -> int: 

56 return y + y // 4 - y // 100 + y // 400 

57 

58 return p(year) % 7 == 4 or p(year - 1) % 7 == 3 

59 

60 

61def week_day(year: int, month: int, day: int) -> int: 

62 if month < 3: 

63 year -= 1 

64 

65 w = ( 

66 year 

67 + year // 4 

68 - year // 100 

69 + year // 400 

70 + DAY_OF_WEEK_TABLE[month - 1] 

71 + day 

72 ) % 7 

73 

74 if not w: 

75 w = 7 

76 

77 return w 

78 

79 

80def days_in_year(year: int) -> int: 

81 if is_leap(year): 

82 return DAYS_PER_L_YEAR 

83 

84 return DAYS_PER_N_YEAR 

85 

86 

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) 

96 

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 

104 

105 seconds += utc_offset 

106 

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 

113 

114 leap_year = 1 # 4-century aligned 

115 

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] 

122 

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] 

129 

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] 

136 

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 

146 

147 month -= 1 

148 

149 # Handle hours, minutes, seconds and microseconds 

150 hour, seconds = divmod(seconds, SECS_PER_HOUR) 

151 minute, second = divmod(seconds, SECS_PER_MIN) 

152 

153 return year, month, day, hour, minute, second, microseconds 

154 

155 

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. 

161 

162 :param d1: The first datetime 

163 :param d2: The second datetime 

164 """ 

165 sign = 1 

166 

167 if d1 == d2: 

168 return PreciseDiff(0, 0, 0, 0, 0, 0, 0, 0) 

169 

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 ) 

176 

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 ) 

186 

187 if d1 > d2: 

188 d1, d2 = d2, d1 

189 sign = -1 

190 

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 

202 

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) 

208 

209 in_same_tz = tz1 == tz2 and tz1 is not None 

210 

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() 

221 

222 if offset1: 

223 d1 = d1 - offset1 

224 

225 if offset2: 

226 d2 = d2 - offset2 

227 

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 

237 

238 if mic_diff < 0: 

239 mic_diff += 1000000 

240 sec_diff -= 1 

241 

242 if sec_diff < 0: 

243 sec_diff += 60 

244 min_diff -= 1 

245 

246 if min_diff < 0: 

247 min_diff += 60 

248 hour_diff -= 1 

249 

250 if hour_diff < 0: 

251 hour_diff += 24 

252 d_diff -= 1 

253 

254 y_diff = d2.year - d1.year 

255 m_diff = d2.month - d1.month 

256 d_diff += d2.day - d1.day 

257 

258 if d_diff < 0: 

259 year = d2.year 

260 month = d2.month 

261 

262 if month == 1: 

263 month = 12 

264 year -= 1 

265 else: 

266 month -= 1 

267 

268 leap = int(is_leap(year)) 

269 

270 days_in_last_month = DAYS_PER_MONTHS[leap][month] 

271 days_in_month = DAYS_PER_MONTHS[int(is_leap(d2.year))][d2.month] 

272 

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 

288 

289 m_diff -= 1 

290 

291 if m_diff < 0: 

292 m_diff += 12 

293 y_diff -= 1 

294 

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 ) 

305 

306 

307def _day_number(year: int, month: int, day: int) -> int: 

308 month = (month + 9) % 12 

309 year = year - month // 10 

310 

311 return ( 

312 365 * year 

313 + year // 4 

314 - year // 100 

315 + year // 400 

316 + (month * 306 + 5) // 10 

317 + (day - 1) 

318 ) 

319 

320 

321def _get_tzinfo_name(tzinfo: datetime.tzinfo | None) -> str | None: 

322 if tzinfo is None: 

323 return None 

324 

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] 

334 

335 return None