@@ -1468,6 +1468,7 @@ class LastWeekOfMonth(_WeekOfMonthMixin, DateOffset):
1468
1468
1469
1469
"""
1470
1470
_prefix = 'LWOM'
1471
+ _adjust_dst = True
1471
1472
1472
1473
def __init__ (self , n = 1 , normalize = False , weekday = None ):
1473
1474
self .n = self ._validate_n (n )
@@ -1727,8 +1728,7 @@ class FY5253(DateOffset):
1727
1728
such as retail, manufacturing and parking industry.
1728
1729
1729
1730
For more information see:
1730
- http://en.wikipedia.org/wiki/4%E2%80%934%E2%80%935_calendar
1731
-
1731
+ http://en.wikipedia.org/wiki/4-4-5_calendar
1732
1732
1733
1733
The year may either:
1734
1734
- end on the last X day of the Y month.
@@ -1922,7 +1922,7 @@ class FY5253Quarter(DateOffset):
1922
1922
such as retail, manufacturing and parking industry.
1923
1923
1924
1924
For more information see:
1925
- http://en.wikipedia.org/wiki/4%E2%80%934%E2%80%935_calendar
1925
+ http://en.wikipedia.org/wiki/4-4-5_calendar
1926
1926
1927
1927
The year may either:
1928
1928
- end on the last X day of the Y month.
@@ -1982,46 +1982,77 @@ def _offset(self):
1982
1982
def isAnchored (self ):
1983
1983
return self .n == 1 and self ._offset .isAnchored ()
1984
1984
1985
+ def _rollback_to_year (self , other ):
1986
+ """roll `other` back to the most recent date that was on a fiscal year
1987
+ end. Return the date of that year-end, the number of full quarters
1988
+ elapsed between that year-end and other, and the remaining Timedelta
1989
+ since the most recent quarter-end.
1990
+
1991
+ Parameters
1992
+ ----------
1993
+ other : datetime or Timestamp
1994
+
1995
+ Returns
1996
+ -------
1997
+ tuple of
1998
+ prev_year_end : Timestamp giving most recent fiscal year end
1999
+ num_qtrs : int
2000
+ tdelta : Timedelta
2001
+ """
2002
+ num_qtrs = 0
2003
+
2004
+ norm = Timestamp (other ).tz_localize (None )
2005
+ start = self ._offset .rollback (norm )
2006
+ # Note: start <= norm and self._offset.onOffset(start)
2007
+
2008
+ if start < norm :
2009
+ # roll adjustment
2010
+ qtr_lens = self .get_weeks (norm )
2011
+
2012
+ # check thet qtr_lens is consistent with self._offset addition
2013
+ end = shift_day (start , days = 7 * sum (qtr_lens ))
2014
+ assert self ._offset .onOffset (end ), (start , end , qtr_lens )
2015
+
2016
+ tdelta = norm - start
2017
+ for qlen in qtr_lens :
2018
+ if qlen * 7 <= tdelta .days :
2019
+ num_qtrs += 1
2020
+ tdelta -= Timedelta (days = qlen * 7 )
2021
+ else :
2022
+ break
2023
+ else :
2024
+ tdelta = Timedelta (0 )
2025
+
2026
+ # Note: we always have tdelta.value >= 0
2027
+ return start , num_qtrs , tdelta
2028
+
1985
2029
@apply_wraps
1986
2030
def apply (self , other ):
1987
- base = other
2031
+ # Note: self.n == 0 is not allowed.
1988
2032
n = self .n
1989
2033
1990
- if n > 0 :
1991
- while n > 0 :
1992
- if not self ._offset .onOffset (other ):
1993
- qtr_lens = self .get_weeks (other )
1994
- start = other - self ._offset
1995
- else :
1996
- start = other
1997
- qtr_lens = self .get_weeks (other + self ._offset )
2034
+ prev_year_end , num_qtrs , tdelta = self ._rollback_to_year (other )
2035
+ res = prev_year_end
2036
+ n += num_qtrs
2037
+ if self .n <= 0 and tdelta .value > 0 :
2038
+ n += 1
1998
2039
1999
- for weeks in qtr_lens :
2000
- start += timedelta (weeks = weeks )
2001
- if start > other :
2002
- other = start
2003
- n -= 1
2004
- break
2040
+ # Possible speedup by handling years first.
2041
+ years = n // 4
2042
+ if years :
2043
+ res += self ._offset * years
2044
+ n -= years * 4
2005
2045
2006
- else :
2007
- n = - n
2008
- while n > 0 :
2009
- if not self ._offset .onOffset (other ):
2010
- qtr_lens = self .get_weeks (other )
2011
- end = other + self ._offset
2012
- else :
2013
- end = other
2014
- qtr_lens = self .get_weeks (other )
2015
-
2016
- for weeks in reversed (qtr_lens ):
2017
- end -= timedelta (weeks = weeks )
2018
- if end < other :
2019
- other = end
2020
- n -= 1
2021
- break
2022
- other = datetime (other .year , other .month , other .day ,
2023
- base .hour , base .minute , base .second , base .microsecond )
2024
- return other
2046
+ # Add an extra day to make *sure* we are getting the quarter lengths
2047
+ # for the upcoming year, not the previous year
2048
+ qtr_lens = self .get_weeks (res + Timedelta (days = 1 ))
2049
+
2050
+ # Note: we always have 0 <= n < 4
2051
+ weeks = sum (qtr_lens [:n ])
2052
+ if weeks :
2053
+ res = shift_day (res , days = weeks * 7 )
2054
+
2055
+ return res
2025
2056
2026
2057
def get_weeks (self , dt ):
2027
2058
ret = [13 ] * 4
@@ -2034,16 +2065,15 @@ def get_weeks(self, dt):
2034
2065
return ret
2035
2066
2036
2067
def year_has_extra_week (self , dt ):
2037
- if self ._offset .onOffset (dt ):
2038
- prev_year_end = dt - self ._offset
2039
- next_year_end = dt
2040
- else :
2041
- next_year_end = dt + self ._offset
2042
- prev_year_end = dt - self ._offset
2043
-
2044
- week_in_year = (next_year_end - prev_year_end ).days / 7
2068
+ # Avoid round-down errors --> normalize to get
2069
+ # e.g. '370D' instead of '360D23H'
2070
+ norm = Timestamp (dt ).normalize ().tz_localize (None )
2045
2071
2046
- return week_in_year == 53
2072
+ next_year_end = self ._offset .rollforward (norm )
2073
+ prev_year_end = norm - self ._offset
2074
+ weeks_in_year = (next_year_end - prev_year_end ).days / 7
2075
+ assert weeks_in_year in [52 , 53 ], weeks_in_year
2076
+ return weeks_in_year == 53
2047
2077
2048
2078
def onOffset (self , dt ):
2049
2079
if self .normalize and not _is_normalized (dt ):
@@ -2056,8 +2086,8 @@ def onOffset(self, dt):
2056
2086
qtr_lens = self .get_weeks (dt )
2057
2087
2058
2088
current = next_year_end
2059
- for qtr_len in qtr_lens [ 0 : 4 ] :
2060
- current += timedelta ( weeks = qtr_len )
2089
+ for qtr_len in qtr_lens :
2090
+ current = shift_day ( current , days = qtr_len * 7 )
2061
2091
if dt == current :
2062
2092
return True
2063
2093
return False
0 commit comments