opendate 0.1.12__py3-none-any.whl → 0.1.19__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of opendate might be problematic. Click here for more details.
- date/__init__.py +54 -16
- date/date.py +769 -714
- date/extras.py +28 -60
- opendate-0.1.19.dist-info/METADATA +762 -0
- opendate-0.1.19.dist-info/RECORD +7 -0
- {opendate-0.1.12.dist-info → opendate-0.1.19.dist-info}/WHEEL +1 -1
- opendate-0.1.12.dist-info/METADATA +0 -65
- opendate-0.1.12.dist-info/RECORD +0 -7
- {opendate-0.1.12.dist-info → opendate-0.1.19.dist-info/licenses}/LICENSE +0 -0
date/date.py
CHANGED
|
@@ -2,13 +2,14 @@ import calendar
|
|
|
2
2
|
import contextlib
|
|
3
3
|
import datetime as _datetime
|
|
4
4
|
import logging
|
|
5
|
+
import operator
|
|
5
6
|
import os
|
|
6
7
|
import re
|
|
7
8
|
import time
|
|
8
9
|
import warnings
|
|
9
10
|
import zoneinfo as _zoneinfo
|
|
10
11
|
from abc import ABC, abstractmethod
|
|
11
|
-
from collections.abc import Callable, Sequence
|
|
12
|
+
from collections.abc import Callable, Iterator, Sequence
|
|
12
13
|
from functools import lru_cache, partial, wraps
|
|
13
14
|
from typing import Self
|
|
14
15
|
|
|
@@ -26,7 +27,6 @@ __all__ = [
|
|
|
26
27
|
'Date',
|
|
27
28
|
'DateTime',
|
|
28
29
|
'Interval',
|
|
29
|
-
'IntervalError',
|
|
30
30
|
'Time',
|
|
31
31
|
'Timezone',
|
|
32
32
|
'EST',
|
|
@@ -39,35 +39,20 @@ __all__ = [
|
|
|
39
39
|
'prefer_utc_timezone',
|
|
40
40
|
'expect_date',
|
|
41
41
|
'expect_datetime',
|
|
42
|
+
'expect_time',
|
|
42
43
|
'Entity',
|
|
43
|
-
'NYSE'
|
|
44
|
+
'NYSE',
|
|
44
45
|
'WEEKDAY_SHORTNAME',
|
|
46
|
+
'WeekDay',
|
|
45
47
|
]
|
|
46
48
|
|
|
47
49
|
|
|
48
50
|
def Timezone(name:str = 'US/Eastern') -> _zoneinfo.ZoneInfo:
|
|
49
|
-
"""
|
|
50
|
-
|
|
51
|
-
Ex: sanity check US/Eastern == America/New_York
|
|
52
|
-
|
|
53
|
-
>>> winter1 = DateTime(2000, 1, 1, 12, tzinfo=Timezone('US/Eastern'))
|
|
54
|
-
>>> winter2 = DateTime(2000, 1, 1, 12, tzinfo=Timezone('America/New_York'))
|
|
55
|
-
|
|
56
|
-
>>> summer1 = DateTime(2000, 7, 1, 12, tzinfo=Timezone('US/Eastern'))
|
|
57
|
-
>>> summer2 = DateTime(2000, 7, 1, 12, tzinfo=Timezone('America/New_York'))
|
|
58
|
-
|
|
59
|
-
>>> winter = [winter1, winter2,
|
|
60
|
-
... winter1.astimezone(Timezone('America/New_York')),
|
|
61
|
-
... winter2.astimezone(Timezone('US/Eastern')),
|
|
62
|
-
... ]
|
|
63
|
-
>>> assert all(x==winter[0] for x in winter)
|
|
64
|
-
|
|
65
|
-
>>> summer = [summer1, summer2,
|
|
66
|
-
... summer1.astimezone(Timezone('America/New_York')),
|
|
67
|
-
... summer2.astimezone(Timezone('US/Eastern')),
|
|
68
|
-
... ]
|
|
69
|
-
>>> assert all(x==summer[0] for x in summer)
|
|
51
|
+
"""Create a timezone object with the specified name.
|
|
70
52
|
|
|
53
|
+
Simple wrapper around Pendulum's Timezone function that ensures
|
|
54
|
+
consistent timezone handling across the library. Note that 'US/Eastern'
|
|
55
|
+
is equivalent to 'America/New_York' for all dates.
|
|
71
56
|
"""
|
|
72
57
|
return _pendulum.tz.Timezone(name)
|
|
73
58
|
|
|
@@ -123,8 +108,8 @@ DATEMATCH = re.compile(r'^(?P<d>N|T|Y|P|M)(?P<n>[-+]?\d+)?(?P<b>b?)?$')
|
|
|
123
108
|
# return entity
|
|
124
109
|
|
|
125
110
|
|
|
126
|
-
def isdateish(x):
|
|
127
|
-
return isinstance(x, _datetime.date | _datetime.datetime | pd.Timestamp | np.datetime64)
|
|
111
|
+
def isdateish(x) -> bool:
|
|
112
|
+
return isinstance(x, _datetime.date | _datetime.datetime | _datetime.time | pd.Timestamp | np.datetime64)
|
|
128
113
|
|
|
129
114
|
|
|
130
115
|
def parse_arg(typ, arg):
|
|
@@ -162,6 +147,9 @@ def expect(func, typ: type[_datetime.date], exclkw: bool = False) -> Callable:
|
|
|
162
147
|
continue
|
|
163
148
|
if typ == _datetime.date:
|
|
164
149
|
kwargs[k] = Date.instance(v)
|
|
150
|
+
continue
|
|
151
|
+
if typ == _datetime.time:
|
|
152
|
+
kwargs[k] = Time.instance(v)
|
|
165
153
|
return func(*args, **kwargs)
|
|
166
154
|
return wrapper
|
|
167
155
|
|
|
@@ -172,8 +160,17 @@ expect_time = partial(expect, typ=_datetime.time)
|
|
|
172
160
|
|
|
173
161
|
|
|
174
162
|
def type_class(typ, obj):
|
|
163
|
+
if isinstance(typ, str):
|
|
164
|
+
if typ == 'Date':
|
|
165
|
+
return Date
|
|
166
|
+
if typ == 'DateTime':
|
|
167
|
+
return DateTime
|
|
168
|
+
if typ == 'Interval':
|
|
169
|
+
return Interval
|
|
175
170
|
if typ:
|
|
176
171
|
return typ
|
|
172
|
+
if obj.__class__ in {_pendulum.Interval, Interval}:
|
|
173
|
+
return Interval
|
|
177
174
|
if obj.__class__ in {_datetime.datetime, _pendulum.DateTime, DateTime}:
|
|
178
175
|
return DateTime
|
|
179
176
|
if obj.__class__ in {_datetime.date, _pendulum.Date, Date}:
|
|
@@ -207,7 +204,21 @@ def store_both(func=None, *, typ=None):
|
|
|
207
204
|
return wrapper
|
|
208
205
|
|
|
209
206
|
|
|
210
|
-
def
|
|
207
|
+
def reset_business(func):
|
|
208
|
+
"""Decorator to reset business mode after function execution.
|
|
209
|
+
"""
|
|
210
|
+
@wraps(func)
|
|
211
|
+
def wrapper(self, *args, **kwargs):
|
|
212
|
+
try:
|
|
213
|
+
return func(self, *args, **kwargs)
|
|
214
|
+
finally:
|
|
215
|
+
self._business = False
|
|
216
|
+
self._start._business = False
|
|
217
|
+
self._end._business = False
|
|
218
|
+
return wrapper
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def prefer_utc_timezone(func, force:bool = False) -> Callable:
|
|
211
222
|
"""Return datetime as UTC.
|
|
212
223
|
"""
|
|
213
224
|
@wraps(func)
|
|
@@ -221,7 +232,7 @@ def prefer_utc_timezone(func, force:bool = False):
|
|
|
221
232
|
return wrapper
|
|
222
233
|
|
|
223
234
|
|
|
224
|
-
def prefer_native_timezone(func, force:bool = False):
|
|
235
|
+
def prefer_native_timezone(func, force:bool = False) -> Callable:
|
|
225
236
|
"""Return datetime as native.
|
|
226
237
|
"""
|
|
227
238
|
@wraps(func)
|
|
@@ -240,7 +251,15 @@ expect_utc_timezone = partial(prefer_utc_timezone, force=True)
|
|
|
240
251
|
|
|
241
252
|
|
|
242
253
|
class Entity(ABC):
|
|
243
|
-
"""
|
|
254
|
+
"""Abstract base class for calendar entities with business day definitions.
|
|
255
|
+
|
|
256
|
+
This class defines the interface for calendar entities that provide
|
|
257
|
+
business day information, such as market open/close times and holidays.
|
|
258
|
+
Not available in pendulum.
|
|
259
|
+
|
|
260
|
+
Concrete implementations (like NYSE) provide specific calendar rules
|
|
261
|
+
for different business contexts.
|
|
262
|
+
"""
|
|
244
263
|
|
|
245
264
|
tz = UTC
|
|
246
265
|
|
|
@@ -261,7 +280,15 @@ class Entity(ABC):
|
|
|
261
280
|
|
|
262
281
|
|
|
263
282
|
class NYSE(Entity):
|
|
264
|
-
"""New York Stock Exchange
|
|
283
|
+
"""New York Stock Exchange calendar entity.
|
|
284
|
+
|
|
285
|
+
Provides business day definitions, market hours, and holidays
|
|
286
|
+
according to the NYSE trading calendar. Uses pandas_market_calendars
|
|
287
|
+
for the underlying implementation.
|
|
288
|
+
|
|
289
|
+
This entity is used as the default for business day calculations
|
|
290
|
+
throughout the library.
|
|
291
|
+
"""
|
|
265
292
|
|
|
266
293
|
BEGDATE = _datetime.date(1900, 1, 1)
|
|
267
294
|
ENDDATE = _datetime.date(2200, 1, 1)
|
|
@@ -289,31 +316,74 @@ class NYSE(Entity):
|
|
|
289
316
|
def business_holidays(begdate=BEGDATE, enddate=ENDDATE) -> set:
|
|
290
317
|
return {Date.instance(d.date())
|
|
291
318
|
for d in map(pd.to_datetime, NYSE.calendar.holidays().holidays)
|
|
292
|
-
if begdate <= d <= enddate}
|
|
319
|
+
if begdate <= d.date() <= enddate}
|
|
293
320
|
|
|
294
321
|
|
|
295
322
|
class DateBusinessMixin:
|
|
323
|
+
"""Mixin class providing business day functionality.
|
|
324
|
+
|
|
325
|
+
This mixin adds business day awareness to Date and DateTime classes,
|
|
326
|
+
allowing date operations to account for weekends and holidays according
|
|
327
|
+
to a specified calendar entity.
|
|
328
|
+
|
|
329
|
+
Features not available in pendulum:
|
|
330
|
+
- Business day mode toggle
|
|
331
|
+
- Entity-specific calendar rules
|
|
332
|
+
- Business-aware date arithmetic
|
|
333
|
+
"""
|
|
296
334
|
|
|
297
335
|
_entity: type[NYSE] = NYSE
|
|
298
336
|
_business: bool = False
|
|
299
337
|
|
|
300
338
|
def business(self) -> Self:
|
|
339
|
+
"""Switch to business day mode for date calculations.
|
|
340
|
+
|
|
341
|
+
In business day mode, date arithmetic only counts business days
|
|
342
|
+
as defined by the associated entity (default NYSE).
|
|
343
|
+
|
|
344
|
+
Returns
|
|
345
|
+
Self instance for method chaining
|
|
346
|
+
"""
|
|
301
347
|
self._business = True
|
|
302
348
|
return self
|
|
303
349
|
|
|
304
350
|
@property
|
|
305
351
|
def b(self) -> Self:
|
|
352
|
+
"""Shorthand property for business() method.
|
|
353
|
+
|
|
354
|
+
Returns
|
|
355
|
+
Self instance for method chaining
|
|
356
|
+
"""
|
|
306
357
|
return self.business()
|
|
307
358
|
|
|
308
359
|
def entity(self, entity: type[NYSE] = NYSE) -> Self:
|
|
360
|
+
"""Set the calendar entity for business day calculations.
|
|
361
|
+
|
|
362
|
+
Parameters
|
|
363
|
+
entity: Calendar entity class (defaults to NYSE)
|
|
364
|
+
|
|
365
|
+
Returns
|
|
366
|
+
Self instance for method chaining
|
|
367
|
+
"""
|
|
309
368
|
self._entity = entity
|
|
310
369
|
return self
|
|
311
370
|
|
|
312
371
|
@store_entity
|
|
313
372
|
def add(self, years: int = 0, months: int = 0, weeks: int = 0, days: int = 0, **kwargs) -> Self:
|
|
314
|
-
"""Add
|
|
315
|
-
|
|
316
|
-
|
|
373
|
+
"""Add time periods to the current date or datetime.
|
|
374
|
+
|
|
375
|
+
Extends pendulum's add method with business day awareness. When in business mode,
|
|
376
|
+
only counts business days for the 'days' parameter.
|
|
377
|
+
|
|
378
|
+
Parameters
|
|
379
|
+
years: Number of years to add
|
|
380
|
+
months: Number of months to add
|
|
381
|
+
weeks: Number of weeks to add
|
|
382
|
+
days: Number of days to add (business days if in business mode)
|
|
383
|
+
**kwargs: Additional time units to add
|
|
384
|
+
|
|
385
|
+
Returns
|
|
386
|
+
New instance with added time
|
|
317
387
|
"""
|
|
318
388
|
_business = self._business
|
|
319
389
|
self._business = False
|
|
@@ -418,63 +488,21 @@ class DateBusinessMixin:
|
|
|
418
488
|
|
|
419
489
|
@expect_date
|
|
420
490
|
def business_open(self) -> bool:
|
|
421
|
-
"""
|
|
422
|
-
|
|
423
|
-
>>> thedate = Date(2021, 4, 19) # Monday
|
|
424
|
-
>>> thedate.business_open()
|
|
425
|
-
True
|
|
426
|
-
>>> thedate = Date(2021, 4, 17) # Saturday
|
|
427
|
-
>>> thedate.business_open()
|
|
428
|
-
False
|
|
429
|
-
>>> thedate = Date(2021, 1, 18) # MLK Day
|
|
430
|
-
>>> thedate.business_open()
|
|
431
|
-
False
|
|
491
|
+
"""Check if the date is a business day (market is open).
|
|
432
492
|
"""
|
|
433
493
|
return self.is_business_day()
|
|
434
494
|
|
|
435
495
|
@expect_date
|
|
436
496
|
def is_business_day(self) -> bool:
|
|
437
|
-
"""
|
|
438
|
-
|
|
439
|
-
>>> thedate = Date(2021, 4, 19) # Monday
|
|
440
|
-
>>> thedate.is_business_day()
|
|
441
|
-
True
|
|
442
|
-
>>> thedate = Date(2021, 4, 17) # Saturday
|
|
443
|
-
>>> thedate.is_business_day()
|
|
444
|
-
False
|
|
445
|
-
>>> thedate = Date(2021, 1, 18) # MLK Day
|
|
446
|
-
>>> thedate.is_business_day()
|
|
447
|
-
False
|
|
448
|
-
>>> thedate = Date(2021, 11, 25) # Thanksgiving
|
|
449
|
-
>>> thedate.is_business_day()
|
|
450
|
-
False
|
|
451
|
-
>>> thedate = Date(2021, 11, 26) # Day after ^
|
|
452
|
-
>>> thedate.is_business_day()
|
|
453
|
-
True
|
|
497
|
+
"""Check if the date is a business day according to the entity calendar.
|
|
454
498
|
"""
|
|
455
499
|
return self in self._entity.business_days()
|
|
456
500
|
|
|
457
501
|
@expect_date
|
|
458
502
|
def business_hours(self) -> 'tuple[DateTime, DateTime]':
|
|
459
|
-
"""
|
|
460
|
-
|
|
461
|
-
Returns (None, None) if not a business day
|
|
503
|
+
"""Get market open and close times for this date.
|
|
462
504
|
|
|
463
|
-
|
|
464
|
-
>>> thedate.business_hours()
|
|
465
|
-
(... 9, 30, ... 16, 0, ...)
|
|
466
|
-
|
|
467
|
-
>>> thedate = Date(2023, 7, 3)
|
|
468
|
-
>>> thedate.business_hours()
|
|
469
|
-
(... 9, 30, ... 13, 0, ...)
|
|
470
|
-
|
|
471
|
-
>>> thedate = Date(2023, 11, 24)
|
|
472
|
-
>>> thedate.business_hours()
|
|
473
|
-
(... 9, 30, ... 13, 0, ...)
|
|
474
|
-
|
|
475
|
-
>>> thedate = Date(2024, 5, 27) # memorial day
|
|
476
|
-
>>> thedate.business_hours()
|
|
477
|
-
(None, None)
|
|
505
|
+
Returns (None, None) if not a business day.
|
|
478
506
|
"""
|
|
479
507
|
return self._entity.business_hours(self, self)\
|
|
480
508
|
.get(self, (None, None))
|
|
@@ -521,35 +549,27 @@ class DateBusinessMixin:
|
|
|
521
549
|
|
|
522
550
|
|
|
523
551
|
class DateExtrasMixin:
|
|
524
|
-
"""
|
|
525
|
-
scope of Pendulum. Ideally these should be removed.
|
|
552
|
+
"""Extended date functionality not provided by Pendulum.
|
|
526
553
|
|
|
527
|
-
|
|
554
|
+
.. note::
|
|
555
|
+
This mixin exists primarily for legacy backward compatibility.
|
|
556
|
+
New code should prefer using built-in methods where possible.
|
|
528
557
|
|
|
529
|
-
|
|
558
|
+
This mixin provides additional date utilities primarily focused on:
|
|
559
|
+
- Financial date calculations (nearest month start/end)
|
|
560
|
+
- Weekday-oriented date navigation
|
|
561
|
+
- Relative date lookups
|
|
530
562
|
|
|
531
|
-
|
|
563
|
+
These methods extend OpenDate functionality with features commonly
|
|
564
|
+
needed in financial applications and reporting scenarios.
|
|
532
565
|
"""
|
|
533
566
|
|
|
534
|
-
def nearest_start_of_month(self):
|
|
535
|
-
"""Get
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
>>> from date import Date
|
|
541
|
-
>>> Date(2015, 1, 1).nearest_start_of_month()
|
|
542
|
-
Date(2015, 1, 1)
|
|
543
|
-
>>> Date(2015, 1, 15).nearest_start_of_month()
|
|
544
|
-
Date(2015, 1, 1)
|
|
545
|
-
>>> Date(2015, 1, 15).b.nearest_start_of_month()
|
|
546
|
-
Date(2015, 1, 2)
|
|
547
|
-
>>> Date(2015, 1, 16).nearest_start_of_month()
|
|
548
|
-
Date(2015, 2, 1)
|
|
549
|
-
>>> Date(2015, 1, 31).nearest_start_of_month()
|
|
550
|
-
Date(2015, 2, 1)
|
|
551
|
-
>>> Date(2015, 1, 31).b.nearest_start_of_month()
|
|
552
|
-
Date(2015, 2, 2)
|
|
567
|
+
def nearest_start_of_month(self) -> Self:
|
|
568
|
+
"""Get the nearest start of month.
|
|
569
|
+
|
|
570
|
+
If day <= 15, returns start of current month.
|
|
571
|
+
If day > 15, returns start of next month.
|
|
572
|
+
In business mode, adjusts to next business day if needed.
|
|
553
573
|
"""
|
|
554
574
|
_business = self._business
|
|
555
575
|
self._business = False
|
|
@@ -563,25 +583,12 @@ class DateExtrasMixin:
|
|
|
563
583
|
return d.business().add(days=1)
|
|
564
584
|
return d
|
|
565
585
|
|
|
566
|
-
def nearest_end_of_month(self):
|
|
567
|
-
"""Get
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
>>> from date import Date
|
|
573
|
-
>>> Date(2015, 1, 1).nearest_end_of_month()
|
|
574
|
-
Date(2014, 12, 31)
|
|
575
|
-
>>> Date(2015, 1, 15).nearest_end_of_month()
|
|
576
|
-
Date(2014, 12, 31)
|
|
577
|
-
>>> Date(2015, 1, 15).b.nearest_end_of_month()
|
|
578
|
-
Date(2014, 12, 31)
|
|
579
|
-
>>> Date(2015, 1, 16).nearest_end_of_month()
|
|
580
|
-
Date(2015, 1, 31)
|
|
581
|
-
>>> Date(2015, 1, 31).nearest_end_of_month()
|
|
582
|
-
Date(2015, 1, 31)
|
|
583
|
-
>>> Date(2015, 1, 31).b.nearest_end_of_month()
|
|
584
|
-
Date(2015, 1, 30)
|
|
586
|
+
def nearest_end_of_month(self) -> Self:
|
|
587
|
+
"""Get the nearest end of month.
|
|
588
|
+
|
|
589
|
+
If day <= 15, returns end of previous month.
|
|
590
|
+
If day > 15, returns end of current month.
|
|
591
|
+
In business mode, adjusts to previous business day if needed.
|
|
585
592
|
"""
|
|
586
593
|
_business = self._business
|
|
587
594
|
self._business = False
|
|
@@ -595,77 +602,135 @@ class DateExtrasMixin:
|
|
|
595
602
|
return d.business().subtract(days=1)
|
|
596
603
|
return d
|
|
597
604
|
|
|
598
|
-
def next_relative_date_of_week_by_day(self, day='MO'):
|
|
599
|
-
"""Get next
|
|
600
|
-
|
|
601
|
-
>>> from date import Date
|
|
602
|
-
>>> Date(2020, 5, 18).next_relative_date_of_week_by_day('SU')
|
|
603
|
-
Date(2020, 5, 24)
|
|
604
|
-
>>> Date(2020, 5, 24).next_relative_date_of_week_by_day('SU')
|
|
605
|
-
Date(2020, 5, 24)
|
|
605
|
+
def next_relative_date_of_week_by_day(self, day='MO') -> Self:
|
|
606
|
+
"""Get next occurrence of the specified weekday (or current date if already that day).
|
|
606
607
|
"""
|
|
607
608
|
if self.weekday() == WEEKDAY_SHORTNAME.get(day):
|
|
608
609
|
return self
|
|
609
610
|
return self.next(WEEKDAY_SHORTNAME.get(day))
|
|
610
611
|
|
|
611
|
-
def weekday_or_previous_friday(self):
|
|
612
|
-
"""Return the date if it is a weekday,
|
|
613
|
-
|
|
614
|
-
>>> from date import Date
|
|
615
|
-
>>> Date(2019, 10, 6).weekday_or_previous_friday() # Sunday
|
|
616
|
-
Date(2019, 10, 4)
|
|
617
|
-
>>> Date(2019, 10, 5).weekday_or_previous_friday() # Saturday
|
|
618
|
-
Date(2019, 10, 4)
|
|
619
|
-
>>> Date(2019, 10, 4).weekday_or_previous_friday() # Friday
|
|
620
|
-
Date(2019, 10, 4)
|
|
621
|
-
>>> Date(2019, 10, 3).weekday_or_previous_friday() # Thursday
|
|
622
|
-
Date(2019, 10, 3)
|
|
612
|
+
def weekday_or_previous_friday(self) -> Self:
|
|
613
|
+
"""Return the date if it is a weekday, otherwise return the previous Friday.
|
|
623
614
|
"""
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
return self.subtract(days=dnum - 4)
|
|
615
|
+
if self.weekday() in {WeekDay.SATURDAY, WeekDay.SUNDAY}:
|
|
616
|
+
return self.previous(WeekDay.FRIDAY)
|
|
627
617
|
return self
|
|
628
618
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
or weekday, [1,2,3,4]
|
|
619
|
+
@classmethod
|
|
620
|
+
def third_wednesday(cls, year, month) -> Self:
|
|
621
|
+
"""Calculate the date of the third Wednesday in a given month/year.
|
|
633
622
|
|
|
634
|
-
|
|
623
|
+
.. deprecated::
|
|
624
|
+
Use Date(year, month, 1).nth_of('month', 3, WeekDay.WEDNESDAY) instead.
|
|
635
625
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
Date(2022, 6, 15)
|
|
643
|
-
>>> Date.third_wednesday(2023, 3)
|
|
644
|
-
Date(2023, 3, 15)
|
|
645
|
-
>>> Date.third_wednesday(2022, 12)
|
|
646
|
-
Date(2022, 12, 21)
|
|
647
|
-
>>> Date.third_wednesday(2023, 6)
|
|
648
|
-
Date(2023, 6, 21)
|
|
626
|
+
Parameters
|
|
627
|
+
year: The year to use
|
|
628
|
+
month: The month to use (1-12)
|
|
629
|
+
|
|
630
|
+
Returns
|
|
631
|
+
A Date object representing the third Wednesday of the specified month
|
|
649
632
|
"""
|
|
650
|
-
|
|
651
|
-
w = third.weekday()
|
|
652
|
-
if w != WeekDay.WEDNESDAY:
|
|
653
|
-
third = third.replace(day=(15 + (WeekDay.WEDNESDAY - w) % 7))
|
|
654
|
-
return third
|
|
633
|
+
return cls(year, month, 1).nth_of('month', 3, WeekDay.WEDNESDAY)
|
|
655
634
|
|
|
656
635
|
|
|
657
636
|
class Date(DateExtrasMixin, DateBusinessMixin, _pendulum.Date):
|
|
658
|
-
"""
|
|
637
|
+
"""Date class extending pendulum.Date with business day and additional functionality.
|
|
638
|
+
|
|
639
|
+
This class inherits all pendulum.Date functionality while adding:
|
|
640
|
+
- Business day calculations with NYSE calendar integration
|
|
641
|
+
- Additional date navigation methods
|
|
642
|
+
- Enhanced parsing capabilities
|
|
643
|
+
- Custom financial date utilities
|
|
644
|
+
|
|
645
|
+
Unlike pendulum.Date, methods that create new instances return Date objects
|
|
646
|
+
that preserve business status and entity association when chained.
|
|
659
647
|
"""
|
|
660
648
|
|
|
661
649
|
def to_string(self, fmt: str) -> str:
|
|
662
|
-
"""Format
|
|
650
|
+
"""Format date to string, handling platform-specific format codes.
|
|
663
651
|
|
|
664
|
-
|
|
665
|
-
'1/5/2022'
|
|
652
|
+
Automatically converts '%-' format codes to '%#' on Windows.
|
|
666
653
|
"""
|
|
667
654
|
return self.strftime(fmt.replace('%-', '%#') if os.name == 'nt' else fmt)
|
|
668
655
|
|
|
656
|
+
@store_entity(typ='Date')
|
|
657
|
+
def replace(self, *args, **kwargs):
|
|
658
|
+
"""Replace method that preserves entity and business status.
|
|
659
|
+
"""
|
|
660
|
+
return _pendulum.Date.replace(self, *args, **kwargs)
|
|
661
|
+
|
|
662
|
+
@store_entity(typ='Date')
|
|
663
|
+
def closest(self, *args, **kwargs):
|
|
664
|
+
"""Closest method that preserves entity and business status.
|
|
665
|
+
"""
|
|
666
|
+
return _pendulum.Date.closest(self, *args, **kwargs)
|
|
667
|
+
|
|
668
|
+
@store_entity(typ='Date')
|
|
669
|
+
def farthest(self, *args, **kwargs):
|
|
670
|
+
"""Farthest method that preserves entity and business status.
|
|
671
|
+
"""
|
|
672
|
+
return _pendulum.Date.farthest(self, *args, **kwargs)
|
|
673
|
+
|
|
674
|
+
@store_entity(typ='Date')
|
|
675
|
+
def average(self, dt=None):
|
|
676
|
+
"""Modify the current instance to the average
|
|
677
|
+
of a given instance (default now) and the current instance.
|
|
678
|
+
|
|
679
|
+
Parameters
|
|
680
|
+
dt: The date to average with (defaults to today)
|
|
681
|
+
|
|
682
|
+
Returns
|
|
683
|
+
A new Date object representing the average date
|
|
684
|
+
"""
|
|
685
|
+
return _pendulum.Date.average(self, dt)
|
|
686
|
+
|
|
687
|
+
@classmethod
|
|
688
|
+
def fromordinal(cls, *args, **kwargs) -> Self:
|
|
689
|
+
"""Create a Date from an ordinal.
|
|
690
|
+
|
|
691
|
+
Parameters
|
|
692
|
+
n: The ordinal value
|
|
693
|
+
|
|
694
|
+
Returns
|
|
695
|
+
Date instance
|
|
696
|
+
"""
|
|
697
|
+
result = _pendulum.Date.fromordinal(*args, **kwargs)
|
|
698
|
+
return cls.instance(result)
|
|
699
|
+
|
|
700
|
+
@classmethod
|
|
701
|
+
def fromtimestamp(cls, timestamp, tz=None) -> Self:
|
|
702
|
+
"""Create a Date from a timestamp.
|
|
703
|
+
|
|
704
|
+
Parameters
|
|
705
|
+
timestamp: Unix timestamp
|
|
706
|
+
tz: Optional timezone (defaults to UTC)
|
|
707
|
+
|
|
708
|
+
Returns
|
|
709
|
+
Date instance
|
|
710
|
+
"""
|
|
711
|
+
# Ensure timezone is always applied to get consistent results
|
|
712
|
+
tz = tz or UTC
|
|
713
|
+
dt = _datetime.datetime.fromtimestamp(timestamp, tz=tz)
|
|
714
|
+
return cls(dt.year, dt.month, dt.day)
|
|
715
|
+
|
|
716
|
+
@store_entity(typ='Date')
|
|
717
|
+
def nth_of(self, unit: str, nth: int, day_of_week: WeekDay) -> Self:
|
|
718
|
+
"""Returns a new instance set to the given occurrence
|
|
719
|
+
of a given day of the week in the current unit.
|
|
720
|
+
|
|
721
|
+
Parameters
|
|
722
|
+
unit: The unit to use ("month", "quarter", or "year")
|
|
723
|
+
nth: The position of the day in the unit (1 to 5)
|
|
724
|
+
day_of_week: The day of the week (pendulum.MONDAY to pendulum.SUNDAY)
|
|
725
|
+
|
|
726
|
+
Returns
|
|
727
|
+
A new Date object for the nth occurrence
|
|
728
|
+
|
|
729
|
+
Raises
|
|
730
|
+
ValueError: If the occurrence can't be found
|
|
731
|
+
"""
|
|
732
|
+
return _pendulum.Date.nth_of(self, unit, nth, day_of_week)
|
|
733
|
+
|
|
669
734
|
@classmethod
|
|
670
735
|
def parse(
|
|
671
736
|
cls,
|
|
@@ -676,84 +741,47 @@ class Date(DateExtrasMixin, DateBusinessMixin, _pendulum.Date):
|
|
|
676
741
|
) -> Self | None:
|
|
677
742
|
"""Convert a string to a date handling many different formats.
|
|
678
743
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
mon-dd-yyyy JUN-23-2006
|
|
722
|
-
>>> Date.parse('20 Jan 2009')
|
|
723
|
-
Date(2009, 1, 20)
|
|
724
|
-
|
|
725
|
-
month dd, yyyy June 23, 2006
|
|
726
|
-
>>> Date.parse('June 23, 2006')
|
|
727
|
-
Date(2006, 6, 23)
|
|
728
|
-
|
|
729
|
-
dd-mon-yy
|
|
730
|
-
>>> Date.parse('23-May-12')
|
|
731
|
-
Date(2012, 5, 23)
|
|
732
|
-
|
|
733
|
-
ddmonyyyy
|
|
734
|
-
>>> Date.parse('23May2012')
|
|
735
|
-
Date(2012, 5, 23)
|
|
736
|
-
|
|
737
|
-
>>> Date.parse('Oct. 24, 2007', fmt='%b. %d, %Y')
|
|
738
|
-
Date(2007, 10, 24)
|
|
739
|
-
|
|
740
|
-
>>> Date.parse('Yesterday') == DateTime.now().subtract(days=1).date()
|
|
741
|
-
True
|
|
742
|
-
>>> Date.parse('TODAY') == Date.today()
|
|
743
|
-
True
|
|
744
|
-
>>> Date.parse('Jan. 13, 2014')
|
|
745
|
-
Date(2014, 1, 13)
|
|
746
|
-
|
|
747
|
-
>>> Date.parse('March') == Date(Date.today().year, 3, Date.today().day)
|
|
748
|
-
True
|
|
749
|
-
|
|
750
|
-
only raise error when we explicitly say so
|
|
751
|
-
>>> Date.parse('bad date') is None
|
|
752
|
-
True
|
|
753
|
-
>>> Date.parse('bad date', raise_err=True)
|
|
754
|
-
Traceback (most recent call last):
|
|
755
|
-
...
|
|
756
|
-
ValueError: Failed to parse date: bad date
|
|
744
|
+
Supports various date formats including:
|
|
745
|
+
- Standard formats: YYYY-MM-DD, MM/DD/YYYY, MM/DD/YY, YYYYMMDD
|
|
746
|
+
- Named months: DD-MON-YYYY, MON-DD-YYYY, Month DD, YYYY
|
|
747
|
+
- Special codes: T (today), Y (yesterday), P (previous business day)
|
|
748
|
+
- Business day offsets: T-3b, P+2b (add/subtract business days)
|
|
749
|
+
- Custom format strings via fmt parameter
|
|
750
|
+
|
|
751
|
+
Parameters
|
|
752
|
+
s: String to parse or None
|
|
753
|
+
fmt: Optional strftime format string for custom parsing
|
|
754
|
+
entity: Calendar entity for business day calculations (default NYSE)
|
|
755
|
+
raise_err: If True, raises ValueError on parse failure instead of returning None
|
|
756
|
+
|
|
757
|
+
Returns
|
|
758
|
+
Date instance or None if parsing fails and raise_err is False
|
|
759
|
+
|
|
760
|
+
Examples
|
|
761
|
+
Standard numeric formats:
|
|
762
|
+
Date.parse('2020-01-15') → Date(2020, 1, 15)
|
|
763
|
+
Date.parse('01/15/2020') → Date(2020, 1, 15)
|
|
764
|
+
Date.parse('01/15/20') → Date(2020, 1, 15)
|
|
765
|
+
Date.parse('20200115') → Date(2020, 1, 15)
|
|
766
|
+
|
|
767
|
+
Named month formats:
|
|
768
|
+
Date.parse('15-Jan-2020') → Date(2020, 1, 15)
|
|
769
|
+
Date.parse('Jan 15, 2020') → Date(2020, 1, 15)
|
|
770
|
+
Date.parse('15JAN2020') → Date(2020, 1, 15)
|
|
771
|
+
|
|
772
|
+
Special codes:
|
|
773
|
+
Date.parse('T') → today's date
|
|
774
|
+
Date.parse('Y') → yesterday's date
|
|
775
|
+
Date.parse('P') → previous business day
|
|
776
|
+
Date.parse('M') → last day of previous month
|
|
777
|
+
|
|
778
|
+
Business day offsets:
|
|
779
|
+
Date.parse('T-3b') → 3 business days ago
|
|
780
|
+
Date.parse('P+2b') → 2 business days after previous business day
|
|
781
|
+
Date.parse('T+5') → 5 calendar days from today
|
|
782
|
+
|
|
783
|
+
Custom format:
|
|
784
|
+
Date.parse('15-Jan-2020', fmt='%d-%b-%Y') → Date(2020, 1, 15)
|
|
757
785
|
"""
|
|
758
786
|
|
|
759
787
|
def date_for_symbol(s):
|
|
@@ -795,7 +823,7 @@ class Date(DateExtrasMixin, DateBusinessMixin, _pendulum.Date):
|
|
|
795
823
|
return
|
|
796
824
|
|
|
797
825
|
with contextlib.suppress(ValueError):
|
|
798
|
-
if float(s) and
|
|
826
|
+
if float(s) and len(s) != 8: # 20000101
|
|
799
827
|
if raise_err:
|
|
800
828
|
raise ValueError('Invalid date: %s', s)
|
|
801
829
|
return
|
|
@@ -872,20 +900,17 @@ class Date(DateExtrasMixin, DateBusinessMixin, _pendulum.Date):
|
|
|
872
900
|
| None,
|
|
873
901
|
raise_err: bool = False,
|
|
874
902
|
) -> Self | None:
|
|
875
|
-
"""
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
>>> Date.instance(Date(2022, 1, 1))
|
|
880
|
-
Date(2022, 1, 1)
|
|
881
|
-
>>> Date.instance(_pendulum.Date(2022, 1, 1))
|
|
882
|
-
Date(2022, 1, 1)
|
|
883
|
-
>>> Date.instance(Date(2022, 1, 1))
|
|
884
|
-
Date(2022, 1, 1)
|
|
885
|
-
>>> Date.instance(np.datetime64('2000-01', 'D'))
|
|
886
|
-
Date(2000, 1, 1)
|
|
887
|
-
>>> Date.instance(None)
|
|
903
|
+
"""Create a Date instance from various date-like objects.
|
|
904
|
+
|
|
905
|
+
Converts datetime.date, datetime.datetime, pandas Timestamp,
|
|
906
|
+
numpy datetime64, and other date-like objects to Date instances.
|
|
888
907
|
|
|
908
|
+
Parameters
|
|
909
|
+
obj: Date-like object to convert
|
|
910
|
+
raise_err: If True, raises ValueError for None/NA values instead of returning None
|
|
911
|
+
|
|
912
|
+
Returns
|
|
913
|
+
Date instance or None if obj is None/NA and raise_err is False
|
|
889
914
|
"""
|
|
890
915
|
if pd.isna(obj):
|
|
891
916
|
if raise_err:
|
|
@@ -901,39 +926,21 @@ class Date(DateExtrasMixin, DateBusinessMixin, _pendulum.Date):
|
|
|
901
926
|
return cls(obj.year, obj.month, obj.day)
|
|
902
927
|
|
|
903
928
|
@classmethod
|
|
904
|
-
def today(cls):
|
|
929
|
+
def today(cls) -> Self:
|
|
905
930
|
d = _datetime.datetime.now(LCL)
|
|
906
931
|
return cls(d.year, d.month, d.day)
|
|
907
932
|
|
|
908
|
-
def isoweek(self):
|
|
909
|
-
"""
|
|
910
|
-
|
|
911
|
-
Standard weeks
|
|
912
|
-
>>> Date(2023, 1, 2).isoweek()
|
|
913
|
-
1
|
|
914
|
-
>>> Date(2023, 4, 27).isoweek()
|
|
915
|
-
17
|
|
916
|
-
>>> Date(2023, 12, 31).isoweek()
|
|
917
|
-
52
|
|
918
|
-
|
|
919
|
-
Belongs to week of previous year
|
|
920
|
-
>>> Date(2023, 1, 1).isoweek()
|
|
921
|
-
52
|
|
933
|
+
def isoweek(self) -> int | None:
|
|
934
|
+
"""Get ISO week number (1-52/53) following ISO week-numbering standard.
|
|
922
935
|
"""
|
|
923
936
|
with contextlib.suppress(Exception):
|
|
924
937
|
return self.isocalendar()[1]
|
|
925
938
|
|
|
926
939
|
def lookback(self, unit='last') -> Self:
|
|
927
|
-
"""
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
>>> Date(2018, 12, 7).b.lookback('day')
|
|
932
|
-
Date(2018, 12, 6)
|
|
933
|
-
>>> Date(2018, 12, 7).b.lookback('week')
|
|
934
|
-
Date(2018, 11, 30)
|
|
935
|
-
>>> Date(2018, 12, 7).b.lookback('month')
|
|
936
|
-
Date(2018, 11, 7)
|
|
940
|
+
"""Get date in the past based on lookback unit.
|
|
941
|
+
|
|
942
|
+
Supported units: 'last'/'day' (1 day), 'week', 'month', 'quarter', 'year'.
|
|
943
|
+
Respects business day mode if enabled.
|
|
937
944
|
"""
|
|
938
945
|
def _lookback(years=0, months=0, weeks=0, days=0):
|
|
939
946
|
_business = self._business
|
|
@@ -955,42 +962,60 @@ class Date(DateExtrasMixin, DateBusinessMixin, _pendulum.Date):
|
|
|
955
962
|
|
|
956
963
|
|
|
957
964
|
class Time(_pendulum.Time):
|
|
965
|
+
"""Time class extending pendulum.Time with additional functionality.
|
|
966
|
+
|
|
967
|
+
This class inherits all pendulum.Time functionality while adding:
|
|
968
|
+
- Enhanced parsing for various time formats
|
|
969
|
+
- Default UTC timezone when created
|
|
970
|
+
- Simple timezone conversion utilities
|
|
971
|
+
|
|
972
|
+
Unlike pendulum.Time, this class has more lenient parsing capabilities
|
|
973
|
+
and different timezone defaults.
|
|
974
|
+
"""
|
|
958
975
|
|
|
959
976
|
@classmethod
|
|
960
977
|
@prefer_utc_timezone
|
|
961
978
|
def parse(cls, s: str | None, fmt: str | None = None, raise_err: bool = False) -> Self | None:
|
|
962
|
-
"""
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
979
|
+
"""Parse time string in various formats.
|
|
980
|
+
|
|
981
|
+
Supported formats:
|
|
982
|
+
- hh:mm or hh.mm
|
|
983
|
+
- hh:mm:ss or hh.mm.ss
|
|
984
|
+
- hh:mm:ss.microseconds
|
|
985
|
+
- Any of above with AM/PM
|
|
986
|
+
- Compact: hhmmss or hhmmss.microseconds
|
|
987
|
+
|
|
988
|
+
Returns Time with UTC timezone by default.
|
|
989
|
+
|
|
990
|
+
Parameters
|
|
991
|
+
s: String to parse or None
|
|
992
|
+
fmt: Optional strftime format string for custom parsing
|
|
993
|
+
raise_err: If True, raises ValueError on parse failure instead of returning None
|
|
994
|
+
|
|
995
|
+
Returns
|
|
996
|
+
Time instance with UTC timezone or None if parsing fails and raise_err is False
|
|
997
|
+
|
|
998
|
+
Examples
|
|
999
|
+
Basic time formats:
|
|
1000
|
+
Time.parse('14:30') → Time(14, 30, 0, 0, tzinfo=UTC)
|
|
1001
|
+
Time.parse('14.30') → Time(14, 30, 0, 0, tzinfo=UTC)
|
|
1002
|
+
Time.parse('14:30:45') → Time(14, 30, 45, 0, tzinfo=UTC)
|
|
1003
|
+
|
|
1004
|
+
With microseconds:
|
|
1005
|
+
Time.parse('14:30:45.123456') → Time(14, 30, 45, 123456000, tzinfo=UTC)
|
|
1006
|
+
Time.parse('14:30:45,500000') → Time(14, 30, 45, 500000000, tzinfo=UTC)
|
|
1007
|
+
|
|
1008
|
+
AM/PM formats:
|
|
1009
|
+
Time.parse('2:30 PM') → Time(14, 30, 0, 0, tzinfo=UTC)
|
|
1010
|
+
Time.parse('11:30 AM') → Time(11, 30, 0, 0, tzinfo=UTC)
|
|
1011
|
+
Time.parse('12:30 PM') → Time(12, 30, 0, 0, tzinfo=UTC)
|
|
1012
|
+
|
|
1013
|
+
Compact formats:
|
|
1014
|
+
Time.parse('143045') → Time(14, 30, 45, 0, tzinfo=UTC)
|
|
1015
|
+
Time.parse('1430') → Time(14, 30, 0, 0, tzinfo=UTC)
|
|
1016
|
+
|
|
1017
|
+
Custom format:
|
|
1018
|
+
Time.parse('14-30-45', fmt='%H-%M-%S') → Time(14, 30, 45, 0, tzinfo=UTC)
|
|
994
1019
|
"""
|
|
995
1020
|
|
|
996
1021
|
def seconds(m):
|
|
@@ -1060,18 +1085,9 @@ class Time(_pendulum.Time):
|
|
|
1060
1085
|
tz: str | _zoneinfo.ZoneInfo | _datetime.tzinfo | None = None,
|
|
1061
1086
|
raise_err: bool = False,
|
|
1062
1087
|
) -> Self | None:
|
|
1063
|
-
"""
|
|
1064
|
-
|
|
1065
|
-
>>> Time.instance(_datetime.time(12, 30, 1))
|
|
1066
|
-
Time(12, 30, 1, tzinfo=Timezone('UTC'))
|
|
1067
|
-
>>> Time.instance(_pendulum.Time(12, 30, 1))
|
|
1068
|
-
Time(12, 30, 1, tzinfo=Timezone('UTC'))
|
|
1069
|
-
>>> Time.instance(None)
|
|
1070
|
-
|
|
1071
|
-
like Pendulum, do not add timzone if no timezone and Time object
|
|
1072
|
-
>>> Time.instance(Time(12, 30, 1))
|
|
1073
|
-
Time(12, 30, 1)
|
|
1088
|
+
"""Create Time instance from time-like object.
|
|
1074
1089
|
|
|
1090
|
+
Adds UTC timezone by default unless obj is already a Time instance.
|
|
1075
1091
|
"""
|
|
1076
1092
|
if pd.isna(obj):
|
|
1077
1093
|
if raise_err:
|
|
@@ -1085,15 +1101,8 @@ class Time(_pendulum.Time):
|
|
|
1085
1101
|
|
|
1086
1102
|
return cls(obj.hour, obj.minute, obj.second, obj.microsecond, tzinfo=tz)
|
|
1087
1103
|
|
|
1088
|
-
def in_timezone(self, tz: str | _zoneinfo.ZoneInfo | _datetime.tzinfo):
|
|
1089
|
-
"""Convert timezone
|
|
1090
|
-
|
|
1091
|
-
>>> Time(12, 0).in_timezone(Timezone('America/Sao_Paulo'))
|
|
1092
|
-
Time(9, 0, 0, tzinfo=Timezone('America/Sao_Paulo'))
|
|
1093
|
-
|
|
1094
|
-
>>> Time(12, 0, tzinfo=Timezone('Europe/Moscow')).in_timezone(Timezone('America/Sao_Paulo'))
|
|
1095
|
-
Time(6, 0, 0, tzinfo=Timezone('America/Sao_Paulo'))
|
|
1096
|
-
|
|
1104
|
+
def in_timezone(self, tz: str | _zoneinfo.ZoneInfo | _datetime.tzinfo) -> Self:
|
|
1105
|
+
"""Convert time to a different timezone.
|
|
1097
1106
|
"""
|
|
1098
1107
|
_dt = DateTime.combine(Date.today(), self, tzinfo=self.tzinfo or UTC)
|
|
1099
1108
|
return _dt.in_timezone(tz).time()
|
|
@@ -1102,14 +1111,114 @@ class Time(_pendulum.Time):
|
|
|
1102
1111
|
|
|
1103
1112
|
|
|
1104
1113
|
class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
1105
|
-
"""
|
|
1114
|
+
"""DateTime class extending pendulum.DateTime with business day and additional functionality.
|
|
1115
|
+
|
|
1116
|
+
This class inherits all pendulum.DateTime functionality while adding:
|
|
1117
|
+
- Business day calculations with NYSE calendar integration
|
|
1118
|
+
- Enhanced timezone handling
|
|
1119
|
+
- Extended parsing capabilities
|
|
1120
|
+
- Custom utility methods for financial applications
|
|
1121
|
+
|
|
1122
|
+
Unlike pendulum.DateTime:
|
|
1123
|
+
- today() returns start of day rather than current time
|
|
1124
|
+
- Methods preserve business status and entity when chaining
|
|
1125
|
+
- Has timezone handling helpers not present in pendulum
|
|
1106
1126
|
"""
|
|
1107
1127
|
|
|
1108
|
-
def epoch(self):
|
|
1128
|
+
def epoch(self) -> float:
|
|
1109
1129
|
"""Translate a datetime object into unix seconds since epoch
|
|
1110
1130
|
"""
|
|
1111
1131
|
return self.timestamp()
|
|
1112
1132
|
|
|
1133
|
+
@store_entity(typ='DateTime')
|
|
1134
|
+
def astimezone(self, *args, **kwargs):
|
|
1135
|
+
"""Convert to a timezone-aware datetime in a different timezone.
|
|
1136
|
+
"""
|
|
1137
|
+
return _pendulum.DateTime.astimezone(self, *args, **kwargs)
|
|
1138
|
+
|
|
1139
|
+
@store_entity(typ='DateTime')
|
|
1140
|
+
def in_timezone(self, *args, **kwargs):
|
|
1141
|
+
"""Convert to a timezone-aware datetime in a different timezone.
|
|
1142
|
+
"""
|
|
1143
|
+
return _pendulum.DateTime.in_timezone(self, *args, **kwargs)
|
|
1144
|
+
|
|
1145
|
+
@store_entity(typ='DateTime')
|
|
1146
|
+
def in_tz(self, *args, **kwargs):
|
|
1147
|
+
"""Convert to a timezone-aware datetime in a different timezone.
|
|
1148
|
+
"""
|
|
1149
|
+
return _pendulum.DateTime.in_tz(self, *args, **kwargs)
|
|
1150
|
+
|
|
1151
|
+
@store_entity(typ='DateTime')
|
|
1152
|
+
def replace(self, *args, **kwargs):
|
|
1153
|
+
"""Replace method that preserves entity and business status.
|
|
1154
|
+
"""
|
|
1155
|
+
return _pendulum.DateTime.replace(self, *args, **kwargs)
|
|
1156
|
+
|
|
1157
|
+
@classmethod
|
|
1158
|
+
def fromordinal(cls, *args, **kwargs) -> Self:
|
|
1159
|
+
"""Create a DateTime from an ordinal.
|
|
1160
|
+
|
|
1161
|
+
Parameters
|
|
1162
|
+
n: The ordinal value
|
|
1163
|
+
|
|
1164
|
+
Returns
|
|
1165
|
+
DateTime instance
|
|
1166
|
+
"""
|
|
1167
|
+
result = _pendulum.DateTime.fromordinal(*args, **kwargs)
|
|
1168
|
+
return cls.instance(result)
|
|
1169
|
+
|
|
1170
|
+
@classmethod
|
|
1171
|
+
def fromtimestamp(cls, timestamp, tz=None) -> Self:
|
|
1172
|
+
"""Create a DateTime from a timestamp.
|
|
1173
|
+
|
|
1174
|
+
Parameters
|
|
1175
|
+
timestamp: Unix timestamp
|
|
1176
|
+
tz: Optional timezone
|
|
1177
|
+
|
|
1178
|
+
Returns
|
|
1179
|
+
DateTime instance
|
|
1180
|
+
"""
|
|
1181
|
+
tz = tz or UTC
|
|
1182
|
+
result = _pendulum.DateTime.fromtimestamp(timestamp, tz)
|
|
1183
|
+
return cls.instance(result)
|
|
1184
|
+
|
|
1185
|
+
@classmethod
|
|
1186
|
+
def strptime(cls, time_str, fmt) -> Self:
|
|
1187
|
+
"""Parse a string into a DateTime according to a format.
|
|
1188
|
+
|
|
1189
|
+
Parameters
|
|
1190
|
+
time_str: String to parse
|
|
1191
|
+
fmt: Format string
|
|
1192
|
+
|
|
1193
|
+
Returns
|
|
1194
|
+
DateTime instance
|
|
1195
|
+
"""
|
|
1196
|
+
result = _pendulum.DateTime.strptime(time_str, fmt)
|
|
1197
|
+
return cls.instance(result)
|
|
1198
|
+
|
|
1199
|
+
@classmethod
|
|
1200
|
+
def utcfromtimestamp(cls, timestamp) -> Self:
|
|
1201
|
+
"""Create a UTC DateTime from a timestamp.
|
|
1202
|
+
|
|
1203
|
+
Parameters
|
|
1204
|
+
timestamp: Unix timestamp
|
|
1205
|
+
|
|
1206
|
+
Returns
|
|
1207
|
+
DateTime instance
|
|
1208
|
+
"""
|
|
1209
|
+
result = _pendulum.DateTime.utcfromtimestamp(timestamp)
|
|
1210
|
+
return cls.instance(result)
|
|
1211
|
+
|
|
1212
|
+
@classmethod
|
|
1213
|
+
def utcnow(cls) -> Self:
|
|
1214
|
+
"""Create a DateTime representing current UTC time.
|
|
1215
|
+
|
|
1216
|
+
Returns
|
|
1217
|
+
DateTime instance
|
|
1218
|
+
"""
|
|
1219
|
+
result = _pendulum.DateTime.utcnow()
|
|
1220
|
+
return cls.instance(result)
|
|
1221
|
+
|
|
1113
1222
|
@classmethod
|
|
1114
1223
|
def now(cls, tz: str | _zoneinfo.ZoneInfo | _datetime.tzinfo | None = None) -> Self:
|
|
1115
1224
|
"""Get a DateTime instance for the current date and time.
|
|
@@ -1126,12 +1235,21 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1126
1235
|
d.microsecond, tzinfo=d.tzinfo, fold=d.fold)
|
|
1127
1236
|
|
|
1128
1237
|
@classmethod
|
|
1129
|
-
def today(cls, tz: str | _zoneinfo.ZoneInfo | None = None):
|
|
1130
|
-
"""
|
|
1238
|
+
def today(cls, tz: str | _zoneinfo.ZoneInfo | None = None) -> Self:
|
|
1239
|
+
"""Create a DateTime object representing today at the start of day.
|
|
1240
|
+
|
|
1241
|
+
Unlike pendulum.today() which returns current time, this method
|
|
1242
|
+
returns a DateTime object at 00:00:00 of the current day.
|
|
1243
|
+
|
|
1244
|
+
Parameters
|
|
1245
|
+
tz: Optional timezone (defaults to local timezone)
|
|
1246
|
+
|
|
1247
|
+
Returns
|
|
1248
|
+
DateTime instance representing start of current day
|
|
1131
1249
|
"""
|
|
1132
1250
|
return DateTime.now(tz).start_of('day')
|
|
1133
1251
|
|
|
1134
|
-
def date(self):
|
|
1252
|
+
def date(self) -> Date:
|
|
1135
1253
|
return Date(self.year, self.month, self.day)
|
|
1136
1254
|
|
|
1137
1255
|
@classmethod
|
|
@@ -1146,25 +1264,13 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1146
1264
|
_tzinfo = tzinfo or time.tzinfo
|
|
1147
1265
|
return DateTime.instance(_datetime.datetime.combine(date, time, tzinfo=_tzinfo))
|
|
1148
1266
|
|
|
1149
|
-
def rfc3339(self):
|
|
1150
|
-
"""
|
|
1151
|
-
>>> DateTime.parse('Fri, 31 Oct 2014 10:55:00')
|
|
1152
|
-
DateTime(2014, 10, 31, 10, 55, 0, tzinfo=Timezone('UTC'))
|
|
1153
|
-
>>> DateTime.parse('Fri, 31 Oct 2014 10:55:00').rfc3339()
|
|
1154
|
-
'2014-10-31T10:55:00+00:00'
|
|
1267
|
+
def rfc3339(self) -> str:
|
|
1268
|
+
"""Return RFC 3339 formatted string (same as isoformat()).
|
|
1155
1269
|
"""
|
|
1156
1270
|
return self.isoformat()
|
|
1157
1271
|
|
|
1158
|
-
def time(self):
|
|
1159
|
-
"""Extract time from
|
|
1160
|
-
|
|
1161
|
-
>>> d = DateTime(2022, 1, 1, 12, 30, 15, tzinfo=EST)
|
|
1162
|
-
>>> d.time()
|
|
1163
|
-
Time(12, 30, 15, tzinfo=Timezone('US/Eastern'))
|
|
1164
|
-
|
|
1165
|
-
>>> d = DateTime(2022, 1, 1, 12, 30, 15, tzinfo=UTC)
|
|
1166
|
-
>>> d.time()
|
|
1167
|
-
Time(12, 30, 15, tzinfo=Timezone('UTC'))
|
|
1272
|
+
def time(self) -> Time:
|
|
1273
|
+
"""Extract time component from datetime (preserving timezone).
|
|
1168
1274
|
"""
|
|
1169
1275
|
return Time.instance(self)
|
|
1170
1276
|
|
|
@@ -1174,38 +1280,46 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1174
1280
|
entity: Entity = NYSE,
|
|
1175
1281
|
raise_err: bool = False
|
|
1176
1282
|
) -> Self | None:
|
|
1177
|
-
"""
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1283
|
+
"""Convert a string or timestamp to a DateTime with extended format support.
|
|
1284
|
+
|
|
1285
|
+
Unlike pendulum's parse, this method supports:
|
|
1286
|
+
- Unix timestamps (int/float, handles milliseconds automatically)
|
|
1287
|
+
- Special codes: T (today), Y (yesterday), P (previous business day)
|
|
1288
|
+
- Business day offsets: T-3b, P+2b (add/subtract business days)
|
|
1289
|
+
- Multiple date-time formats beyond ISO 8601
|
|
1290
|
+
- Combined date and time strings with various separators
|
|
1291
|
+
|
|
1292
|
+
Parameters
|
|
1293
|
+
s: String or timestamp to parse
|
|
1294
|
+
entity: Calendar entity for business day calculations (default NYSE)
|
|
1295
|
+
raise_err: If True, raises ValueError on parse failure instead of returning None
|
|
1296
|
+
|
|
1297
|
+
Returns
|
|
1298
|
+
DateTime instance or None if parsing fails and raise_err is False
|
|
1299
|
+
|
|
1300
|
+
Examples
|
|
1301
|
+
Unix timestamps:
|
|
1302
|
+
DateTime.parse(1609459200) → DateTime(2021, 1, 1, 0, 0, 0, tzinfo=LCL)
|
|
1303
|
+
DateTime.parse(1609459200000) → DateTime(2021, 1, 1, 0, 0, 0, tzinfo=LCL)
|
|
1304
|
+
|
|
1305
|
+
ISO 8601 format:
|
|
1306
|
+
DateTime.parse('2020-01-15T14:30:00') → DateTime(2020, 1, 15, 14, 30, 0)
|
|
1307
|
+
|
|
1308
|
+
Date and time separated:
|
|
1309
|
+
DateTime.parse('2020-01-15 14:30:00') → DateTime(2020, 1, 15, 14, 30, 0, tzinfo=LCL)
|
|
1310
|
+
DateTime.parse('01/15/2020:14:30:00') → DateTime(2020, 1, 15, 14, 30, 0, tzinfo=LCL)
|
|
1311
|
+
|
|
1312
|
+
Date only (time defaults to 00:00:00):
|
|
1313
|
+
DateTime.parse('2020-01-15') → DateTime(2020, 1, 15, 0, 0, 0)
|
|
1314
|
+
DateTime.parse('01/15/2020') → DateTime(2020, 1, 15, 0, 0, 0)
|
|
1315
|
+
|
|
1316
|
+
Time only (uses today's date):
|
|
1317
|
+
DateTime.parse('14:30:00') → DateTime(today's year, month, day, 14, 30, 0, tzinfo=LCL)
|
|
1318
|
+
|
|
1319
|
+
Special codes:
|
|
1320
|
+
DateTime.parse('T') → today at 00:00:00
|
|
1321
|
+
DateTime.parse('Y') → yesterday at 00:00:00
|
|
1322
|
+
DateTime.parse('P') → previous business day at 00:00:00
|
|
1209
1323
|
"""
|
|
1210
1324
|
if not s:
|
|
1211
1325
|
if raise_err:
|
|
@@ -1216,6 +1330,8 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1216
1330
|
raise TypeError(f'Invalid type for datetime parse: {s.__class__}')
|
|
1217
1331
|
|
|
1218
1332
|
if isinstance(s, int | float):
|
|
1333
|
+
if len(str(int(s))) == 13:
|
|
1334
|
+
s /= 1000 # Convert from milliseconds to seconds
|
|
1219
1335
|
iso = _datetime.datetime.fromtimestamp(s).isoformat()
|
|
1220
1336
|
return cls.parse(iso).replace(tzinfo=LCL)
|
|
1221
1337
|
|
|
@@ -1254,41 +1370,23 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1254
1370
|
tz: str | _zoneinfo.ZoneInfo | _datetime.tzinfo | None = None,
|
|
1255
1371
|
raise_err: bool = False,
|
|
1256
1372
|
) -> Self | None:
|
|
1257
|
-
"""
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
>>> DateTime.instance(Date(2022, 1, 1))
|
|
1262
|
-
DateTime(2022, 1, 1, 0, 0, 0, tzinfo=Timezone('...'))
|
|
1263
|
-
>>> DateTime.instance(_datetime.datetime(2022, 1, 1, 0, 0, 0))
|
|
1264
|
-
DateTime(2022, 1, 1, 0, 0, 0, tzinfo=Timezone('...'))
|
|
1265
|
-
>>> DateTime.instance(_pendulum.DateTime(2022, 1, 1, 0, 0, 0))
|
|
1266
|
-
DateTime(2022, 1, 1, 0, 0, 0, tzinfo=Timezone('...'))
|
|
1267
|
-
>>> DateTime.instance(None)
|
|
1268
|
-
|
|
1269
|
-
like Pendulum, do not add timzone if no timezone and DateTime object
|
|
1270
|
-
>>> DateTime.instance(DateTime(2022, 1, 1, 0, 0, 0))
|
|
1271
|
-
DateTime(2022, 1, 1, 0, 0, 0)
|
|
1272
|
-
>>> DateTime.instance(DateTime(2000, 1, 1))
|
|
1273
|
-
DateTime(2000, 1, 1, 0, 0, 0)
|
|
1274
|
-
|
|
1275
|
-
no tz -> UTC
|
|
1276
|
-
>>> DateTime.instance(Time(4, 4, 21))
|
|
1277
|
-
DateTime(..., 4, 4, 21, tzinfo=Timezone('UTC'))
|
|
1278
|
-
|
|
1279
|
-
tzinfo on time -> time tzinfo (precedence)
|
|
1280
|
-
>>> DateTime.instance(Time(4, 4, 21, tzinfo=UTC))
|
|
1281
|
-
DateTime(..., 4, 4, 21, tzinfo=Timezone('UTC'))
|
|
1282
|
-
>>> DateTime.instance(Time(4, 4, 21, tzinfo=LCL))
|
|
1283
|
-
DateTime(..., 4, 4, 21, tzinfo=Timezone('...'))
|
|
1284
|
-
|
|
1285
|
-
>>> DateTime.instance(np.datetime64('2000-01', 'D'))
|
|
1286
|
-
DateTime(2000, 1, 1, 0, 0, 0, tzinfo=Timezone('UTC'))
|
|
1287
|
-
|
|
1288
|
-
Convert date to datetime (will use native time zone)
|
|
1289
|
-
>>> DateTime.instance(_datetime.date(2000, 1, 1))
|
|
1290
|
-
DateTime(2000, 1, 1, 0, 0, 0, tzinfo=Timezone('...'))
|
|
1373
|
+
"""Create a DateTime instance from various datetime-like objects.
|
|
1374
|
+
|
|
1375
|
+
Provides unified interface for converting different date/time types
|
|
1376
|
+
including pandas and numpy datetime objects into DateTime instances.
|
|
1291
1377
|
|
|
1378
|
+
Unlike pendulum, this method:
|
|
1379
|
+
- Handles pandas Timestamp and numpy datetime64 objects
|
|
1380
|
+
- Adds timezone (UTC by default) when none is specified
|
|
1381
|
+
- Has special handling for Time objects (combines with current date)
|
|
1382
|
+
|
|
1383
|
+
Parameters
|
|
1384
|
+
obj: Date, datetime, time, or compatible object to convert
|
|
1385
|
+
tz: Optional timezone to apply (if None, uses obj's timezone or UTC)
|
|
1386
|
+
raise_err: If True, raises ValueError for None/NA values instead of returning None
|
|
1387
|
+
|
|
1388
|
+
Returns
|
|
1389
|
+
DateTime instance or None if obj is None/NA and raise_err is False
|
|
1292
1390
|
"""
|
|
1293
1391
|
if pd.isna(obj):
|
|
1294
1392
|
if raise_err:
|
|
@@ -1321,25 +1419,91 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1321
1419
|
obj.second, obj.microsecond, tzinfo=tz)
|
|
1322
1420
|
|
|
1323
1421
|
|
|
1324
|
-
class
|
|
1325
|
-
|
|
1422
|
+
class Interval(_pendulum.Interval):
|
|
1423
|
+
"""Interval class extending pendulum.Interval with business day awareness.
|
|
1326
1424
|
|
|
1425
|
+
This class represents the difference between two dates or datetimes with
|
|
1426
|
+
additional support for business day calculations, entity awareness, and
|
|
1427
|
+
financial period calculations.
|
|
1327
1428
|
|
|
1328
|
-
|
|
1429
|
+
Unlike pendulum.Interval:
|
|
1430
|
+
- Has business day mode that only counts business days
|
|
1431
|
+
- Preserves entity association (e.g., NYSE)
|
|
1432
|
+
- Additional financial methods like yearfrac()
|
|
1433
|
+
- Support for range operations that respect business days
|
|
1434
|
+
"""
|
|
1329
1435
|
|
|
1330
1436
|
_business: bool = False
|
|
1331
1437
|
_entity: type[NYSE] = NYSE
|
|
1332
1438
|
|
|
1333
|
-
def
|
|
1334
|
-
|
|
1335
|
-
|
|
1439
|
+
def __new__(cls, begdate: Date | DateTime, enddate: Date | DateTime) -> Self:
|
|
1440
|
+
assert begdate and enddate, 'Interval dates cannot be None'
|
|
1441
|
+
instance = super().__new__(cls, begdate, enddate, False)
|
|
1442
|
+
return instance
|
|
1443
|
+
|
|
1444
|
+
def __init__(self, begdate: Date | DateTime, enddate: Date | DateTime) -> None:
|
|
1445
|
+
super().__init__(begdate, enddate, False)
|
|
1446
|
+
self._sign = 1 if begdate <= enddate else -1
|
|
1447
|
+
if begdate <= enddate:
|
|
1448
|
+
self._start = begdate
|
|
1449
|
+
self._end = enddate
|
|
1450
|
+
else:
|
|
1451
|
+
self._start = enddate
|
|
1452
|
+
self._end = begdate
|
|
1453
|
+
|
|
1454
|
+
@staticmethod
|
|
1455
|
+
def _get_quarter_start(date: Date | DateTime) -> Date | DateTime:
|
|
1456
|
+
"""Get the start date of the quarter containing the given date.
|
|
1457
|
+
"""
|
|
1458
|
+
quarter_month = ((date.month - 1) // 3) * 3 + 1
|
|
1459
|
+
return date.replace(month=quarter_month, day=1)
|
|
1460
|
+
|
|
1461
|
+
@staticmethod
|
|
1462
|
+
def _get_quarter_end(date: Date | DateTime) -> Date | DateTime:
|
|
1463
|
+
"""Get the end date of the quarter containing the given date.
|
|
1464
|
+
"""
|
|
1465
|
+
quarter_month = ((date.month - 1) // 3) * 3 + 3
|
|
1466
|
+
return date.replace(month=quarter_month).end_of('month')
|
|
1467
|
+
|
|
1468
|
+
def _get_unit_handlers(self, unit: str) -> dict:
|
|
1469
|
+
"""Get handlers for the specified time unit.
|
|
1470
|
+
|
|
1471
|
+
Returns a dict with:
|
|
1472
|
+
get_start: Function to get start of period containing date
|
|
1473
|
+
get_end: Function to get end of period containing date
|
|
1474
|
+
advance: Function to advance to next period start
|
|
1475
|
+
"""
|
|
1476
|
+
if unit == 'quarter':
|
|
1477
|
+
return {
|
|
1478
|
+
'get_start': self._get_quarter_start,
|
|
1479
|
+
'get_end': self._get_quarter_end,
|
|
1480
|
+
'advance': lambda date: self._get_quarter_start(date.add(months=3)),
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
if unit == 'decade':
|
|
1484
|
+
return {
|
|
1485
|
+
'get_start': lambda date: date.start_of('decade'),
|
|
1486
|
+
'get_end': lambda date: date.end_of('decade'),
|
|
1487
|
+
'advance': lambda date: date.add(years=10).start_of('decade'),
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
if unit == 'century':
|
|
1491
|
+
return {
|
|
1492
|
+
'get_start': lambda date: date.start_of('century'),
|
|
1493
|
+
'get_end': lambda date: date.end_of('century'),
|
|
1494
|
+
'advance': lambda date: date.add(years=100).start_of('century'),
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
return {
|
|
1498
|
+
'get_start': lambda date: date.start_of(unit),
|
|
1499
|
+
'get_end': lambda date: date.end_of(unit),
|
|
1500
|
+
'advance': lambda date: date.add(**{f'{unit}s': 1}).start_of(unit),
|
|
1501
|
+
}
|
|
1336
1502
|
|
|
1337
1503
|
def business(self) -> Self:
|
|
1338
1504
|
self._business = True
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
if self.enddate:
|
|
1342
|
-
self.enddate.business()
|
|
1505
|
+
self._start.business()
|
|
1506
|
+
self._end.business()
|
|
1343
1507
|
return self
|
|
1344
1508
|
|
|
1345
1509
|
@property
|
|
@@ -1348,249 +1512,120 @@ class Interval:
|
|
|
1348
1512
|
|
|
1349
1513
|
def entity(self, e: type[NYSE] = NYSE) -> Self:
|
|
1350
1514
|
self._entity = e
|
|
1351
|
-
if self.
|
|
1352
|
-
self.
|
|
1353
|
-
if self.
|
|
1354
|
-
self.
|
|
1515
|
+
if self._start:
|
|
1516
|
+
self._end._entity = e
|
|
1517
|
+
if self._end:
|
|
1518
|
+
self._end._entity = e
|
|
1355
1519
|
return self
|
|
1356
1520
|
|
|
1357
|
-
def
|
|
1358
|
-
"""
|
|
1359
|
-
|
|
1360
|
-
The combinations are as follows:
|
|
1361
|
-
|
|
1362
|
-
beg end num action
|
|
1363
|
-
--- --- --- ---------------------
|
|
1364
|
-
- - - Error, underspecified
|
|
1365
|
-
set set set Error, overspecified
|
|
1366
|
-
set set -
|
|
1367
|
-
set - - end=max date
|
|
1368
|
-
- set - beg=min date
|
|
1369
|
-
- - set end=max date, beg=end - num
|
|
1370
|
-
set - set end=beg + num
|
|
1371
|
-
- set set beg=end - num
|
|
1372
|
-
|
|
1373
|
-
Basic/legacy cases
|
|
1374
|
-
>>> Interval(Date(2014, 4, 3), None).b.range(3)
|
|
1375
|
-
(Date(2014, 4, 3), Date(2014, 4, 8))
|
|
1376
|
-
>>> Interval(None, Date(2014, 7, 27)).range(20)
|
|
1377
|
-
(Date(2014, 7, 7), Date(2014, 7, 27))
|
|
1378
|
-
>>> Interval(None, Date(2014, 7, 27)).b.range(20)
|
|
1379
|
-
(Date(2014, 6, 27), Date(2014, 7, 27))
|
|
1380
|
-
|
|
1381
|
-
Do not modify dates if both are provided
|
|
1382
|
-
>>> Interval(Date(2024, 7, 25), Date(2024, 7, 25)).b.range(None)
|
|
1383
|
-
(Date(2024, 7, 25), Date(2024, 7, 25))
|
|
1384
|
-
>>> Interval(Date(2024, 7, 27), Date(2024, 7, 27)).b.range(None)
|
|
1385
|
-
(Date(2024, 7, 27), Date(2024, 7, 27))
|
|
1386
|
-
|
|
1387
|
-
Edge cases (7/27/24 is weekend)
|
|
1388
|
-
>>> Interval(Date(2024, 7, 27), None).b.range(0)
|
|
1389
|
-
(Date(2024, 7, 27), Date(2024, 7, 27))
|
|
1390
|
-
>>> Interval(None, Date(2024, 7, 27)).b.range(0)
|
|
1391
|
-
(Date(2024, 7, 27), Date(2024, 7, 27))
|
|
1392
|
-
>>> Interval(Date(2024, 7, 27), None).b.range(1)
|
|
1393
|
-
(Date(2024, 7, 27), Date(2024, 7, 29))
|
|
1394
|
-
>>> Interval(None, Date(2024, 7, 27)).b.range(1)
|
|
1395
|
-
(Date(2024, 7, 26), Date(2024, 7, 27))
|
|
1521
|
+
def is_business_day_range(self) -> list[bool]:
|
|
1522
|
+
"""Generate boolean values indicating whether each day in the range is a business day.
|
|
1396
1523
|
"""
|
|
1397
|
-
|
|
1524
|
+
self._business = False
|
|
1525
|
+
for thedate in self.range('days'):
|
|
1526
|
+
yield thedate.is_business_day()
|
|
1398
1527
|
|
|
1399
|
-
|
|
1528
|
+
@reset_business
|
|
1529
|
+
def range(self, unit: str = 'days', amount: int = 1) -> Iterator[DateTime | Date]:
|
|
1530
|
+
"""Generate dates/datetimes over the interval.
|
|
1400
1531
|
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
raise IntervalError('Missing begdate, enddate, and window')
|
|
1405
|
-
if not begdate and not enddate and window:
|
|
1406
|
-
raise IntervalError('Missing begdate and enddate, window specified')
|
|
1532
|
+
Parameters
|
|
1533
|
+
unit: Time unit ('days', 'weeks', 'months', 'years')
|
|
1534
|
+
amount: Step size (e.g., every N units)
|
|
1407
1535
|
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
else:
|
|
1413
|
-
enddate = begdate.add(days=window) if window else begdate
|
|
1536
|
+
In business mode (for 'days' only), skips non-business days.
|
|
1537
|
+
"""
|
|
1538
|
+
_business = self._business
|
|
1539
|
+
parent_range = _pendulum.Interval.range
|
|
1414
1540
|
|
|
1415
|
-
|
|
1416
|
-
|
|
1541
|
+
def _range_generator():
|
|
1542
|
+
if unit != 'days':
|
|
1543
|
+
yield from (type(d).instance(d) for d in parent_range(self, unit, amount))
|
|
1544
|
+
return
|
|
1417
1545
|
|
|
1418
|
-
|
|
1546
|
+
if self._sign == 1:
|
|
1547
|
+
op = operator.le
|
|
1548
|
+
this = self._start
|
|
1549
|
+
thru = self._end
|
|
1550
|
+
else:
|
|
1551
|
+
op = operator.ge
|
|
1552
|
+
this = self._end
|
|
1553
|
+
thru = self._start
|
|
1554
|
+
|
|
1555
|
+
while op(this, thru):
|
|
1556
|
+
if _business:
|
|
1557
|
+
if this.is_business_day():
|
|
1558
|
+
yield this
|
|
1559
|
+
else:
|
|
1560
|
+
yield this
|
|
1561
|
+
this = this.add(days=self._sign * amount)
|
|
1419
1562
|
|
|
1420
|
-
|
|
1421
|
-
"""Is business date range.
|
|
1563
|
+
return _range_generator()
|
|
1422
1564
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1565
|
+
@property
|
|
1566
|
+
@reset_business
|
|
1567
|
+
def days(self) -> int:
|
|
1568
|
+
"""Get number of days in the interval (respects business mode and sign).
|
|
1427
1569
|
"""
|
|
1428
|
-
|
|
1429
|
-
yield thedate.is_business_day()
|
|
1570
|
+
return self._sign * len(tuple(self.range('days'))) - self._sign
|
|
1430
1571
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
defaults to include ALL days (not just business days)
|
|
1438
|
-
|
|
1439
|
-
>>> next(Interval(Date(2014,7,16), Date(2014,7,16)).series())
|
|
1440
|
-
Date(2014, 7, 16)
|
|
1441
|
-
>>> next(Interval(Date(2014,7,12), Date(2014,7,16)).series())
|
|
1442
|
-
Date(2014, 7, 12)
|
|
1443
|
-
>>> len(list(Interval(Date(2014,7,12), Date(2014,7,16)).series()))
|
|
1444
|
-
5
|
|
1445
|
-
>>> len(list(Interval(Date(2014,7,12), None).series(window=4)))
|
|
1446
|
-
5
|
|
1447
|
-
>>> len(list(Interval(Date(2014,7,16)).series(window=4)))
|
|
1448
|
-
5
|
|
1449
|
-
|
|
1450
|
-
Weekend and a holiday
|
|
1451
|
-
>>> len(list(Interval(Date(2014,7,3), Date(2014,7,5)).b.series()))
|
|
1452
|
-
1
|
|
1453
|
-
>>> len(list(Interval(Date(2014,7,17), Date(2014,7,16)).series()))
|
|
1454
|
-
Traceback (most recent call last):
|
|
1455
|
-
...
|
|
1456
|
-
AssertionError: Begdate must be earlier or equal to Enddate
|
|
1457
|
-
|
|
1458
|
-
since != business day and want business days
|
|
1459
|
-
1/[3,10]/2015 is a Saturday, 1/7/2015 is a Wednesday
|
|
1460
|
-
>>> len(list(Interval(Date(2015,1,3), Date(2015,1,7)).b.series()))
|
|
1461
|
-
3
|
|
1462
|
-
>>> len(list(Interval(Date(2015,1,3), None).b.series(window=3)))
|
|
1463
|
-
3
|
|
1464
|
-
>>> len(list(Interval(Date(2015,1,3), Date(2015,1,10)).b.series()))
|
|
1465
|
-
5
|
|
1466
|
-
>>> len(list(Interval(Date(2015,1,3), None).b.series(window=5)))
|
|
1467
|
-
5
|
|
1468
|
-
"""
|
|
1469
|
-
window = abs(int(window))
|
|
1470
|
-
since, until = self.begdate, self.enddate
|
|
1471
|
-
_business = self._business
|
|
1472
|
-
assert until or since, 'Since or until is required'
|
|
1473
|
-
if not since and until:
|
|
1474
|
-
since = (until.business() if _business else
|
|
1475
|
-
until).subtract(days=window)
|
|
1476
|
-
elif since and not until:
|
|
1477
|
-
until = (since.business() if _business else
|
|
1478
|
-
since).add(days=window)
|
|
1479
|
-
assert since <= until, 'Since date must be earlier or equal to Until date'
|
|
1480
|
-
thedate = since
|
|
1481
|
-
while thedate <= until:
|
|
1482
|
-
if _business:
|
|
1483
|
-
if thedate.is_business_day():
|
|
1484
|
-
yield thedate
|
|
1485
|
-
else:
|
|
1486
|
-
yield thedate
|
|
1487
|
-
thedate = thedate.add(days=1)
|
|
1488
|
-
|
|
1489
|
-
def start_of_series(self, unit='month') -> list[Date]:
|
|
1490
|
-
"""Return a series between and inclusive of begdate and enddate.
|
|
1491
|
-
|
|
1492
|
-
>>> Interval(Date(2018, 1, 5), Date(2018, 4, 5)).start_of_series('month')
|
|
1493
|
-
[Date(2018, 1, 1), Date(2018, 2, 1), Date(2018, 3, 1), Date(2018, 4, 1)]
|
|
1494
|
-
>>> Interval(Date(2018, 4, 30), Date(2018, 7, 30)).start_of_series('month')
|
|
1495
|
-
[Date(2018, 4, 1), Date(2018, 5, 1), Date(2018, 6, 1), Date(2018, 7, 1)]
|
|
1496
|
-
>>> Interval(Date(2018, 1, 5), Date(2018, 4, 5)).start_of_series('week')
|
|
1497
|
-
[Date(2018, 1, 1), Date(2018, 1, 8), ..., Date(2018, 4, 2)]
|
|
1498
|
-
"""
|
|
1499
|
-
begdate = self.begdate.start_of(unit)
|
|
1500
|
-
enddate = self.enddate.start_of(unit)
|
|
1501
|
-
interval = _pendulum.interval(begdate, enddate)
|
|
1502
|
-
return [Date.instance(d).start_of(unit) for d in interval.range(f'{unit}s')]
|
|
1503
|
-
|
|
1504
|
-
def end_of_series(self, unit='month') -> list[Date]:
|
|
1505
|
-
"""Return a series between and inclusive of begdate and enddate.
|
|
1506
|
-
|
|
1507
|
-
>>> Interval(Date(2018, 1, 5), Date(2018, 4, 5)).end_of_series('month')
|
|
1508
|
-
[Date(2018, 1, 31), Date(2018, 2, 28), Date(2018, 3, 31), Date(2018, 4, 30)]
|
|
1509
|
-
>>> Interval(Date(2018, 4, 30), Date(2018, 7, 30)).end_of_series('month')
|
|
1510
|
-
[Date(2018, 4, 30), Date(2018, 5, 31), Date(2018, 6, 30), Date(2018, 7, 31)]
|
|
1511
|
-
>>> Interval(Date(2018, 1, 5), Date(2018, 4, 5)).end_of_series('week')
|
|
1512
|
-
[Date(2018, 1, 7), Date(2018, 1, 14), ..., Date(2018, 4, 8)]
|
|
1572
|
+
@property
|
|
1573
|
+
def months(self) -> float:
|
|
1574
|
+
"""Get number of months in the interval including fractional parts.
|
|
1575
|
+
|
|
1576
|
+
Overrides pendulum's months property to return a float instead of an integer.
|
|
1577
|
+
Calculates fractional months based on actual day counts within partial months.
|
|
1513
1578
|
"""
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1579
|
+
year_diff = self._end.year - self._start.year
|
|
1580
|
+
month_diff = self._end.month - self._start.month
|
|
1581
|
+
total_months = year_diff * 12 + month_diff
|
|
1582
|
+
|
|
1583
|
+
if self._end.day >= self._start.day:
|
|
1584
|
+
day_diff = self._end.day - self._start.day
|
|
1585
|
+
days_in_month = calendar.monthrange(self._start.year, self._start.month)[1]
|
|
1586
|
+
fraction = day_diff / days_in_month
|
|
1587
|
+
else:
|
|
1588
|
+
total_months -= 1
|
|
1589
|
+
days_in_start_month = calendar.monthrange(self._start.year, self._start.month)[1]
|
|
1590
|
+
day_diff = (days_in_start_month - self._start.day) + self._end.day
|
|
1591
|
+
fraction = day_diff / days_in_start_month
|
|
1518
1592
|
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
>>> Interval(Date(2018, 9, 6), Date(2018, 9, 10)).b.days()
|
|
1527
|
-
2
|
|
1528
|
-
>>> Interval(Date(2018, 9, 10), Date(2018, 9, 6)).b.days()
|
|
1529
|
-
-2
|
|
1593
|
+
return self._sign * (total_months + fraction)
|
|
1594
|
+
|
|
1595
|
+
@property
|
|
1596
|
+
def quarters(self) -> float:
|
|
1597
|
+
"""Get approximate number of quarters in the interval.
|
|
1598
|
+
|
|
1599
|
+
Note: This is an approximation using day count / 365 * 4.
|
|
1530
1600
|
"""
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
return (self.enddate - self.begdate).days
|
|
1537
|
-
if self.begdate < self.enddate:
|
|
1538
|
-
return len(list(self.series())) - 1
|
|
1539
|
-
_reverse = Interval(self.enddate, self.begdate)
|
|
1540
|
-
_reverse._entity = self._entity
|
|
1541
|
-
_reverse._business = self._business
|
|
1542
|
-
return -len(list(_reverse.series())) + 1
|
|
1543
|
-
|
|
1544
|
-
def quarters(self):
|
|
1545
|
-
"""Return the number of quarters between two dates
|
|
1546
|
-
TODO: good enough implementation; refine rules to be heuristically precise
|
|
1547
|
-
|
|
1548
|
-
>>> round(Interval(Date(2020, 1, 1), Date(2020, 2, 16)).quarters(), 2)
|
|
1549
|
-
0.5
|
|
1550
|
-
>>> round(Interval(Date(2020, 1, 1), Date(2020, 4, 1)).quarters(), 2)
|
|
1551
|
-
1.0
|
|
1552
|
-
>>> round(Interval(Date(2020, 1, 1), Date(2020, 7, 1)).quarters(), 2)
|
|
1553
|
-
1.99
|
|
1554
|
-
>>> round(Interval(Date(2020, 1, 1), Date(2020, 8, 1)).quarters(), 2)
|
|
1555
|
-
2.33
|
|
1601
|
+
return self._sign * 4 * self.days / 365.0
|
|
1602
|
+
|
|
1603
|
+
@property
|
|
1604
|
+
def years(self) -> int:
|
|
1605
|
+
"""Get number of complete years in the interval (always floors).
|
|
1556
1606
|
"""
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
'
|
|
1579
|
-
>>> '{:.4f}'.format(Interval(begdate, enddate).years(2))
|
|
1580
|
-
'42.8306'
|
|
1581
|
-
>>> '{:.4f}'.format(Interval(begdate, enddate).years(3))
|
|
1582
|
-
'42.2438'
|
|
1583
|
-
>>> '{:.4f}'.format(Interval(begdate, enddate).years(4))
|
|
1584
|
-
'42.2194'
|
|
1585
|
-
>>> '{:.4f}'.format(Interval(enddate, begdate).years(4))
|
|
1586
|
-
'-42.2194'
|
|
1587
|
-
|
|
1588
|
-
Excel has a known leap year bug when year == 1900 (=YEARFRAC("1900-1-1", "1900-12-1", 1) -> 0.9178)
|
|
1589
|
-
The bug originated from Lotus 1-2-3, and was purposely implemented in Excel for the purpose of backward compatibility.
|
|
1590
|
-
>>> begdate = Date(1900, 1, 1)
|
|
1591
|
-
>>> enddate = Date(1900, 12, 1)
|
|
1592
|
-
>>> '{:.4f}'.format(Interval(begdate, enddate).years(4))
|
|
1593
|
-
'0.9167'
|
|
1607
|
+
year_diff = self._end.year - self._start.year
|
|
1608
|
+
if self._end.month < self._start.month or \
|
|
1609
|
+
(self._end.month == self._start.month and self._end.day < self._start.day):
|
|
1610
|
+
year_diff -= 1
|
|
1611
|
+
return self._sign * year_diff
|
|
1612
|
+
|
|
1613
|
+
def yearfrac(self, basis: int = 0) -> float:
|
|
1614
|
+
"""Calculate the fraction of years between two dates (Excel-compatible).
|
|
1615
|
+
|
|
1616
|
+
This method provides precise calculation using various day count conventions
|
|
1617
|
+
used in finance. Results are tested against Excel for compatibility.
|
|
1618
|
+
|
|
1619
|
+
Parameters
|
|
1620
|
+
basis: Day count convention to use:
|
|
1621
|
+
0 = US (NASD) 30/360 (default)
|
|
1622
|
+
1 = Actual/actual
|
|
1623
|
+
2 = Actual/360
|
|
1624
|
+
3 = Actual/365
|
|
1625
|
+
4 = European 30/360
|
|
1626
|
+
|
|
1627
|
+
Note: Excel has a known leap year bug for year 1900 which is intentionally
|
|
1628
|
+
replicated for compatibility (1900 is treated as a leap year even though it wasn't).
|
|
1594
1629
|
"""
|
|
1595
1630
|
|
|
1596
1631
|
def average_year_length(date1, date2):
|
|
@@ -1675,32 +1710,84 @@ class Interval:
|
|
|
1675
1710
|
(date1day + date1month * 30 + date1year * 360)
|
|
1676
1711
|
return daydiff360 / 360
|
|
1677
1712
|
|
|
1678
|
-
|
|
1679
|
-
if enddate is None:
|
|
1680
|
-
return
|
|
1681
|
-
|
|
1682
|
-
sign = 1
|
|
1683
|
-
if begdate > enddate:
|
|
1684
|
-
begdate, enddate = enddate, begdate
|
|
1685
|
-
sign = -1
|
|
1686
|
-
if begdate == enddate:
|
|
1713
|
+
if self._start == self._end:
|
|
1687
1714
|
return 0.0
|
|
1688
|
-
|
|
1689
1715
|
if basis == 0:
|
|
1690
|
-
return basis0(
|
|
1716
|
+
return basis0(self._start, self._end) * self._sign
|
|
1691
1717
|
if basis == 1:
|
|
1692
|
-
return basis1(
|
|
1718
|
+
return basis1(self._start, self._end) * self._sign
|
|
1693
1719
|
if basis == 2:
|
|
1694
|
-
return basis2(
|
|
1720
|
+
return basis2(self._start, self._end) * self._sign
|
|
1695
1721
|
if basis == 3:
|
|
1696
|
-
return basis3(
|
|
1722
|
+
return basis3(self._start, self._end) * self._sign
|
|
1697
1723
|
if basis == 4:
|
|
1698
|
-
return basis4(
|
|
1724
|
+
return basis4(self._start, self._end) * self._sign
|
|
1699
1725
|
|
|
1700
1726
|
raise ValueError('Basis range [0, 4]. Unknown basis {basis}.')
|
|
1701
1727
|
|
|
1728
|
+
@reset_business
|
|
1729
|
+
def start_of(self, unit: str = 'month') -> list[Date | DateTime]:
|
|
1730
|
+
"""Return the start of each unit within the interval.
|
|
1731
|
+
|
|
1732
|
+
Parameters
|
|
1733
|
+
unit: Time unit ('month', 'week', 'year', 'quarter')
|
|
1734
|
+
|
|
1735
|
+
Returns
|
|
1736
|
+
List of Date or DateTime objects representing start of each unit
|
|
1737
|
+
|
|
1738
|
+
In business mode, each start date is adjusted to the next business day
|
|
1739
|
+
if it falls on a non-business day.
|
|
1740
|
+
"""
|
|
1741
|
+
handlers = self._get_unit_handlers(unit)
|
|
1742
|
+
result = []
|
|
1743
|
+
|
|
1744
|
+
current = handlers['get_start'](self._start)
|
|
1745
|
+
|
|
1746
|
+
if self._business:
|
|
1747
|
+
current._entity = self._entity
|
|
1748
|
+
|
|
1749
|
+
while current <= self._end:
|
|
1750
|
+
if self._business:
|
|
1751
|
+
current = current._business_or_next()
|
|
1752
|
+
result.append(current)
|
|
1753
|
+
current = handlers['advance'](current)
|
|
1754
|
+
|
|
1755
|
+
return result
|
|
1756
|
+
|
|
1757
|
+
@reset_business
|
|
1758
|
+
def end_of(self, unit: str = 'month') -> list[Date | DateTime]:
|
|
1759
|
+
"""Return the end of each unit within the interval.
|
|
1760
|
+
|
|
1761
|
+
Parameters
|
|
1762
|
+
unit: Time unit ('month', 'week', 'year', 'quarter')
|
|
1763
|
+
|
|
1764
|
+
Returns
|
|
1765
|
+
List of Date or DateTime objects representing end of each unit
|
|
1702
1766
|
|
|
1703
|
-
|
|
1767
|
+
In business mode, each end date is adjusted to the previous business day
|
|
1768
|
+
if it falls on a non-business day.
|
|
1769
|
+
"""
|
|
1770
|
+
handlers = self._get_unit_handlers(unit)
|
|
1771
|
+
result = []
|
|
1772
|
+
|
|
1773
|
+
current = handlers['get_start'](self._start)
|
|
1774
|
+
|
|
1775
|
+
if self._business:
|
|
1776
|
+
current._entity = self._entity
|
|
1777
|
+
|
|
1778
|
+
while current <= self._end:
|
|
1779
|
+
end_date = handlers['get_end'](current)
|
|
1780
|
+
|
|
1781
|
+
if self._business:
|
|
1782
|
+
end_date = end_date._business_or_previous()
|
|
1783
|
+
result.append(end_date)
|
|
1784
|
+
|
|
1785
|
+
current = handlers['advance'](current)
|
|
1786
|
+
|
|
1787
|
+
return result
|
|
1788
|
+
|
|
1789
|
+
|
|
1790
|
+
def create_ics(begdate, enddate, summary, location) -> str:
|
|
1704
1791
|
"""Create a simple .ics file per RFC 5545 guidelines."""
|
|
1705
1792
|
|
|
1706
1793
|
return f"""BEGIN:VCALENDAR
|
|
@@ -1714,35 +1801,3 @@ LOCATION:{location}
|
|
|
1714
1801
|
END:VEVENT
|
|
1715
1802
|
END:VCALENDAR
|
|
1716
1803
|
"""
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
# apply any missing Date functions
|
|
1720
|
-
for func in (
|
|
1721
|
-
'average',
|
|
1722
|
-
'closest',
|
|
1723
|
-
'farthest',
|
|
1724
|
-
'fromordinal',
|
|
1725
|
-
'fromtimestamp',
|
|
1726
|
-
'nth_of',
|
|
1727
|
-
'replace',
|
|
1728
|
-
):
|
|
1729
|
-
setattr(Date, func, store_entity(getattr(_pendulum.Date, func), typ=Date))
|
|
1730
|
-
|
|
1731
|
-
# apply any missing DateTime functions
|
|
1732
|
-
for func in (
|
|
1733
|
-
'astimezone',
|
|
1734
|
-
'date',
|
|
1735
|
-
'fromordinal',
|
|
1736
|
-
'fromtimestamp',
|
|
1737
|
-
'in_timezone',
|
|
1738
|
-
'in_tz',
|
|
1739
|
-
'replace',
|
|
1740
|
-
'strptime',
|
|
1741
|
-
'utcfromtimestamp',
|
|
1742
|
-
'utcnow',
|
|
1743
|
-
):
|
|
1744
|
-
setattr(DateTime, func, store_entity(getattr(_pendulum.DateTime, func), typ=DateTime))
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
if __name__ == '__main__':
|
|
1748
|
-
__import__('doctest').testmod(optionflags=4 | 8 | 32)
|