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

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

208 statements  

1from __future__ import annotations 

2 

3import operator 

4 

5from datetime import date 

6from datetime import datetime 

7from datetime import timedelta 

8from typing import TYPE_CHECKING 

9from typing import Generic 

10from typing import TypeVar 

11from typing import cast 

12from typing import overload 

13 

14import pendulum 

15 

16from pendulum.constants import MONTHS_PER_YEAR 

17from pendulum.duration import Duration 

18from pendulum.helpers import precise_diff 

19 

20 

21if TYPE_CHECKING: 

22 from collections.abc import Iterator 

23 

24 from typing_extensions import Self 

25 from typing_extensions import SupportsIndex 

26 

27 from pendulum.helpers import PreciseDiff 

28 from pendulum.locales.locale import Locale 

29 

30 

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

32 

33 

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

35 """ 

36 An interval of time between two datetimes. 

37 """ 

38 

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

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

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

42 ): 

43 raise ValueError( 

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

45 ) 

46 

47 if ( 

48 isinstance(start, datetime) 

49 and isinstance(end, datetime) 

50 and ( 

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

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

53 ) 

54 ): 

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

56 

57 if absolute and start > end: 

58 end, start = start, end 

59 

60 _start = start 

61 _end = end 

62 if isinstance(start, pendulum.DateTime): 

63 _start = cast( 

64 "_T", 

65 datetime( 

66 start.year, 

67 start.month, 

68 start.day, 

69 start.hour, 

70 start.minute, 

71 start.second, 

72 start.microsecond, 

73 tzinfo=start.tzinfo, 

74 fold=start.fold, 

75 ), 

76 ) 

77 elif isinstance(start, pendulum.Date): 

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

79 

80 if isinstance(end, pendulum.DateTime): 

81 _end = cast( 

82 "_T", 

83 datetime( 

84 end.year, 

85 end.month, 

86 end.day, 

87 end.hour, 

88 end.minute, 

89 end.second, 

90 end.microsecond, 

91 tzinfo=end.tzinfo, 

92 fold=end.fold, 

93 ), 

94 ) 

95 elif isinstance(end, pendulum.Date): 

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

97 

98 # Fixing issues with datetime.__sub__() 

99 # not handling offsets if the tzinfo is the same 

100 if ( 

101 isinstance(_start, datetime) 

102 and isinstance(_end, datetime) 

103 and _start.tzinfo is _end.tzinfo 

104 ): 

105 if _start.tzinfo is not None: 

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

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

108 

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

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

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

112 

113 delta: timedelta = _end - _start 

114 

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

116 

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

118 super().__init__() 

119 

120 _start: _T 

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

122 if isinstance(start, datetime): 

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

124 else: 

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

126 

127 _start = start 

128 else: 

129 if isinstance(start, pendulum.DateTime): 

130 _start = cast( 

131 "_T", 

132 datetime( 

133 start.year, 

134 start.month, 

135 start.day, 

136 start.hour, 

137 start.minute, 

138 start.second, 

139 start.microsecond, 

140 tzinfo=start.tzinfo, 

141 ), 

142 ) 

143 else: 

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

145 

146 _end: _T 

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

148 if isinstance(end, datetime): 

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

150 else: 

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

152 

153 _end = end 

154 else: 

155 if isinstance(end, pendulum.DateTime): 

156 _end = cast( 

157 "_T", 

158 datetime( 

159 end.year, 

160 end.month, 

161 end.day, 

162 end.hour, 

163 end.minute, 

164 end.second, 

165 end.microsecond, 

166 tzinfo=end.tzinfo, 

167 ), 

168 ) 

169 else: 

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

171 

172 self._invert = False 

173 if start > end: 

174 self._invert = True 

175 

176 if absolute: 

177 end, start = start, end 

178 _end, _start = _start, _end 

179 

180 self._absolute = absolute 

181 self._start: _T = start 

182 self._end: _T = end 

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

184 

185 @property 

186 def years(self) -> int: 

187 return self._delta.years 

188 

189 @property 

190 def months(self) -> int: 

191 return self._delta.months 

192 

193 @property 

194 def weeks(self) -> int: 

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

196 

197 @property 

198 def days(self) -> int: 

199 return self._days 

200 

201 @property 

202 def remaining_days(self) -> int: 

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

204 

205 @property 

206 def hours(self) -> int: 

207 return self._delta.hours 

208 

209 @property 

210 def minutes(self) -> int: 

211 return self._delta.minutes 

212 

213 @property 

214 def start(self) -> _T: 

215 return self._start 

216 

217 @property 

218 def end(self) -> _T: 

219 return self._end 

220 

221 def in_years(self) -> int: 

222 """ 

223 Gives the duration of the Interval in full years. 

224 """ 

225 return self.years 

226 

227 def in_months(self) -> int: 

228 """ 

229 Gives the duration of the Interval in full months. 

230 """ 

231 return self.years * MONTHS_PER_YEAR + self.months 

232 

233 def in_weeks(self) -> int: 

234 days = self.in_days() 

235 sign = 1 

236 

237 if days < 0: 

238 sign = -1 

239 

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

241 

242 def in_days(self) -> int: 

243 return self._delta.total_days 

244 

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

246 """ 

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

248 

249 Ex: 6 jours 23 heures 58 minutes 

250 

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

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

253 """ 

254 from pendulum.locales.locale import Locale 

255 

256 intervals = [ 

257 ("year", self.years), 

258 ("month", self.months), 

259 ("week", self.weeks), 

260 ("day", self.remaining_days), 

261 ("hour", self.hours), 

262 ("minute", self.minutes), 

263 ("second", self.remaining_seconds), 

264 ] 

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

266 parts = [] 

267 for interval in intervals: 

268 unit, interval_count = interval 

269 if abs(interval_count) > 0: 

270 translation = loaded_locale.translation( 

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

272 ) 

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

274 

275 if not parts: 

276 count: str | int = 0 

277 if abs(self.microseconds) > 0: 

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

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

280 else: 

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

282 

283 translation = loaded_locale.translation(unit) 

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

285 

286 return separator.join(parts) 

287 

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

289 method = "add" 

290 op = operator.le 

291 if not self._absolute and self.invert: 

292 method = "subtract" 

293 op = operator.ge 

294 

295 start, end = self.start, self.end 

296 

297 i = amount 

298 while op(start, end): 

299 yield start 

300 

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

302 

303 i += amount 

304 

305 def as_duration(self) -> Duration: 

306 """ 

307 Return the Interval as a Duration. 

308 """ 

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

310 

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

312 return self.range("days") 

313 

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

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

316 

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

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

319 

320 __radd__ = __add__ # type: ignore[assignment] 

321 

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

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

324 

325 def __neg__(self) -> Self: 

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

327 

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

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

330 

331 __rmul__ = __mul__ # type: ignore[assignment] 

332 

333 @overload # type: ignore[override] 

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

335 

336 @overload 

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

338 

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

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

341 

342 __div__ = __floordiv__ # type: ignore[assignment] 

343 

344 @overload # type: ignore[override] 

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

346 

347 @overload 

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

349 

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

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

352 

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

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

355 

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

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

358 

359 def __abs__(self) -> Self: 

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

361 

362 def __repr__(self) -> str: 

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

364 

365 def __str__(self) -> str: 

366 return self.__repr__() 

367 

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

369 # Only needed for PyPy 

370 assert isinstance(other, timedelta) 

371 

372 if isinstance(other, Interval): 

373 other = other.as_timedelta() 

374 

375 td = self.as_timedelta() 

376 

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

378 

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

380 start, end = self.start, self.end 

381 

382 if self._invert and self._absolute: 

383 end, start = start, end 

384 

385 return start, end, self._absolute 

386 

387 def __reduce__( 

388 self, 

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

390 return self.__reduce_ex__(2) 

391 

392 def __reduce_ex__( 

393 self, protocol: SupportsIndex 

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

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

396 

397 def __hash__(self) -> int: 

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

399 

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

401 if isinstance(other, Interval): 

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

403 other.start, 

404 other.end, 

405 other._absolute, 

406 ) 

407 else: 

408 return self.as_duration() == other 

409 

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

411 return not self.__eq__(other)