Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1from datetime import date, datetime, timedelta 

2import functools 

3import operator 

4from typing import Any, Optional 

5import warnings 

6 

7from dateutil.easter import easter 

8import numpy as np 

9 

10from pandas._libs.tslibs import ( 

11 NaT, 

12 OutOfBoundsDatetime, 

13 Period, 

14 Timedelta, 

15 Timestamp, 

16 ccalendar, 

17 conversion, 

18 delta_to_nanoseconds, 

19 frequencies as libfrequencies, 

20 normalize_date, 

21 offsets as liboffsets, 

22 timezones, 

23) 

24from pandas._libs.tslibs.offsets import ( 

25 ApplyTypeError, 

26 BaseOffset, 

27 _get_calendar, 

28 _is_normalized, 

29 _to_dt64, 

30 apply_index_wraps, 

31 as_datetime, 

32 roll_yearday, 

33 shift_month, 

34) 

35from pandas.errors import AbstractMethodError 

36from pandas.util._decorators import Appender, Substitution, cache_readonly 

37 

38from pandas.core.dtypes.inference import is_list_like 

39 

40__all__ = [ 

41 "Day", 

42 "BusinessDay", 

43 "BDay", 

44 "CustomBusinessDay", 

45 "CDay", 

46 "CBMonthEnd", 

47 "CBMonthBegin", 

48 "MonthBegin", 

49 "BMonthBegin", 

50 "MonthEnd", 

51 "BMonthEnd", 

52 "SemiMonthEnd", 

53 "SemiMonthBegin", 

54 "BusinessHour", 

55 "CustomBusinessHour", 

56 "YearBegin", 

57 "BYearBegin", 

58 "YearEnd", 

59 "BYearEnd", 

60 "QuarterBegin", 

61 "BQuarterBegin", 

62 "QuarterEnd", 

63 "BQuarterEnd", 

64 "LastWeekOfMonth", 

65 "FY5253Quarter", 

66 "FY5253", 

67 "Week", 

68 "WeekOfMonth", 

69 "Easter", 

70 "Hour", 

71 "Minute", 

72 "Second", 

73 "Milli", 

74 "Micro", 

75 "Nano", 

76 "DateOffset", 

77] 

78 

79# convert to/from datetime/timestamp to allow invalid Timestamp ranges to 

80# pass thru 

81 

82 

83def as_timestamp(obj): 

84 if isinstance(obj, Timestamp): 

85 return obj 

86 try: 

87 return Timestamp(obj) 

88 except (OutOfBoundsDatetime): 

89 pass 

90 return obj 

91 

92 

93def apply_wraps(func): 

94 @functools.wraps(func) 

95 def wrapper(self, other): 

96 if other is NaT: 

97 return NaT 

98 elif isinstance(other, (timedelta, Tick, DateOffset)): 

99 # timedelta path 

100 return func(self, other) 

101 elif isinstance(other, (np.datetime64, datetime, date)): 

102 other = as_timestamp(other) 

103 

104 tz = getattr(other, "tzinfo", None) 

105 nano = getattr(other, "nanosecond", 0) 

106 

107 try: 

108 if self._adjust_dst and isinstance(other, Timestamp): 

109 other = other.tz_localize(None) 

110 

111 result = func(self, other) 

112 

113 if self._adjust_dst: 

114 result = conversion.localize_pydatetime(result, tz) 

115 

116 result = Timestamp(result) 

117 if self.normalize: 

118 result = result.normalize() 

119 

120 # nanosecond may be deleted depending on offset process 

121 if not self.normalize and nano != 0: 

122 if not isinstance(self, Nano) and result.nanosecond != nano: 

123 if result.tz is not None: 

124 # convert to UTC 

125 value = conversion.tz_convert_single( 

126 result.value, timezones.UTC, result.tz 

127 ) 

128 else: 

129 value = result.value 

130 result = Timestamp(value + nano) 

131 

132 if tz is not None and result.tzinfo is None: 

133 result = conversion.localize_pydatetime(result, tz) 

134 

135 except OutOfBoundsDatetime: 

136 result = func(self, as_datetime(other)) 

137 

138 if self.normalize: 

139 # normalize_date returns normal datetime 

140 result = normalize_date(result) 

141 

142 if tz is not None and result.tzinfo is None: 

143 result = conversion.localize_pydatetime(result, tz) 

144 

145 result = Timestamp(result) 

146 

147 return result 

148 

149 return wrapper 

150 

151 

152# --------------------------------------------------------------------- 

153# DateOffset 

154 

155 

156class DateOffset(BaseOffset): 

157 """ 

158 Standard kind of date increment used for a date range. 

159 

160 Works exactly like relativedelta in terms of the keyword args you 

161 pass in, use of the keyword n is discouraged-- you would be better 

162 off specifying n in the keywords you use, but regardless it is 

163 there for you. n is needed for DateOffset subclasses. 

164 

165 DateOffset work as follows. Each offset specify a set of dates 

166 that conform to the DateOffset. For example, Bday defines this 

167 set to be the set of dates that are weekdays (M-F). To test if a 

168 date is in the set of a DateOffset dateOffset we can use the 

169 is_on_offset method: dateOffset.is_on_offset(date). 

170 

171 If a date is not on a valid date, the rollback and rollforward 

172 methods can be used to roll the date to the nearest valid date 

173 before/after the date. 

174 

175 DateOffsets can be created to move dates forward a given number of 

176 valid dates. For example, Bday(2) can be added to a date to move 

177 it two business days forward. If the date does not start on a 

178 valid date, first it is moved to a valid date. Thus pseudo code 

179 is: 

180 

181 def __add__(date): 

182 date = rollback(date) # does nothing if date is valid 

183 return date + <n number of periods> 

184 

185 When a date offset is created for a negative number of periods, 

186 the date is first rolled forward. The pseudo code is: 

187 

188 def __add__(date): 

189 date = rollforward(date) # does nothing is date is valid 

190 return date + <n number of periods> 

191 

192 Zero presents a problem. Should it roll forward or back? We 

193 arbitrarily have it rollforward: 

194 

195 date + BDay(0) == BDay.rollforward(date) 

196 

197 Since 0 is a bit weird, we suggest avoiding its use. 

198 

199 Parameters 

200 ---------- 

201 n : int, default 1 

202 The number of time periods the offset represents. 

203 normalize : bool, default False 

204 Whether to round the result of a DateOffset addition down to the 

205 previous midnight. 

206 **kwds 

207 Temporal parameter that add to or replace the offset value. 

208 

209 Parameters that **add** to the offset (like Timedelta): 

210 

211 - years 

212 - months 

213 - weeks 

214 - days 

215 - hours 

216 - minutes 

217 - seconds 

218 - microseconds 

219 - nanoseconds 

220 

221 Parameters that **replace** the offset value: 

222 

223 - year 

224 - month 

225 - day 

226 - weekday 

227 - hour 

228 - minute 

229 - second 

230 - microsecond 

231 - nanosecond. 

232 

233 See Also 

234 -------- 

235 dateutil.relativedelta.relativedelta : The relativedelta type is designed 

236 to be applied to an existing datetime an can replace specific components of 

237 that datetime, or represents an interval of time. 

238 

239 Examples 

240 -------- 

241 >>> from pandas.tseries.offsets import DateOffset 

242 >>> ts = pd.Timestamp('2017-01-01 09:10:11') 

243 >>> ts + DateOffset(months=3) 

244 Timestamp('2017-04-01 09:10:11') 

245 

246 >>> ts = pd.Timestamp('2017-01-01 09:10:11') 

247 >>> ts + DateOffset(months=2) 

248 Timestamp('2017-03-01 09:10:11') 

249 """ 

250 

251 _params = cache_readonly(BaseOffset._params.fget) 

252 _use_relativedelta = False 

253 _adjust_dst = False 

254 _attributes = frozenset(["n", "normalize"] + list(liboffsets.relativedelta_kwds)) 

255 _deprecations = frozenset(["isAnchored", "onOffset"]) 

256 

257 # default for prior pickles 

258 normalize = False 

259 

260 def __init__(self, n=1, normalize=False, **kwds): 

261 BaseOffset.__init__(self, n, normalize) 

262 

263 off, use_rd = liboffsets._determine_offset(kwds) 

264 object.__setattr__(self, "_offset", off) 

265 object.__setattr__(self, "_use_relativedelta", use_rd) 

266 for key in kwds: 

267 val = kwds[key] 

268 object.__setattr__(self, key, val) 

269 

270 @apply_wraps 

271 def apply(self, other): 

272 if self._use_relativedelta: 

273 other = as_datetime(other) 

274 

275 if len(self.kwds) > 0: 

276 tzinfo = getattr(other, "tzinfo", None) 

277 if tzinfo is not None and self._use_relativedelta: 

278 # perform calculation in UTC 

279 other = other.replace(tzinfo=None) 

280 

281 if self.n > 0: 

282 for i in range(self.n): 

283 other = other + self._offset 

284 else: 

285 for i in range(-self.n): 

286 other = other - self._offset 

287 

288 if tzinfo is not None and self._use_relativedelta: 

289 # bring tz back from UTC calculation 

290 other = conversion.localize_pydatetime(other, tzinfo) 

291 

292 return as_timestamp(other) 

293 else: 

294 return other + timedelta(self.n) 

295 

296 @apply_index_wraps 

297 def apply_index(self, i): 

298 """ 

299 Vectorized apply of DateOffset to DatetimeIndex, 

300 raises NotImplentedError for offsets without a 

301 vectorized implementation. 

302 

303 Parameters 

304 ---------- 

305 i : DatetimeIndex 

306 

307 Returns 

308 ------- 

309 y : DatetimeIndex 

310 """ 

311 

312 if type(self) is not DateOffset: 

313 raise NotImplementedError( 

314 f"DateOffset subclass {type(self).__name__} " 

315 "does not have a vectorized implementation" 

316 ) 

317 kwds = self.kwds 

318 relativedelta_fast = { 

319 "years", 

320 "months", 

321 "weeks", 

322 "days", 

323 "hours", 

324 "minutes", 

325 "seconds", 

326 "microseconds", 

327 } 

328 # relativedelta/_offset path only valid for base DateOffset 

329 if self._use_relativedelta and set(kwds).issubset(relativedelta_fast): 

330 

331 months = (kwds.get("years", 0) * 12 + kwds.get("months", 0)) * self.n 

332 if months: 

333 shifted = liboffsets.shift_months(i.asi8, months) 

334 i = type(i)(shifted, dtype=i.dtype) 

335 

336 weeks = (kwds.get("weeks", 0)) * self.n 

337 if weeks: 

338 # integer addition on PeriodIndex is deprecated, 

339 # so we directly use _time_shift instead 

340 asper = i.to_period("W") 

341 if not isinstance(asper._data, np.ndarray): 

342 # unwrap PeriodIndex --> PeriodArray 

343 asper = asper._data 

344 shifted = asper._time_shift(weeks) 

345 i = shifted.to_timestamp() + i.to_perioddelta("W") 

346 

347 timedelta_kwds = { 

348 k: v 

349 for k, v in kwds.items() 

350 if k in ["days", "hours", "minutes", "seconds", "microseconds"] 

351 } 

352 if timedelta_kwds: 

353 delta = Timedelta(**timedelta_kwds) 

354 i = i + (self.n * delta) 

355 return i 

356 elif not self._use_relativedelta and hasattr(self, "_offset"): 

357 # timedelta 

358 return i + (self._offset * self.n) 

359 else: 

360 # relativedelta with other keywords 

361 kwd = set(kwds) - relativedelta_fast 

362 raise NotImplementedError( 

363 "DateOffset with relativedelta " 

364 f"keyword(s) {kwd} not able to be " 

365 "applied vectorized" 

366 ) 

367 

368 def is_anchored(self): 

369 # TODO: Does this make sense for the general case? It would help 

370 # if there were a canonical docstring for what is_anchored means. 

371 return self.n == 1 

372 

373 def onOffset(self, dt): 

374 warnings.warn( 

375 "onOffset is a deprecated, use is_on_offset instead", 

376 FutureWarning, 

377 stacklevel=2, 

378 ) 

379 return self.is_on_offset(dt) 

380 

381 def isAnchored(self): 

382 warnings.warn( 

383 "isAnchored is a deprecated, use is_anchored instead", 

384 FutureWarning, 

385 stacklevel=2, 

386 ) 

387 return self.is_anchored() 

388 

389 # TODO: Combine this with BusinessMixin version by defining a whitelisted 

390 # set of attributes on each object rather than the existing behavior of 

391 # iterating over internal ``__dict__`` 

392 def _repr_attrs(self): 

393 exclude = {"n", "inc", "normalize"} 

394 attrs = [] 

395 for attr in sorted(self.__dict__): 

396 if attr.startswith("_") or attr == "kwds": 

397 continue 

398 elif attr not in exclude: 

399 value = getattr(self, attr) 

400 attrs.append(f"{attr}={value}") 

401 

402 out = "" 

403 if attrs: 

404 out += ": " + ", ".join(attrs) 

405 return out 

406 

407 @property 

408 def name(self): 

409 return self.rule_code 

410 

411 def rollback(self, dt): 

412 """ 

413 Roll provided date backward to next offset only if not on offset. 

414 

415 Returns 

416 ------- 

417 TimeStamp 

418 Rolled timestamp if not on offset, otherwise unchanged timestamp. 

419 """ 

420 dt = as_timestamp(dt) 

421 if not self.is_on_offset(dt): 

422 dt = dt - type(self)(1, normalize=self.normalize, **self.kwds) 

423 return dt 

424 

425 def rollforward(self, dt): 

426 """ 

427 Roll provided date forward to next offset only if not on offset. 

428 

429 Returns 

430 ------- 

431 TimeStamp 

432 Rolled timestamp if not on offset, otherwise unchanged timestamp. 

433 """ 

434 dt = as_timestamp(dt) 

435 if not self.is_on_offset(dt): 

436 dt = dt + type(self)(1, normalize=self.normalize, **self.kwds) 

437 return dt 

438 

439 def is_on_offset(self, dt): 

440 if self.normalize and not _is_normalized(dt): 

441 return False 

442 # XXX, see #1395 

443 if type(self) == DateOffset or isinstance(self, Tick): 

444 return True 

445 

446 # Default (slow) method for determining if some date is a member of the 

447 # date range generated by this offset. Subclasses may have this 

448 # re-implemented in a nicer way. 

449 a = dt 

450 b = (dt + self) - self 

451 return a == b 

452 

453 # way to get around weirdness with rule_code 

454 @property 

455 def _prefix(self): 

456 raise NotImplementedError("Prefix not defined") 

457 

458 @property 

459 def rule_code(self): 

460 return self._prefix 

461 

462 @cache_readonly 

463 def freqstr(self): 

464 try: 

465 code = self.rule_code 

466 except NotImplementedError: 

467 return repr(self) 

468 

469 if self.n != 1: 

470 fstr = f"{self.n}{code}" 

471 else: 

472 fstr = code 

473 

474 try: 

475 if self._offset: 

476 fstr += self._offset_str() 

477 except AttributeError: 

478 # TODO: standardize `_offset` vs `offset` naming convention 

479 pass 

480 

481 return fstr 

482 

483 def _offset_str(self): 

484 return "" 

485 

486 @property 

487 def nanos(self): 

488 raise ValueError(f"{self} is a non-fixed frequency") 

489 

490 

491class SingleConstructorOffset(DateOffset): 

492 @classmethod 

493 def _from_name(cls, suffix=None): 

494 # default _from_name calls cls with no args 

495 if suffix: 

496 raise ValueError(f"Bad freq suffix {suffix}") 

497 return cls() 

498 

499 

500class _CustomMixin: 

501 """ 

502 Mixin for classes that define and validate calendar, holidays, 

503 and weekdays attributes. 

504 """ 

505 

506 def __init__(self, weekmask, holidays, calendar): 

507 calendar, holidays = _get_calendar( 

508 weekmask=weekmask, holidays=holidays, calendar=calendar 

509 ) 

510 # Custom offset instances are identified by the 

511 # following two attributes. See DateOffset._params() 

512 # holidays, weekmask 

513 

514 object.__setattr__(self, "weekmask", weekmask) 

515 object.__setattr__(self, "holidays", holidays) 

516 object.__setattr__(self, "calendar", calendar) 

517 

518 

519class BusinessMixin: 

520 """ 

521 Mixin to business types to provide related functions. 

522 """ 

523 

524 @property 

525 def offset(self): 

526 """ 

527 Alias for self._offset. 

528 """ 

529 # Alias for backward compat 

530 return self._offset 

531 

532 def _repr_attrs(self): 

533 if self.offset: 

534 attrs = [f"offset={repr(self.offset)}"] 

535 else: 

536 attrs = None 

537 out = "" 

538 if attrs: 

539 out += ": " + ", ".join(attrs) 

540 return out 

541 

542 

543class BusinessDay(BusinessMixin, SingleConstructorOffset): 

544 """ 

545 DateOffset subclass representing possibly n business days. 

546 """ 

547 

548 _prefix = "B" 

549 _adjust_dst = True 

550 _attributes = frozenset(["n", "normalize", "offset"]) 

551 

552 def __init__(self, n=1, normalize=False, offset=timedelta(0)): 

553 BaseOffset.__init__(self, n, normalize) 

554 object.__setattr__(self, "_offset", offset) 

555 

556 def _offset_str(self): 

557 def get_str(td): 

558 off_str = "" 

559 if td.days > 0: 

560 off_str += str(td.days) + "D" 

561 if td.seconds > 0: 

562 s = td.seconds 

563 hrs = int(s / 3600) 

564 if hrs != 0: 

565 off_str += str(hrs) + "H" 

566 s -= hrs * 3600 

567 mts = int(s / 60) 

568 if mts != 0: 

569 off_str += str(mts) + "Min" 

570 s -= mts * 60 

571 if s != 0: 

572 off_str += str(s) + "s" 

573 if td.microseconds > 0: 

574 off_str += str(td.microseconds) + "us" 

575 return off_str 

576 

577 if isinstance(self.offset, timedelta): 

578 zero = timedelta(0, 0, 0) 

579 if self.offset >= zero: 

580 off_str = "+" + get_str(self.offset) 

581 else: 

582 off_str = "-" + get_str(-self.offset) 

583 return off_str 

584 else: 

585 return "+" + repr(self.offset) 

586 

587 @apply_wraps 

588 def apply(self, other): 

589 if isinstance(other, datetime): 

590 n = self.n 

591 wday = other.weekday() 

592 

593 # avoid slowness below by operating on weeks first 

594 weeks = n // 5 

595 if n <= 0 and wday > 4: 

596 # roll forward 

597 n += 1 

598 

599 n -= 5 * weeks 

600 

601 # n is always >= 0 at this point 

602 if n == 0 and wday > 4: 

603 # roll back 

604 days = 4 - wday 

605 elif wday > 4: 

606 # roll forward 

607 days = (7 - wday) + (n - 1) 

608 elif wday + n <= 4: 

609 # shift by n days without leaving the current week 

610 days = n 

611 else: 

612 # shift by n days plus 2 to get past the weekend 

613 days = n + 2 

614 

615 result = other + timedelta(days=7 * weeks + days) 

616 if self.offset: 

617 result = result + self.offset 

618 return result 

619 

620 elif isinstance(other, (timedelta, Tick)): 

621 return BDay(self.n, offset=self.offset + other, normalize=self.normalize) 

622 else: 

623 raise ApplyTypeError( 

624 "Only know how to combine business day with datetime or timedelta." 

625 ) 

626 

627 @apply_index_wraps 

628 def apply_index(self, i): 

629 time = i.to_perioddelta("D") 

630 # to_period rolls forward to next BDay; track and 

631 # reduce n where it does when rolling forward 

632 asper = i.to_period("B") 

633 if not isinstance(asper._data, np.ndarray): 

634 # unwrap PeriodIndex --> PeriodArray 

635 asper = asper._data 

636 

637 if self.n > 0: 

638 shifted = (i.to_perioddelta("B") - time).asi8 != 0 

639 

640 # Integer-array addition is deprecated, so we use 

641 # _time_shift directly 

642 roll = np.where(shifted, self.n - 1, self.n) 

643 shifted = asper._addsub_int_array(roll, operator.add) 

644 else: 

645 # Integer addition is deprecated, so we use _time_shift directly 

646 roll = self.n 

647 shifted = asper._time_shift(roll) 

648 

649 result = shifted.to_timestamp() + time 

650 return result 

651 

652 def is_on_offset(self, dt): 

653 if self.normalize and not _is_normalized(dt): 

654 return False 

655 return dt.weekday() < 5 

656 

657 

658class BusinessHourMixin(BusinessMixin): 

659 def __init__(self, start="09:00", end="17:00", offset=timedelta(0)): 

660 # must be validated here to equality check 

661 if not is_list_like(start): 

662 start = [start] 

663 if not len(start): 

664 raise ValueError("Must include at least 1 start time") 

665 

666 if not is_list_like(end): 

667 end = [end] 

668 if not len(end): 

669 raise ValueError("Must include at least 1 end time") 

670 

671 start = np.array([liboffsets._validate_business_time(x) for x in start]) 

672 end = np.array([liboffsets._validate_business_time(x) for x in end]) 

673 

674 # Validation of input 

675 if len(start) != len(end): 

676 raise ValueError("number of starting time and ending time must be the same") 

677 num_openings = len(start) 

678 

679 # sort starting and ending time by starting time 

680 index = np.argsort(start) 

681 

682 # convert to tuple so that start and end are hashable 

683 start = tuple(start[index]) 

684 end = tuple(end[index]) 

685 

686 total_secs = 0 

687 for i in range(num_openings): 

688 total_secs += self._get_business_hours_by_sec(start[i], end[i]) 

689 total_secs += self._get_business_hours_by_sec( 

690 end[i], start[(i + 1) % num_openings] 

691 ) 

692 if total_secs != 24 * 60 * 60: 

693 raise ValueError( 

694 "invalid starting and ending time(s): " 

695 "opening hours should not touch or overlap with " 

696 "one another" 

697 ) 

698 

699 object.__setattr__(self, "start", start) 

700 object.__setattr__(self, "end", end) 

701 object.__setattr__(self, "_offset", offset) 

702 

703 @cache_readonly 

704 def next_bday(self): 

705 """ 

706 Used for moving to next business day. 

707 """ 

708 if self.n >= 0: 

709 nb_offset = 1 

710 else: 

711 nb_offset = -1 

712 if self._prefix.startswith("C"): 

713 # CustomBusinessHour 

714 return CustomBusinessDay( 

715 n=nb_offset, 

716 weekmask=self.weekmask, 

717 holidays=self.holidays, 

718 calendar=self.calendar, 

719 ) 

720 else: 

721 return BusinessDay(n=nb_offset) 

722 

723 def _next_opening_time(self, other, sign=1): 

724 """ 

725 If self.n and sign have the same sign, return the earliest opening time 

726 later than or equal to current time. 

727 Otherwise the latest opening time earlier than or equal to current 

728 time. 

729 

730 Opening time always locates on BusinessDay. 

731 However, closing time may not if business hour extends over midnight. 

732 

733 Parameters 

734 ---------- 

735 other : datetime 

736 Current time. 

737 sign : int, default 1. 

738 Either 1 or -1. Going forward in time if it has the same sign as 

739 self.n. Going backward in time otherwise. 

740 

741 Returns 

742 ------- 

743 result : datetime 

744 Next opening time. 

745 """ 

746 earliest_start = self.start[0] 

747 latest_start = self.start[-1] 

748 

749 if not self.next_bday.is_on_offset(other): 

750 # today is not business day 

751 other = other + sign * self.next_bday 

752 if self.n * sign >= 0: 

753 hour, minute = earliest_start.hour, earliest_start.minute 

754 else: 

755 hour, minute = latest_start.hour, latest_start.minute 

756 else: 

757 if self.n * sign >= 0: 

758 if latest_start < other.time(): 

759 # current time is after latest starting time in today 

760 other = other + sign * self.next_bday 

761 hour, minute = earliest_start.hour, earliest_start.minute 

762 else: 

763 # find earliest starting time no earlier than current time 

764 for st in self.start: 

765 if other.time() <= st: 

766 hour, minute = st.hour, st.minute 

767 break 

768 else: 

769 if other.time() < earliest_start: 

770 # current time is before earliest starting time in today 

771 other = other + sign * self.next_bday 

772 hour, minute = latest_start.hour, latest_start.minute 

773 else: 

774 # find latest starting time no later than current time 

775 for st in reversed(self.start): 

776 if other.time() >= st: 

777 hour, minute = st.hour, st.minute 

778 break 

779 

780 return datetime(other.year, other.month, other.day, hour, minute) 

781 

782 def _prev_opening_time(self, other): 

783 """ 

784 If n is positive, return the latest opening time earlier than or equal 

785 to current time. 

786 Otherwise the earliest opening time later than or equal to current 

787 time. 

788 

789 Parameters 

790 ---------- 

791 other : datetime 

792 Current time. 

793 

794 Returns 

795 ------- 

796 result : datetime 

797 Previous opening time. 

798 """ 

799 return self._next_opening_time(other, sign=-1) 

800 

801 def _get_business_hours_by_sec(self, start, end): 

802 """ 

803 Return business hours in a day by seconds. 

804 """ 

805 # create dummy datetime to calculate businesshours in a day 

806 dtstart = datetime(2014, 4, 1, start.hour, start.minute) 

807 day = 1 if start < end else 2 

808 until = datetime(2014, 4, day, end.hour, end.minute) 

809 return int((until - dtstart).total_seconds()) 

810 

811 @apply_wraps 

812 def rollback(self, dt): 

813 """ 

814 Roll provided date backward to next offset only if not on offset. 

815 """ 

816 if not self.is_on_offset(dt): 

817 if self.n >= 0: 

818 dt = self._prev_opening_time(dt) 

819 else: 

820 dt = self._next_opening_time(dt) 

821 return self._get_closing_time(dt) 

822 return dt 

823 

824 @apply_wraps 

825 def rollforward(self, dt): 

826 """ 

827 Roll provided date forward to next offset only if not on offset. 

828 """ 

829 if not self.is_on_offset(dt): 

830 if self.n >= 0: 

831 return self._next_opening_time(dt) 

832 else: 

833 return self._prev_opening_time(dt) 

834 return dt 

835 

836 def _get_closing_time(self, dt): 

837 """ 

838 Get the closing time of a business hour interval by its opening time. 

839 

840 Parameters 

841 ---------- 

842 dt : datetime 

843 Opening time of a business hour interval. 

844 

845 Returns 

846 ------- 

847 result : datetime 

848 Corresponding closing time. 

849 """ 

850 for i, st in enumerate(self.start): 

851 if st.hour == dt.hour and st.minute == dt.minute: 

852 return dt + timedelta( 

853 seconds=self._get_business_hours_by_sec(st, self.end[i]) 

854 ) 

855 assert False 

856 

857 @apply_wraps 

858 def apply(self, other): 

859 if isinstance(other, datetime): 

860 # used for detecting edge condition 

861 nanosecond = getattr(other, "nanosecond", 0) 

862 # reset timezone and nanosecond 

863 # other may be a Timestamp, thus not use replace 

864 other = datetime( 

865 other.year, 

866 other.month, 

867 other.day, 

868 other.hour, 

869 other.minute, 

870 other.second, 

871 other.microsecond, 

872 ) 

873 n = self.n 

874 

875 # adjust other to reduce number of cases to handle 

876 if n >= 0: 

877 if other.time() in self.end or not self._is_on_offset(other): 

878 other = self._next_opening_time(other) 

879 else: 

880 if other.time() in self.start: 

881 # adjustment to move to previous business day 

882 other = other - timedelta(seconds=1) 

883 if not self._is_on_offset(other): 

884 other = self._next_opening_time(other) 

885 other = self._get_closing_time(other) 

886 

887 # get total business hours by sec in one business day 

888 businesshours = sum( 

889 self._get_business_hours_by_sec(st, en) 

890 for st, en in zip(self.start, self.end) 

891 ) 

892 

893 bd, r = divmod(abs(n * 60), businesshours // 60) 

894 if n < 0: 

895 bd, r = -bd, -r 

896 

897 # adjust by business days first 

898 if bd != 0: 

899 if isinstance(self, _CustomMixin): # GH 30593 

900 skip_bd = CustomBusinessDay( 

901 n=bd, 

902 weekmask=self.weekmask, 

903 holidays=self.holidays, 

904 calendar=self.calendar, 

905 ) 

906 else: 

907 skip_bd = BusinessDay(n=bd) 

908 # midnight business hour may not on BusinessDay 

909 if not self.next_bday.is_on_offset(other): 

910 prev_open = self._prev_opening_time(other) 

911 remain = other - prev_open 

912 other = prev_open + skip_bd + remain 

913 else: 

914 other = other + skip_bd 

915 

916 # remaining business hours to adjust 

917 bhour_remain = timedelta(minutes=r) 

918 

919 if n >= 0: 

920 while bhour_remain != timedelta(0): 

921 # business hour left in this business time interval 

922 bhour = ( 

923 self._get_closing_time(self._prev_opening_time(other)) - other 

924 ) 

925 if bhour_remain < bhour: 

926 # finish adjusting if possible 

927 other += bhour_remain 

928 bhour_remain = timedelta(0) 

929 else: 

930 # go to next business time interval 

931 bhour_remain -= bhour 

932 other = self._next_opening_time(other + bhour) 

933 else: 

934 while bhour_remain != timedelta(0): 

935 # business hour left in this business time interval 

936 bhour = self._next_opening_time(other) - other 

937 if ( 

938 bhour_remain > bhour 

939 or bhour_remain == bhour 

940 and nanosecond != 0 

941 ): 

942 # finish adjusting if possible 

943 other += bhour_remain 

944 bhour_remain = timedelta(0) 

945 else: 

946 # go to next business time interval 

947 bhour_remain -= bhour 

948 other = self._get_closing_time( 

949 self._next_opening_time( 

950 other + bhour - timedelta(seconds=1) 

951 ) 

952 ) 

953 

954 return other 

955 else: 

956 raise ApplyTypeError("Only know how to combine business hour with datetime") 

957 

958 def is_on_offset(self, dt): 

959 if self.normalize and not _is_normalized(dt): 

960 return False 

961 

962 if dt.tzinfo is not None: 

963 dt = datetime( 

964 dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond 

965 ) 

966 # Valid BH can be on the different BusinessDay during midnight 

967 # Distinguish by the time spent from previous opening time 

968 return self._is_on_offset(dt) 

969 

970 def _is_on_offset(self, dt): 

971 """ 

972 Slight speedups using calculated values. 

973 """ 

974 # if self.normalize and not _is_normalized(dt): 

975 # return False 

976 # Valid BH can be on the different BusinessDay during midnight 

977 # Distinguish by the time spent from previous opening time 

978 if self.n >= 0: 

979 op = self._prev_opening_time(dt) 

980 else: 

981 op = self._next_opening_time(dt) 

982 span = (dt - op).total_seconds() 

983 businesshours = 0 

984 for i, st in enumerate(self.start): 

985 if op.hour == st.hour and op.minute == st.minute: 

986 businesshours = self._get_business_hours_by_sec(st, self.end[i]) 

987 if span <= businesshours: 

988 return True 

989 else: 

990 return False 

991 

992 def _repr_attrs(self): 

993 out = super()._repr_attrs() 

994 hours = ",".join( 

995 f'{st.strftime("%H:%M")}-{en.strftime("%H:%M")}' 

996 for st, en in zip(self.start, self.end) 

997 ) 

998 attrs = [f"{self._prefix}={hours}"] 

999 out += ": " + ", ".join(attrs) 

1000 return out 

1001 

1002 

1003class BusinessHour(BusinessHourMixin, SingleConstructorOffset): 

1004 """ 

1005 DateOffset subclass representing possibly n business hours. 

1006 """ 

1007 

1008 _prefix = "BH" 

1009 _anchor = 0 

1010 _attributes = frozenset(["n", "normalize", "start", "end", "offset"]) 

1011 

1012 def __init__( 

1013 self, n=1, normalize=False, start="09:00", end="17:00", offset=timedelta(0) 

1014 ): 

1015 BaseOffset.__init__(self, n, normalize) 

1016 super().__init__(start=start, end=end, offset=offset) 

1017 

1018 

1019class CustomBusinessDay(_CustomMixin, BusinessDay): 

1020 """ 

1021 DateOffset subclass representing possibly n custom business days, 

1022 excluding holidays. 

1023 

1024 Parameters 

1025 ---------- 

1026 n : int, default 1 

1027 normalize : bool, default False 

1028 Normalize start/end dates to midnight before generating date range. 

1029 weekmask : str, Default 'Mon Tue Wed Thu Fri' 

1030 Weekmask of valid business days, passed to ``numpy.busdaycalendar``. 

1031 holidays : list 

1032 List/array of dates to exclude from the set of valid business days, 

1033 passed to ``numpy.busdaycalendar``. 

1034 calendar : pd.HolidayCalendar or np.busdaycalendar 

1035 offset : timedelta, default timedelta(0) 

1036 """ 

1037 

1038 _prefix = "C" 

1039 _attributes = frozenset( 

1040 ["n", "normalize", "weekmask", "holidays", "calendar", "offset"] 

1041 ) 

1042 

1043 def __init__( 

1044 self, 

1045 n=1, 

1046 normalize=False, 

1047 weekmask="Mon Tue Wed Thu Fri", 

1048 holidays=None, 

1049 calendar=None, 

1050 offset=timedelta(0), 

1051 ): 

1052 BaseOffset.__init__(self, n, normalize) 

1053 object.__setattr__(self, "_offset", offset) 

1054 

1055 _CustomMixin.__init__(self, weekmask, holidays, calendar) 

1056 

1057 @apply_wraps 

1058 def apply(self, other): 

1059 if self.n <= 0: 

1060 roll = "forward" 

1061 else: 

1062 roll = "backward" 

1063 

1064 if isinstance(other, datetime): 

1065 date_in = other 

1066 np_dt = np.datetime64(date_in.date()) 

1067 

1068 np_incr_dt = np.busday_offset( 

1069 np_dt, self.n, roll=roll, busdaycal=self.calendar 

1070 ) 

1071 

1072 dt_date = np_incr_dt.astype(datetime) 

1073 result = datetime.combine(dt_date, date_in.time()) 

1074 

1075 if self.offset: 

1076 result = result + self.offset 

1077 return result 

1078 

1079 elif isinstance(other, (timedelta, Tick)): 

1080 return BDay(self.n, offset=self.offset + other, normalize=self.normalize) 

1081 else: 

1082 raise ApplyTypeError( 

1083 "Only know how to combine trading day with " 

1084 "datetime, datetime64 or timedelta." 

1085 ) 

1086 

1087 def apply_index(self, i): 

1088 raise NotImplementedError 

1089 

1090 def is_on_offset(self, dt): 

1091 if self.normalize and not _is_normalized(dt): 

1092 return False 

1093 day64 = _to_dt64(dt, "datetime64[D]") 

1094 return np.is_busday(day64, busdaycal=self.calendar) 

1095 

1096 

1097class CustomBusinessHour(_CustomMixin, BusinessHourMixin, SingleConstructorOffset): 

1098 """ 

1099 DateOffset subclass representing possibly n custom business days. 

1100 """ 

1101 

1102 _prefix = "CBH" 

1103 _anchor = 0 

1104 _attributes = frozenset( 

1105 ["n", "normalize", "weekmask", "holidays", "calendar", "start", "end", "offset"] 

1106 ) 

1107 

1108 def __init__( 

1109 self, 

1110 n=1, 

1111 normalize=False, 

1112 weekmask="Mon Tue Wed Thu Fri", 

1113 holidays=None, 

1114 calendar=None, 

1115 start="09:00", 

1116 end="17:00", 

1117 offset=timedelta(0), 

1118 ): 

1119 BaseOffset.__init__(self, n, normalize) 

1120 object.__setattr__(self, "_offset", offset) 

1121 

1122 _CustomMixin.__init__(self, weekmask, holidays, calendar) 

1123 BusinessHourMixin.__init__(self, start=start, end=end, offset=offset) 

1124 

1125 

1126# --------------------------------------------------------------------- 

1127# Month-Based Offset Classes 

1128 

1129 

1130class MonthOffset(SingleConstructorOffset): 

1131 _adjust_dst = True 

1132 _attributes = frozenset(["n", "normalize"]) 

1133 

1134 __init__ = BaseOffset.__init__ 

1135 

1136 @property 

1137 def name(self): 

1138 if self.is_anchored: 

1139 return self.rule_code 

1140 else: 

1141 month = ccalendar.MONTH_ALIASES[self.n] 

1142 return f"{self.code_rule}-{month}" 

1143 

1144 def is_on_offset(self, dt): 

1145 if self.normalize and not _is_normalized(dt): 

1146 return False 

1147 return dt.day == self._get_offset_day(dt) 

1148 

1149 @apply_wraps 

1150 def apply(self, other): 

1151 compare_day = self._get_offset_day(other) 

1152 n = liboffsets.roll_convention(other.day, self.n, compare_day) 

1153 return shift_month(other, n, self._day_opt) 

1154 

1155 @apply_index_wraps 

1156 def apply_index(self, i): 

1157 shifted = liboffsets.shift_months(i.asi8, self.n, self._day_opt) 

1158 # TODO: going through __new__ raises on call to _validate_frequency; 

1159 # are we passing incorrect freq? 

1160 return type(i)._simple_new(shifted, freq=i.freq, dtype=i.dtype) 

1161 

1162 

1163class MonthEnd(MonthOffset): 

1164 """ 

1165 DateOffset of one month end. 

1166 """ 

1167 

1168 _prefix = "M" 

1169 _day_opt = "end" 

1170 

1171 

1172class MonthBegin(MonthOffset): 

1173 """ 

1174 DateOffset of one month at beginning. 

1175 """ 

1176 

1177 _prefix = "MS" 

1178 _day_opt = "start" 

1179 

1180 

1181class BusinessMonthEnd(MonthOffset): 

1182 """ 

1183 DateOffset increments between business EOM dates. 

1184 """ 

1185 

1186 _prefix = "BM" 

1187 _day_opt = "business_end" 

1188 

1189 

1190class BusinessMonthBegin(MonthOffset): 

1191 """ 

1192 DateOffset of one business month at beginning. 

1193 """ 

1194 

1195 _prefix = "BMS" 

1196 _day_opt = "business_start" 

1197 

1198 

1199class _CustomBusinessMonth(_CustomMixin, BusinessMixin, MonthOffset): 

1200 """ 

1201 DateOffset subclass representing custom business month(s). 

1202 

1203 Increments between %(bound)s of month dates. 

1204 

1205 Parameters 

1206 ---------- 

1207 n : int, default 1 

1208 The number of months represented. 

1209 normalize : bool, default False 

1210 Normalize start/end dates to midnight before generating date range. 

1211 weekmask : str, Default 'Mon Tue Wed Thu Fri' 

1212 Weekmask of valid business days, passed to ``numpy.busdaycalendar``. 

1213 holidays : list 

1214 List/array of dates to exclude from the set of valid business days, 

1215 passed to ``numpy.busdaycalendar``. 

1216 calendar : pd.HolidayCalendar or np.busdaycalendar 

1217 Calendar to integrate. 

1218 offset : timedelta, default timedelta(0) 

1219 Time offset to apply. 

1220 """ 

1221 

1222 _attributes = frozenset( 

1223 ["n", "normalize", "weekmask", "holidays", "calendar", "offset"] 

1224 ) 

1225 

1226 is_on_offset = DateOffset.is_on_offset # override MonthOffset method 

1227 apply_index = DateOffset.apply_index # override MonthOffset method 

1228 

1229 def __init__( 

1230 self, 

1231 n=1, 

1232 normalize=False, 

1233 weekmask="Mon Tue Wed Thu Fri", 

1234 holidays=None, 

1235 calendar=None, 

1236 offset=timedelta(0), 

1237 ): 

1238 BaseOffset.__init__(self, n, normalize) 

1239 object.__setattr__(self, "_offset", offset) 

1240 

1241 _CustomMixin.__init__(self, weekmask, holidays, calendar) 

1242 

1243 @cache_readonly 

1244 def cbday_roll(self): 

1245 """ 

1246 Define default roll function to be called in apply method. 

1247 """ 

1248 cbday = CustomBusinessDay(n=self.n, normalize=False, **self.kwds) 

1249 

1250 if self._prefix.endswith("S"): 

1251 # MonthBegin 

1252 roll_func = cbday.rollforward 

1253 else: 

1254 # MonthEnd 

1255 roll_func = cbday.rollback 

1256 return roll_func 

1257 

1258 @cache_readonly 

1259 def m_offset(self): 

1260 if self._prefix.endswith("S"): 

1261 # MonthBegin 

1262 moff = MonthBegin(n=1, normalize=False) 

1263 else: 

1264 # MonthEnd 

1265 moff = MonthEnd(n=1, normalize=False) 

1266 return moff 

1267 

1268 @cache_readonly 

1269 def month_roll(self): 

1270 """ 

1271 Define default roll function to be called in apply method. 

1272 """ 

1273 if self._prefix.endswith("S"): 

1274 # MonthBegin 

1275 roll_func = self.m_offset.rollback 

1276 else: 

1277 # MonthEnd 

1278 roll_func = self.m_offset.rollforward 

1279 return roll_func 

1280 

1281 @apply_wraps 

1282 def apply(self, other): 

1283 # First move to month offset 

1284 cur_month_offset_date = self.month_roll(other) 

1285 

1286 # Find this custom month offset 

1287 compare_date = self.cbday_roll(cur_month_offset_date) 

1288 n = liboffsets.roll_convention(other.day, self.n, compare_date.day) 

1289 

1290 new = cur_month_offset_date + n * self.m_offset 

1291 result = self.cbday_roll(new) 

1292 return result 

1293 

1294 

1295@Substitution(bound="end") 

1296@Appender(_CustomBusinessMonth.__doc__) 

1297class CustomBusinessMonthEnd(_CustomBusinessMonth): 

1298 _prefix = "CBM" 

1299 

1300 

1301@Substitution(bound="beginning") 

1302@Appender(_CustomBusinessMonth.__doc__) 

1303class CustomBusinessMonthBegin(_CustomBusinessMonth): 

1304 _prefix = "CBMS" 

1305 

1306 

1307# --------------------------------------------------------------------- 

1308# Semi-Month Based Offset Classes 

1309 

1310 

1311class SemiMonthOffset(DateOffset): 

1312 _adjust_dst = True 

1313 _default_day_of_month = 15 

1314 _min_day_of_month = 2 

1315 _attributes = frozenset(["n", "normalize", "day_of_month"]) 

1316 

1317 def __init__(self, n=1, normalize=False, day_of_month=None): 

1318 BaseOffset.__init__(self, n, normalize) 

1319 

1320 if day_of_month is None: 

1321 object.__setattr__(self, "day_of_month", self._default_day_of_month) 

1322 else: 

1323 object.__setattr__(self, "day_of_month", int(day_of_month)) 

1324 if not self._min_day_of_month <= self.day_of_month <= 27: 

1325 raise ValueError( 

1326 "day_of_month must be " 

1327 f"{self._min_day_of_month}<=day_of_month<=27, " 

1328 f"got {self.day_of_month}" 

1329 ) 

1330 

1331 @classmethod 

1332 def _from_name(cls, suffix=None): 

1333 return cls(day_of_month=suffix) 

1334 

1335 @property 

1336 def rule_code(self): 

1337 suffix = f"-{self.day_of_month}" 

1338 return self._prefix + suffix 

1339 

1340 @apply_wraps 

1341 def apply(self, other): 

1342 # shift `other` to self.day_of_month, incrementing `n` if necessary 

1343 n = liboffsets.roll_convention(other.day, self.n, self.day_of_month) 

1344 

1345 days_in_month = ccalendar.get_days_in_month(other.year, other.month) 

1346 

1347 # For SemiMonthBegin on other.day == 1 and 

1348 # SemiMonthEnd on other.day == days_in_month, 

1349 # shifting `other` to `self.day_of_month` _always_ requires 

1350 # incrementing/decrementing `n`, regardless of whether it is 

1351 # initially positive. 

1352 if type(self) is SemiMonthBegin and (self.n <= 0 and other.day == 1): 

1353 n -= 1 

1354 elif type(self) is SemiMonthEnd and (self.n > 0 and other.day == days_in_month): 

1355 n += 1 

1356 

1357 return self._apply(n, other) 

1358 

1359 def _apply(self, n, other): 

1360 """ 

1361 Handle specific apply logic for child classes. 

1362 """ 

1363 raise AbstractMethodError(self) 

1364 

1365 @apply_index_wraps 

1366 def apply_index(self, i): 

1367 # determine how many days away from the 1st of the month we are 

1368 dti = i 

1369 days_from_start = i.to_perioddelta("M").asi8 

1370 delta = Timedelta(days=self.day_of_month - 1).value 

1371 

1372 # get boolean array for each element before the day_of_month 

1373 before_day_of_month = days_from_start < delta 

1374 

1375 # get boolean array for each element after the day_of_month 

1376 after_day_of_month = days_from_start > delta 

1377 

1378 # determine the correct n for each date in i 

1379 roll = self._get_roll(i, before_day_of_month, after_day_of_month) 

1380 

1381 # isolate the time since it will be striped away one the next line 

1382 time = i.to_perioddelta("D") 

1383 

1384 # apply the correct number of months 

1385 

1386 # integer-array addition on PeriodIndex is deprecated, 

1387 # so we use _addsub_int_array directly 

1388 asper = i.to_period("M") 

1389 if not isinstance(asper._data, np.ndarray): 

1390 # unwrap PeriodIndex --> PeriodArray 

1391 asper = asper._data 

1392 

1393 shifted = asper._addsub_int_array(roll // 2, operator.add) 

1394 i = type(dti)(shifted.to_timestamp()) 

1395 

1396 # apply the correct day 

1397 i = self._apply_index_days(i, roll) 

1398 

1399 return i + time 

1400 

1401 def _get_roll(self, i, before_day_of_month, after_day_of_month): 

1402 """ 

1403 Return an array with the correct n for each date in i. 

1404 

1405 The roll array is based on the fact that i gets rolled back to 

1406 the first day of the month. 

1407 """ 

1408 raise AbstractMethodError(self) 

1409 

1410 def _apply_index_days(self, i, roll): 

1411 """ 

1412 Apply the correct day for each date in i. 

1413 """ 

1414 raise AbstractMethodError(self) 

1415 

1416 

1417class SemiMonthEnd(SemiMonthOffset): 

1418 """ 

1419 Two DateOffset's per month repeating on the last 

1420 day of the month and day_of_month. 

1421 

1422 Parameters 

1423 ---------- 

1424 n : int 

1425 normalize : bool, default False 

1426 day_of_month : int, {1, 3,...,27}, default 15 

1427 """ 

1428 

1429 _prefix = "SM" 

1430 _min_day_of_month = 1 

1431 

1432 def is_on_offset(self, dt): 

1433 if self.normalize and not _is_normalized(dt): 

1434 return False 

1435 days_in_month = ccalendar.get_days_in_month(dt.year, dt.month) 

1436 return dt.day in (self.day_of_month, days_in_month) 

1437 

1438 def _apply(self, n, other): 

1439 months = n // 2 

1440 day = 31 if n % 2 else self.day_of_month 

1441 return shift_month(other, months, day) 

1442 

1443 def _get_roll(self, i, before_day_of_month, after_day_of_month): 

1444 n = self.n 

1445 is_month_end = i.is_month_end 

1446 if n > 0: 

1447 roll_end = np.where(is_month_end, 1, 0) 

1448 roll_before = np.where(before_day_of_month, n, n + 1) 

1449 roll = roll_end + roll_before 

1450 elif n == 0: 

1451 roll_after = np.where(after_day_of_month, 2, 0) 

1452 roll_before = np.where(~after_day_of_month, 1, 0) 

1453 roll = roll_before + roll_after 

1454 else: 

1455 roll = np.where(after_day_of_month, n + 2, n + 1) 

1456 return roll 

1457 

1458 def _apply_index_days(self, i, roll): 

1459 """ 

1460 Add days portion of offset to DatetimeIndex i. 

1461 

1462 Parameters 

1463 ---------- 

1464 i : DatetimeIndex 

1465 roll : ndarray[int64_t] 

1466 

1467 Returns 

1468 ------- 

1469 result : DatetimeIndex 

1470 """ 

1471 nanos = (roll % 2) * Timedelta(days=self.day_of_month).value 

1472 i += nanos.astype("timedelta64[ns]") 

1473 return i + Timedelta(days=-1) 

1474 

1475 

1476class SemiMonthBegin(SemiMonthOffset): 

1477 """ 

1478 Two DateOffset's per month repeating on the first 

1479 day of the month and day_of_month. 

1480 

1481 Parameters 

1482 ---------- 

1483 n : int 

1484 normalize : bool, default False 

1485 day_of_month : int, {2, 3,...,27}, default 15 

1486 """ 

1487 

1488 _prefix = "SMS" 

1489 

1490 def is_on_offset(self, dt): 

1491 if self.normalize and not _is_normalized(dt): 

1492 return False 

1493 return dt.day in (1, self.day_of_month) 

1494 

1495 def _apply(self, n, other): 

1496 months = n // 2 + n % 2 

1497 day = 1 if n % 2 else self.day_of_month 

1498 return shift_month(other, months, day) 

1499 

1500 def _get_roll(self, i, before_day_of_month, after_day_of_month): 

1501 n = self.n 

1502 is_month_start = i.is_month_start 

1503 if n > 0: 

1504 roll = np.where(before_day_of_month, n, n + 1) 

1505 elif n == 0: 

1506 roll_start = np.where(is_month_start, 0, 1) 

1507 roll_after = np.where(after_day_of_month, 1, 0) 

1508 roll = roll_start + roll_after 

1509 else: 

1510 roll_after = np.where(after_day_of_month, n + 2, n + 1) 

1511 roll_start = np.where(is_month_start, -1, 0) 

1512 roll = roll_after + roll_start 

1513 return roll 

1514 

1515 def _apply_index_days(self, i, roll): 

1516 """ 

1517 Add days portion of offset to DatetimeIndex i. 

1518 

1519 Parameters 

1520 ---------- 

1521 i : DatetimeIndex 

1522 roll : ndarray[int64_t] 

1523 

1524 Returns 

1525 ------- 

1526 result : DatetimeIndex 

1527 """ 

1528 nanos = (roll % 2) * Timedelta(days=self.day_of_month - 1).value 

1529 return i + nanos.astype("timedelta64[ns]") 

1530 

1531 

1532# --------------------------------------------------------------------- 

1533# Week-Based Offset Classes 

1534 

1535 

1536class Week(DateOffset): 

1537 """ 

1538 Weekly offset. 

1539 

1540 Parameters 

1541 ---------- 

1542 weekday : int, default None 

1543 Always generate specific day of week. 0 for Monday. 

1544 """ 

1545 

1546 _adjust_dst = True 

1547 _inc = timedelta(weeks=1) 

1548 _prefix = "W" 

1549 _attributes = frozenset(["n", "normalize", "weekday"]) 

1550 

1551 def __init__(self, n=1, normalize=False, weekday=None): 

1552 BaseOffset.__init__(self, n, normalize) 

1553 object.__setattr__(self, "weekday", weekday) 

1554 

1555 if self.weekday is not None: 

1556 if self.weekday < 0 or self.weekday > 6: 

1557 raise ValueError(f"Day must be 0<=day<=6, got {self.weekday}") 

1558 

1559 def is_anchored(self): 

1560 return self.n == 1 and self.weekday is not None 

1561 

1562 @apply_wraps 

1563 def apply(self, other): 

1564 if self.weekday is None: 

1565 return other + self.n * self._inc 

1566 

1567 if not isinstance(other, datetime): 

1568 raise TypeError( 

1569 f"Cannot add {type(other).__name__} to {type(self).__name__}" 

1570 ) 

1571 

1572 k = self.n 

1573 otherDay = other.weekday() 

1574 if otherDay != self.weekday: 

1575 other = other + timedelta((self.weekday - otherDay) % 7) 

1576 if k > 0: 

1577 k -= 1 

1578 

1579 return other + timedelta(weeks=k) 

1580 

1581 @apply_index_wraps 

1582 def apply_index(self, i): 

1583 if self.weekday is None: 

1584 # integer addition on PeriodIndex is deprecated, 

1585 # so we use _time_shift directly 

1586 asper = i.to_period("W") 

1587 if not isinstance(asper._data, np.ndarray): 

1588 # unwrap PeriodIndex --> PeriodArray 

1589 asper = asper._data 

1590 

1591 shifted = asper._time_shift(self.n) 

1592 return shifted.to_timestamp() + i.to_perioddelta("W") 

1593 else: 

1594 return self._end_apply_index(i) 

1595 

1596 def _end_apply_index(self, dtindex): 

1597 """ 

1598 Add self to the given DatetimeIndex, specialized for case where 

1599 self.weekday is non-null. 

1600 

1601 Parameters 

1602 ---------- 

1603 dtindex : DatetimeIndex 

1604 

1605 Returns 

1606 ------- 

1607 result : DatetimeIndex 

1608 """ 

1609 off = dtindex.to_perioddelta("D") 

1610 

1611 base, mult = libfrequencies.get_freq_code(self.freqstr) 

1612 base_period = dtindex.to_period(base) 

1613 if not isinstance(base_period._data, np.ndarray): 

1614 # unwrap PeriodIndex --> PeriodArray 

1615 base_period = base_period._data 

1616 

1617 if self.n > 0: 

1618 # when adding, dates on end roll to next 

1619 normed = dtindex - off + Timedelta(1, "D") - Timedelta(1, "ns") 

1620 roll = np.where( 

1621 base_period.to_timestamp(how="end") == normed, self.n, self.n - 1 

1622 ) 

1623 # integer-array addition on PeriodIndex is deprecated, 

1624 # so we use _addsub_int_array directly 

1625 shifted = base_period._addsub_int_array(roll, operator.add) 

1626 base = shifted.to_timestamp(how="end") 

1627 else: 

1628 # integer addition on PeriodIndex is deprecated, 

1629 # so we use _time_shift directly 

1630 roll = self.n 

1631 base = base_period._time_shift(roll).to_timestamp(how="end") 

1632 

1633 return base + off + Timedelta(1, "ns") - Timedelta(1, "D") 

1634 

1635 def is_on_offset(self, dt): 

1636 if self.normalize and not _is_normalized(dt): 

1637 return False 

1638 elif self.weekday is None: 

1639 return True 

1640 return dt.weekday() == self.weekday 

1641 

1642 @property 

1643 def rule_code(self): 

1644 suffix = "" 

1645 if self.weekday is not None: 

1646 weekday = ccalendar.int_to_weekday[self.weekday] 

1647 suffix = f"-{weekday}" 

1648 return self._prefix + suffix 

1649 

1650 @classmethod 

1651 def _from_name(cls, suffix=None): 

1652 if not suffix: 

1653 weekday = None 

1654 else: 

1655 weekday = ccalendar.weekday_to_int[suffix] 

1656 return cls(weekday=weekday) 

1657 

1658 

1659class _WeekOfMonthMixin: 

1660 """ 

1661 Mixin for methods common to WeekOfMonth and LastWeekOfMonth. 

1662 """ 

1663 

1664 @apply_wraps 

1665 def apply(self, other): 

1666 compare_day = self._get_offset_day(other) 

1667 

1668 months = self.n 

1669 if months > 0 and compare_day > other.day: 

1670 months -= 1 

1671 elif months <= 0 and compare_day < other.day: 

1672 months += 1 

1673 

1674 shifted = shift_month(other, months, "start") 

1675 to_day = self._get_offset_day(shifted) 

1676 return liboffsets.shift_day(shifted, to_day - shifted.day) 

1677 

1678 def is_on_offset(self, dt): 

1679 if self.normalize and not _is_normalized(dt): 

1680 return False 

1681 return dt.day == self._get_offset_day(dt) 

1682 

1683 

1684class WeekOfMonth(_WeekOfMonthMixin, DateOffset): 

1685 """ 

1686 Describes monthly dates like "the Tuesday of the 2nd week of each month". 

1687 

1688 Parameters 

1689 ---------- 

1690 n : int 

1691 week : int {0, 1, 2, 3, ...}, default 0 

1692 A specific integer for the week of the month. 

1693 e.g. 0 is 1st week of month, 1 is the 2nd week, etc. 

1694 weekday : int {0, 1, ..., 6}, default 0 

1695 A specific integer for the day of the week. 

1696 

1697 - 0 is Monday 

1698 - 1 is Tuesday 

1699 - 2 is Wednesday 

1700 - 3 is Thursday 

1701 - 4 is Friday 

1702 - 5 is Saturday 

1703 - 6 is Sunday. 

1704 """ 

1705 

1706 _prefix = "WOM" 

1707 _adjust_dst = True 

1708 _attributes = frozenset(["n", "normalize", "week", "weekday"]) 

1709 

1710 def __init__(self, n=1, normalize=False, week=0, weekday=0): 

1711 BaseOffset.__init__(self, n, normalize) 

1712 object.__setattr__(self, "weekday", weekday) 

1713 object.__setattr__(self, "week", week) 

1714 

1715 if self.weekday < 0 or self.weekday > 6: 

1716 raise ValueError(f"Day must be 0<=day<=6, got {self.weekday}") 

1717 if self.week < 0 or self.week > 3: 

1718 raise ValueError(f"Week must be 0<=week<=3, got {self.week}") 

1719 

1720 def _get_offset_day(self, other): 

1721 """ 

1722 Find the day in the same month as other that has the same 

1723 weekday as self.weekday and is the self.week'th such day in the month. 

1724 

1725 Parameters 

1726 ---------- 

1727 other : datetime 

1728 

1729 Returns 

1730 ------- 

1731 day : int 

1732 """ 

1733 mstart = datetime(other.year, other.month, 1) 

1734 wday = mstart.weekday() 

1735 shift_days = (self.weekday - wday) % 7 

1736 return 1 + shift_days + self.week * 7 

1737 

1738 @property 

1739 def rule_code(self): 

1740 weekday = ccalendar.int_to_weekday.get(self.weekday, "") 

1741 return f"{self._prefix}-{self.week + 1}{weekday}" 

1742 

1743 @classmethod 

1744 def _from_name(cls, suffix=None): 

1745 if not suffix: 

1746 raise ValueError(f"Prefix {repr(cls._prefix)} requires a suffix.") 

1747 # TODO: handle n here... 

1748 # only one digit weeks (1 --> week 0, 2 --> week 1, etc.) 

1749 week = int(suffix[0]) - 1 

1750 weekday = ccalendar.weekday_to_int[suffix[1:]] 

1751 return cls(week=week, weekday=weekday) 

1752 

1753 

1754class LastWeekOfMonth(_WeekOfMonthMixin, DateOffset): 

1755 """ 

1756 Describes monthly dates in last week of month like "the last Tuesday of 

1757 each month". 

1758 

1759 Parameters 

1760 ---------- 

1761 n : int, default 1 

1762 weekday : int {0, 1, ..., 6}, default 0 

1763 A specific integer for the day of the week. 

1764 

1765 - 0 is Monday 

1766 - 1 is Tuesday 

1767 - 2 is Wednesday 

1768 - 3 is Thursday 

1769 - 4 is Friday 

1770 - 5 is Saturday 

1771 - 6 is Sunday. 

1772 """ 

1773 

1774 _prefix = "LWOM" 

1775 _adjust_dst = True 

1776 _attributes = frozenset(["n", "normalize", "weekday"]) 

1777 

1778 def __init__(self, n=1, normalize=False, weekday=0): 

1779 BaseOffset.__init__(self, n, normalize) 

1780 object.__setattr__(self, "weekday", weekday) 

1781 

1782 if self.n == 0: 

1783 raise ValueError("N cannot be 0") 

1784 

1785 if self.weekday < 0 or self.weekday > 6: 

1786 raise ValueError(f"Day must be 0<=day<=6, got {self.weekday}") 

1787 

1788 def _get_offset_day(self, other): 

1789 """ 

1790 Find the day in the same month as other that has the same 

1791 weekday as self.weekday and is the last such day in the month. 

1792 

1793 Parameters 

1794 ---------- 

1795 other: datetime 

1796 

1797 Returns 

1798 ------- 

1799 day: int 

1800 """ 

1801 dim = ccalendar.get_days_in_month(other.year, other.month) 

1802 mend = datetime(other.year, other.month, dim) 

1803 wday = mend.weekday() 

1804 shift_days = (wday - self.weekday) % 7 

1805 return dim - shift_days 

1806 

1807 @property 

1808 def rule_code(self): 

1809 weekday = ccalendar.int_to_weekday.get(self.weekday, "") 

1810 return f"{self._prefix}-{weekday}" 

1811 

1812 @classmethod 

1813 def _from_name(cls, suffix=None): 

1814 if not suffix: 

1815 raise ValueError(f"Prefix {repr(cls._prefix)} requires a suffix.") 

1816 # TODO: handle n here... 

1817 weekday = ccalendar.weekday_to_int[suffix] 

1818 return cls(weekday=weekday) 

1819 

1820 

1821# --------------------------------------------------------------------- 

1822# Quarter-Based Offset Classes 

1823 

1824 

1825class QuarterOffset(DateOffset): 

1826 """ 

1827 Quarter representation - doesn't call super. 

1828 """ 

1829 

1830 _default_startingMonth: Optional[int] = None 

1831 _from_name_startingMonth: Optional[int] = None 

1832 _adjust_dst = True 

1833 _attributes = frozenset(["n", "normalize", "startingMonth"]) 

1834 # TODO: Consider combining QuarterOffset and YearOffset __init__ at some 

1835 # point. Also apply_index, is_on_offset, rule_code if 

1836 # startingMonth vs month attr names are resolved 

1837 

1838 def __init__(self, n=1, normalize=False, startingMonth=None): 

1839 BaseOffset.__init__(self, n, normalize) 

1840 

1841 if startingMonth is None: 

1842 startingMonth = self._default_startingMonth 

1843 object.__setattr__(self, "startingMonth", startingMonth) 

1844 

1845 def is_anchored(self): 

1846 return self.n == 1 and self.startingMonth is not None 

1847 

1848 @classmethod 

1849 def _from_name(cls, suffix=None): 

1850 kwargs = {} 

1851 if suffix: 

1852 kwargs["startingMonth"] = ccalendar.MONTH_TO_CAL_NUM[suffix] 

1853 else: 

1854 if cls._from_name_startingMonth is not None: 

1855 kwargs["startingMonth"] = cls._from_name_startingMonth 

1856 return cls(**kwargs) 

1857 

1858 @property 

1859 def rule_code(self): 

1860 month = ccalendar.MONTH_ALIASES[self.startingMonth] 

1861 return f"{self._prefix}-{month}" 

1862 

1863 @apply_wraps 

1864 def apply(self, other): 

1865 # months_since: find the calendar quarter containing other.month, 

1866 # e.g. if other.month == 8, the calendar quarter is [Jul, Aug, Sep]. 

1867 # Then find the month in that quarter containing an is_on_offset date for 

1868 # self. `months_since` is the number of months to shift other.month 

1869 # to get to this on-offset month. 

1870 months_since = other.month % 3 - self.startingMonth % 3 

1871 qtrs = liboffsets.roll_qtrday( 

1872 other, self.n, self.startingMonth, day_opt=self._day_opt, modby=3 

1873 ) 

1874 months = qtrs * 3 - months_since 

1875 return shift_month(other, months, self._day_opt) 

1876 

1877 def is_on_offset(self, dt): 

1878 if self.normalize and not _is_normalized(dt): 

1879 return False 

1880 mod_month = (dt.month - self.startingMonth) % 3 

1881 return mod_month == 0 and dt.day == self._get_offset_day(dt) 

1882 

1883 @apply_index_wraps 

1884 def apply_index(self, dtindex): 

1885 shifted = liboffsets.shift_quarters( 

1886 dtindex.asi8, self.n, self.startingMonth, self._day_opt 

1887 ) 

1888 # TODO: going through __new__ raises on call to _validate_frequency; 

1889 # are we passing incorrect freq? 

1890 return type(dtindex)._simple_new( 

1891 shifted, freq=dtindex.freq, dtype=dtindex.dtype 

1892 ) 

1893 

1894 

1895class BQuarterEnd(QuarterOffset): 

1896 """ 

1897 DateOffset increments between business Quarter dates. 

1898 

1899 startingMonth = 1 corresponds to dates like 1/31/2007, 4/30/2007, ... 

1900 startingMonth = 2 corresponds to dates like 2/28/2007, 5/31/2007, ... 

1901 startingMonth = 3 corresponds to dates like 3/30/2007, 6/29/2007, ... 

1902 """ 

1903 

1904 _outputName = "BusinessQuarterEnd" 

1905 _default_startingMonth = 3 

1906 _from_name_startingMonth = 12 

1907 _prefix = "BQ" 

1908 _day_opt = "business_end" 

1909 

1910 

1911# TODO: This is basically the same as BQuarterEnd 

1912class BQuarterBegin(QuarterOffset): 

1913 _outputName = "BusinessQuarterBegin" 

1914 # I suspect this is wrong for *all* of them. 

1915 _default_startingMonth = 3 

1916 _from_name_startingMonth = 1 

1917 _prefix = "BQS" 

1918 _day_opt = "business_start" 

1919 

1920 

1921class QuarterEnd(QuarterOffset): 

1922 """ 

1923 DateOffset increments between business Quarter dates. 

1924 

1925 startingMonth = 1 corresponds to dates like 1/31/2007, 4/30/2007, ... 

1926 startingMonth = 2 corresponds to dates like 2/28/2007, 5/31/2007, ... 

1927 startingMonth = 3 corresponds to dates like 3/31/2007, 6/30/2007, ... 

1928 """ 

1929 

1930 _outputName = "QuarterEnd" 

1931 _default_startingMonth = 3 

1932 _prefix = "Q" 

1933 _day_opt = "end" 

1934 

1935 

1936class QuarterBegin(QuarterOffset): 

1937 _outputName = "QuarterBegin" 

1938 _default_startingMonth = 3 

1939 _from_name_startingMonth = 1 

1940 _prefix = "QS" 

1941 _day_opt = "start" 

1942 

1943 

1944# --------------------------------------------------------------------- 

1945# Year-Based Offset Classes 

1946 

1947 

1948class YearOffset(DateOffset): 

1949 """ 

1950 DateOffset that just needs a month. 

1951 """ 

1952 

1953 _adjust_dst = True 

1954 _attributes = frozenset(["n", "normalize", "month"]) 

1955 

1956 def _get_offset_day(self, other): 

1957 # override BaseOffset method to use self.month instead of other.month 

1958 # TODO: there may be a more performant way to do this 

1959 return liboffsets.get_day_of_month( 

1960 other.replace(month=self.month), self._day_opt 

1961 ) 

1962 

1963 @apply_wraps 

1964 def apply(self, other): 

1965 years = roll_yearday(other, self.n, self.month, self._day_opt) 

1966 months = years * 12 + (self.month - other.month) 

1967 return shift_month(other, months, self._day_opt) 

1968 

1969 @apply_index_wraps 

1970 def apply_index(self, dtindex): 

1971 shifted = liboffsets.shift_quarters( 

1972 dtindex.asi8, self.n, self.month, self._day_opt, modby=12 

1973 ) 

1974 # TODO: going through __new__ raises on call to _validate_frequency; 

1975 # are we passing incorrect freq? 

1976 return type(dtindex)._simple_new( 

1977 shifted, freq=dtindex.freq, dtype=dtindex.dtype 

1978 ) 

1979 

1980 def is_on_offset(self, dt): 

1981 if self.normalize and not _is_normalized(dt): 

1982 return False 

1983 return dt.month == self.month and dt.day == self._get_offset_day(dt) 

1984 

1985 def __init__(self, n=1, normalize=False, month=None): 

1986 BaseOffset.__init__(self, n, normalize) 

1987 

1988 month = month if month is not None else self._default_month 

1989 object.__setattr__(self, "month", month) 

1990 

1991 if self.month < 1 or self.month > 12: 

1992 raise ValueError("Month must go from 1 to 12") 

1993 

1994 @classmethod 

1995 def _from_name(cls, suffix=None): 

1996 kwargs = {} 

1997 if suffix: 

1998 kwargs["month"] = ccalendar.MONTH_TO_CAL_NUM[suffix] 

1999 return cls(**kwargs) 

2000 

2001 @property 

2002 def rule_code(self): 

2003 month = ccalendar.MONTH_ALIASES[self.month] 

2004 return f"{self._prefix}-{month}" 

2005 

2006 

2007class BYearEnd(YearOffset): 

2008 """ 

2009 DateOffset increments between business EOM dates. 

2010 """ 

2011 

2012 _outputName = "BusinessYearEnd" 

2013 _default_month = 12 

2014 _prefix = "BA" 

2015 _day_opt = "business_end" 

2016 

2017 

2018class BYearBegin(YearOffset): 

2019 """ 

2020 DateOffset increments between business year begin dates. 

2021 """ 

2022 

2023 _outputName = "BusinessYearBegin" 

2024 _default_month = 1 

2025 _prefix = "BAS" 

2026 _day_opt = "business_start" 

2027 

2028 

2029class YearEnd(YearOffset): 

2030 """ 

2031 DateOffset increments between calendar year ends. 

2032 """ 

2033 

2034 _default_month = 12 

2035 _prefix = "A" 

2036 _day_opt = "end" 

2037 

2038 

2039class YearBegin(YearOffset): 

2040 """ 

2041 DateOffset increments between calendar year begin dates. 

2042 """ 

2043 

2044 _default_month = 1 

2045 _prefix = "AS" 

2046 _day_opt = "start" 

2047 

2048 

2049# --------------------------------------------------------------------- 

2050# Special Offset Classes 

2051 

2052 

2053class FY5253(DateOffset): 

2054 """ 

2055 Describes 52-53 week fiscal year. This is also known as a 4-4-5 calendar. 

2056 

2057 It is used by companies that desire that their 

2058 fiscal year always end on the same day of the week. 

2059 

2060 It is a method of managing accounting periods. 

2061 It is a common calendar structure for some industries, 

2062 such as retail, manufacturing and parking industry. 

2063 

2064 For more information see: 

2065 http://en.wikipedia.org/wiki/4-4-5_calendar 

2066 

2067 The year may either: 

2068 

2069 - end on the last X day of the Y month. 

2070 - end on the last X day closest to the last day of the Y month. 

2071 

2072 X is a specific day of the week. 

2073 Y is a certain month of the year 

2074 

2075 Parameters 

2076 ---------- 

2077 n : int 

2078 weekday : int {0, 1, ..., 6}, default 0 

2079 A specific integer for the day of the week. 

2080 

2081 - 0 is Monday 

2082 - 1 is Tuesday 

2083 - 2 is Wednesday 

2084 - 3 is Thursday 

2085 - 4 is Friday 

2086 - 5 is Saturday 

2087 - 6 is Sunday. 

2088 

2089 startingMonth : int {1, 2, ... 12}, default 1 

2090 The month in which the fiscal year ends. 

2091 

2092 variation : str, default "nearest" 

2093 Method of employing 4-4-5 calendar. 

2094 

2095 There are two options: 

2096 

2097 - "nearest" means year end is **weekday** closest to last day of month in year. 

2098 - "last" means year end is final **weekday** of the final month in fiscal year. 

2099 """ 

2100 

2101 _prefix = "RE" 

2102 _adjust_dst = True 

2103 _attributes = frozenset(["weekday", "startingMonth", "variation"]) 

2104 

2105 def __init__( 

2106 self, n=1, normalize=False, weekday=0, startingMonth=1, variation="nearest" 

2107 ): 

2108 BaseOffset.__init__(self, n, normalize) 

2109 object.__setattr__(self, "startingMonth", startingMonth) 

2110 object.__setattr__(self, "weekday", weekday) 

2111 

2112 object.__setattr__(self, "variation", variation) 

2113 

2114 if self.n == 0: 

2115 raise ValueError("N cannot be 0") 

2116 

2117 if self.variation not in ["nearest", "last"]: 

2118 raise ValueError(f"{self.variation} is not a valid variation") 

2119 

2120 def is_anchored(self): 

2121 return ( 

2122 self.n == 1 and self.startingMonth is not None and self.weekday is not None 

2123 ) 

2124 

2125 def is_on_offset(self, dt): 

2126 if self.normalize and not _is_normalized(dt): 

2127 return False 

2128 dt = datetime(dt.year, dt.month, dt.day) 

2129 year_end = self.get_year_end(dt) 

2130 

2131 if self.variation == "nearest": 

2132 # We have to check the year end of "this" cal year AND the previous 

2133 return year_end == dt or self.get_year_end(shift_month(dt, -1, None)) == dt 

2134 else: 

2135 return year_end == dt 

2136 

2137 @apply_wraps 

2138 def apply(self, other): 

2139 norm = Timestamp(other).normalize() 

2140 

2141 n = self.n 

2142 prev_year = self.get_year_end(datetime(other.year - 1, self.startingMonth, 1)) 

2143 cur_year = self.get_year_end(datetime(other.year, self.startingMonth, 1)) 

2144 next_year = self.get_year_end(datetime(other.year + 1, self.startingMonth, 1)) 

2145 

2146 prev_year = conversion.localize_pydatetime(prev_year, other.tzinfo) 

2147 cur_year = conversion.localize_pydatetime(cur_year, other.tzinfo) 

2148 next_year = conversion.localize_pydatetime(next_year, other.tzinfo) 

2149 

2150 # Note: next_year.year == other.year + 1, so we will always 

2151 # have other < next_year 

2152 if norm == prev_year: 

2153 n -= 1 

2154 elif norm == cur_year: 

2155 pass 

2156 elif n > 0: 

2157 if norm < prev_year: 

2158 n -= 2 

2159 elif prev_year < norm < cur_year: 

2160 n -= 1 

2161 elif cur_year < norm < next_year: 

2162 pass 

2163 else: 

2164 if cur_year < norm < next_year: 

2165 n += 1 

2166 elif prev_year < norm < cur_year: 

2167 pass 

2168 elif ( 

2169 norm.year == prev_year.year 

2170 and norm < prev_year 

2171 and prev_year - norm <= timedelta(6) 

2172 ): 

2173 # GH#14774, error when next_year.year == cur_year.year 

2174 # e.g. prev_year == datetime(2004, 1, 3), 

2175 # other == datetime(2004, 1, 1) 

2176 n -= 1 

2177 else: 

2178 assert False 

2179 

2180 shifted = datetime(other.year + n, self.startingMonth, 1) 

2181 result = self.get_year_end(shifted) 

2182 result = datetime( 

2183 result.year, 

2184 result.month, 

2185 result.day, 

2186 other.hour, 

2187 other.minute, 

2188 other.second, 

2189 other.microsecond, 

2190 ) 

2191 return result 

2192 

2193 def get_year_end(self, dt): 

2194 assert dt.tzinfo is None 

2195 

2196 dim = ccalendar.get_days_in_month(dt.year, self.startingMonth) 

2197 target_date = datetime(dt.year, self.startingMonth, dim) 

2198 wkday_diff = self.weekday - target_date.weekday() 

2199 if wkday_diff == 0: 

2200 # year_end is the same for "last" and "nearest" cases 

2201 return target_date 

2202 

2203 if self.variation == "last": 

2204 days_forward = (wkday_diff % 7) - 7 

2205 

2206 # days_forward is always negative, so we always end up 

2207 # in the same year as dt 

2208 return target_date + timedelta(days=days_forward) 

2209 else: 

2210 # variation == "nearest": 

2211 days_forward = wkday_diff % 7 

2212 if days_forward <= 3: 

2213 # The upcoming self.weekday is closer than the previous one 

2214 return target_date + timedelta(days_forward) 

2215 else: 

2216 # The previous self.weekday is closer than the upcoming one 

2217 return target_date + timedelta(days_forward - 7) 

2218 

2219 @property 

2220 def rule_code(self): 

2221 prefix = self._prefix 

2222 suffix = self.get_rule_code_suffix() 

2223 return f"{prefix}-{suffix}" 

2224 

2225 def _get_suffix_prefix(self): 

2226 if self.variation == "nearest": 

2227 return "N" 

2228 else: 

2229 return "L" 

2230 

2231 def get_rule_code_suffix(self): 

2232 prefix = self._get_suffix_prefix() 

2233 month = ccalendar.MONTH_ALIASES[self.startingMonth] 

2234 weekday = ccalendar.int_to_weekday[self.weekday] 

2235 return f"{prefix}-{month}-{weekday}" 

2236 

2237 @classmethod 

2238 def _parse_suffix(cls, varion_code, startingMonth_code, weekday_code): 

2239 if varion_code == "N": 

2240 variation = "nearest" 

2241 elif varion_code == "L": 

2242 variation = "last" 

2243 else: 

2244 raise ValueError(f"Unable to parse varion_code: {varion_code}") 

2245 

2246 startingMonth = ccalendar.MONTH_TO_CAL_NUM[startingMonth_code] 

2247 weekday = ccalendar.weekday_to_int[weekday_code] 

2248 

2249 return { 

2250 "weekday": weekday, 

2251 "startingMonth": startingMonth, 

2252 "variation": variation, 

2253 } 

2254 

2255 @classmethod 

2256 def _from_name(cls, *args): 

2257 return cls(**cls._parse_suffix(*args)) 

2258 

2259 

2260class FY5253Quarter(DateOffset): 

2261 """ 

2262 DateOffset increments between business quarter dates 

2263 for 52-53 week fiscal year (also known as a 4-4-5 calendar). 

2264 

2265 It is used by companies that desire that their 

2266 fiscal year always end on the same day of the week. 

2267 

2268 It is a method of managing accounting periods. 

2269 It is a common calendar structure for some industries, 

2270 such as retail, manufacturing and parking industry. 

2271 

2272 For more information see: 

2273 http://en.wikipedia.org/wiki/4-4-5_calendar 

2274 

2275 The year may either: 

2276 

2277 - end on the last X day of the Y month. 

2278 - end on the last X day closest to the last day of the Y month. 

2279 

2280 X is a specific day of the week. 

2281 Y is a certain month of the year 

2282 

2283 startingMonth = 1 corresponds to dates like 1/31/2007, 4/30/2007, ... 

2284 startingMonth = 2 corresponds to dates like 2/28/2007, 5/31/2007, ... 

2285 startingMonth = 3 corresponds to dates like 3/30/2007, 6/29/2007, ... 

2286 

2287 Parameters 

2288 ---------- 

2289 n : int 

2290 weekday : int {0, 1, ..., 6}, default 0 

2291 A specific integer for the day of the week. 

2292 

2293 - 0 is Monday 

2294 - 1 is Tuesday 

2295 - 2 is Wednesday 

2296 - 3 is Thursday 

2297 - 4 is Friday 

2298 - 5 is Saturday 

2299 - 6 is Sunday. 

2300 

2301 startingMonth : int {1, 2, ..., 12}, default 1 

2302 The month in which fiscal years end. 

2303 

2304 qtr_with_extra_week : int {1, 2, 3, 4}, default 1 

2305 The quarter number that has the leap or 14 week when needed. 

2306 

2307 variation : str, default "nearest" 

2308 Method of employing 4-4-5 calendar. 

2309 

2310 There are two options: 

2311 

2312 - "nearest" means year end is **weekday** closest to last day of month in year. 

2313 - "last" means year end is final **weekday** of the final month in fiscal year. 

2314 """ 

2315 

2316 _prefix = "REQ" 

2317 _adjust_dst = True 

2318 _attributes = frozenset( 

2319 ["weekday", "startingMonth", "qtr_with_extra_week", "variation"] 

2320 ) 

2321 

2322 def __init__( 

2323 self, 

2324 n=1, 

2325 normalize=False, 

2326 weekday=0, 

2327 startingMonth=1, 

2328 qtr_with_extra_week=1, 

2329 variation="nearest", 

2330 ): 

2331 BaseOffset.__init__(self, n, normalize) 

2332 

2333 object.__setattr__(self, "startingMonth", startingMonth) 

2334 object.__setattr__(self, "weekday", weekday) 

2335 object.__setattr__(self, "qtr_with_extra_week", qtr_with_extra_week) 

2336 object.__setattr__(self, "variation", variation) 

2337 

2338 if self.n == 0: 

2339 raise ValueError("N cannot be 0") 

2340 

2341 @cache_readonly 

2342 def _offset(self): 

2343 return FY5253( 

2344 startingMonth=self.startingMonth, 

2345 weekday=self.weekday, 

2346 variation=self.variation, 

2347 ) 

2348 

2349 def is_anchored(self): 

2350 return self.n == 1 and self._offset.is_anchored() 

2351 

2352 def _rollback_to_year(self, other): 

2353 """ 

2354 Roll `other` back to the most recent date that was on a fiscal year 

2355 end. 

2356 

2357 Return the date of that year-end, the number of full quarters 

2358 elapsed between that year-end and other, and the remaining Timedelta 

2359 since the most recent quarter-end. 

2360 

2361 Parameters 

2362 ---------- 

2363 other : datetime or Timestamp 

2364 

2365 Returns 

2366 ------- 

2367 tuple of 

2368 prev_year_end : Timestamp giving most recent fiscal year end 

2369 num_qtrs : int 

2370 tdelta : Timedelta 

2371 """ 

2372 num_qtrs = 0 

2373 

2374 norm = Timestamp(other).tz_localize(None) 

2375 start = self._offset.rollback(norm) 

2376 # Note: start <= norm and self._offset.is_on_offset(start) 

2377 

2378 if start < norm: 

2379 # roll adjustment 

2380 qtr_lens = self.get_weeks(norm) 

2381 

2382 # check thet qtr_lens is consistent with self._offset addition 

2383 end = liboffsets.shift_day(start, days=7 * sum(qtr_lens)) 

2384 assert self._offset.is_on_offset(end), (start, end, qtr_lens) 

2385 

2386 tdelta = norm - start 

2387 for qlen in qtr_lens: 

2388 if qlen * 7 <= tdelta.days: 

2389 num_qtrs += 1 

2390 tdelta -= Timedelta(days=qlen * 7) 

2391 else: 

2392 break 

2393 else: 

2394 tdelta = Timedelta(0) 

2395 

2396 # Note: we always have tdelta.value >= 0 

2397 return start, num_qtrs, tdelta 

2398 

2399 @apply_wraps 

2400 def apply(self, other): 

2401 # Note: self.n == 0 is not allowed. 

2402 n = self.n 

2403 

2404 prev_year_end, num_qtrs, tdelta = self._rollback_to_year(other) 

2405 res = prev_year_end 

2406 n += num_qtrs 

2407 if self.n <= 0 and tdelta.value > 0: 

2408 n += 1 

2409 

2410 # Possible speedup by handling years first. 

2411 years = n // 4 

2412 if years: 

2413 res += self._offset * years 

2414 n -= years * 4 

2415 

2416 # Add an extra day to make *sure* we are getting the quarter lengths 

2417 # for the upcoming year, not the previous year 

2418 qtr_lens = self.get_weeks(res + Timedelta(days=1)) 

2419 

2420 # Note: we always have 0 <= n < 4 

2421 weeks = sum(qtr_lens[:n]) 

2422 if weeks: 

2423 res = liboffsets.shift_day(res, days=weeks * 7) 

2424 

2425 return res 

2426 

2427 def get_weeks(self, dt): 

2428 ret = [13] * 4 

2429 

2430 year_has_extra_week = self.year_has_extra_week(dt) 

2431 

2432 if year_has_extra_week: 

2433 ret[self.qtr_with_extra_week - 1] = 14 

2434 

2435 return ret 

2436 

2437 def year_has_extra_week(self, dt): 

2438 # Avoid round-down errors --> normalize to get 

2439 # e.g. '370D' instead of '360D23H' 

2440 norm = Timestamp(dt).normalize().tz_localize(None) 

2441 

2442 next_year_end = self._offset.rollforward(norm) 

2443 prev_year_end = norm - self._offset 

2444 weeks_in_year = (next_year_end - prev_year_end).days / 7 

2445 assert weeks_in_year in [52, 53], weeks_in_year 

2446 return weeks_in_year == 53 

2447 

2448 def is_on_offset(self, dt): 

2449 if self.normalize and not _is_normalized(dt): 

2450 return False 

2451 if self._offset.is_on_offset(dt): 

2452 return True 

2453 

2454 next_year_end = dt - self._offset 

2455 

2456 qtr_lens = self.get_weeks(dt) 

2457 

2458 current = next_year_end 

2459 for qtr_len in qtr_lens: 

2460 current = liboffsets.shift_day(current, days=qtr_len * 7) 

2461 if dt == current: 

2462 return True 

2463 return False 

2464 

2465 @property 

2466 def rule_code(self): 

2467 suffix = self._offset.get_rule_code_suffix() 

2468 qtr = self.qtr_with_extra_week 

2469 return f"{self._prefix}-{suffix}-{qtr}" 

2470 

2471 @classmethod 

2472 def _from_name(cls, *args): 

2473 return cls( 

2474 **dict(FY5253._parse_suffix(*args[:-1]), qtr_with_extra_week=int(args[-1])) 

2475 ) 

2476 

2477 

2478class Easter(DateOffset): 

2479 """ 

2480 DateOffset for the Easter holiday using logic defined in dateutil. 

2481 

2482 Right now uses the revised method which is valid in years 1583-4099. 

2483 """ 

2484 

2485 _adjust_dst = True 

2486 _attributes = frozenset(["n", "normalize"]) 

2487 

2488 __init__ = BaseOffset.__init__ 

2489 

2490 @apply_wraps 

2491 def apply(self, other): 

2492 current_easter = easter(other.year) 

2493 current_easter = datetime( 

2494 current_easter.year, current_easter.month, current_easter.day 

2495 ) 

2496 current_easter = conversion.localize_pydatetime(current_easter, other.tzinfo) 

2497 

2498 n = self.n 

2499 if n >= 0 and other < current_easter: 

2500 n -= 1 

2501 elif n < 0 and other > current_easter: 

2502 n += 1 

2503 # TODO: Why does this handle the 0 case the opposite of others? 

2504 

2505 # NOTE: easter returns a datetime.date so we have to convert to type of 

2506 # other 

2507 new = easter(other.year + n) 

2508 new = datetime( 

2509 new.year, 

2510 new.month, 

2511 new.day, 

2512 other.hour, 

2513 other.minute, 

2514 other.second, 

2515 other.microsecond, 

2516 ) 

2517 return new 

2518 

2519 def is_on_offset(self, dt): 

2520 if self.normalize and not _is_normalized(dt): 

2521 return False 

2522 return date(dt.year, dt.month, dt.day) == easter(dt.year) 

2523 

2524 

2525# --------------------------------------------------------------------- 

2526# Ticks 

2527 

2528 

2529def _tick_comp(op): 

2530 assert op not in [operator.eq, operator.ne] 

2531 

2532 def f(self, other): 

2533 try: 

2534 return op(self.delta, other.delta) 

2535 except AttributeError: 

2536 # comparing with a non-Tick object 

2537 raise TypeError( 

2538 f"Invalid comparison between {type(self).__name__} " 

2539 f"and {type(other).__name__}" 

2540 ) 

2541 

2542 f.__name__ = f"__{op.__name__}__" 

2543 return f 

2544 

2545 

2546class Tick(liboffsets._Tick, SingleConstructorOffset): 

2547 _inc = Timedelta(microseconds=1000) 

2548 _prefix = "undefined" 

2549 _attributes = frozenset(["n", "normalize"]) 

2550 

2551 def __init__(self, n=1, normalize=False): 

2552 BaseOffset.__init__(self, n, normalize) 

2553 if normalize: 

2554 raise ValueError( 

2555 "Tick offset with `normalize=True` are not allowed." 

2556 ) # GH#21427 

2557 

2558 __gt__ = _tick_comp(operator.gt) 

2559 __ge__ = _tick_comp(operator.ge) 

2560 __lt__ = _tick_comp(operator.lt) 

2561 __le__ = _tick_comp(operator.le) 

2562 

2563 def __add__(self, other): 

2564 if isinstance(other, Tick): 

2565 if type(self) == type(other): 

2566 return type(self)(self.n + other.n) 

2567 else: 

2568 return _delta_to_tick(self.delta + other.delta) 

2569 elif isinstance(other, Period): 

2570 return other + self 

2571 try: 

2572 return self.apply(other) 

2573 except ApplyTypeError: 

2574 return NotImplemented 

2575 except OverflowError: 

2576 raise OverflowError( 

2577 f"the add operation between {self} and {other} will overflow" 

2578 ) 

2579 

2580 def __eq__(self, other: Any) -> bool: 

2581 if isinstance(other, str): 

2582 from pandas.tseries.frequencies import to_offset 

2583 

2584 try: 

2585 # GH#23524 if to_offset fails, we are dealing with an 

2586 # incomparable type so == is False and != is True 

2587 other = to_offset(other) 

2588 except ValueError: 

2589 # e.g. "infer" 

2590 return False 

2591 

2592 if isinstance(other, Tick): 

2593 return self.delta == other.delta 

2594 else: 

2595 return False 

2596 

2597 # This is identical to DateOffset.__hash__, but has to be redefined here 

2598 # for Python 3, because we've redefined __eq__. 

2599 def __hash__(self): 

2600 return hash(self._params) 

2601 

2602 def __ne__(self, other): 

2603 if isinstance(other, str): 

2604 from pandas.tseries.frequencies import to_offset 

2605 

2606 try: 

2607 # GH#23524 if to_offset fails, we are dealing with an 

2608 # incomparable type so == is False and != is True 

2609 other = to_offset(other) 

2610 except ValueError: 

2611 # e.g. "infer" 

2612 return True 

2613 

2614 if isinstance(other, Tick): 

2615 return self.delta != other.delta 

2616 else: 

2617 return True 

2618 

2619 @property 

2620 def delta(self): 

2621 return self.n * self._inc 

2622 

2623 @property 

2624 def nanos(self): 

2625 return delta_to_nanoseconds(self.delta) 

2626 

2627 # TODO: Should Tick have its own apply_index? 

2628 def apply(self, other): 

2629 # Timestamp can handle tz and nano sec, thus no need to use apply_wraps 

2630 if isinstance(other, Timestamp): 

2631 

2632 # GH 15126 

2633 # in order to avoid a recursive 

2634 # call of __add__ and __radd__ if there is 

2635 # an exception, when we call using the + operator, 

2636 # we directly call the known method 

2637 result = other.__add__(self) 

2638 if result is NotImplemented: 

2639 raise OverflowError 

2640 return result 

2641 elif isinstance(other, (datetime, np.datetime64, date)): 

2642 return as_timestamp(other) + self 

2643 

2644 if isinstance(other, timedelta): 

2645 return other + self.delta 

2646 elif isinstance(other, type(self)): 

2647 return type(self)(self.n + other.n) 

2648 

2649 raise ApplyTypeError(f"Unhandled type: {type(other).__name__}") 

2650 

2651 def is_anchored(self): 

2652 return False 

2653 

2654 

2655def _delta_to_tick(delta): 

2656 if delta.microseconds == 0 and getattr(delta, "nanoseconds", 0) == 0: 

2657 # nanoseconds only for pd.Timedelta 

2658 if delta.seconds == 0: 

2659 return Day(delta.days) 

2660 else: 

2661 seconds = delta.days * 86400 + delta.seconds 

2662 if seconds % 3600 == 0: 

2663 return Hour(seconds / 3600) 

2664 elif seconds % 60 == 0: 

2665 return Minute(seconds / 60) 

2666 else: 

2667 return Second(seconds) 

2668 else: 

2669 nanos = delta_to_nanoseconds(delta) 

2670 if nanos % 1000000 == 0: 

2671 return Milli(nanos // 1000000) 

2672 elif nanos % 1000 == 0: 

2673 return Micro(nanos // 1000) 

2674 else: # pragma: no cover 

2675 return Nano(nanos) 

2676 

2677 

2678class Day(Tick): 

2679 _inc = Timedelta(days=1) 

2680 _prefix = "D" 

2681 

2682 

2683class Hour(Tick): 

2684 _inc = Timedelta(hours=1) 

2685 _prefix = "H" 

2686 

2687 

2688class Minute(Tick): 

2689 _inc = Timedelta(minutes=1) 

2690 _prefix = "T" 

2691 

2692 

2693class Second(Tick): 

2694 _inc = Timedelta(seconds=1) 

2695 _prefix = "S" 

2696 

2697 

2698class Milli(Tick): 

2699 _inc = Timedelta(milliseconds=1) 

2700 _prefix = "L" 

2701 

2702 

2703class Micro(Tick): 

2704 _inc = Timedelta(microseconds=1) 

2705 _prefix = "U" 

2706 

2707 

2708class Nano(Tick): 

2709 _inc = Timedelta(nanoseconds=1) 

2710 _prefix = "N" 

2711 

2712 

2713BDay = BusinessDay 

2714BMonthEnd = BusinessMonthEnd 

2715BMonthBegin = BusinessMonthBegin 

2716CBMonthEnd = CustomBusinessMonthEnd 

2717CBMonthBegin = CustomBusinessMonthBegin 

2718CDay = CustomBusinessDay 

2719 

2720# --------------------------------------------------------------------- 

2721 

2722 

2723def generate_range(start=None, end=None, periods=None, offset=BDay()): 

2724 """ 

2725 Generates a sequence of dates corresponding to the specified time 

2726 offset. Similar to dateutil.rrule except uses pandas DateOffset 

2727 objects to represent time increments. 

2728 

2729 Parameters 

2730 ---------- 

2731 start : datetime, (default None) 

2732 end : datetime, (default None) 

2733 periods : int, (default None) 

2734 offset : DateOffset, (default BDay()) 

2735 

2736 Notes 

2737 ----- 

2738 * This method is faster for generating weekdays than dateutil.rrule 

2739 * At least two of (start, end, periods) must be specified. 

2740 * If both start and end are specified, the returned dates will 

2741 satisfy start <= date <= end. 

2742 

2743 Returns 

2744 ------- 

2745 dates : generator object 

2746 """ 

2747 from pandas.tseries.frequencies import to_offset 

2748 

2749 offset = to_offset(offset) 

2750 

2751 start = Timestamp(start) 

2752 start = start if start is not NaT else None 

2753 end = Timestamp(end) 

2754 end = end if end is not NaT else None 

2755 

2756 if start and not offset.is_on_offset(start): 

2757 start = offset.rollforward(start) 

2758 

2759 elif end and not offset.is_on_offset(end): 

2760 end = offset.rollback(end) 

2761 

2762 if periods is None and end < start and offset.n >= 0: 

2763 end = None 

2764 periods = 0 

2765 

2766 if end is None: 

2767 end = start + (periods - 1) * offset 

2768 

2769 if start is None: 

2770 start = end - (periods - 1) * offset 

2771 

2772 cur = start 

2773 if offset.n >= 0: 

2774 while cur <= end: 

2775 yield cur 

2776 

2777 if cur == end: 

2778 # GH#24252 avoid overflows by not performing the addition 

2779 # in offset.apply unless we have to 

2780 break 

2781 

2782 # faster than cur + offset 

2783 next_date = offset.apply(cur) 

2784 if next_date <= cur: 

2785 raise ValueError(f"Offset {offset} did not increment date") 

2786 cur = next_date 

2787 else: 

2788 while cur >= end: 

2789 yield cur 

2790 

2791 if cur == end: 

2792 # GH#24252 avoid overflows by not performing the addition 

2793 # in offset.apply unless we have to 

2794 break 

2795 

2796 # faster than cur + offset 

2797 next_date = offset.apply(cur) 

2798 if next_date >= cur: 

2799 raise ValueError(f"Offset {offset} did not decrement date") 

2800 cur = next_date 

2801 

2802 

2803prefix_mapping = { 

2804 offset._prefix: offset 

2805 for offset in [ 

2806 YearBegin, # 'AS' 

2807 YearEnd, # 'A' 

2808 BYearBegin, # 'BAS' 

2809 BYearEnd, # 'BA' 

2810 BusinessDay, # 'B' 

2811 BusinessMonthBegin, # 'BMS' 

2812 BusinessMonthEnd, # 'BM' 

2813 BQuarterEnd, # 'BQ' 

2814 BQuarterBegin, # 'BQS' 

2815 BusinessHour, # 'BH' 

2816 CustomBusinessDay, # 'C' 

2817 CustomBusinessMonthEnd, # 'CBM' 

2818 CustomBusinessMonthBegin, # 'CBMS' 

2819 CustomBusinessHour, # 'CBH' 

2820 MonthEnd, # 'M' 

2821 MonthBegin, # 'MS' 

2822 Nano, # 'N' 

2823 SemiMonthEnd, # 'SM' 

2824 SemiMonthBegin, # 'SMS' 

2825 Week, # 'W' 

2826 Second, # 'S' 

2827 Minute, # 'T' 

2828 Micro, # 'U' 

2829 QuarterEnd, # 'Q' 

2830 QuarterBegin, # 'QS' 

2831 Milli, # 'L' 

2832 Hour, # 'H' 

2833 Day, # 'D' 

2834 WeekOfMonth, # 'WOM' 

2835 FY5253, 

2836 FY5253Quarter, 

2837 ] 

2838}