opendate 0.1.13__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 +449 -653
- 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.13.dist-info → opendate-0.1.19.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.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,9 +39,11 @@ __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
|
|
|
@@ -49,27 +51,8 @@ def Timezone(name:str = 'US/Eastern') -> _zoneinfo.ZoneInfo:
|
|
|
49
51
|
"""Create a timezone object with the specified name.
|
|
50
52
|
|
|
51
53
|
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
|
|
54
|
+
consistent timezone handling across the library. Note that 'US/Eastern'
|
|
55
|
+
is equivalent to 'America/New_York' for all dates.
|
|
73
56
|
"""
|
|
74
57
|
return _pendulum.tz.Timezone(name)
|
|
75
58
|
|
|
@@ -125,8 +108,8 @@ DATEMATCH = re.compile(r'^(?P<d>N|T|Y|P|M)(?P<n>[-+]?\d+)?(?P<b>b?)?$')
|
|
|
125
108
|
# return entity
|
|
126
109
|
|
|
127
110
|
|
|
128
|
-
def isdateish(x):
|
|
129
|
-
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)
|
|
130
113
|
|
|
131
114
|
|
|
132
115
|
def parse_arg(typ, arg):
|
|
@@ -164,6 +147,9 @@ def expect(func, typ: type[_datetime.date], exclkw: bool = False) -> Callable:
|
|
|
164
147
|
continue
|
|
165
148
|
if typ == _datetime.date:
|
|
166
149
|
kwargs[k] = Date.instance(v)
|
|
150
|
+
continue
|
|
151
|
+
if typ == _datetime.time:
|
|
152
|
+
kwargs[k] = Time.instance(v)
|
|
167
153
|
return func(*args, **kwargs)
|
|
168
154
|
return wrapper
|
|
169
155
|
|
|
@@ -218,7 +204,21 @@ def store_both(func=None, *, typ=None):
|
|
|
218
204
|
return wrapper
|
|
219
205
|
|
|
220
206
|
|
|
221
|
-
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:
|
|
222
222
|
"""Return datetime as UTC.
|
|
223
223
|
"""
|
|
224
224
|
@wraps(func)
|
|
@@ -232,7 +232,7 @@ def prefer_utc_timezone(func, force:bool = False):
|
|
|
232
232
|
return wrapper
|
|
233
233
|
|
|
234
234
|
|
|
235
|
-
def prefer_native_timezone(func, force:bool = False):
|
|
235
|
+
def prefer_native_timezone(func, force:bool = False) -> Callable:
|
|
236
236
|
"""Return datetime as native.
|
|
237
237
|
"""
|
|
238
238
|
@wraps(func)
|
|
@@ -316,7 +316,7 @@ class NYSE(Entity):
|
|
|
316
316
|
def business_holidays(begdate=BEGDATE, enddate=ENDDATE) -> set:
|
|
317
317
|
return {Date.instance(d.date())
|
|
318
318
|
for d in map(pd.to_datetime, NYSE.calendar.holidays().holidays)
|
|
319
|
-
if begdate <= d <= enddate}
|
|
319
|
+
if begdate <= d.date() <= enddate}
|
|
320
320
|
|
|
321
321
|
|
|
322
322
|
class DateBusinessMixin:
|
|
@@ -488,63 +488,21 @@ class DateBusinessMixin:
|
|
|
488
488
|
|
|
489
489
|
@expect_date
|
|
490
490
|
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
|
|
491
|
+
"""Check if the date is a business day (market is open).
|
|
502
492
|
"""
|
|
503
493
|
return self.is_business_day()
|
|
504
494
|
|
|
505
495
|
@expect_date
|
|
506
496
|
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
|
|
497
|
+
"""Check if the date is a business day according to the entity calendar.
|
|
524
498
|
"""
|
|
525
499
|
return self in self._entity.business_days()
|
|
526
500
|
|
|
527
501
|
@expect_date
|
|
528
502
|
def business_hours(self) -> 'tuple[DateTime, DateTime]':
|
|
529
|
-
"""
|
|
530
|
-
|
|
531
|
-
Returns (None, None) if not a business day
|
|
532
|
-
|
|
533
|
-
>>> thedate = Date(2023, 1, 5)
|
|
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, ...)
|
|
503
|
+
"""Get market open and close times for this date.
|
|
544
504
|
|
|
545
|
-
|
|
546
|
-
>>> thedate.business_hours()
|
|
547
|
-
(None, None)
|
|
505
|
+
Returns (None, None) if not a business day.
|
|
548
506
|
"""
|
|
549
507
|
return self._entity.business_hours(self, self)\
|
|
550
508
|
.get(self, (None, None))
|
|
@@ -593,34 +551,25 @@ class DateBusinessMixin:
|
|
|
593
551
|
class DateExtrasMixin:
|
|
594
552
|
"""Extended date functionality not provided by Pendulum.
|
|
595
553
|
|
|
554
|
+
.. note::
|
|
555
|
+
This mixin exists primarily for legacy backward compatibility.
|
|
556
|
+
New code should prefer using built-in methods where possible.
|
|
557
|
+
|
|
596
558
|
This mixin provides additional date utilities primarily focused on:
|
|
597
559
|
- Financial date calculations (nearest month start/end)
|
|
598
560
|
- Weekday-oriented date navigation
|
|
599
561
|
- Relative date lookups
|
|
600
562
|
|
|
601
|
-
These methods extend
|
|
563
|
+
These methods extend OpenDate functionality with features commonly
|
|
602
564
|
needed in financial applications and reporting scenarios.
|
|
603
565
|
"""
|
|
604
566
|
|
|
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)
|
|
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.
|
|
624
573
|
"""
|
|
625
574
|
_business = self._business
|
|
626
575
|
self._business = False
|
|
@@ -634,25 +583,12 @@ class DateExtrasMixin:
|
|
|
634
583
|
return d.business().add(days=1)
|
|
635
584
|
return d
|
|
636
585
|
|
|
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)
|
|
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.
|
|
656
592
|
"""
|
|
657
593
|
_business = self._business
|
|
658
594
|
self._business = False
|
|
@@ -666,48 +602,27 @@ class DateExtrasMixin:
|
|
|
666
602
|
return d.business().subtract(days=1)
|
|
667
603
|
return d
|
|
668
604
|
|
|
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)
|
|
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).
|
|
677
607
|
"""
|
|
678
608
|
if self.weekday() == WEEKDAY_SHORTNAME.get(day):
|
|
679
609
|
return self
|
|
680
610
|
return self.next(WEEKDAY_SHORTNAME.get(day))
|
|
681
611
|
|
|
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)
|
|
612
|
+
def weekday_or_previous_friday(self) -> Self:
|
|
613
|
+
"""Return the date if it is a weekday, otherwise return the previous Friday.
|
|
694
614
|
"""
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
return self.subtract(days=dnum - 4)
|
|
615
|
+
if self.weekday() in {WeekDay.SATURDAY, WeekDay.SUNDAY}:
|
|
616
|
+
return self.previous(WeekDay.FRIDAY)
|
|
698
617
|
return self
|
|
699
618
|
|
|
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
619
|
@classmethod
|
|
708
|
-
def third_wednesday(cls, year, month):
|
|
620
|
+
def third_wednesday(cls, year, month) -> Self:
|
|
709
621
|
"""Calculate the date of the third Wednesday in a given month/year.
|
|
710
622
|
|
|
623
|
+
.. deprecated::
|
|
624
|
+
Use Date(year, month, 1).nth_of('month', 3, WeekDay.WEDNESDAY) instead.
|
|
625
|
+
|
|
711
626
|
Parameters
|
|
712
627
|
year: The year to use
|
|
713
628
|
month: The month to use (1-12)
|
|
@@ -715,11 +630,7 @@ class DateExtrasMixin:
|
|
|
715
630
|
Returns
|
|
716
631
|
A Date object representing the third Wednesday of the specified month
|
|
717
632
|
"""
|
|
718
|
-
|
|
719
|
-
w = third.weekday()
|
|
720
|
-
if w != WeekDay.WEDNESDAY:
|
|
721
|
-
third = third.replace(day=(15 + (WeekDay.WEDNESDAY - w) % 7))
|
|
722
|
-
return third
|
|
633
|
+
return cls(year, month, 1).nth_of('month', 3, WeekDay.WEDNESDAY)
|
|
723
634
|
|
|
724
635
|
|
|
725
636
|
class Date(DateExtrasMixin, DateBusinessMixin, _pendulum.Date):
|
|
@@ -736,10 +647,9 @@ class Date(DateExtrasMixin, DateBusinessMixin, _pendulum.Date):
|
|
|
736
647
|
"""
|
|
737
648
|
|
|
738
649
|
def to_string(self, fmt: str) -> str:
|
|
739
|
-
"""Format
|
|
650
|
+
"""Format date to string, handling platform-specific format codes.
|
|
740
651
|
|
|
741
|
-
|
|
742
|
-
'1/5/2022'
|
|
652
|
+
Automatically converts '%-' format codes to '%#' on Windows.
|
|
743
653
|
"""
|
|
744
654
|
return self.strftime(fmt.replace('%-', '%#') if os.name == 'nt' else fmt)
|
|
745
655
|
|
|
@@ -772,10 +682,10 @@ class Date(DateExtrasMixin, DateBusinessMixin, _pendulum.Date):
|
|
|
772
682
|
Returns
|
|
773
683
|
A new Date object representing the average date
|
|
774
684
|
"""
|
|
775
|
-
return _pendulum.Date.average(self)
|
|
685
|
+
return _pendulum.Date.average(self, dt)
|
|
776
686
|
|
|
777
687
|
@classmethod
|
|
778
|
-
def fromordinal(cls, *args, **kwargs):
|
|
688
|
+
def fromordinal(cls, *args, **kwargs) -> Self:
|
|
779
689
|
"""Create a Date from an ordinal.
|
|
780
690
|
|
|
781
691
|
Parameters
|
|
@@ -788,7 +698,7 @@ class Date(DateExtrasMixin, DateBusinessMixin, _pendulum.Date):
|
|
|
788
698
|
return cls.instance(result)
|
|
789
699
|
|
|
790
700
|
@classmethod
|
|
791
|
-
def fromtimestamp(cls, timestamp, tz=None):
|
|
701
|
+
def fromtimestamp(cls, timestamp, tz=None) -> Self:
|
|
792
702
|
"""Create a Date from a timestamp.
|
|
793
703
|
|
|
794
704
|
Parameters
|
|
@@ -831,84 +741,47 @@ class Date(DateExtrasMixin, DateBusinessMixin, _pendulum.Date):
|
|
|
831
741
|
) -> Self | None:
|
|
832
742
|
"""Convert a string to a date handling many different formats.
|
|
833
743
|
|
|
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
|
|
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)
|
|
912
785
|
"""
|
|
913
786
|
|
|
914
787
|
def date_for_symbol(s):
|
|
@@ -1027,20 +900,17 @@ class Date(DateExtrasMixin, DateBusinessMixin, _pendulum.Date):
|
|
|
1027
900
|
| None,
|
|
1028
901
|
raise_err: bool = False,
|
|
1029
902
|
) -> 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)
|
|
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.
|
|
1043
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
|
|
1044
914
|
"""
|
|
1045
915
|
if pd.isna(obj):
|
|
1046
916
|
if raise_err:
|
|
@@ -1056,39 +926,21 @@ class Date(DateExtrasMixin, DateBusinessMixin, _pendulum.Date):
|
|
|
1056
926
|
return cls(obj.year, obj.month, obj.day)
|
|
1057
927
|
|
|
1058
928
|
@classmethod
|
|
1059
|
-
def today(cls):
|
|
929
|
+
def today(cls) -> Self:
|
|
1060
930
|
d = _datetime.datetime.now(LCL)
|
|
1061
931
|
return cls(d.year, d.month, d.day)
|
|
1062
932
|
|
|
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
|
|
933
|
+
def isoweek(self) -> int | None:
|
|
934
|
+
"""Get ISO week number (1-52/53) following ISO week-numbering standard.
|
|
1077
935
|
"""
|
|
1078
936
|
with contextlib.suppress(Exception):
|
|
1079
937
|
return self.isocalendar()[1]
|
|
1080
938
|
|
|
1081
939
|
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)
|
|
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.
|
|
1092
944
|
"""
|
|
1093
945
|
def _lookback(years=0, months=0, weeks=0, days=0):
|
|
1094
946
|
_business = self._business
|
|
@@ -1124,38 +976,46 @@ class Time(_pendulum.Time):
|
|
|
1124
976
|
@classmethod
|
|
1125
977
|
@prefer_utc_timezone
|
|
1126
978
|
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
|
-
|
|
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)
|
|
1159
1019
|
"""
|
|
1160
1020
|
|
|
1161
1021
|
def seconds(m):
|
|
@@ -1225,18 +1085,9 @@ class Time(_pendulum.Time):
|
|
|
1225
1085
|
tz: str | _zoneinfo.ZoneInfo | _datetime.tzinfo | None = None,
|
|
1226
1086
|
raise_err: bool = False,
|
|
1227
1087
|
) -> 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)
|
|
1088
|
+
"""Create Time instance from time-like object.
|
|
1239
1089
|
|
|
1090
|
+
Adds UTC timezone by default unless obj is already a Time instance.
|
|
1240
1091
|
"""
|
|
1241
1092
|
if pd.isna(obj):
|
|
1242
1093
|
if raise_err:
|
|
@@ -1250,15 +1101,8 @@ class Time(_pendulum.Time):
|
|
|
1250
1101
|
|
|
1251
1102
|
return cls(obj.hour, obj.minute, obj.second, obj.microsecond, tzinfo=tz)
|
|
1252
1103
|
|
|
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
|
-
|
|
1104
|
+
def in_timezone(self, tz: str | _zoneinfo.ZoneInfo | _datetime.tzinfo) -> Self:
|
|
1105
|
+
"""Convert time to a different timezone.
|
|
1262
1106
|
"""
|
|
1263
1107
|
_dt = DateTime.combine(Date.today(), self, tzinfo=self.tzinfo or UTC)
|
|
1264
1108
|
return _dt.in_timezone(tz).time()
|
|
@@ -1281,7 +1125,7 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1281
1125
|
- Has timezone handling helpers not present in pendulum
|
|
1282
1126
|
"""
|
|
1283
1127
|
|
|
1284
|
-
def epoch(self):
|
|
1128
|
+
def epoch(self) -> float:
|
|
1285
1129
|
"""Translate a datetime object into unix seconds since epoch
|
|
1286
1130
|
"""
|
|
1287
1131
|
return self.timestamp()
|
|
@@ -1311,7 +1155,7 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1311
1155
|
return _pendulum.DateTime.replace(self, *args, **kwargs)
|
|
1312
1156
|
|
|
1313
1157
|
@classmethod
|
|
1314
|
-
def fromordinal(cls, *args, **kwargs):
|
|
1158
|
+
def fromordinal(cls, *args, **kwargs) -> Self:
|
|
1315
1159
|
"""Create a DateTime from an ordinal.
|
|
1316
1160
|
|
|
1317
1161
|
Parameters
|
|
@@ -1324,7 +1168,7 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1324
1168
|
return cls.instance(result)
|
|
1325
1169
|
|
|
1326
1170
|
@classmethod
|
|
1327
|
-
def fromtimestamp(cls, timestamp, tz=None):
|
|
1171
|
+
def fromtimestamp(cls, timestamp, tz=None) -> Self:
|
|
1328
1172
|
"""Create a DateTime from a timestamp.
|
|
1329
1173
|
|
|
1330
1174
|
Parameters
|
|
@@ -1339,7 +1183,7 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1339
1183
|
return cls.instance(result)
|
|
1340
1184
|
|
|
1341
1185
|
@classmethod
|
|
1342
|
-
def strptime(cls, time_str, fmt):
|
|
1186
|
+
def strptime(cls, time_str, fmt) -> Self:
|
|
1343
1187
|
"""Parse a string into a DateTime according to a format.
|
|
1344
1188
|
|
|
1345
1189
|
Parameters
|
|
@@ -1353,7 +1197,7 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1353
1197
|
return cls.instance(result)
|
|
1354
1198
|
|
|
1355
1199
|
@classmethod
|
|
1356
|
-
def utcfromtimestamp(cls, timestamp):
|
|
1200
|
+
def utcfromtimestamp(cls, timestamp) -> Self:
|
|
1357
1201
|
"""Create a UTC DateTime from a timestamp.
|
|
1358
1202
|
|
|
1359
1203
|
Parameters
|
|
@@ -1366,7 +1210,7 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1366
1210
|
return cls.instance(result)
|
|
1367
1211
|
|
|
1368
1212
|
@classmethod
|
|
1369
|
-
def utcnow(cls):
|
|
1213
|
+
def utcnow(cls) -> Self:
|
|
1370
1214
|
"""Create a DateTime representing current UTC time.
|
|
1371
1215
|
|
|
1372
1216
|
Returns
|
|
@@ -1391,7 +1235,7 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1391
1235
|
d.microsecond, tzinfo=d.tzinfo, fold=d.fold)
|
|
1392
1236
|
|
|
1393
1237
|
@classmethod
|
|
1394
|
-
def today(cls, tz: str | _zoneinfo.ZoneInfo | None = None):
|
|
1238
|
+
def today(cls, tz: str | _zoneinfo.ZoneInfo | None = None) -> Self:
|
|
1395
1239
|
"""Create a DateTime object representing today at the start of day.
|
|
1396
1240
|
|
|
1397
1241
|
Unlike pendulum.today() which returns current time, this method
|
|
@@ -1405,7 +1249,7 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1405
1249
|
"""
|
|
1406
1250
|
return DateTime.now(tz).start_of('day')
|
|
1407
1251
|
|
|
1408
|
-
def date(self):
|
|
1252
|
+
def date(self) -> Date:
|
|
1409
1253
|
return Date(self.year, self.month, self.day)
|
|
1410
1254
|
|
|
1411
1255
|
@classmethod
|
|
@@ -1420,25 +1264,13 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1420
1264
|
_tzinfo = tzinfo or time.tzinfo
|
|
1421
1265
|
return DateTime.instance(_datetime.datetime.combine(date, time, tzinfo=_tzinfo))
|
|
1422
1266
|
|
|
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'
|
|
1267
|
+
def rfc3339(self) -> str:
|
|
1268
|
+
"""Return RFC 3339 formatted string (same as isoformat()).
|
|
1429
1269
|
"""
|
|
1430
1270
|
return self.isoformat()
|
|
1431
1271
|
|
|
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'))
|
|
1272
|
+
def time(self) -> Time:
|
|
1273
|
+
"""Extract time component from datetime (preserving timezone).
|
|
1442
1274
|
"""
|
|
1443
1275
|
return Time.instance(self)
|
|
1444
1276
|
|
|
@@ -1451,41 +1283,43 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1451
1283
|
"""Convert a string or timestamp to a DateTime with extended format support.
|
|
1452
1284
|
|
|
1453
1285
|
Unlike pendulum's parse, this method supports:
|
|
1454
|
-
- Unix timestamps (int/float)
|
|
1455
|
-
- Special codes (
|
|
1456
|
-
- Business day offsets
|
|
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)
|
|
1457
1289
|
- Multiple date-time formats beyond ISO 8601
|
|
1290
|
+
- Combined date and time strings with various separators
|
|
1458
1291
|
|
|
1459
1292
|
Parameters
|
|
1460
1293
|
s: String or timestamp to parse
|
|
1461
|
-
entity: Calendar entity for business day calculations
|
|
1462
|
-
raise_err:
|
|
1294
|
+
entity: Calendar entity for business day calculations (default NYSE)
|
|
1295
|
+
raise_err: If True, raises ValueError on parse failure instead of returning None
|
|
1463
1296
|
|
|
1464
1297
|
Returns
|
|
1465
1298
|
DateTime instance or None if parsing fails and raise_err is False
|
|
1466
1299
|
|
|
1467
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)
|
|
1468
1304
|
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
DateTime(2022, 1, 1, 0, 0, 0, tzinfo=Timezone('...'))
|
|
1305
|
+
ISO 8601 format:
|
|
1306
|
+
DateTime.parse('2020-01-15T14:30:00') → DateTime(2020, 1, 15, 14, 30, 0)
|
|
1472
1307
|
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
DateTime(2014, 10, 31, 14, 55, 0, tzinfo=Timezone('US/Eastern'))
|
|
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)
|
|
1477
1311
|
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
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)
|
|
1481
1315
|
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
DateTime(2014, 10, 31, 18, 55, 0, tzinfo=tzutc())
|
|
1316
|
+
Time only (uses today's date):
|
|
1317
|
+
DateTime.parse('14:30:00') → DateTime(today's year, month, day, 14, 30, 0, tzinfo=LCL)
|
|
1485
1318
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
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
|
|
1489
1323
|
"""
|
|
1490
1324
|
if not s:
|
|
1491
1325
|
if raise_err:
|
|
@@ -1538,44 +1372,21 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1538
1372
|
) -> Self | None:
|
|
1539
1373
|
"""Create a DateTime instance from various datetime-like objects.
|
|
1540
1374
|
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
DateTime instances.
|
|
1375
|
+
Provides unified interface for converting different date/time types
|
|
1376
|
+
including pandas and numpy datetime objects into DateTime instances.
|
|
1544
1377
|
|
|
1545
1378
|
Unlike pendulum, this method:
|
|
1546
1379
|
- Handles pandas Timestamp and numpy datetime64 objects
|
|
1547
1380
|
- Adds timezone (UTC by default) when none is specified
|
|
1548
|
-
- Has special handling for
|
|
1381
|
+
- Has special handling for Time objects (combines with current date)
|
|
1549
1382
|
|
|
1550
1383
|
Parameters
|
|
1551
1384
|
obj: Date, datetime, time, or compatible object to convert
|
|
1552
1385
|
tz: Optional timezone to apply (if None, uses obj's timezone or UTC)
|
|
1553
|
-
raise_err:
|
|
1386
|
+
raise_err: If True, raises ValueError for None/NA values instead of returning None
|
|
1554
1387
|
|
|
1555
1388
|
Returns
|
|
1556
1389
|
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
1390
|
"""
|
|
1580
1391
|
if pd.isna(obj):
|
|
1581
1392
|
if raise_err:
|
|
@@ -1608,25 +1419,91 @@ class DateTime(DateBusinessMixin, _pendulum.DateTime):
|
|
|
1608
1419
|
obj.second, obj.microsecond, tzinfo=tz)
|
|
1609
1420
|
|
|
1610
1421
|
|
|
1611
|
-
class
|
|
1612
|
-
|
|
1422
|
+
class Interval(_pendulum.Interval):
|
|
1423
|
+
"""Interval class extending pendulum.Interval with business day awareness.
|
|
1613
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.
|
|
1614
1428
|
|
|
1615
|
-
|
|
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
|
+
"""
|
|
1616
1435
|
|
|
1617
1436
|
_business: bool = False
|
|
1618
1437
|
_entity: type[NYSE] = NYSE
|
|
1619
1438
|
|
|
1620
|
-
def
|
|
1621
|
-
|
|
1622
|
-
|
|
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
|
+
}
|
|
1623
1502
|
|
|
1624
1503
|
def business(self) -> Self:
|
|
1625
1504
|
self._business = True
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
if self.enddate:
|
|
1629
|
-
self.enddate.business()
|
|
1505
|
+
self._start.business()
|
|
1506
|
+
self._end.business()
|
|
1630
1507
|
return self
|
|
1631
1508
|
|
|
1632
1509
|
@property
|
|
@@ -1635,249 +1512,120 @@ class Interval:
|
|
|
1635
1512
|
|
|
1636
1513
|
def entity(self, e: type[NYSE] = NYSE) -> Self:
|
|
1637
1514
|
self._entity = e
|
|
1638
|
-
if self.
|
|
1639
|
-
self.
|
|
1640
|
-
if self.
|
|
1641
|
-
self.
|
|
1515
|
+
if self._start:
|
|
1516
|
+
self._end._entity = e
|
|
1517
|
+
if self._end:
|
|
1518
|
+
self._end._entity = e
|
|
1642
1519
|
return self
|
|
1643
1520
|
|
|
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))
|
|
1521
|
+
def is_business_day_range(self) -> list[bool]:
|
|
1522
|
+
"""Generate boolean values indicating whether each day in the range is a business day.
|
|
1683
1523
|
"""
|
|
1684
|
-
|
|
1524
|
+
self._business = False
|
|
1525
|
+
for thedate in self.range('days'):
|
|
1526
|
+
yield thedate.is_business_day()
|
|
1685
1527
|
|
|
1686
|
-
|
|
1528
|
+
@reset_business
|
|
1529
|
+
def range(self, unit: str = 'days', amount: int = 1) -> Iterator[DateTime | Date]:
|
|
1530
|
+
"""Generate dates/datetimes over the interval.
|
|
1687
1531
|
|
|
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')
|
|
1532
|
+
Parameters
|
|
1533
|
+
unit: Time unit ('days', 'weeks', 'months', 'years')
|
|
1534
|
+
amount: Step size (e.g., every N units)
|
|
1694
1535
|
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
else:
|
|
1700
|
-
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
|
|
1701
1540
|
|
|
1702
|
-
|
|
1703
|
-
|
|
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
|
|
1704
1545
|
|
|
1705
|
-
|
|
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)
|
|
1706
1562
|
|
|
1707
|
-
|
|
1708
|
-
"""Is business date range.
|
|
1563
|
+
return _range_generator()
|
|
1709
1564
|
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1565
|
+
@property
|
|
1566
|
+
@reset_business
|
|
1567
|
+
def days(self) -> int:
|
|
1568
|
+
"""Get number of days in the interval (respects business mode and sign).
|
|
1714
1569
|
"""
|
|
1715
|
-
|
|
1716
|
-
yield thedate.is_business_day()
|
|
1570
|
+
return self._sign * len(tuple(self.range('days'))) - self._sign
|
|
1717
1571
|
|
|
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)]
|
|
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.
|
|
1800
1578
|
"""
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
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
|
|
1805
1592
|
|
|
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
|
|
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.
|
|
1817
1600
|
"""
|
|
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
|
|
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).
|
|
1843
1606
|
"""
|
|
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'
|
|
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).
|
|
1881
1629
|
"""
|
|
1882
1630
|
|
|
1883
1631
|
def average_year_length(date1, date2):
|
|
@@ -1962,32 +1710,84 @@ class Interval:
|
|
|
1962
1710
|
(date1day + date1month * 30 + date1year * 360)
|
|
1963
1711
|
return daydiff360 / 360
|
|
1964
1712
|
|
|
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:
|
|
1713
|
+
if self._start == self._end:
|
|
1974
1714
|
return 0.0
|
|
1975
|
-
|
|
1976
1715
|
if basis == 0:
|
|
1977
|
-
return basis0(
|
|
1716
|
+
return basis0(self._start, self._end) * self._sign
|
|
1978
1717
|
if basis == 1:
|
|
1979
|
-
return basis1(
|
|
1718
|
+
return basis1(self._start, self._end) * self._sign
|
|
1980
1719
|
if basis == 2:
|
|
1981
|
-
return basis2(
|
|
1720
|
+
return basis2(self._start, self._end) * self._sign
|
|
1982
1721
|
if basis == 3:
|
|
1983
|
-
return basis3(
|
|
1722
|
+
return basis3(self._start, self._end) * self._sign
|
|
1984
1723
|
if basis == 4:
|
|
1985
|
-
return basis4(
|
|
1724
|
+
return basis4(self._start, self._end) * self._sign
|
|
1986
1725
|
|
|
1987
1726
|
raise ValueError('Basis range [0, 4]. Unknown basis {basis}.')
|
|
1988
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
|
|
1989
1756
|
|
|
1990
|
-
|
|
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
|
|
1766
|
+
|
|
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:
|
|
1991
1791
|
"""Create a simple .ics file per RFC 5545 guidelines."""
|
|
1992
1792
|
|
|
1993
1793
|
return f"""BEGIN:VCALENDAR
|
|
@@ -2001,7 +1801,3 @@ LOCATION:{location}
|
|
|
2001
1801
|
END:VEVENT
|
|
2002
1802
|
END:VCALENDAR
|
|
2003
1803
|
"""
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
if __name__ == '__main__':
|
|
2007
|
-
__import__('doctest').testmod(optionflags=4 | 8 | 32)
|