22
33from typing import Any , Self
44
5+ from dateutil .relativedelta import relativedelta
6+ from pydantic import BaseModel , Field
7+
58
69def period (pstr : str = "" ) -> Period :
710 """Create a period object from a period string"""
@@ -17,155 +20,149 @@ def find_first_of(st: str, possible: str) -> int:
1720 return lowi
1821
1922
20- def safediv (x : int , d : int ) -> int :
21- return x // d if x >= 0 else - (- x // d )
22-
23-
24- def safemod (x : int , d : int ) -> int :
25- return x % d if x >= 0 else - (- x % d )
26-
27-
28- class Period :
29- def __init__ (self , months : int = 0 , days : int = 0 ) -> None :
30- self ._months = months
31- self ._days = days
23+ class Period (BaseModel ):
24+ months : int = Field (default = 0 , ge = 0 , description = "Number of months in the period" )
25+ days : int = Field (default = 0 , ge = 0 , description = "Number of days in the period" )
26+ hours : int = Field (default = 0 , ge = 0 , description = "Number of hours in the period" )
3227
3328 @classmethod
3429 def make (cls , data : Any ) -> Self :
3530 if isinstance (data , cls ):
3631 return data
3732 elif isinstance (data , str ):
38- return cls ().add_tenure (data )
33+ return cls ().add_period (data )
34+ elif isinstance (data , relativedelta ):
35+ return cls (
36+ months = data .years * 12 + data .months ,
37+ days = data .days ,
38+ hours = data .hours ,
39+ )
3940 else :
4041 raise TypeError ("Cannot convert %s to Period" % data )
4142
4243 def isempty (self ) -> bool :
43- return self ._months == 0 and self ._days == 0
44+ return self .months == 0 and self .days == 0 and self . hours == 0
4445
4546 def add_days (self , days : int ) -> None :
46- self ._days += days
47+ if days < 0 :
48+ raise ValueError ("Negative days are not supported" )
49+ self .days += days
4750
4851 def add_weeks (self , weeks : int ) -> None :
49- self ._days += int (7 * weeks )
52+ if weeks < 0 :
53+ raise ValueError ("Negative weeks are not supported" )
54+ self .days += int (7 * weeks )
5055
5156 def add_months (self , months : int ) -> None :
52- self ._months += months
57+ if months < 0 :
58+ raise ValueError ("Negative months are not supported" )
59+ self .months += months
5360
5461 def add_years (self , years : int ) -> None :
55- self ._months += int (12 * years )
62+ if years < 0 :
63+ raise ValueError ("Negative years are not supported" )
64+ self .months += int (12 * years )
65+
66+ def add_hours (self , hours : int ) -> None :
67+ if hours < 0 :
68+ raise ValueError ("Negative hours are not supported" )
69+ self .hours += hours
70+ if self .hours >= 24 :
71+ self .days += self .hours // 24
72+ self .hours = self .hours % 24
5673
5774 @property
5875 def years (self ) -> int :
59- return safediv ( self ._months , 12 )
76+ return self .months // 12
6077
6178 @property
62- def months (self ) -> int :
63- return safemod (self ._months , 12 )
79+ def months_remaining (self ) -> int :
80+ """Months remaining after extracting years from the period"""
81+ return self .months % 12
6482
6583 @property
6684 def weeks (self ) -> int :
67- return safediv (self ._days , 7 )
85+ """Weeks in the period after extracting months and years from the period"""
86+ return self .days // 7
6887
6988 @property
70- def days (self ) -> int :
71- return safemod (self ._days , 7 )
72-
73- @property
74- def totaldays (self ) -> int :
75- return 30 * self ._months + self ._days
76-
77- def __repr__ (self ) -> str :
78- """The period string"""
79- return self .components ()
80-
81- def __str__ (self ) -> str :
82- return self .__repr__ ()
89+ def days_remaining (self ) -> int :
90+ """Days remaining after extracting weeks from the period"""
91+ return self .days % 7
8392
84- def components (self ) -> str :
93+ def to_str (self ) -> str :
8594 """The period string"""
8695 p = ""
87- neg = self .totaldays < 0
8896 y = self .years
8997 m = self .months
9098 w = self .weeks
9199 d = self .days
92- if y :
93- p = "%sY" % abs (y )
94- if m :
95- p = "%s%sM" % (p , abs (m ))
96- if w :
97- p = "%s%sW" % (p , abs (w ))
98- if d :
99- p = "%s%sD" % (p , abs (d ))
100- return "-" + p if neg else p
101-
102- def simple (self ) -> str :
103- """A string representation with only one period delimiter."""
104- if self ._days :
105- return "%sD" % self .totaldays
106- elif self .months :
107- return "%sM" % self ._months
108- elif self .years :
109- return "%sY" % self .years
110- else :
111- return ""
112-
113- def add_tenure (self , pstr : str ) -> Self :
114- if isinstance (pstr , self .__class__ ):
115- self ._months += pstr ._months
116- self ._days += pstr ._days
100+ if y := self .years :
101+ p = "%sY" % y
102+ if m := self .months_remaining :
103+ p = "%s%sM" % (p , m )
104+ if w := self .weeks :
105+ p = "%s%sW" % (p , w )
106+ if d := self .days_remaining :
107+ p = "%s%sD" % (p , d )
108+ if h := self .hours :
109+ p = "%s%sH" % (p , h )
110+ return p
111+
112+ def to_relativedelta (self ) -> relativedelta :
113+ """Convert the period to a dateutil.relativedelta object"""
114+ return relativedelta (
115+ years = self .years ,
116+ months = self .months_remaining ,
117+ weeks = self .weeks ,
118+ days = self .days_remaining ,
119+ hours = self .hours ,
120+ )
121+
122+ def add_period (self , pstr : str | Self ) -> Self :
123+ if isinstance (pstr , Period ):
124+ self .add_days (pstr .days )
125+ self .add_months (pstr .months )
126+ self .add_hours (pstr .hours )
117127 return self
118128 st = str (pstr ).upper ()
119- done = False
120- sign = 1
121- while not done :
122- if not st :
123- done = True
124- else :
125- ip = find_first_of (st , "DWMY" )
126- if ip == - 1 :
127- raise ValueError ("Unknown period %s" % pstr )
128- p = st [ip ]
129- v = int (st [:ip ])
130- sign = sign if v > 0 else - sign
131- v = sign * abs (v )
132- if p == "D" :
133- self .add_days (v )
134- elif p == "W" :
135- self .add_weeks (v )
136- elif p == "M" :
137- self .add_months (v )
138- elif p == "Y" :
139- self .add_years (v )
140- ip += 1
141- st = st [ip :]
129+ while st :
130+ ip = find_first_of (st , "HDWMY" )
131+ if ip == - 1 :
132+ raise ValueError ("Unknown period %s" % pstr )
133+ p = st [ip ]
134+ v = int (st [:ip ])
135+ if v < 0 :
136+ raise ValueError ("Negative values are not supported" )
137+ if p == "D" :
138+ self .add_days (v )
139+ elif p == "W" :
140+ self .add_weeks (v )
141+ elif p == "M" :
142+ self .add_months (v )
143+ elif p == "H" :
144+ self .add_hours (v )
145+ elif p == "Y" :
146+ self .add_years (v )
147+ ip += 1
148+ st = st [ip :]
142149 return self
143150
144151 def __add__ (self , other : Any ) -> Self :
145152 p = self .make (other )
146- return self .__class__ (self ._months + p ._months , self ._days + p ._days )
153+ period = self .model_copy ()
154+ period .add_days (p .days )
155+ period .add_months (p .months )
156+ period .add_hours (p .hours )
157+ return period
147158
148159 def __radd__ (self , other : Any ) -> Self :
149160 return self + other
150161
151- def __sub__ (self , other : Any ) -> Self :
152- p = self .make (other )
153- return self .__class__ (self ._months - p ._months , self ._days - p ._days )
154-
155- def __rsub__ (self , other : Any ) -> Self :
156- return self .make (other ) - self
157-
158- def __gt__ (self , other : Any ) -> bool :
159- return self .totaldays > self .make (other ).totaldays
160-
161- def __lt__ (self , other : Any ) -> bool :
162- return self .totaldays < self .make (other ).totaldays
163-
164- def __ge__ (self , other : Any ) -> bool :
165- return self .totaldays >= self .make (other ).totaldays
166-
167- def __le__ (self , other : Any ) -> bool :
168- return self .totaldays <= self .make (other ).totaldays
169-
170162 def __eq__ (self , other : Any ) -> bool :
171- return self .totaldays == self .make (other ).totaldays
163+ period = self .make (other )
164+ return (
165+ self .months == period .months
166+ and self .days == period .days
167+ and self .hours == period .hours
168+ )
0 commit comments