Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/pendulum/duration.py: 49%

273 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-30 06:11 +0000

1from __future__ import annotations 

2 

3from datetime import timedelta 

4from typing import TYPE_CHECKING 

5from typing import cast 

6from typing import overload 

7 

8import pendulum 

9 

10from pendulum.constants import SECONDS_PER_DAY 

11from pendulum.constants import SECONDS_PER_HOUR 

12from pendulum.constants import SECONDS_PER_MINUTE 

13from pendulum.constants import US_PER_SECOND 

14from pendulum.utils._compat import PYPY 

15 

16 

17if TYPE_CHECKING: 

18 from typing_extensions import Self 

19 

20 

21def _divide_and_round(a: float, b: float) -> int: 

22 """divide a by b and round result to the nearest integer 

23 

24 When the ratio is exactly half-way between two integers, 

25 the even integer is returned. 

26 """ 

27 # Based on the reference implementation for divmod_near 

28 # in Objects/longobject.c. 

29 q, r = divmod(a, b) 

30 

31 # The output of divmod() is either a float or an int, 

32 # but we always want it to be an int. 

33 q = int(q) 

34 

35 # round up if either r / b > 0.5, or r / b == 0.5 and q is odd. 

36 # The expression r / b > 0.5 is equivalent to 2 * r > b if b is 

37 # positive, 2 * r < b if b negative. 

38 r *= 2 

39 greater_than_half = r > b if b > 0 else r < b 

40 if greater_than_half or r == b and q % 2 == 1: 

41 q += 1 

42 

43 return q 

44 

45 

46class Duration(timedelta): 

47 """ 

48 Replacement for the standard timedelta class. 

49 

50 Provides several improvements over the base class. 

51 """ 

52 

53 _total: float = 0 

54 _years: int = 0 

55 _months: int = 0 

56 _weeks: int = 0 

57 _days: int = 0 

58 _remaining_days: int = 0 

59 _seconds: int = 0 

60 _microseconds: int = 0 

61 

62 _y = None 

63 _m = None 

64 _w = None 

65 _d = None 

66 _h = None 

67 _i = None 

68 _s = None 

69 _invert = None 

70 

71 def __new__( 

72 cls, 

73 days: float = 0, 

74 seconds: float = 0, 

75 microseconds: float = 0, 

76 milliseconds: float = 0, 

77 minutes: float = 0, 

78 hours: float = 0, 

79 weeks: float = 0, 

80 years: float = 0, 

81 months: float = 0, 

82 ) -> Self: 

83 if not isinstance(years, int) or not isinstance(months, int): 

84 raise ValueError("Float year and months are not supported") 

85 

86 self = timedelta.__new__( 

87 cls, 

88 days + years * 365 + months * 30, 

89 seconds, 

90 microseconds, 

91 milliseconds, 

92 minutes, 

93 hours, 

94 weeks, 

95 ) 

96 

97 # Intuitive normalization 

98 total = self.total_seconds() - (years * 365 + months * 30) * SECONDS_PER_DAY 

99 self._total = total 

100 

101 m = 1 

102 if total < 0: 

103 m = -1 

104 

105 self._microseconds = round(total % m * 1e6) 

106 self._seconds = abs(int(total)) % SECONDS_PER_DAY * m 

107 

108 _days = abs(int(total)) // SECONDS_PER_DAY * m 

109 self._days = _days 

110 self._remaining_days = abs(_days) % 7 * m 

111 self._weeks = abs(_days) // 7 * m 

112 self._months = months 

113 self._years = years 

114 

115 return self 

116 

117 def total_minutes(self) -> float: 

118 return self.total_seconds() / SECONDS_PER_MINUTE 

119 

120 def total_hours(self) -> float: 

121 return self.total_seconds() / SECONDS_PER_HOUR 

122 

123 def total_days(self) -> float: 

124 return self.total_seconds() / SECONDS_PER_DAY 

125 

126 def total_weeks(self) -> float: 

127 return self.total_days() / 7 

128 

129 if PYPY: 

130 

131 def total_seconds(self) -> float: 

132 days = 0 

133 

134 if hasattr(self, "_years"): 

135 days += self._years * 365 

136 

137 if hasattr(self, "_months"): 

138 days += self._months * 30 

139 

140 if hasattr(self, "_remaining_days"): 

141 days += self._weeks * 7 + self._remaining_days 

142 else: 

143 days += self._days 

144 

145 return ( 

146 (days * SECONDS_PER_DAY + self._seconds) * US_PER_SECOND 

147 + self._microseconds 

148 ) / US_PER_SECOND 

149 

150 @property 

151 def years(self) -> int: 

152 return self._years 

153 

154 @property 

155 def months(self) -> int: 

156 return self._months 

157 

158 @property 

159 def weeks(self) -> int: 

160 return self._weeks 

161 

162 if PYPY: 

163 

164 @property 

165 def days(self) -> int: 

166 return self._years * 365 + self._months * 30 + self._days 

167 

168 @property 

169 def remaining_days(self) -> int: 

170 return self._remaining_days 

171 

172 @property 

173 def hours(self) -> int: 

174 if self._h is None: 

175 seconds = self._seconds 

176 self._h = 0 

177 if abs(seconds) >= 3600: 

178 self._h = (abs(seconds) // 3600 % 24) * self._sign(seconds) 

179 

180 return self._h 

181 

182 @property 

183 def minutes(self) -> int: 

184 if self._i is None: 

185 seconds = self._seconds 

186 self._i = 0 

187 if abs(seconds) >= 60: 

188 self._i = (abs(seconds) // 60 % 60) * self._sign(seconds) 

189 

190 return self._i 

191 

192 @property 

193 def seconds(self) -> int: 

194 return self._seconds 

195 

196 @property 

197 def remaining_seconds(self) -> int: 

198 if self._s is None: 

199 self._s = self._seconds 

200 self._s = abs(self._s) % 60 * self._sign(self._s) 

201 

202 return self._s 

203 

204 @property 

205 def microseconds(self) -> int: 

206 return self._microseconds 

207 

208 @property 

209 def invert(self) -> bool: 

210 if self._invert is None: 

211 self._invert = self.total_seconds() < 0 

212 

213 return self._invert 

214 

215 def in_weeks(self) -> int: 

216 return int(self.total_weeks()) 

217 

218 def in_days(self) -> int: 

219 return int(self.total_days()) 

220 

221 def in_hours(self) -> int: 

222 return int(self.total_hours()) 

223 

224 def in_minutes(self) -> int: 

225 return int(self.total_minutes()) 

226 

227 def in_seconds(self) -> int: 

228 return int(self.total_seconds()) 

229 

230 def in_words(self, locale: str | None = None, separator: str = " ") -> str: 

231 """ 

232 Get the current interval in words in the current locale. 

233 

234 Ex: 6 jours 23 heures 58 minutes 

235 

236 :param locale: The locale to use. Defaults to current locale. 

237 :param separator: The separator to use between each unit 

238 """ 

239 periods = [ 

240 ("year", self.years), 

241 ("month", self.months), 

242 ("week", self.weeks), 

243 ("day", self.remaining_days), 

244 ("hour", self.hours), 

245 ("minute", self.minutes), 

246 ("second", self.remaining_seconds), 

247 ] 

248 

249 if locale is None: 

250 locale = pendulum.get_locale() 

251 

252 loaded_locale = pendulum.locale(locale) 

253 

254 parts = [] 

255 for period in periods: 

256 unit, period_count = period 

257 if abs(period_count) > 0: 

258 translation = loaded_locale.translation( 

259 f"units.{unit}.{loaded_locale.plural(abs(period_count))}" 

260 ) 

261 parts.append(translation.format(period_count)) 

262 

263 if not parts: 

264 count: int | str = 0 

265 if abs(self.microseconds) > 0: 

266 unit = f"units.second.{loaded_locale.plural(1)}" 

267 count = f"{abs(self.microseconds) / 1e6:.2f}" 

268 else: 

269 unit = f"units.microsecond.{loaded_locale.plural(0)}" 

270 translation = loaded_locale.translation(unit) 

271 parts.append(translation.format(count)) 

272 

273 return separator.join(parts) 

274 

275 def _sign(self, value: float) -> int: 

276 if value < 0: 

277 return -1 

278 

279 return 1 

280 

281 def as_timedelta(self) -> timedelta: 

282 """ 

283 Return the interval as a native timedelta. 

284 """ 

285 return timedelta(seconds=self.total_seconds()) 

286 

287 def __str__(self) -> str: 

288 return self.in_words() 

289 

290 def __repr__(self) -> str: 

291 rep = f"{self.__class__.__name__}(" 

292 

293 if self._years: 

294 rep += f"years={self._years}, " 

295 

296 if self._months: 

297 rep += f"months={self._months}, " 

298 

299 if self._weeks: 

300 rep += f"weeks={self._weeks}, " 

301 

302 if self._days: 

303 rep += f"days={self._remaining_days}, " 

304 

305 if self.hours: 

306 rep += f"hours={self.hours}, " 

307 

308 if self.minutes: 

309 rep += f"minutes={self.minutes}, " 

310 

311 if self.remaining_seconds: 

312 rep += f"seconds={self.remaining_seconds}, " 

313 

314 if self.microseconds: 

315 rep += f"microseconds={self.microseconds}, " 

316 

317 rep += ")" 

318 

319 return rep.replace(", )", ")") 

320 

321 def __add__(self, other: timedelta) -> Self: 

322 if isinstance(other, timedelta): 

323 return self.__class__(seconds=self.total_seconds() + other.total_seconds()) 

324 

325 return NotImplemented 

326 

327 __radd__ = __add__ 

328 

329 def __sub__(self, other: timedelta) -> Self: 

330 if isinstance(other, timedelta): 

331 return self.__class__(seconds=self.total_seconds() - other.total_seconds()) 

332 

333 return NotImplemented 

334 

335 def __neg__(self) -> Self: 

336 return self.__class__( 

337 years=-self._years, 

338 months=-self._months, 

339 weeks=-self._weeks, 

340 days=-self._remaining_days, 

341 seconds=-self._seconds, 

342 microseconds=-self._microseconds, 

343 ) 

344 

345 def _to_microseconds(self) -> int: 

346 return (self._days * (24 * 3600) + self._seconds) * 1000000 + self._microseconds 

347 

348 def __mul__(self, other: int | float) -> Self: 

349 if isinstance(other, int): 

350 return self.__class__( 

351 years=self._years * other, 

352 months=self._months * other, 

353 seconds=self._total * other, 

354 ) 

355 

356 if isinstance(other, float): 

357 usec = self._to_microseconds() 

358 a, b = other.as_integer_ratio() 

359 

360 return self.__class__(0, 0, _divide_and_round(usec * a, b)) 

361 

362 return NotImplemented 

363 

364 __rmul__ = __mul__ 

365 

366 @overload 

367 def __floordiv__(self, other: timedelta) -> int: 

368 ... 

369 

370 @overload 

371 def __floordiv__(self, other: int) -> Self: 

372 ... 

373 

374 def __floordiv__(self, other: int | timedelta) -> int | Duration: 

375 if not isinstance(other, (int, timedelta)): 

376 return NotImplemented 

377 

378 usec = self._to_microseconds() 

379 if isinstance(other, timedelta): 

380 return cast( 

381 int, usec // other._to_microseconds() # type: ignore[attr-defined] 

382 ) 

383 

384 if isinstance(other, int): 

385 return self.__class__( 

386 0, 

387 0, 

388 usec // other, 

389 years=self._years // other, 

390 months=self._months // other, 

391 ) 

392 

393 @overload 

394 def __truediv__(self, other: timedelta) -> float: 

395 ... 

396 

397 @overload 

398 def __truediv__(self, other: float) -> Self: 

399 ... 

400 

401 def __truediv__(self, other: int | float | timedelta) -> Self | float: 

402 if not isinstance(other, (int, float, timedelta)): 

403 return NotImplemented 

404 

405 usec = self._to_microseconds() 

406 if isinstance(other, timedelta): 

407 return cast( 

408 float, usec / other._to_microseconds() # type: ignore[attr-defined] 

409 ) 

410 

411 if isinstance(other, int): 

412 return self.__class__( 

413 0, 

414 0, 

415 _divide_and_round(usec, other), 

416 years=_divide_and_round(self._years, other), 

417 months=_divide_and_round(self._months, other), 

418 ) 

419 

420 if isinstance(other, float): 

421 a, b = other.as_integer_ratio() 

422 

423 return self.__class__( 

424 0, 

425 0, 

426 _divide_and_round(b * usec, a), 

427 years=_divide_and_round(self._years * b, a), 

428 months=_divide_and_round(self._months, other), 

429 ) 

430 

431 __div__ = __floordiv__ 

432 

433 def __mod__(self, other: timedelta) -> Self: 

434 if isinstance(other, timedelta): 

435 r = self._to_microseconds() % other._to_microseconds() # type: ignore[attr-defined] # noqa: E501 

436 

437 return self.__class__(0, 0, r) 

438 

439 return NotImplemented 

440 

441 def __divmod__(self, other: timedelta) -> tuple[int, Duration]: 

442 if isinstance(other, timedelta): 

443 q, r = divmod(self._to_microseconds(), other._to_microseconds()) # type: ignore[attr-defined] # noqa: E501 

444 

445 return q, self.__class__(0, 0, r) 

446 

447 return NotImplemented 

448 

449 def __deepcopy__(self, _: dict[int, Self]) -> Self: 

450 return self.__class__( 

451 days=self.remaining_days, 

452 seconds=self.remaining_seconds, 

453 microseconds=self.microseconds, 

454 minutes=self.minutes, 

455 hours=self.hours, 

456 years=self.years, 

457 months=self.months, 

458 ) 

459 

460 

461Duration.min = Duration(days=-999999999) 

462Duration.max = Duration( 

463 days=999999999, hours=23, minutes=59, seconds=59, microseconds=999999 

464) 

465Duration.resolution = Duration(microseconds=1) 

466 

467 

468class AbsoluteDuration(Duration): 

469 """ 

470 Duration that expresses a time difference in absolute values. 

471 """ 

472 

473 def __new__( 

474 cls, 

475 days: float = 0, 

476 seconds: float = 0, 

477 microseconds: float = 0, 

478 milliseconds: float = 0, 

479 minutes: float = 0, 

480 hours: float = 0, 

481 weeks: float = 0, 

482 years: float = 0, 

483 months: float = 0, 

484 ) -> AbsoluteDuration: 

485 if not isinstance(years, int) or not isinstance(months, int): 

486 raise ValueError("Float year and months are not supported") 

487 

488 self = timedelta.__new__( 

489 cls, days, seconds, microseconds, milliseconds, minutes, hours, weeks 

490 ) 

491 

492 # We need to compute the total_seconds() value 

493 # on a native timedelta object 

494 delta = timedelta( 

495 days, seconds, microseconds, milliseconds, minutes, hours, weeks 

496 ) 

497 

498 # Intuitive normalization 

499 self._total = delta.total_seconds() 

500 total = abs(self._total) 

501 

502 self._microseconds = round(total % 1 * 1e6) 

503 days, self._seconds = divmod(int(total), SECONDS_PER_DAY) 

504 self._days = abs(days + years * 365 + months * 30) 

505 self._weeks, self._remaining_days = divmod(days, 7) 

506 self._months = abs(months) 

507 self._years = abs(years) 

508 

509 return self 

510 

511 def total_seconds(self) -> float: 

512 return abs(self._total) 

513 

514 @property 

515 def invert(self) -> bool: 

516 if self._invert is None: 

517 self._invert = self._total < 0 

518 

519 return self._invert