opendate 0.1.13__py3-none-any.whl → 0.1.20__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 +56 -16
- date/date.py +519 -672
- date/extras.py +28 -60
- opendate-0.1.20.dist-info/METADATA +762 -0
- opendate-0.1.20.dist-info/RECORD +7 -0
- {opendate-0.1.13.dist-info → opendate-0.1.20.dist-info}/WHEEL +1 -1
- opendate-0.1.13.dist-info/METADATA +0 -65
- opendate-0.1.13.dist-info/RECORD +0 -7
- {opendate-0.1.13.dist-info → opendate-0.1.20.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,9 +39,12 @@ __all__ = [
|
|
|
39
39
|
'prefer_utc_timezone',
|
|
40
40
|
'expect_date',
|
|
41
41
|
'expect_datetime',
|
|
42
|
+
'expect_time',
|
|
43
|
+
'expect_date_or_datetime',
|
|
42
44
|
'Entity',
|
|
43
|
-
'NYSE'
|
|
45
|
+
'NYSE',
|
|
44
46
|
'WEEKDAY_SHORTNAME',
|
|
47
|
+
'WeekDay',
|
|
45
48
|
]
|
|
46
49
|
|
|
47
50
|
|
|
@@ -49,27 +52,8 @@ def Timezone(name:str = 'US/Eastern') -> _zoneinfo.ZoneInfo:
|
|
|
49
52
|
"""Create a timezone object with the specified name.
|
|
50
53
|
|
|
51
54
|
Simple wrapper around Pendulum's Timezone function that ensures
|
|
52
|
-
consistent timezone handling across the library.
|
|
53
|
-
|
|
54
|
-
Parameters
|
|
55
|
-
name: Timezone name (e.g., 'US/Eastern', 'UTC')
|
|
56
|
-
|
|
57
|
-
Returns
|
|
58
|
-
A timezone object for the specified timezone
|
|
59
|
-
|
|
60
|
-
Examples
|
|
61
|
-
|
|
62
|
-
US/Eastern is equivalent to America/New_York:
|
|
63
|
-
>>> winter1 = DateTime(2000, 1, 1, 12, tzinfo=Timezone('US/Eastern'))
|
|
64
|
-
>>> winter2 = DateTime(2000, 1, 1, 12, tzinfo=Timezone('America/New_York'))
|
|
65
|
-
>>> winter1 == winter2
|
|
66
|
-
True
|
|
67
|
-
|
|
68
|
-
This works in both summer and winter:
|
|
69
|
-
>>> summer1 = DateTime(2000, 7, 1, 12, tzinfo=Timezone('US/Eastern'))
|
|
70
|
-
>>> summer2 = DateTime(2000, 7, 1, 12, tzinfo=Timezone('America/New_York'))
|
|
71
|
-
>>> summer1 == summer2
|
|
72
|
-
True
|
|
55
|
+
consistent timezone handling across the library. Note that 'US/Eastern'
|
|
56
|
+
is equivalent to 'America/New_York' for all dates.
|
|
73
57
|
"""
|
|
74
58
|
return _pendulum.tz.Timezone(name)
|
|
75
59
|
|
|
@@ -125,22 +109,39 @@ DATEMATCH = re.compile(r'^(?P<d>N|T|Y|P|M)(?P<n>[-+]?\d+)?(?P<b>b?)?$')
|
|
|
125
109
|
# return entity
|
|
126
110
|
|
|
127
111
|
|
|
128
|
-
def isdateish(x):
|
|
129
|
-
return isinstance(x, _datetime.date | _datetime.datetime | pd.Timestamp | np.datetime64)
|
|
112
|
+
def isdateish(x) -> bool:
|
|
113
|
+
return isinstance(x, _datetime.date | _datetime.datetime | _datetime.time | pd.Timestamp | np.datetime64)
|
|
130
114
|
|
|
131
115
|
|
|
132
116
|
def parse_arg(typ, arg):
|
|
133
|
-
|
|
134
|
-
|
|
117
|
+
"""Parse argument to specified type or 'smart' to preserve Date/DateTime.
|
|
118
|
+
"""
|
|
119
|
+
if not isdateish(arg):
|
|
120
|
+
return arg
|
|
121
|
+
|
|
122
|
+
if typ == 'smart':
|
|
123
|
+
if isinstance(arg, Date | DateTime):
|
|
124
|
+
return arg
|
|
125
|
+
if isinstance(arg, _datetime.datetime | pd.Timestamp | np.datetime64):
|
|
135
126
|
return DateTime.instance(arg)
|
|
136
|
-
if
|
|
127
|
+
if isinstance(arg, _datetime.date):
|
|
137
128
|
return Date.instance(arg)
|
|
138
|
-
if
|
|
129
|
+
if isinstance(arg, _datetime.time):
|
|
139
130
|
return Time.instance(arg)
|
|
131
|
+
return arg
|
|
132
|
+
|
|
133
|
+
if typ == _datetime.datetime:
|
|
134
|
+
return DateTime.instance(arg)
|
|
135
|
+
if typ == _datetime.date:
|
|
136
|
+
return Date.instance(arg)
|
|
137
|
+
if typ == _datetime.time:
|
|
138
|
+
return Time.instance(arg)
|
|
140
139
|
return arg
|
|
141
140
|
|
|
142
141
|
|
|
143
142
|
def parse_args(typ, *args):
|
|
143
|
+
"""Parse args to specified type or 'smart' mode.
|
|
144
|
+
"""
|
|
144
145
|
this = []
|
|
145
146
|
for a in args:
|
|
146
147
|
if isinstance(a, Sequence) and not isinstance(a, str):
|
|
@@ -150,27 +151,31 @@ def parse_args(typ, *args):
|
|
|
150
151
|
return this
|
|
151
152
|
|
|
152
153
|
|
|
153
|
-
def expect(func, typ: type[_datetime.date], exclkw: bool = False) -> Callable:
|
|
154
|
-
"""Decorator to force input type of date/datetime inputs
|
|
154
|
+
def expect(func=None, *, typ: type[_datetime.date] | str = None, exclkw: bool = False) -> Callable:
|
|
155
|
+
"""Decorator to force input type of date/datetime inputs.
|
|
156
|
+
|
|
157
|
+
typ can be _datetime.date, _datetime.datetime, _datetime.time, or 'smart'
|
|
155
158
|
"""
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if
|
|
163
|
-
kwargs[k] =
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
159
|
+
def decorator(func):
|
|
160
|
+
@wraps(func)
|
|
161
|
+
def wrapper(*args, **kwargs):
|
|
162
|
+
args = parse_args(typ, *args)
|
|
163
|
+
if not exclkw:
|
|
164
|
+
for k, v in kwargs.items():
|
|
165
|
+
if isdateish(v):
|
|
166
|
+
kwargs[k] = parse_arg(typ, v)
|
|
167
|
+
return func(*args, **kwargs)
|
|
168
|
+
return wrapper
|
|
169
|
+
|
|
170
|
+
if func is None:
|
|
171
|
+
return decorator
|
|
172
|
+
return decorator(func)
|
|
169
173
|
|
|
170
174
|
|
|
171
175
|
expect_date = partial(expect, typ=_datetime.date)
|
|
172
176
|
expect_datetime = partial(expect, typ=_datetime.datetime)
|
|
173
177
|
expect_time = partial(expect, typ=_datetime.time)
|
|
178
|
+
expect_date_or_datetime = partial(expect, typ='smart')
|
|
174
179
|
|
|
175
180
|
|
|
176
181
|
def type_class(typ, obj):
|
|
@@ -218,7 +223,49 @@ def store_both(func=None, *, typ=None):
|
|
|
218
223
|
return wrapper
|
|
219
224
|
|
|
220
225
|
|
|
221
|
-
def
|
|
226
|
+
def reset_business(func):
|
|
227
|
+
"""Decorator to reset business mode after function execution.
|
|
228
|
+
"""
|
|
229
|
+
@wraps(func)
|
|
230
|
+
def wrapper(self, *args, **kwargs):
|
|
231
|
+
try:
|
|
232
|
+
return func(self, *args, **kwargs)
|
|
233
|
+
finally:
|
|
234
|
+
self._business = False
|
|
235
|
+
self._start._business = False
|
|
236
|
+
self._end._business = False
|
|
237
|
+
return wrapper
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def normalize_date_datetime_pairs(func):
|
|
241
|
+
"""Decorator to normalize mixed Date/DateTime pairs to DateTime.
|
|
242
|
+
"""
|
|
243
|
+
@wraps(func)
|
|
244
|
+
def wrapper(*args, **kwargs):
|
|
245
|
+
if len(args) >= 3:
|
|
246
|
+
cls_or_self, begdate, enddate = args[0], args[1], args[2]
|
|
247
|
+
rest_args = args[3:]
|
|
248
|
+
|
|
249
|
+
tz = UTC
|
|
250
|
+
if isinstance(begdate, DateTime) and begdate.tzinfo:
|
|
251
|
+
tz = begdate.tzinfo
|
|
252
|
+
elif isinstance(enddate, DateTime) and enddate.tzinfo:
|
|
253
|
+
tz = enddate.tzinfo
|
|
254
|
+
|
|
255
|
+
if isinstance(begdate, Date) and not isinstance(begdate, DateTime):
|
|
256
|
+
if isinstance(enddate, DateTime):
|
|
257
|
+
begdate = DateTime(begdate.year, begdate.month, begdate.day, tzinfo=tz)
|
|
258
|
+
elif isinstance(enddate, Date) and not isinstance(enddate, DateTime):
|
|
259
|
+
if isinstance(begdate, DateTime):
|
|
260
|
+
enddate = DateTime(enddate.year, enddate.month, enddate.day, tzinfo=tz)
|
|
261
|
+
|
|
262
|
+
args = (cls_or_self, begdate, enddate) + rest_args
|
|
263
|
+
|
|
264
|
+
return func(*args, **kwargs)
|
|
265
|
+
return wrapper
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def prefer_utc_timezone(func, force:bool = False) -> Callable:
|
|
222
269
|
"""Return datetime as UTC.
|
|
223
270
|
"""
|
|
224
271
|
@wraps(func)
|
|
@@ -232,7 +279,7 @@ def prefer_utc_timezone(func, force:bool = False):
|
|
|
232
279
|
return wrapper
|
|
233
280
|
|
|
234
281
|
|
|
235
|
-
def prefer_native_timezone(func, force:bool = False):
|
|
282
|
+
def prefer_native_timezone(func, force:bool = False) -> Callable:
|
|
236
283
|
"""Return datetime as native.
|
|
237
284
|
"""
|
|
238
285
|
@wraps(func)
|
|
@@ -316,7 +363,7 @@ class NYSE(Entity):
|
|
|
316
363
|
def business_holidays(begdate=BEGDATE, enddate=ENDDATE) -> set:
|
|
317
364
|
return {Date.instance(d.date())
|
|
318
365
|
for d in map(pd.to_datetime, NYSE.calendar.holidays().holidays)
|
|
319
|
-
if begdate <= d <= enddate}
|
|
366
|
+
if begdate <= d.date() <= enddate}
|
|
320
367
|
|
|
321
368
|
|
|
322
369
|
class DateBusinessMixin:
|
|
@@ -488,63 +535,21 @@ class DateBusinessMixin:
|
|
|
488
535
|
|
|
489
536
|
@expect_date
|
|
490
537
|
def business_open(self) -> bool:
|
|
491
|
-
"""
|
|
492
|
-
|
|
493
|
-
>>> thedate = Date(2021, 4, 19) # Monday
|
|
494
|
-
>>> thedate.business_open()
|
|
495
|
-
True
|
|
496
|
-
>>> thedate = Date(2021, 4, 17) # Saturday
|
|
497
|
-
>>> thedate.business_open()
|
|
498
|
-
False
|
|
499
|
-
>>> thedate = Date(2021, 1, 18) # MLK Day
|
|
500
|
-
>>> thedate.business_open()
|
|
501
|
-
False
|
|
538
|
+
"""Check if the date is a business day (market is open).
|
|
502
539
|
"""
|
|
503
540
|
return self.is_business_day()
|
|
504
541
|
|
|
505
542
|
@expect_date
|
|
506
543
|
def is_business_day(self) -> bool:
|
|
507
|
-
"""
|
|
508
|
-
|
|
509
|
-
>>> thedate = Date(2021, 4, 19) # Monday
|
|
510
|
-
>>> thedate.is_business_day()
|
|
511
|
-
True
|
|
512
|
-
>>> thedate = Date(2021, 4, 17) # Saturday
|
|
513
|
-
>>> thedate.is_business_day()
|
|
514
|
-
False
|
|
515
|
-
>>> thedate = Date(2021, 1, 18) # MLK Day
|
|
516
|
-
>>> thedate.is_business_day()
|
|
517
|
-
False
|
|
518
|
-
>>> thedate = Date(2021, 11, 25) # Thanksgiving
|
|
519
|
-
>>> thedate.is_business_day()
|
|
520
|
-
False
|
|
521
|
-
>>> thedate = Date(2021, 11, 26) # Day after ^
|
|
522
|
-
>>> thedate.is_business_day()
|
|
523
|
-
True
|
|
544
|
+
"""Check if the date is a business day according to the entity calendar.
|
|
524
545
|
"""
|
|
525
546
|
return self in self._entity.business_days()
|
|
526
547
|
|
|
527
548
|
@expect_date
|
|
528
549
|
def business_hours(self) -> 'tuple[DateTime, DateTime]':
|
|
529
|
-
"""
|
|
530
|
-
|
|
531
|
-
Returns (None, None) if not a business day
|
|
550
|
+
"""Get market open and close times for this date.
|
|
532
551
|
|
|
533
|
-
|
|
534
|
-
>>> thedate.business_hours()
|
|
535
|
-
(... 9, 30, ... 16, 0, ...)
|
|
536
|
-
|
|
537
|
-
>>> thedate = Date(2023, 7, 3)
|
|
538
|
-
>>> thedate.business_hours()
|
|
539
|
-
(... 9, 30, ... 13, 0, ...)
|
|
540
|
-
|
|
541
|
-
>>> thedate = Date(2023, 11, 24)
|
|
542
|
-
>>> thedate.business_hours()
|
|
543
|
-
(... 9, 30, ... 13, 0, ...)
|
|
544
|
-
|
|
545
|
-
>>> thedate = Date(2024, 5, 27) # memorial day
|
|
546
|
-
>>> thedate.business_hours()
|
|
547
|
-
(None, None)
|
|
552
|
+
Returns (None, None) if not a business day.
|
|
548
553
|
"""
|
|
549
554
|
return self._entity.business_hours(self, self)\
|
|
550
555
|
.get(self, (None, None))
|
|
@@ -593,34 +598,25 @@ class DateBusinessMixin:
|
|
|
593
598
|
class DateExtrasMixin:
|
|
594
599
|
"""Extended date functionality not provided by Pendulum.
|
|
595
600
|
|
|
601
|
+
.. note::
|
|
602
|
+
This mixin exists primarily for legacy backward compatibility.
|
|
603
|
+
New code should prefer using built-in methods where possible.
|
|
604
|
+
|
|
596
605
|
This mixin provides additional date utilities primarily focused on:
|
|
597
606
|
- Financial date calculations (nearest month start/end)
|
|
598
607
|
- Weekday-oriented date navigation
|
|
599
608
|
- Relative date lookups
|
|
600
609
|
|
|
601
|
-
These methods extend
|
|
610
|
+
These methods extend OpenDate functionality with features commonly
|
|
602
611
|
needed in financial applications and reporting scenarios.
|
|
603
612
|
"""
|
|
604
613
|
|
|
605
|
-
def nearest_start_of_month(self):
|
|
606
|
-
"""Get
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
>>> from date import Date
|
|
612
|
-
>>> Date(2015, 1, 1).nearest_start_of_month()
|
|
613
|
-
Date(2015, 1, 1)
|
|
614
|
-
>>> Date(2015, 1, 15).nearest_start_of_month()
|
|
615
|
-
Date(2015, 1, 1)
|
|
616
|
-
>>> Date(2015, 1, 15).b.nearest_start_of_month()
|
|
617
|
-
Date(2015, 1, 2)
|
|
618
|
-
>>> Date(2015, 1, 16).nearest_start_of_month()
|
|
619
|
-
Date(2015, 2, 1)
|
|
620
|
-
>>> Date(2015, 1, 31).nearest_start_of_month()
|
|
621
|
-
Date(2015, 2, 1)
|
|
622
|
-
>>> Date(2015, 1, 31).b.nearest_start_of_month()
|
|
623
|
-
Date(2015, 2, 2)
|
|
614
|
+
def nearest_start_of_month(self) -> Self:
|
|
615
|
+
"""Get the nearest start of month.
|
|
616
|
+
|
|
617
|
+
If day <= 15, returns start of current month.
|
|
618
|
+
If day > 15, returns start of next month.
|
|
619
|
+
In business mode, adjusts to next business day if needed.
|
|
624
620
|
"""
|
|
625
621
|
_business = self._business
|
|
626
622
|
self._business = False
|
|
@@ -634,25 +630,12 @@ class DateExtrasMixin:
|
|
|
634
630
|
return d.business().add(days=1)
|
|
635
631
|
return d
|
|
636
632
|
|
|
637
|
-
def nearest_end_of_month(self):
|
|
638
|
-
"""Get
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
>>> from date import Date
|
|
644
|
-
>>> Date(2015, 1, 1).nearest_end_of_month()
|
|
645
|
-
Date(2014, 12, 31)
|
|
646
|
-
>>> Date(2015, 1, 15).nearest_end_of_month()
|
|
647
|
-
Date(2014, 12, 31)
|
|
648
|
-
>>> Date(2015, 1, 15).b.nearest_end_of_month()
|
|
649
|
-
Date(2014, 12, 31)
|
|
650
|
-
>>> Date(2015, 1, 16).nearest_end_of_month()
|
|
651
|
-
Date(2015, 1, 31)
|
|
652
|
-
>>> Date(2015, 1, 31).nearest_end_of_month()
|
|
653
|
-
Date(2015, 1, 31)
|
|
654
|
-
>>> Date(2015, 1, 31).b.nearest_end_of_month()
|
|
655
|
-
Date(2015, 1, 30)
|
|
633
|
+
def nearest_end_of_month(self) -> Self:
|
|
634
|
+
"""Get the nearest end of month.
|
|
635
|
+
|
|
636
|
+
If day <= 15, returns end of previous month.
|
|
637
|
+
If day > 15, returns end of current month.
|
|
638
|
+
In business mode, adjusts to previous business day if needed.
|
|
656
639
|
"""
|
|
657
640
|
_business = self._business
|
|
658
641
|
self._business = False
|
|
@@ -666,48 +649,27 @@ class DateExtrasMixin:
|
|
|
666
649
|
return d.business().subtract(days=1)
|
|
667
650
|
return d
|
|
668
651
|
|
|
669
|
-
def next_relative_date_of_week_by_day(self, day='MO'):
|
|
670
|
-
"""Get next
|
|
671
|
-
|
|
672
|
-
>>> from date import Date
|
|
673
|
-
>>> Date(2020, 5, 18).next_relative_date_of_week_by_day('SU')
|
|
674
|
-
Date(2020, 5, 24)
|
|
675
|
-
>>> Date(2020, 5, 24).next_relative_date_of_week_by_day('SU')
|
|
676
|
-
Date(2020, 5, 24)
|
|
652
|
+
def next_relative_date_of_week_by_day(self, day='MO') -> Self:
|
|
653
|
+
"""Get next occurrence of the specified weekday (or current date if already that day).
|
|
677
654
|
"""
|
|
678
655
|
if self.weekday() == WEEKDAY_SHORTNAME.get(day):
|
|
679
656
|
return self
|
|
680
657
|
return self.next(WEEKDAY_SHORTNAME.get(day))
|
|
681
658
|
|
|
682
|
-
def weekday_or_previous_friday(self):
|
|
683
|
-
"""Return the date if it is a weekday,
|
|
684
|
-
|
|
685
|
-
>>> from date import Date
|
|
686
|
-
>>> Date(2019, 10, 6).weekday_or_previous_friday() # Sunday
|
|
687
|
-
Date(2019, 10, 4)
|
|
688
|
-
>>> Date(2019, 10, 5).weekday_or_previous_friday() # Saturday
|
|
689
|
-
Date(2019, 10, 4)
|
|
690
|
-
>>> Date(2019, 10, 4).weekday_or_previous_friday() # Friday
|
|
691
|
-
Date(2019, 10, 4)
|
|
692
|
-
>>> Date(2019, 10, 3).weekday_or_previous_friday() # Thursday
|
|
693
|
-
Date(2019, 10, 3)
|
|
659
|
+
def weekday_or_previous_friday(self) -> Self:
|
|
660
|
+
"""Return the date if it is a weekday, otherwise return the previous Friday.
|
|
694
661
|
"""
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
return self.subtract(days=dnum - 4)
|
|
662
|
+
if self.weekday() in {WeekDay.SATURDAY, WeekDay.SUNDAY}:
|
|
663
|
+
return self.previous(WeekDay.FRIDAY)
|
|
698
664
|
return self
|
|
699
665
|
|
|
700
|
-
"""
|
|
701
|
-
create a simple nth weekday function that accounts for
|
|
702
|
-
[1,2,3,4] and weekday as options
|
|
703
|
-
or weekday, [1,2,3,4]
|
|
704
|
-
|
|
705
|
-
"""
|
|
706
|
-
|
|
707
666
|
@classmethod
|
|
708
|
-
def third_wednesday(cls, year, month):
|
|
667
|
+
def third_wednesday(cls, year, month) -> Self:
|
|
709
668
|
"""Calculate the date of the third Wednesday in a given month/year.
|
|
710
669
|
|
|
670
|
+
.. deprecated::
|
|
671
|
+
Use Date(year, month, 1).nth_of('month', 3, WeekDay.WEDNESDAY) instead.
|
|
672
|
+
|
|
711
673
|
Parameters
|
|
712
674
|
year: The year to use
|
|
713
675
|
month: The month to use (1-12)
|
|
@@ -715,11 +677,7 @@ class DateExtrasMixin:
|
|
|
715
677
|
Returns
|
|
716
678
|
A Date object representing the third Wednesday of the specified month
|
|
717
679
|
"""
|
|
718
|
-
|
|
719
|
-
w = third.weekday()
|
|
720
|
-
if w != WeekDay.WEDNESDAY:
|
|
721
|
-
third = third.replace(day=(15 + (WeekDay.WEDNESDAY - w) % 7))
|
|
722
|
-
return third
|
|
680
|
+
return cls(year, month, 1).nth_of('month', 3, WeekDay.WEDNESDAY)
|
|
723
681
|
|
|
724
682
|
|
|
725
683
|
class Date(DateExtrasMixin, DateBusinessMixin, _pendulum.Date):
|
|
@@ -736,10 +694,9 @@ class Date(DateExtrasMixin, DateBusinessMixin, _pendulum.Date):
|
|
|
736
694
|
"""
|
|
737
695
|
|
|
738
696
|
def to_string(self, fmt: str) -> str:
|
|
739
|
-
"""Format
|
|
697
|
+
"""Format date to string, handling platform-specific format codes.
|
|
740
698
|
|
|
741
|
-
|
|
742
|
-
'1/5/2022'
|
|
699
|
+
Automatically converts '%-' format codes to '%#' on Windows.
|
|
743
700
|
"""
|
|
744
701
|
return self.strftime(fmt.replace('%-', '%#') if os.name == 'nt' else fmt)
|
|
745
702
|
|
|
@@ -772,10 +729,10 @@ class Date(DateExtrasMixin, DateBusinessMixin, _pendulum.Date):
|
|
|
772
729
|
Returns
|
|
773
730
|
A new Date object representing the average date
|
|
774
731
|
"""
|
|
775
|
-
return _pendulum.Date.average(self)
|
|
732
|
+
return _pendulum.Date.average(self, dt)
|
|
776
733
|
|
|
777
734
|
@classmethod
|
|
778
|
-
def fromordinal(cls, *args, **kwargs):
|
|
735
|
+
def fromordinal(cls, *args, **kwargs) -> Self:
|
|
779
736
|
"""Create a Date from an ordinal.
|
|
780
737
|
|
|
781
738
|
Parameters
|
|
@@ -788,7 +745,7 @@ class Date(DateExtrasMixin, DateBusinessMixin, _pendulum.Date):
|
|
|
788
745
|
return cls.instance(result)
|
|
789
746
|
|
|
790
747
|
@classmethod
|
|
791
|
-
def fromtimestamp(cls, timestamp, tz=None):
|
|
748
|
+
def fromtimestamp(cls, timestamp, tz=None) -> Self:
|
|
792
749
|
"""Create a Date from a timestamp.
|
|
793
750
|
|
|
794
751
|
Parameters
|
|
@@ -831,84 +788,47 @@ class Date(DateExtrasMixin, DateBusinessMixin, _pendulum.Date):
|
|
|
831
788
|
) -> Self | None:
|
|
832
789
|
"""Convert a string to a date handling many different formats.
|
|
833
790
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
mon-dd-yyyy JUN-23-2006
|
|
877
|
-
>>> Date.parse('20 Jan 2009')
|
|
878
|
-
Date(2009, 1, 20)
|
|
879
|
-
|
|
880
|
-
month dd, yyyy June 23, 2006
|
|
881
|
-
>>> Date.parse('June 23, 2006')
|
|
882
|
-
Date(2006, 6, 23)
|
|
883
|
-
|
|
884
|
-
dd-mon-yy
|
|
885
|
-
>>> Date.parse('23-May-12')
|
|
886
|
-
Date(2012, 5, 23)
|
|
887
|
-
|
|
888
|
-
ddmonyyyy
|
|
889
|
-
>>> Date.parse('23May2012')
|
|
890
|
-
Date(2012, 5, 23)
|
|
891
|
-
|
|
892
|
-
>>> Date.parse('Oct. 24, 2007', fmt='%b. %d, %Y')
|
|
893
|
-
Date(2007, 10, 24)
|
|
894
|
-
|
|
895
|
-
>>> Date.parse('Yesterday') == DateTime.now().subtract(days=1).date()
|
|
896
|
-
True
|
|
897
|
-
>>> Date.parse('TODAY') == Date.today()
|
|
898
|
-
True
|
|
899
|
-
>>> Date.parse('Jan. 13, 2014')
|
|
900
|
-
Date(2014, 1, 13)
|
|
901
|
-
|
|
902
|
-
>>> Date.parse('March') == Date(Date.today().year, 3, Date.today().day)
|
|
903
|
-
True
|
|
904
|
-
|
|
905
|
-
only raise error when we explicitly say so
|
|
906
|
-
>>> Date.parse('bad date') is None
|
|
907
|
-
True
|
|
908
|
-
>>> Date.parse('bad date', raise_err=True)
|
|
909
|
-
Traceback (most recent call last):
|
|
910
|
-
...
|
|
911
|
-
ValueError: Failed to parse date: bad date
|
|
791
|
+
Supports various date formats including:
|
|
792
|
+
- Standard formats: YYYY-MM-DD, MM/DD/YYYY, MM/DD/YY, YYYYMMDD
|
|
793
|
+
- Named months: DD-MON-YYYY, MON-DD-YYYY, Month DD, YYYY
|
|
794
|
+
- Special codes: T (today), Y (yesterday), P (previous business day)
|
|
795
|
+
- Business day offsets: T-3b, P+2b (add/subtract business days)
|
|
796
|
+
- Custom format strings via fmt parameter
|
|
797
|
+
|
|
798
|
+
Parameters
|
|
799
|
+
s: String to parse or None
|
|
800
|
+
fmt: Optional strftime format string for custom parsing
|
|
801
|
+
entity: Calendar entity for business day calculations (default NYSE)
|
|
802
|
+
raise_err: If True, raises ValueError on parse failure instead of returning None
|
|
803
|
+
|
|
804
|
+
Returns
|
|
805
|
+
Date instance or None if parsing fails and raise_err is False
|
|
806
|
+
|
|
807
|
+
Examples
|
|
808
|
+
Standard numeric formats:
|
|
809
|
+
Date.parse('2020-01-15') → Date(2020, 1, 15)
|
|
810
|
+
Date.parse('01/15/2020') → Date(2020, 1, 15)
|
|
811
|
+
Date.parse('01/15/20') → Date(2020, 1, 15)
|
|
812
|
+
Date.parse('20200115') → Date(2020, 1, 15)
|
|
813
|
+
|
|
814
|
+
Named month formats:
|
|
815
|
+
Date.parse('15-Jan-2020') → Date(2020, 1, 15)
|
|
816
|
+
Date.parse('Jan 15, 2020') → Date(2020, 1, 15)
|
|
817
|
+
Date.parse('15JAN2020') → Date(2020, 1, 15)
|
|
818
|
+
|
|
819
|
+
Special codes:
|
|
820
|
+
Date.parse('T') → today's date
|
|
821
|
+
Date.parse('Y') → yesterday's date
|
|
822
|
+
Date.parse('P') → previous business day
|
|
823
|
+
Date.parse('M') → last day of previous month
|
|
824
|
+
|
|
825
|
+
Business day offsets:
|
|
826
|
+
Date.parse('T-3b') → 3 business days ago
|
|
827
|
+
Date.parse('P+2b') → 2 business days after previous business day
|
|
828
|
+
Date.parse('T+5') → 5 calendar days from today
|
|
829
|
+
|
|
830
|
+
Custom format:
|
|
831
|
+
Date.parse('15-Jan-2020', fmt='%d-%b-%Y') → Date(2020, 1, 15)
|
|
912
832
|
"""
|
|
913
833
|
|
|
914
834
|
def date_for_symbol(s):
|
|
@@ -1027,20 +947,17 @@ class Date(DateExtrasMixin, DateBusinessMixin, _pendulum.Date):
|
|
|
1027
947
|
| None,
|
|
1028
948
|
raise_err: bool = False,
|
|
1029
949
|
) -> Self | None:
|
|
1030
|
-
"""
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
>>> Date.instance(Date(2022, 1, 1))
|
|
1035
|
-
Date(2022, 1, 1)
|
|
1036
|
-
>>> Date.instance(_pendulum.Date(2022, 1, 1))
|
|
1037
|
-
Date(2022, 1, 1)
|
|
1038
|
-
>>> Date.instance(Date(2022, 1, 1))
|
|
1039
|
-
Date(2022, 1, 1)
|
|
1040
|
-
>>> Date.instance(np.datetime64('2000-01', 'D'))
|
|
1041
|
-
Date(2000, 1, 1)
|
|
1042
|
-
>>> Date.instance(None)
|
|
950
|
+
"""Create a Date instance from various date-like objects.
|
|
951
|
+
|
|
952
|
+
Converts datetime.date, datetime.datetime, pandas Timestamp,
|
|
953
|
+
numpy datetime64, and other date-like objects to Date instances.
|
|
1043
954
|
|
|
955
|
+
Parameters
|
|
956
|
+
obj: Date-like object to convert
|
|
957
|
+
raise_err: If True, raises ValueError for None/NA values instead of returning None
|
|
958
|
+
|
|
959
|
+
Returns
|
|
960
|
+
Date instance or None if obj is None/NA and raise_err is False
|
|
1044
961
|
"""
|
|
1045
962
|
if pd.isna(obj):
|
|
1046
963
|
if raise_err:
|
|
@@ -1056,39 +973,21 @@ class Date(DateExtrasMixin, DateBusinessMixin, _pendulum.Date):
|
|
|
1056
973
|
return cls(obj.year, obj.month, obj.day)
|
|
1057
974
|
|
|
1058
975
|
@classmethod
|
|
1059
|
-
def today(cls):
|
|
976
|
+
def today(cls) -> Self:
|
|
1060
977
|
d = _datetime.datetime.now(LCL)
|
|
1061
978
|
return cls(d.year, d.month, d.day)
|
|
1062
979
|
|
|
1063
|
-
def isoweek(self):
|
|
1064
|
-
"""
|
|
1065
|
-
|
|
1066
|
-
Standard weeks
|
|
1067
|
-
>>> Date(2023, 1, 2).isoweek()
|
|
1068
|
-
1
|
|
1069
|
-
>>> Date(2023, 4, 27).isoweek()
|
|
1070
|
-
17
|
|
1071
|
-
>>> Date(2023, 12, 31).isoweek()
|
|
1072
|
-
52
|
|
1073
|
-
|
|
1074
|
-
Belongs to week of previous year
|
|
1075
|
-
>>> Date(2023, 1, 1).isoweek()
|
|
1076
|
-
52
|
|
980
|
+
def isoweek(self) -> int | None:
|
|
981
|
+
"""Get ISO week number (1-52/53) following ISO week-numbering standard.
|
|
1077
982
|
"""
|
|
1078
983
|
with contextlib.suppress(Exception):
|
|
1079
984
|
return self.isocalendar()[1]
|
|
1080
985
|
|
|
1081
986
|
def lookback(self, unit='last') -> Self:
|
|
1082
|
-
"""
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
>>> Date(2018, 12, 7).b.lookback('day')
|
|
1087
|
-
Date(2018, 12, 6)
|
|
1088
|
-
>>> Date(2018, 12, 7).b.lookback('week')
|
|
1089
|
-
Date(2018, 11, 30)
|
|
1090
|
-
>>> Date(2018, 12, 7).b.lookback('month')
|
|
1091
|
-
Date(2018, 11, 7)
|
|
987
|
+
"""Get date in the past based on lookback unit.
|
|
988
|
+
|
|
989
|
+
Supported units: 'last'/'day' (1 day), 'week', 'month', 'quarter', 'year'.
|
|
990
|
+
Respects business day mode if enabled.
|
|
1092
991
|
"""
|
|
1093
992
|
def _lookback(years=0, months=0, weeks=0, days=0):
|
|
1094
993
|
_business = self._business
|
|
@@ -1124,38 +1023,46 @@ class Time(_pendulum.Time):
|
|
|
1124
1023
|
@classmethod
|
|
1125
1024
|
@prefer_utc_timezone
|
|
1126
1025
|
def parse(cls, s: str | None, fmt: str | None = None, raise_err: bool = False) -> Self | None:
|
|
1127
|
-
"""
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1026
|
+
"""Parse time string in various formats.
|
|
1027
|
+
|
|
1028
|
+
Supported formats:
|
|
1029
|
+
- hh:mm or hh.mm
|
|
1030
|
+
- hh:mm:ss or hh.mm.ss
|
|
1031
|
+
- hh:mm:ss.microseconds
|
|
1032
|
+
- Any of above with AM/PM
|
|
1033
|
+
- Compact: hhmmss or hhmmss.microseconds
|
|
1034
|
+
|
|
1035
|
+
Returns Time with UTC timezone by default.
|
|
1036
|
+
|
|
1037
|
+
Parameters
|
|
1038
|
+
s: String to parse or None
|
|
1039
|
+
fmt: Optional strftime format string for custom parsing
|
|
1040
|
+
raise_err: If True, raises ValueError on parse failure instead of returning None
|
|
1041
|
+
|
|
1042
|
+
Returns
|
|
1043
|
+
Time instance with UTC timezone or None if parsing fails and raise_err is False
|
|
1044
|
+
|
|
1045
|
+
Examples
|
|
1046
|
+
Basic time formats:
|
|
1047
|
+
Time.parse('14:30') → Time(14, 30, 0, 0, tzinfo=UTC)
|
|
1048
|
+
Time.parse('14.30') → Time(14, 30, 0, 0, tzinfo=UTC)
|
|
1049
|
+
Time.parse('14:30:45') → Time(14, 30, 45, 0, tzinfo=UTC)
|
|
1050
|
+
|
|
1051
|
+
With microseconds:
|
|
1052
|
+
Time.parse('14:30:45.123456') → Time(14, 30, 45, 123456000, tzinfo=UTC)
|
|
1053
|
+
Time.parse('14:30:45,500000') → Time(14, 30, 45, 500000000, tzinfo=UTC)
|
|
1054
|
+
|
|
1055
|
+
AM/PM formats:
|
|
1056
|
+
Time.parse('2:30 PM') → Time(14, 30, 0, 0, tzinfo=UTC)
|
|
1057
|
+
Time.parse('11:30 AM') → Time(11, 30, 0, 0, tzinfo=UTC)
|
|
1058
|
+
Time.parse('12:30 PM') → Time(12, 30, 0, 0, tzinfo=UTC)
|
|
1059
|
+
|
|
1060
|
+
Compact formats:
|
|
1061
|
+
Time.parse('143045') → Time(14, 30, 45, 0, tzinfo=UTC)
|
|
1062
|
+
Time.parse('1430') → Time(14, 30, 0, 0, tzinfo=UTC)
|
|
1063
|
+
|
|
1064
|
+
Custom format:
|
|
1065
|
+
Time.parse('14-30-45', fmt='%H-%M-%S') → Time(14, 30, 45, 0, tzinfo=UTC)
|
|
1159
1066
|
"""
|
|
1160
1067
|
|
|
1161
1068
|
def seconds(m):
|
|
@@ -1225,18 +1132,9 @@ class Time(_pendulum.Time):
|
|
|
1225
1132
|
tz: str | _zoneinfo.ZoneInfo | _datetime.tzinfo | None = None,
|
|
1226
1133
|
raise_err: bool = False,
|
|
1227
1134
|
) -> Self | None:
|
|
1228
|
-
"""
|
|
1229
|
-
|
|
1230
|
-
>>> Time.instance(_datetime.time(12, 30, 1))
|
|
1231
|
-
Time(12, 30, 1, tzinfo=Timezone('UTC'))
|
|
1232
|
-
>>> Time.instance(_pendulum.Time(12, 30, 1))
|
|
1233
|
-
Time(12, 30, 1, tzinfo=Timezone('UTC'))
|
|
1234
|
-
>>> Time.instance(None)
|
|
1235
|
-
|
|
1236
|
-
like Pendulum, do not add timzone if no timezone and Time object
|
|
1237
|
-
>>> Time.instance(Time(12, 30, 1))
|
|
1238
|
-
Time(12, 30, 1)
|
|
1135
|
+
"""Create Time instance from time-like object.
|
|
1239
1136
|
|
|
1137
|
+
Adds UTC timezone by default unless obj is already a Time instance.
|
|
1240
1138
|
"""
|
|
1241
1139
|
if pd.isna(obj):
|
|
1242
1140
|
if raise_err:
|
|
@@ -1250,15 +1148,8 @@ class Time(_pendulum.Time):
|
|
|
1250
1148
|
|
|
1251
1149
|
return cls(obj.hour, obj.minute, obj.second, obj.microsecond, tzinfo=tz)
|
|
1252
1150
|
|
|
1253
|
-
def in_timezone(self, tz: str | _zoneinfo.ZoneInfo | _datetime.tzinfo):
|
|
1254
|
-
"""Convert timezone
|
|
1255
|
-
|
|
1256
|
-
>>> Time(12, 0).in_timezone(Timezone('America/Sao_Paulo'))
|
|
1257
|
-
Time(9, 0, 0, tzinfo=Timezone('America/Sao_Paulo'))
|
|
1258
|
-
|
|
1259
|
-
>>> Time(12, 0, tzinfo=Timezone('Europe/Moscow')).in_timezone(Timezone('America/Sao_Paulo'))
|
|
1260
|
-
Time(6, 0, 0, tzinfo=Timezone('America/Sao_Paulo'))
|
|
1261
|
-
|
|
1151
|
+
def in_timezone(self, tz: str | _zoneinfo.ZoneInfo | _datetime.tzinfo) -> Self:
|
|
1152
|
+
"""Convert time to a different timezone.
|
|
1262
1153
|
"""
|
|
1263
1154
|
_dt = DateTime.combine(Date.today(), self, tzinfo=self.tzinfo or UTC)
|
|
1264
1155
|
return _dt.in_timezone(tz).time()
|
|
@@ -1281,7 +1172,7 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1281
1172
|
- Has timezone handling helpers not present in pendulum
|
|
1282
1173
|
"""
|
|
1283
1174
|
|
|
1284
|
-
def epoch(self):
|
|
1175
|
+
def epoch(self) -> float:
|
|
1285
1176
|
"""Translate a datetime object into unix seconds since epoch
|
|
1286
1177
|
"""
|
|
1287
1178
|
return self.timestamp()
|
|
@@ -1311,7 +1202,7 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1311
1202
|
return _pendulum.DateTime.replace(self, *args, **kwargs)
|
|
1312
1203
|
|
|
1313
1204
|
@classmethod
|
|
1314
|
-
def fromordinal(cls, *args, **kwargs):
|
|
1205
|
+
def fromordinal(cls, *args, **kwargs) -> Self:
|
|
1315
1206
|
"""Create a DateTime from an ordinal.
|
|
1316
1207
|
|
|
1317
1208
|
Parameters
|
|
@@ -1324,7 +1215,7 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1324
1215
|
return cls.instance(result)
|
|
1325
1216
|
|
|
1326
1217
|
@classmethod
|
|
1327
|
-
def fromtimestamp(cls, timestamp, tz=None):
|
|
1218
|
+
def fromtimestamp(cls, timestamp, tz=None) -> Self:
|
|
1328
1219
|
"""Create a DateTime from a timestamp.
|
|
1329
1220
|
|
|
1330
1221
|
Parameters
|
|
@@ -1339,7 +1230,7 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1339
1230
|
return cls.instance(result)
|
|
1340
1231
|
|
|
1341
1232
|
@classmethod
|
|
1342
|
-
def strptime(cls, time_str, fmt):
|
|
1233
|
+
def strptime(cls, time_str, fmt) -> Self:
|
|
1343
1234
|
"""Parse a string into a DateTime according to a format.
|
|
1344
1235
|
|
|
1345
1236
|
Parameters
|
|
@@ -1353,7 +1244,7 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1353
1244
|
return cls.instance(result)
|
|
1354
1245
|
|
|
1355
1246
|
@classmethod
|
|
1356
|
-
def utcfromtimestamp(cls, timestamp):
|
|
1247
|
+
def utcfromtimestamp(cls, timestamp) -> Self:
|
|
1357
1248
|
"""Create a UTC DateTime from a timestamp.
|
|
1358
1249
|
|
|
1359
1250
|
Parameters
|
|
@@ -1366,7 +1257,7 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1366
1257
|
return cls.instance(result)
|
|
1367
1258
|
|
|
1368
1259
|
@classmethod
|
|
1369
|
-
def utcnow(cls):
|
|
1260
|
+
def utcnow(cls) -> Self:
|
|
1370
1261
|
"""Create a DateTime representing current UTC time.
|
|
1371
1262
|
|
|
1372
1263
|
Returns
|
|
@@ -1391,7 +1282,7 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1391
1282
|
d.microsecond, tzinfo=d.tzinfo, fold=d.fold)
|
|
1392
1283
|
|
|
1393
1284
|
@classmethod
|
|
1394
|
-
def today(cls, tz: str | _zoneinfo.ZoneInfo | None = None):
|
|
1285
|
+
def today(cls, tz: str | _zoneinfo.ZoneInfo | None = None) -> Self:
|
|
1395
1286
|
"""Create a DateTime object representing today at the start of day.
|
|
1396
1287
|
|
|
1397
1288
|
Unlike pendulum.today() which returns current time, this method
|
|
@@ -1405,7 +1296,7 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1405
1296
|
"""
|
|
1406
1297
|
return DateTime.now(tz).start_of('day')
|
|
1407
1298
|
|
|
1408
|
-
def date(self):
|
|
1299
|
+
def date(self) -> Date:
|
|
1409
1300
|
return Date(self.year, self.month, self.day)
|
|
1410
1301
|
|
|
1411
1302
|
@classmethod
|
|
@@ -1420,25 +1311,13 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1420
1311
|
_tzinfo = tzinfo or time.tzinfo
|
|
1421
1312
|
return DateTime.instance(_datetime.datetime.combine(date, time, tzinfo=_tzinfo))
|
|
1422
1313
|
|
|
1423
|
-
def rfc3339(self):
|
|
1424
|
-
"""
|
|
1425
|
-
>>> DateTime.parse('Fri, 31 Oct 2014 10:55:00')
|
|
1426
|
-
DateTime(2014, 10, 31, 10, 55, 0, tzinfo=Timezone('UTC'))
|
|
1427
|
-
>>> DateTime.parse('Fri, 31 Oct 2014 10:55:00').rfc3339()
|
|
1428
|
-
'2014-10-31T10:55:00+00:00'
|
|
1314
|
+
def rfc3339(self) -> str:
|
|
1315
|
+
"""Return RFC 3339 formatted string (same as isoformat()).
|
|
1429
1316
|
"""
|
|
1430
1317
|
return self.isoformat()
|
|
1431
1318
|
|
|
1432
|
-
def time(self):
|
|
1433
|
-
"""Extract time from
|
|
1434
|
-
|
|
1435
|
-
>>> d = DateTime(2022, 1, 1, 12, 30, 15, tzinfo=EST)
|
|
1436
|
-
>>> d.time()
|
|
1437
|
-
Time(12, 30, 15, tzinfo=Timezone('US/Eastern'))
|
|
1438
|
-
|
|
1439
|
-
>>> d = DateTime(2022, 1, 1, 12, 30, 15, tzinfo=UTC)
|
|
1440
|
-
>>> d.time()
|
|
1441
|
-
Time(12, 30, 15, tzinfo=Timezone('UTC'))
|
|
1319
|
+
def time(self) -> Time:
|
|
1320
|
+
"""Extract time component from datetime (preserving timezone).
|
|
1442
1321
|
"""
|
|
1443
1322
|
return Time.instance(self)
|
|
1444
1323
|
|
|
@@ -1451,41 +1330,43 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1451
1330
|
"""Convert a string or timestamp to a DateTime with extended format support.
|
|
1452
1331
|
|
|
1453
1332
|
Unlike pendulum's parse, this method supports:
|
|
1454
|
-
- Unix timestamps (int/float)
|
|
1455
|
-
- Special codes (
|
|
1456
|
-
- Business day offsets
|
|
1333
|
+
- Unix timestamps (int/float, handles milliseconds automatically)
|
|
1334
|
+
- Special codes: T (today), Y (yesterday), P (previous business day)
|
|
1335
|
+
- Business day offsets: T-3b, P+2b (add/subtract business days)
|
|
1457
1336
|
- Multiple date-time formats beyond ISO 8601
|
|
1337
|
+
- Combined date and time strings with various separators
|
|
1458
1338
|
|
|
1459
1339
|
Parameters
|
|
1460
1340
|
s: String or timestamp to parse
|
|
1461
|
-
entity: Calendar entity for business day calculations
|
|
1462
|
-
raise_err:
|
|
1341
|
+
entity: Calendar entity for business day calculations (default NYSE)
|
|
1342
|
+
raise_err: If True, raises ValueError on parse failure instead of returning None
|
|
1463
1343
|
|
|
1464
1344
|
Returns
|
|
1465
1345
|
DateTime instance or None if parsing fails and raise_err is False
|
|
1466
1346
|
|
|
1467
1347
|
Examples
|
|
1348
|
+
Unix timestamps:
|
|
1349
|
+
DateTime.parse(1609459200) → DateTime(2021, 1, 1, 0, 0, 0, tzinfo=LCL)
|
|
1350
|
+
DateTime.parse(1609459200000) → DateTime(2021, 1, 1, 0, 0, 0, tzinfo=LCL)
|
|
1468
1351
|
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
DateTime(2022, 1, 1, 0, 0, 0, tzinfo=Timezone('...'))
|
|
1352
|
+
ISO 8601 format:
|
|
1353
|
+
DateTime.parse('2020-01-15T14:30:00') → DateTime(2020, 1, 15, 14, 30, 0)
|
|
1472
1354
|
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
DateTime(2014, 10, 31, 14, 55, 0, tzinfo=Timezone('US/Eastern'))
|
|
1355
|
+
Date and time separated:
|
|
1356
|
+
DateTime.parse('2020-01-15 14:30:00') → DateTime(2020, 1, 15, 14, 30, 0, tzinfo=LCL)
|
|
1357
|
+
DateTime.parse('01/15/2020:14:30:00') → DateTime(2020, 1, 15, 14, 30, 0, tzinfo=LCL)
|
|
1477
1358
|
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1359
|
+
Date only (time defaults to 00:00:00):
|
|
1360
|
+
DateTime.parse('2020-01-15') → DateTime(2020, 1, 15, 0, 0, 0)
|
|
1361
|
+
DateTime.parse('01/15/2020') → DateTime(2020, 1, 15, 0, 0, 0)
|
|
1481
1362
|
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
DateTime(2014, 10, 31, 18, 55, 0, tzinfo=tzutc())
|
|
1363
|
+
Time only (uses today's date):
|
|
1364
|
+
DateTime.parse('14:30:00') → DateTime(today's year, month, day, 14, 30, 0, tzinfo=LCL)
|
|
1485
1365
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1366
|
+
Special codes:
|
|
1367
|
+
DateTime.parse('T') → today at 00:00:00
|
|
1368
|
+
DateTime.parse('Y') → yesterday at 00:00:00
|
|
1369
|
+
DateTime.parse('P') → previous business day at 00:00:00
|
|
1489
1370
|
"""
|
|
1490
1371
|
if not s:
|
|
1491
1372
|
if raise_err:
|
|
@@ -1538,44 +1419,21 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1538
1419
|
) -> Self | None:
|
|
1539
1420
|
"""Create a DateTime instance from various datetime-like objects.
|
|
1540
1421
|
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
DateTime instances.
|
|
1422
|
+
Provides unified interface for converting different date/time types
|
|
1423
|
+
including pandas and numpy datetime objects into DateTime instances.
|
|
1544
1424
|
|
|
1545
1425
|
Unlike pendulum, this method:
|
|
1546
1426
|
- Handles pandas Timestamp and numpy datetime64 objects
|
|
1547
1427
|
- Adds timezone (UTC by default) when none is specified
|
|
1548
|
-
- Has special handling for
|
|
1428
|
+
- Has special handling for Time objects (combines with current date)
|
|
1549
1429
|
|
|
1550
1430
|
Parameters
|
|
1551
1431
|
obj: Date, datetime, time, or compatible object to convert
|
|
1552
1432
|
tz: Optional timezone to apply (if None, uses obj's timezone or UTC)
|
|
1553
|
-
raise_err:
|
|
1433
|
+
raise_err: If True, raises ValueError for None/NA values instead of returning None
|
|
1554
1434
|
|
|
1555
1435
|
Returns
|
|
1556
1436
|
DateTime instance or None if obj is None/NA and raise_err is False
|
|
1557
|
-
|
|
1558
|
-
Examples
|
|
1559
|
-
|
|
1560
|
-
From Python datetime types:
|
|
1561
|
-
>>> DateTime.instance(_datetime.date(2022, 1, 1))
|
|
1562
|
-
DateTime(2022, 1, 1, 0, 0, 0, tzinfo=Timezone('...'))
|
|
1563
|
-
>>> DateTime.instance(_datetime.datetime(2022, 1, 1, 0, 0, 0))
|
|
1564
|
-
DateTime(2022, 1, 1, 0, 0, 0, tzinfo=Timezone('...'))
|
|
1565
|
-
|
|
1566
|
-
Preserves timezone behavior:
|
|
1567
|
-
>>> DateTime.instance(DateTime(2022, 1, 1, 0, 0, 0))
|
|
1568
|
-
DateTime(2022, 1, 1, 0, 0, 0)
|
|
1569
|
-
|
|
1570
|
-
From Time objects:
|
|
1571
|
-
>>> DateTime.instance(Time(4, 4, 21))
|
|
1572
|
-
DateTime(..., 4, 4, 21, tzinfo=Timezone('UTC'))
|
|
1573
|
-
>>> DateTime.instance(Time(4, 4, 21, tzinfo=UTC))
|
|
1574
|
-
DateTime(..., 4, 4, 21, tzinfo=Timezone('UTC'))
|
|
1575
|
-
|
|
1576
|
-
From numpy/pandas datetime:
|
|
1577
|
-
>>> DateTime.instance(np.datetime64('2000-01', 'D'))
|
|
1578
|
-
DateTime(2000, 1, 1, 0, 0, 0, tzinfo=Timezone('UTC'))
|
|
1579
1437
|
"""
|
|
1580
1438
|
if pd.isna(obj):
|
|
1581
1439
|
if raise_err:
|
|
@@ -1608,25 +1466,95 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1608
1466
|
obj.second, obj.microsecond, tzinfo=tz)
|
|
1609
1467
|
|
|
1610
1468
|
|
|
1611
|
-
class
|
|
1612
|
-
|
|
1469
|
+
class Interval(_pendulum.Interval):
|
|
1470
|
+
"""Interval class extending pendulum.Interval with business day awareness.
|
|
1613
1471
|
|
|
1472
|
+
This class represents the difference between two dates or datetimes with
|
|
1473
|
+
additional support for business day calculations, entity awareness, and
|
|
1474
|
+
financial period calculations.
|
|
1614
1475
|
|
|
1615
|
-
|
|
1476
|
+
Unlike pendulum.Interval:
|
|
1477
|
+
- Has business day mode that only counts business days
|
|
1478
|
+
- Preserves entity association (e.g., NYSE)
|
|
1479
|
+
- Additional financial methods like yearfrac()
|
|
1480
|
+
- Support for range operations that respect business days
|
|
1481
|
+
"""
|
|
1616
1482
|
|
|
1617
1483
|
_business: bool = False
|
|
1618
1484
|
_entity: type[NYSE] = NYSE
|
|
1619
1485
|
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1486
|
+
@expect_date_or_datetime
|
|
1487
|
+
@normalize_date_datetime_pairs
|
|
1488
|
+
def __new__(cls, begdate: Date | DateTime, enddate: Date | DateTime) -> Self:
|
|
1489
|
+
assert begdate and enddate, 'Interval dates cannot be None'
|
|
1490
|
+
instance = super().__new__(cls, begdate, enddate, False)
|
|
1491
|
+
return instance
|
|
1492
|
+
|
|
1493
|
+
@expect_date_or_datetime
|
|
1494
|
+
@normalize_date_datetime_pairs
|
|
1495
|
+
def __init__(self, begdate: Date | DateTime, enddate: Date | DateTime) -> None:
|
|
1496
|
+
super().__init__(begdate, enddate, False)
|
|
1497
|
+
self._sign = 1 if begdate <= enddate else -1
|
|
1498
|
+
if begdate <= enddate:
|
|
1499
|
+
self._start = begdate
|
|
1500
|
+
self._end = enddate
|
|
1501
|
+
else:
|
|
1502
|
+
self._start = enddate
|
|
1503
|
+
self._end = begdate
|
|
1504
|
+
|
|
1505
|
+
@staticmethod
|
|
1506
|
+
def _get_quarter_start(date: Date | DateTime) -> Date | DateTime:
|
|
1507
|
+
"""Get the start date of the quarter containing the given date.
|
|
1508
|
+
"""
|
|
1509
|
+
quarter_month = ((date.month - 1) // 3) * 3 + 1
|
|
1510
|
+
return date.replace(month=quarter_month, day=1)
|
|
1511
|
+
|
|
1512
|
+
@staticmethod
|
|
1513
|
+
def _get_quarter_end(date: Date | DateTime) -> Date | DateTime:
|
|
1514
|
+
"""Get the end date of the quarter containing the given date.
|
|
1515
|
+
"""
|
|
1516
|
+
quarter_month = ((date.month - 1) // 3) * 3 + 3
|
|
1517
|
+
return date.replace(month=quarter_month).end_of('month')
|
|
1518
|
+
|
|
1519
|
+
def _get_unit_handlers(self, unit: str) -> dict:
|
|
1520
|
+
"""Get handlers for the specified time unit.
|
|
1521
|
+
|
|
1522
|
+
Returns a dict with:
|
|
1523
|
+
get_start: Function to get start of period containing date
|
|
1524
|
+
get_end: Function to get end of period containing date
|
|
1525
|
+
advance: Function to advance to next period start
|
|
1526
|
+
"""
|
|
1527
|
+
if unit == 'quarter':
|
|
1528
|
+
return {
|
|
1529
|
+
'get_start': self._get_quarter_start,
|
|
1530
|
+
'get_end': self._get_quarter_end,
|
|
1531
|
+
'advance': lambda date: self._get_quarter_start(date.add(months=3)),
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
if unit == 'decade':
|
|
1535
|
+
return {
|
|
1536
|
+
'get_start': lambda date: date.start_of('decade'),
|
|
1537
|
+
'get_end': lambda date: date.end_of('decade'),
|
|
1538
|
+
'advance': lambda date: date.add(years=10).start_of('decade'),
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
if unit == 'century':
|
|
1542
|
+
return {
|
|
1543
|
+
'get_start': lambda date: date.start_of('century'),
|
|
1544
|
+
'get_end': lambda date: date.end_of('century'),
|
|
1545
|
+
'advance': lambda date: date.add(years=100).start_of('century'),
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
return {
|
|
1549
|
+
'get_start': lambda date: date.start_of(unit),
|
|
1550
|
+
'get_end': lambda date: date.end_of(unit),
|
|
1551
|
+
'advance': lambda date: date.add(**{f'{unit}s': 1}).start_of(unit),
|
|
1552
|
+
}
|
|
1623
1553
|
|
|
1624
1554
|
def business(self) -> Self:
|
|
1625
1555
|
self._business = True
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
if self.enddate:
|
|
1629
|
-
self.enddate.business()
|
|
1556
|
+
self._start.business()
|
|
1557
|
+
self._end.business()
|
|
1630
1558
|
return self
|
|
1631
1559
|
|
|
1632
1560
|
@property
|
|
@@ -1635,249 +1563,120 @@ class Interval:
|
|
|
1635
1563
|
|
|
1636
1564
|
def entity(self, e: type[NYSE] = NYSE) -> Self:
|
|
1637
1565
|
self._entity = e
|
|
1638
|
-
if self.
|
|
1639
|
-
self.
|
|
1640
|
-
if self.
|
|
1641
|
-
self.
|
|
1566
|
+
if self._start:
|
|
1567
|
+
self._end._entity = e
|
|
1568
|
+
if self._end:
|
|
1569
|
+
self._end._entity = e
|
|
1642
1570
|
return self
|
|
1643
1571
|
|
|
1644
|
-
def
|
|
1645
|
-
"""
|
|
1646
|
-
|
|
1647
|
-
The combinations are as follows:
|
|
1648
|
-
|
|
1649
|
-
beg end num action
|
|
1650
|
-
--- --- --- ---------------------
|
|
1651
|
-
- - - Error, underspecified
|
|
1652
|
-
set set set Error, overspecified
|
|
1653
|
-
set set -
|
|
1654
|
-
set - - end=max date
|
|
1655
|
-
- set - beg=min date
|
|
1656
|
-
- - set end=max date, beg=end - num
|
|
1657
|
-
set - set end=beg + num
|
|
1658
|
-
- set set beg=end - num
|
|
1659
|
-
|
|
1660
|
-
Basic/legacy cases
|
|
1661
|
-
>>> Interval(Date(2014, 4, 3), None).b.range(3)
|
|
1662
|
-
(Date(2014, 4, 3), Date(2014, 4, 8))
|
|
1663
|
-
>>> Interval(None, Date(2014, 7, 27)).range(20)
|
|
1664
|
-
(Date(2014, 7, 7), Date(2014, 7, 27))
|
|
1665
|
-
>>> Interval(None, Date(2014, 7, 27)).b.range(20)
|
|
1666
|
-
(Date(2014, 6, 27), Date(2014, 7, 27))
|
|
1667
|
-
|
|
1668
|
-
Do not modify dates if both are provided
|
|
1669
|
-
>>> Interval(Date(2024, 7, 25), Date(2024, 7, 25)).b.range(None)
|
|
1670
|
-
(Date(2024, 7, 25), Date(2024, 7, 25))
|
|
1671
|
-
>>> Interval(Date(2024, 7, 27), Date(2024, 7, 27)).b.range(None)
|
|
1672
|
-
(Date(2024, 7, 27), Date(2024, 7, 27))
|
|
1673
|
-
|
|
1674
|
-
Edge cases (7/27/24 is weekend)
|
|
1675
|
-
>>> Interval(Date(2024, 7, 27), None).b.range(0)
|
|
1676
|
-
(Date(2024, 7, 27), Date(2024, 7, 27))
|
|
1677
|
-
>>> Interval(None, Date(2024, 7, 27)).b.range(0)
|
|
1678
|
-
(Date(2024, 7, 27), Date(2024, 7, 27))
|
|
1679
|
-
>>> Interval(Date(2024, 7, 27), None).b.range(1)
|
|
1680
|
-
(Date(2024, 7, 27), Date(2024, 7, 29))
|
|
1681
|
-
>>> Interval(None, Date(2024, 7, 27)).b.range(1)
|
|
1682
|
-
(Date(2024, 7, 26), Date(2024, 7, 27))
|
|
1572
|
+
def is_business_day_range(self) -> list[bool]:
|
|
1573
|
+
"""Generate boolean values indicating whether each day in the range is a business day.
|
|
1683
1574
|
"""
|
|
1684
|
-
|
|
1575
|
+
self._business = False
|
|
1576
|
+
for thedate in self.range('days'):
|
|
1577
|
+
yield thedate.is_business_day()
|
|
1685
1578
|
|
|
1686
|
-
|
|
1579
|
+
@reset_business
|
|
1580
|
+
def range(self, unit: str = 'days', amount: int = 1) -> Iterator[DateTime | Date]:
|
|
1581
|
+
"""Generate dates/datetimes over the interval.
|
|
1687
1582
|
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
raise IntervalError('Missing begdate, enddate, and window')
|
|
1692
|
-
if not begdate and not enddate and window:
|
|
1693
|
-
raise IntervalError('Missing begdate and enddate, window specified')
|
|
1583
|
+
Parameters
|
|
1584
|
+
unit: Time unit ('days', 'weeks', 'months', 'years')
|
|
1585
|
+
amount: Step size (e.g., every N units)
|
|
1694
1586
|
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
else:
|
|
1700
|
-
enddate = begdate.add(days=window) if window else begdate
|
|
1587
|
+
In business mode (for 'days' only), skips non-business days.
|
|
1588
|
+
"""
|
|
1589
|
+
_business = self._business
|
|
1590
|
+
parent_range = _pendulum.Interval.range
|
|
1701
1591
|
|
|
1702
|
-
|
|
1703
|
-
|
|
1592
|
+
def _range_generator():
|
|
1593
|
+
if unit != 'days':
|
|
1594
|
+
yield from (type(d).instance(d) for d in parent_range(self, unit, amount))
|
|
1595
|
+
return
|
|
1704
1596
|
|
|
1705
|
-
|
|
1597
|
+
if self._sign == 1:
|
|
1598
|
+
op = operator.le
|
|
1599
|
+
this = self._start
|
|
1600
|
+
thru = self._end
|
|
1601
|
+
else:
|
|
1602
|
+
op = operator.ge
|
|
1603
|
+
this = self._end
|
|
1604
|
+
thru = self._start
|
|
1605
|
+
|
|
1606
|
+
while op(this, thru):
|
|
1607
|
+
if _business:
|
|
1608
|
+
if this.is_business_day():
|
|
1609
|
+
yield this
|
|
1610
|
+
else:
|
|
1611
|
+
yield this
|
|
1612
|
+
this = this.add(days=self._sign * amount)
|
|
1706
1613
|
|
|
1707
|
-
|
|
1708
|
-
"""Is business date range.
|
|
1614
|
+
return _range_generator()
|
|
1709
1615
|
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1616
|
+
@property
|
|
1617
|
+
@reset_business
|
|
1618
|
+
def days(self) -> int:
|
|
1619
|
+
"""Get number of days in the interval (respects business mode and sign).
|
|
1714
1620
|
"""
|
|
1715
|
-
|
|
1716
|
-
yield thedate.is_business_day()
|
|
1621
|
+
return self._sign * len(tuple(self.range('days'))) - self._sign
|
|
1717
1622
|
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
defaults to include ALL days (not just business days)
|
|
1725
|
-
|
|
1726
|
-
>>> next(Interval(Date(2014,7,16), Date(2014,7,16)).series())
|
|
1727
|
-
Date(2014, 7, 16)
|
|
1728
|
-
>>> next(Interval(Date(2014,7,12), Date(2014,7,16)).series())
|
|
1729
|
-
Date(2014, 7, 12)
|
|
1730
|
-
>>> len(list(Interval(Date(2014,7,12), Date(2014,7,16)).series()))
|
|
1731
|
-
5
|
|
1732
|
-
>>> len(list(Interval(Date(2014,7,12), None).series(window=4)))
|
|
1733
|
-
5
|
|
1734
|
-
>>> len(list(Interval(Date(2014,7,16)).series(window=4)))
|
|
1735
|
-
5
|
|
1736
|
-
|
|
1737
|
-
Weekend and a holiday
|
|
1738
|
-
>>> len(list(Interval(Date(2014,7,3), Date(2014,7,5)).b.series()))
|
|
1739
|
-
1
|
|
1740
|
-
>>> len(list(Interval(Date(2014,7,17), Date(2014,7,16)).series()))
|
|
1741
|
-
Traceback (most recent call last):
|
|
1742
|
-
...
|
|
1743
|
-
AssertionError: Begdate must be earlier or equal to Enddate
|
|
1744
|
-
|
|
1745
|
-
since != business day and want business days
|
|
1746
|
-
1/[3,10]/2015 is a Saturday, 1/7/2015 is a Wednesday
|
|
1747
|
-
>>> len(list(Interval(Date(2015,1,3), Date(2015,1,7)).b.series()))
|
|
1748
|
-
3
|
|
1749
|
-
>>> len(list(Interval(Date(2015,1,3), None).b.series(window=3)))
|
|
1750
|
-
3
|
|
1751
|
-
>>> len(list(Interval(Date(2015,1,3), Date(2015,1,10)).b.series()))
|
|
1752
|
-
5
|
|
1753
|
-
>>> len(list(Interval(Date(2015,1,3), None).b.series(window=5)))
|
|
1754
|
-
5
|
|
1755
|
-
"""
|
|
1756
|
-
window = abs(int(window))
|
|
1757
|
-
since, until = self.begdate, self.enddate
|
|
1758
|
-
_business = self._business
|
|
1759
|
-
assert until or since, 'Since or until is required'
|
|
1760
|
-
if not since and until:
|
|
1761
|
-
since = (until.business() if _business else
|
|
1762
|
-
until).subtract(days=window)
|
|
1763
|
-
elif since and not until:
|
|
1764
|
-
until = (since.business() if _business else
|
|
1765
|
-
since).add(days=window)
|
|
1766
|
-
assert since <= until, 'Since date must be earlier or equal to Until date'
|
|
1767
|
-
thedate = since
|
|
1768
|
-
while thedate <= until:
|
|
1769
|
-
if _business:
|
|
1770
|
-
if thedate.is_business_day():
|
|
1771
|
-
yield thedate
|
|
1772
|
-
else:
|
|
1773
|
-
yield thedate
|
|
1774
|
-
thedate = thedate.add(days=1)
|
|
1775
|
-
|
|
1776
|
-
def start_of_series(self, unit='month') -> list[Date]:
|
|
1777
|
-
"""Return a series between and inclusive of begdate and enddate.
|
|
1778
|
-
|
|
1779
|
-
>>> Interval(Date(2018, 1, 5), Date(2018, 4, 5)).start_of_series('month')
|
|
1780
|
-
[Date(2018, 1, 1), Date(2018, 2, 1), Date(2018, 3, 1), Date(2018, 4, 1)]
|
|
1781
|
-
>>> Interval(Date(2018, 4, 30), Date(2018, 7, 30)).start_of_series('month')
|
|
1782
|
-
[Date(2018, 4, 1), Date(2018, 5, 1), Date(2018, 6, 1), Date(2018, 7, 1)]
|
|
1783
|
-
>>> Interval(Date(2018, 1, 5), Date(2018, 4, 5)).start_of_series('week')
|
|
1784
|
-
[Date(2018, 1, 1), Date(2018, 1, 8), ..., Date(2018, 4, 2)]
|
|
1785
|
-
"""
|
|
1786
|
-
begdate = self.begdate.start_of(unit)
|
|
1787
|
-
enddate = self.enddate.start_of(unit)
|
|
1788
|
-
interval = _pendulum.interval(begdate, enddate)
|
|
1789
|
-
return [Date.instance(d).start_of(unit) for d in interval.range(f'{unit}s')]
|
|
1790
|
-
|
|
1791
|
-
def end_of_series(self, unit='month') -> list[Date]:
|
|
1792
|
-
"""Return a series between and inclusive of begdate and enddate.
|
|
1793
|
-
|
|
1794
|
-
>>> Interval(Date(2018, 1, 5), Date(2018, 4, 5)).end_of_series('month')
|
|
1795
|
-
[Date(2018, 1, 31), Date(2018, 2, 28), Date(2018, 3, 31), Date(2018, 4, 30)]
|
|
1796
|
-
>>> Interval(Date(2018, 4, 30), Date(2018, 7, 30)).end_of_series('month')
|
|
1797
|
-
[Date(2018, 4, 30), Date(2018, 5, 31), Date(2018, 6, 30), Date(2018, 7, 31)]
|
|
1798
|
-
>>> Interval(Date(2018, 1, 5), Date(2018, 4, 5)).end_of_series('week')
|
|
1799
|
-
[Date(2018, 1, 7), Date(2018, 1, 14), ..., Date(2018, 4, 8)]
|
|
1623
|
+
@property
|
|
1624
|
+
def months(self) -> float:
|
|
1625
|
+
"""Get number of months in the interval including fractional parts.
|
|
1626
|
+
|
|
1627
|
+
Overrides pendulum's months property to return a float instead of an integer.
|
|
1628
|
+
Calculates fractional months based on actual day counts within partial months.
|
|
1800
1629
|
"""
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1630
|
+
year_diff = self._end.year - self._start.year
|
|
1631
|
+
month_diff = self._end.month - self._start.month
|
|
1632
|
+
total_months = year_diff * 12 + month_diff
|
|
1633
|
+
|
|
1634
|
+
if self._end.day >= self._start.day:
|
|
1635
|
+
day_diff = self._end.day - self._start.day
|
|
1636
|
+
days_in_month = calendar.monthrange(self._start.year, self._start.month)[1]
|
|
1637
|
+
fraction = day_diff / days_in_month
|
|
1638
|
+
else:
|
|
1639
|
+
total_months -= 1
|
|
1640
|
+
days_in_start_month = calendar.monthrange(self._start.year, self._start.month)[1]
|
|
1641
|
+
day_diff = (days_in_start_month - self._start.day) + self._end.day
|
|
1642
|
+
fraction = day_diff / days_in_start_month
|
|
1805
1643
|
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
>>> Interval(Date(2018, 9, 6), Date(2018, 9, 10)).b.days()
|
|
1814
|
-
2
|
|
1815
|
-
>>> Interval(Date(2018, 9, 10), Date(2018, 9, 6)).b.days()
|
|
1816
|
-
-2
|
|
1644
|
+
return self._sign * (total_months + fraction)
|
|
1645
|
+
|
|
1646
|
+
@property
|
|
1647
|
+
def quarters(self) -> float:
|
|
1648
|
+
"""Get approximate number of quarters in the interval.
|
|
1649
|
+
|
|
1650
|
+
Note: This is an approximation using day count / 365 * 4.
|
|
1817
1651
|
"""
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
return (self.enddate - self.begdate).days
|
|
1824
|
-
if self.begdate < self.enddate:
|
|
1825
|
-
return len(list(self.series())) - 1
|
|
1826
|
-
_reverse = Interval(self.enddate, self.begdate)
|
|
1827
|
-
_reverse._entity = self._entity
|
|
1828
|
-
_reverse._business = self._business
|
|
1829
|
-
return -len(list(_reverse.series())) + 1
|
|
1830
|
-
|
|
1831
|
-
def quarters(self):
|
|
1832
|
-
"""Return the number of quarters between two dates
|
|
1833
|
-
TODO: good enough implementation; refine rules to be heuristically precise
|
|
1834
|
-
|
|
1835
|
-
>>> round(Interval(Date(2020, 1, 1), Date(2020, 2, 16)).quarters(), 2)
|
|
1836
|
-
0.5
|
|
1837
|
-
>>> round(Interval(Date(2020, 1, 1), Date(2020, 4, 1)).quarters(), 2)
|
|
1838
|
-
1.0
|
|
1839
|
-
>>> round(Interval(Date(2020, 1, 1), Date(2020, 7, 1)).quarters(), 2)
|
|
1840
|
-
1.99
|
|
1841
|
-
>>> round(Interval(Date(2020, 1, 1), Date(2020, 8, 1)).quarters(), 2)
|
|
1842
|
-
2.33
|
|
1652
|
+
return self._sign * 4 * self.days / 365.0
|
|
1653
|
+
|
|
1654
|
+
@property
|
|
1655
|
+
def years(self) -> int:
|
|
1656
|
+
"""Get number of complete years in the interval (always floors).
|
|
1843
1657
|
"""
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
'
|
|
1866
|
-
>>> '{:.4f}'.format(Interval(begdate, enddate).years(2))
|
|
1867
|
-
'42.8306'
|
|
1868
|
-
>>> '{:.4f}'.format(Interval(begdate, enddate).years(3))
|
|
1869
|
-
'42.2438'
|
|
1870
|
-
>>> '{:.4f}'.format(Interval(begdate, enddate).years(4))
|
|
1871
|
-
'42.2194'
|
|
1872
|
-
>>> '{:.4f}'.format(Interval(enddate, begdate).years(4))
|
|
1873
|
-
'-42.2194'
|
|
1874
|
-
|
|
1875
|
-
Excel has a known leap year bug when year == 1900 (=YEARFRAC("1900-1-1", "1900-12-1", 1) -> 0.9178)
|
|
1876
|
-
The bug originated from Lotus 1-2-3, and was purposely implemented in Excel for the purpose of backward compatibility.
|
|
1877
|
-
>>> begdate = Date(1900, 1, 1)
|
|
1878
|
-
>>> enddate = Date(1900, 12, 1)
|
|
1879
|
-
>>> '{:.4f}'.format(Interval(begdate, enddate).years(4))
|
|
1880
|
-
'0.9167'
|
|
1658
|
+
year_diff = self._end.year - self._start.year
|
|
1659
|
+
if self._end.month < self._start.month or \
|
|
1660
|
+
(self._end.month == self._start.month and self._end.day < self._start.day):
|
|
1661
|
+
year_diff -= 1
|
|
1662
|
+
return self._sign * year_diff
|
|
1663
|
+
|
|
1664
|
+
def yearfrac(self, basis: int = 0) -> float:
|
|
1665
|
+
"""Calculate the fraction of years between two dates (Excel-compatible).
|
|
1666
|
+
|
|
1667
|
+
This method provides precise calculation using various day count conventions
|
|
1668
|
+
used in finance. Results are tested against Excel for compatibility.
|
|
1669
|
+
|
|
1670
|
+
Parameters
|
|
1671
|
+
basis: Day count convention to use:
|
|
1672
|
+
0 = US (NASD) 30/360 (default)
|
|
1673
|
+
1 = Actual/actual
|
|
1674
|
+
2 = Actual/360
|
|
1675
|
+
3 = Actual/365
|
|
1676
|
+
4 = European 30/360
|
|
1677
|
+
|
|
1678
|
+
Note: Excel has a known leap year bug for year 1900 which is intentionally
|
|
1679
|
+
replicated for compatibility (1900 is treated as a leap year even though it wasn't).
|
|
1881
1680
|
"""
|
|
1882
1681
|
|
|
1883
1682
|
def average_year_length(date1, date2):
|
|
@@ -1962,32 +1761,84 @@ class Interval:
|
|
|
1962
1761
|
(date1day + date1month * 30 + date1year * 360)
|
|
1963
1762
|
return daydiff360 / 360
|
|
1964
1763
|
|
|
1965
|
-
|
|
1966
|
-
if enddate is None:
|
|
1967
|
-
return
|
|
1968
|
-
|
|
1969
|
-
sign = 1
|
|
1970
|
-
if begdate > enddate:
|
|
1971
|
-
begdate, enddate = enddate, begdate
|
|
1972
|
-
sign = -1
|
|
1973
|
-
if begdate == enddate:
|
|
1764
|
+
if self._start == self._end:
|
|
1974
1765
|
return 0.0
|
|
1975
|
-
|
|
1976
1766
|
if basis == 0:
|
|
1977
|
-
return basis0(
|
|
1767
|
+
return basis0(self._start, self._end) * self._sign
|
|
1978
1768
|
if basis == 1:
|
|
1979
|
-
return basis1(
|
|
1769
|
+
return basis1(self._start, self._end) * self._sign
|
|
1980
1770
|
if basis == 2:
|
|
1981
|
-
return basis2(
|
|
1771
|
+
return basis2(self._start, self._end) * self._sign
|
|
1982
1772
|
if basis == 3:
|
|
1983
|
-
return basis3(
|
|
1773
|
+
return basis3(self._start, self._end) * self._sign
|
|
1984
1774
|
if basis == 4:
|
|
1985
|
-
return basis4(
|
|
1775
|
+
return basis4(self._start, self._end) * self._sign
|
|
1986
1776
|
|
|
1987
1777
|
raise ValueError('Basis range [0, 4]. Unknown basis {basis}.')
|
|
1988
1778
|
|
|
1779
|
+
@reset_business
|
|
1780
|
+
def start_of(self, unit: str = 'month') -> list[Date | DateTime]:
|
|
1781
|
+
"""Return the start of each unit within the interval.
|
|
1782
|
+
|
|
1783
|
+
Parameters
|
|
1784
|
+
unit: Time unit ('month', 'week', 'year', 'quarter')
|
|
1785
|
+
|
|
1786
|
+
Returns
|
|
1787
|
+
List of Date or DateTime objects representing start of each unit
|
|
1788
|
+
|
|
1789
|
+
In business mode, each start date is adjusted to the next business day
|
|
1790
|
+
if it falls on a non-business day.
|
|
1791
|
+
"""
|
|
1792
|
+
handlers = self._get_unit_handlers(unit)
|
|
1793
|
+
result = []
|
|
1794
|
+
|
|
1795
|
+
current = handlers['get_start'](self._start)
|
|
1796
|
+
|
|
1797
|
+
if self._business:
|
|
1798
|
+
current._entity = self._entity
|
|
1989
1799
|
|
|
1990
|
-
|
|
1800
|
+
while current <= self._end:
|
|
1801
|
+
if self._business:
|
|
1802
|
+
current = current._business_or_next()
|
|
1803
|
+
result.append(current)
|
|
1804
|
+
current = handlers['advance'](current)
|
|
1805
|
+
|
|
1806
|
+
return result
|
|
1807
|
+
|
|
1808
|
+
@reset_business
|
|
1809
|
+
def end_of(self, unit: str = 'month') -> list[Date | DateTime]:
|
|
1810
|
+
"""Return the end of each unit within the interval.
|
|
1811
|
+
|
|
1812
|
+
Parameters
|
|
1813
|
+
unit: Time unit ('month', 'week', 'year', 'quarter')
|
|
1814
|
+
|
|
1815
|
+
Returns
|
|
1816
|
+
List of Date or DateTime objects representing end of each unit
|
|
1817
|
+
|
|
1818
|
+
In business mode, each end date is adjusted to the previous business day
|
|
1819
|
+
if it falls on a non-business day.
|
|
1820
|
+
"""
|
|
1821
|
+
handlers = self._get_unit_handlers(unit)
|
|
1822
|
+
result = []
|
|
1823
|
+
|
|
1824
|
+
current = handlers['get_start'](self._start)
|
|
1825
|
+
|
|
1826
|
+
if self._business:
|
|
1827
|
+
current._entity = self._entity
|
|
1828
|
+
|
|
1829
|
+
while current <= self._end:
|
|
1830
|
+
end_date = handlers['get_end'](current)
|
|
1831
|
+
|
|
1832
|
+
if self._business:
|
|
1833
|
+
end_date = end_date._business_or_previous()
|
|
1834
|
+
result.append(end_date)
|
|
1835
|
+
|
|
1836
|
+
current = handlers['advance'](current)
|
|
1837
|
+
|
|
1838
|
+
return result
|
|
1839
|
+
|
|
1840
|
+
|
|
1841
|
+
def create_ics(begdate, enddate, summary, location) -> str:
|
|
1991
1842
|
"""Create a simple .ics file per RFC 5545 guidelines."""
|
|
1992
1843
|
|
|
1993
1844
|
return f"""BEGIN:VCALENDAR
|
|
@@ -2001,7 +1852,3 @@ LOCATION:{location}
|
|
|
2001
1852
|
END:VEVENT
|
|
2002
1853
|
END:VCALENDAR
|
|
2003
1854
|
"""
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
if __name__ == '__main__':
|
|
2007
|
-
__import__('doctest').testmod(optionflags=4 | 8 | 32)
|