Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pendulum/interval.py: 37%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

212 statements  

1from __future__ import annotations 

2 

3import copy 

4import operator 

5 

6from datetime import date 

7from datetime import datetime 

8from datetime import timedelta 

9from typing import TYPE_CHECKING 

10from typing import Any 

11from typing import Generic 

12from typing import TypeVar 

13from typing import cast 

14from typing import overload 

15 

16import pendulum 

17 

18from pendulum.constants import MONTHS_PER_YEAR 

19from pendulum.duration import Duration 

20from pendulum.helpers import precise_diff 

21 

22 

23if TYPE_CHECKING: 

24 from collections.abc import Iterator 

25 

26 from typing_extensions import Self 

27 from typing_extensions import SupportsIndex 

28 

29 from pendulum.helpers import PreciseDiff 

30 from pendulum.locales.locale import Locale 

31 

32 

33_T = TypeVar("_T", bound=date) 

34 

35 

36class Interval(Duration, Generic[_T]): 

37 """ 

38 An interval of time between two datetimes. 

39 """ 

40 

41 def __new__(cls, start: _T, end: _T, absolute: bool = False) -> Self: 

42 if (isinstance(start, datetime) and not isinstance(end, datetime)) or ( 

43 not isinstance(start, datetime) and isinstance(end, datetime) 

44 ): 

45 raise ValueError( 

46 "Both start and end of an Interval must have the same type" 

47 ) 

48 

49 if ( 

50 isinstance(start, datetime) 

51 and isinstance(end, datetime) 

52 and ( 

53 (start.tzinfo is None and end.tzinfo is not None) 

54 or (start.tzinfo is not None and end.tzinfo is None) 

55 ) 

56 ): 

57 raise TypeError("can't compare offset-naive and offset-aware datetimes") 

58 

59 if absolute and start > end: 

60 end, start = start, end 

61 

62 _start = start 

63 _end = end 

64 if isinstance(start, pendulum.DateTime): 

65 _start = cast( 

66 "_T", 

67 datetime( 

68 start.year, 

69 start.month, 

70 start.day, 

71 start.hour, 

72 start.minute, 

73 start.second, 

74 start.microsecond, 

75 tzinfo=start.tzinfo, 

76 fold=start.fold, 

77 ), 

78 ) 

79 elif isinstance(start, pendulum.Date): 

80 _start = cast("_T", date(start.year, start.month, start.day)) 

81 

82 if isinstance(end, pendulum.DateTime): 

83 _end = cast( 

84 "_T", 

85 datetime( 

86 end.year, 

87 end.month, 

88 end.day, 

89 end.hour, 

90 end.minute, 

91 end.second, 

92 end.microsecond, 

93 tzinfo=end.tzinfo, 

94 fold=end.fold, 

95 ), 

96 ) 

97 elif isinstance(end, pendulum.Date): 

98 _end = cast("_T", date(end.year, end.month, end.day)) 

99 

100 # Fixing issues with datetime.__sub__() 

101 # not handling offsets if the tzinfo is the same 

102 if ( 

103 isinstance(_start, datetime) 

104 and isinstance(_end, datetime) 

105 and _start.tzinfo is _end.tzinfo 

106 ): 

107 if _start.tzinfo is not None: 

108 offset = cast("timedelta", cast("datetime", start).utcoffset()) 

109 _start = cast("_T", (_start - offset).replace(tzinfo=None)) 

110 

111 if isinstance(end, datetime) and _end.tzinfo is not None: 

112 offset = cast("timedelta", end.utcoffset()) 

113 _end = cast("_T", (_end - offset).replace(tzinfo=None)) 

114 

115 delta: timedelta = _end - _start 

116 

117 return super().__new__(cls, seconds=delta.total_seconds()) 

118 

119 def __init__(self, start: _T, end: _T, absolute: bool = False) -> None: 

120 super().__init__() 

121 

122 _start: _T 

123 if not isinstance(start, pendulum.Date): 

124 if isinstance(start, datetime): 

125 start = cast("_T", pendulum.instance(start)) 

126 else: 

127 start = cast("_T", pendulum.date(start.year, start.month, start.day)) 

128 

129 _start = start 

130 else: 

131 if isinstance(start, pendulum.DateTime): 

132 _start = cast( 

133 "_T", 

134 datetime( 

135 start.year, 

136 start.month, 

137 start.day, 

138 start.hour, 

139 start.minute, 

140 start.second, 

141 start.microsecond, 

142 tzinfo=start.tzinfo, 

143 ), 

144 ) 

145 else: 

146 _start = cast("_T", date(start.year, start.month, start.day)) 

147 

148 _end: _T 

149 if not isinstance(end, pendulum.Date): 

150 if isinstance(end, datetime): 

151 end = cast("_T", pendulum.instance(end)) 

152 else: 

153 end = cast("_T", pendulum.date(end.year, end.month, end.day)) 

154 

155 _end = end 

156 else: 

157 if isinstance(end, pendulum.DateTime): 

158 _end = cast( 

159 "_T", 

160 datetime( 

161 end.year, 

162 end.month, 

163 end.day, 

164 end.hour, 

165 end.minute, 

166 end.second, 

167 end.microsecond, 

168 tzinfo=end.tzinfo, 

169 ), 

170 ) 

171 else: 

172 _end = cast("_T", date(end.year, end.month, end.day)) 

173 

174 self._invert = False 

175 if start > end: 

176 self._invert = True 

177 

178 if absolute: 

179 end, start = start, end 

180 _end, _start = _start, _end 

181 

182 self._absolute = absolute 

183 self._start: _T = start 

184 self._end: _T = end 

185 self._delta: PreciseDiff = precise_diff(_start, _end) 

186 

187 @property 

188 def years(self) -> int: 

189 return self._delta.years 

190 

191 @property 

192 def months(self) -> int: 

193 return self._delta.months 

194 

195 @property 

196 def weeks(self) -> int: 

197 return abs(self._delta.days) // 7 * self._sign(self._delta.days) 

198 

199 @property 

200 def days(self) -> int: 

201 return self._days 

202 

203 @property 

204 def remaining_days(self) -> int: 

205 return abs(self._delta.days) % 7 * self._sign(self._days) 

206 

207 @property 

208 def hours(self) -> int: 

209 return self._delta.hours 

210 

211 @property 

212 def minutes(self) -> int: 

213 return self._delta.minutes 

214 

215 @property 

216 def start(self) -> _T: 

217 return self._start 

218 

219 @property 

220 def end(self) -> _T: 

221 return self._end 

222 

223 def in_years(self) -> int: 

224 """ 

225 Gives the duration of the Interval in full years. 

226 """ 

227 return self.years 

228 

229 def in_months(self) -> int: 

230 """ 

231 Gives the duration of the Interval in full months. 

232 """ 

233 return self.years * MONTHS_PER_YEAR + self.months 

234 

235 def in_weeks(self) -> int: 

236 days = self.in_days() 

237 sign = 1 

238 

239 if days < 0: 

240 sign = -1 

241 

242 return sign * (abs(days) // 7) 

243 

244 def in_days(self) -> int: 

245 return self._delta.total_days 

246 

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

248 """ 

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

250 

251 Ex: 6 jours 23 heures 58 minutes 

252 

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

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

255 """ 

256 from pendulum.locales.locale import Locale 

257 

258 intervals = [ 

259 ("year", self.years), 

260 ("month", self.months), 

261 ("week", self.weeks), 

262 ("day", self.remaining_days), 

263 ("hour", self.hours), 

264 ("minute", self.minutes), 

265 ("second", self.remaining_seconds), 

266 ] 

267 loaded_locale: Locale = Locale.load(locale or pendulum.get_locale()) 

268 parts = [] 

269 for interval in intervals: 

270 unit, interval_count = interval 

271 if abs(interval_count) > 0: 

272 translation = loaded_locale.translation( 

273 f"units.{unit}.{loaded_locale.plural(abs(interval_count))}" 

274 ) 

275 parts.append(translation.format(interval_count)) 

276 

277 if not parts: 

278 count: str | int = 0 

279 if abs(self.microseconds) > 0: 

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

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

282 else: 

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

284 

285 translation = loaded_locale.translation(unit) 

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

287 

288 return separator.join(parts) 

289 

290 def range(self, unit: str, amount: int = 1) -> Iterator[_T]: 

291 method = "add" 

292 op = operator.le 

293 if not self._absolute and self.invert: 

294 method = "subtract" 

295 op = operator.ge 

296 

297 start, end = self.start, self.end 

298 

299 i = amount 

300 while op(start, end): 

301 yield start 

302 

303 start = getattr(self.start, method)(**{unit: i}) 

304 

305 i += amount 

306 

307 def as_duration(self) -> Duration: 

308 """ 

309 Return the Interval as a Duration. 

310 """ 

311 return Duration(seconds=self.total_seconds()) 

312 

313 def __iter__(self) -> Iterator[_T]: 

314 return self.range("days") 

315 

316 def __contains__(self, item: _T) -> bool: 

317 return self.start <= item <= self.end 

318 

319 def __add__(self, other: timedelta) -> Duration: # type: ignore[override] 

320 return self.as_duration().__add__(other) 

321 

322 __radd__ = __add__ # type: ignore[assignment] 

323 

324 def __sub__(self, other: timedelta) -> Duration: # type: ignore[override] 

325 return self.as_duration().__sub__(other) 

326 

327 def __neg__(self) -> Self: 

328 return self.__class__(self.end, self.start, self._absolute) 

329 

330 def __mul__(self, other: int | float) -> Duration: # type: ignore[override] 

331 return self.as_duration().__mul__(other) 

332 

333 __rmul__ = __mul__ # type: ignore[assignment] 

334 

335 @overload # type: ignore[override] 

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

337 

338 @overload 

339 def __floordiv__(self, other: int) -> Duration: ... 

340 

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

342 return self.as_duration().__floordiv__(other) 

343 

344 __div__ = __floordiv__ # type: ignore[assignment] 

345 

346 @overload # type: ignore[override] 

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

348 

349 @overload 

350 def __truediv__(self, other: float) -> Duration: ... 

351 

352 def __truediv__(self, other: float | timedelta) -> Duration | float: 

353 return self.as_duration().__truediv__(other) 

354 

355 def __mod__(self, other: timedelta) -> Duration: # type: ignore[override] 

356 return self.as_duration().__mod__(other) 

357 

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

359 return self.as_duration().__divmod__(other) 

360 

361 def __abs__(self) -> Self: 

362 return self.__class__(self.start, self.end, absolute=True) 

363 

364 def __repr__(self) -> str: 

365 return f"<Interval [{self._start} -> {self._end}]>" 

366 

367 def __str__(self) -> str: 

368 return self.__repr__() 

369 

370 def _cmp(self, other: timedelta) -> int: 

371 # Only needed for PyPy 

372 assert isinstance(other, timedelta) 

373 

374 if isinstance(other, Interval): 

375 other = other.as_timedelta() 

376 

377 td = self.as_timedelta() 

378 

379 return 0 if td == other else 1 if td > other else -1 

380 

381 def _getstate(self, protocol: SupportsIndex = 3) -> tuple[_T, _T, bool]: 

382 start, end = self.start, self.end 

383 

384 if self._invert and self._absolute: 

385 end, start = start, end 

386 

387 return start, end, self._absolute 

388 

389 def __reduce__( 

390 self, 

391 ) -> tuple[type[Self], tuple[_T, _T, bool]]: 

392 return self.__reduce_ex__(2) 

393 

394 def __reduce_ex__( 

395 self, protocol: SupportsIndex 

396 ) -> tuple[type[Self], tuple[_T, _T, bool]]: 

397 return self.__class__, self._getstate(protocol) 

398 

399 def __hash__(self) -> int: 

400 return hash((self.start, self.end, self._absolute)) 

401 

402 def __eq__(self, other: object) -> bool: 

403 if isinstance(other, Interval): 

404 return (self.start, self.end, self._absolute) == ( 

405 other.start, 

406 other.end, 

407 other._absolute, 

408 ) 

409 else: 

410 return self.as_duration() == other 

411 

412 def __ne__(self, other: object) -> bool: 

413 return not self.__eq__(other) 

414 

415 def __deepcopy__(self, memo: dict[int, Any]) -> Self: 

416 return self.__class__( 

417 copy.deepcopy(self.start, memo), 

418 copy.deepcopy(self.end, memo), 

419 self._absolute, 

420 )