Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/core/arrays/period.py: 24%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

498 statements  

1from __future__ import annotations 

2 

3from datetime import timedelta 

4import operator 

5from typing import ( 

6 TYPE_CHECKING, 

7 Any, 

8 Literal, 

9 Self, 

10 TypeVar, 

11 cast, 

12) 

13import warnings 

14 

15import numpy as np 

16 

17from pandas._libs import ( 

18 algos as libalgos, 

19 lib, 

20) 

21from pandas._libs.arrays import NDArrayBacked 

22from pandas._libs.tslibs import ( 

23 BaseOffset, 

24 Day, 

25 NaT, 

26 NaTType, 

27 Resolution, 

28 Timedelta, 

29 add_overflowsafe, 

30 astype_overflowsafe, 

31 dt64arr_to_periodarr as c_dt64arr_to_periodarr, 

32 get_unit_from_dtype, 

33 iNaT, 

34 parsing, 

35 period as libperiod, 

36 to_offset, 

37) 

38from pandas._libs.tslibs.ccalendar import MONTH_NUMBERS 

39from pandas._libs.tslibs.dtypes import ( 

40 FreqGroup, 

41 PeriodDtypeBase, 

42) 

43from pandas._libs.tslibs.offsets import ( 

44 Tick, 

45 delta_to_tick, 

46) 

47from pandas._libs.tslibs.period import ( 

48 DIFFERENT_FREQ, 

49 INT_TO_PERIOD_DEPR_MSG, 

50 IncompatibleFrequency, 

51 Period, 

52 get_period_field_arr, 

53 period_asfreq_arr, 

54) 

55from pandas.errors import Pandas4Warning 

56from pandas.util._decorators import ( 

57 cache_readonly, 

58 set_module, 

59) 

60from pandas.util._exceptions import find_stack_level 

61 

62from pandas.core.dtypes.common import ( 

63 ensure_object, 

64 pandas_dtype, 

65) 

66from pandas.core.dtypes.dtypes import ( 

67 DatetimeTZDtype, 

68 PeriodDtype, 

69) 

70from pandas.core.dtypes.generic import ( 

71 ABCIndex, 

72 ABCPeriodIndex, 

73 ABCSeries, 

74 ABCTimedeltaArray, 

75) 

76from pandas.core.dtypes.missing import isna 

77 

78from pandas.core.arrays import ( 

79 ExtensionArray, 

80 datetimelike as dtl, 

81) 

82import pandas.core.common as com 

83from pandas.core.construction import extract_array 

84 

85if TYPE_CHECKING: 

86 from collections.abc import ( 

87 Callable, 

88 Sequence, 

89 ) 

90 

91 from pandas._typing import ( 

92 AnyArrayLike, 

93 Dtype, 

94 DtypeObj, 

95 FillnaOptions, 

96 NpDtype, 

97 NumpySorter, 

98 NumpyValueArrayLike, 

99 npt, 

100 ) 

101 

102 from pandas.core.dtypes.dtypes import ExtensionDtype 

103 

104 from pandas.core.arrays import ( 

105 DatetimeArray, 

106 TimedeltaArray, 

107 ) 

108 

109 

110BaseOffsetT = TypeVar("BaseOffsetT", bound=BaseOffset) 

111 

112 

113def _field_accessor(name: str, docstring: str | None = None): 

114 def f(self): 

115 base = self.dtype._dtype_code 

116 result = get_period_field_arr(name, self.asi8, base) 

117 return result 

118 

119 f.__name__ = name 

120 f.__doc__ = docstring 

121 return property(f) 

122 

123 

124@set_module("pandas.arrays") 

125class PeriodArray(dtl.DatelikeOps, libperiod.PeriodMixin): 

126 """ 

127 Pandas ExtensionArray for storing Period data. 

128 

129 Users should use :func:`~pandas.array` to create new instances. 

130 

131 Parameters 

132 ---------- 

133 values : Union[PeriodArray, Series[period], ndarray[int], PeriodIndex] 

134 The data to store. These should be arrays that can be directly 

135 converted to ordinals without inference or copy (PeriodArray, 

136 ndarray[int64]), or a box around such an array (Series[period], 

137 PeriodIndex). 

138 dtype : PeriodDtype, optional 

139 A PeriodDtype instance from which to extract a `freq`. If both 

140 `freq` and `dtype` are specified, then the frequencies must match. 

141 copy : bool, default False 

142 Whether to copy the ordinals before storing. 

143 

144 Attributes 

145 ---------- 

146 None 

147 

148 Methods 

149 ------- 

150 None 

151 

152 See Also 

153 -------- 

154 Period: Represents a period of time. 

155 PeriodIndex : Immutable Index for period data. 

156 period_range: Create a fixed-frequency PeriodArray. 

157 array: Construct a pandas array. 

158 

159 Notes 

160 ----- 

161 There are two components to a PeriodArray 

162 

163 - ordinals : integer ndarray 

164 - freq : pd.tseries.offsets.Offset 

165 

166 The values are physically stored as a 1-D ndarray of integers. These are 

167 called "ordinals" and represent some kind of offset from a base. 

168 

169 The `freq` indicates the span covered by each element of the array. 

170 All elements in the PeriodArray have the same `freq`. 

171 

172 Examples 

173 -------- 

174 >>> pd.arrays.PeriodArray(pd.PeriodIndex(["2023-01-01", "2023-01-02"], freq="D")) 

175 <PeriodArray> 

176 ['2023-01-01', '2023-01-02'] 

177 Length: 2, dtype: period[D] 

178 """ 

179 

180 # array priority higher than numpy scalars 

181 __array_priority__ = 1000 

182 _typ = "periodarray" # ABCPeriodArray 

183 _internal_fill_value = np.int64(iNaT) 

184 _recognized_scalars = (Period,) 

185 _is_recognized_dtype: Callable[[DtypeObj], bool] = lambda x: isinstance( 

186 x, PeriodDtype 

187 ) # check_compatible_with checks freq match 

188 

189 @property 

190 def _scalar_type(self) -> type[Period]: 

191 return Period 

192 

193 # Names others delegate to us 

194 _other_ops: list[str] = [] 

195 _bool_ops: list[str] = ["is_leap_year"] 

196 _object_ops: list[str] = ["start_time", "end_time", "freq"] 

197 _field_ops: list[str] = [ 

198 "year", 

199 "month", 

200 "day", 

201 "hour", 

202 "minute", 

203 "second", 

204 "weekofyear", 

205 "week", 

206 "day_of_week", 

207 "day_of_year", 

208 "quarter", 

209 "qyear", 

210 "days_in_month", 

211 ] 

212 _datetimelike_ops: list[str] = _field_ops + _object_ops + _bool_ops 

213 _datetimelike_methods: list[str] = ["strftime", "to_timestamp", "asfreq"] 

214 

215 _dtype: PeriodDtype 

216 

217 # -------------------------------------------------------------------- 

218 # Constructors 

219 

220 def __init__(self, values, dtype: Dtype | None = None, copy: bool = False) -> None: 

221 if dtype is not None: 

222 dtype = pandas_dtype(dtype) 

223 if not isinstance(dtype, PeriodDtype): 

224 raise ValueError(f"Invalid dtype {dtype} for PeriodArray") 

225 

226 if isinstance(values, ABCSeries): 

227 values = values._values 

228 if not isinstance(values, type(self)): 

229 raise TypeError("Incorrect dtype") 

230 

231 elif isinstance(values, ABCPeriodIndex): 

232 values = values._values 

233 

234 if isinstance(values, type(self)): 

235 if dtype is not None and dtype != values.dtype: 

236 raise raise_on_incompatible(values, dtype.freq) 

237 values, dtype = values._ndarray, values.dtype 

238 

239 if not copy: 

240 values = np.asarray(values, dtype="int64") 

241 else: 

242 values = np.array(values, dtype="int64", copy=copy) 

243 if dtype is None: 

244 raise ValueError("dtype is not specified and cannot be inferred") 

245 dtype = cast("PeriodDtype", dtype) 

246 NDArrayBacked.__init__(self, values, dtype) 

247 

248 # error: Signature of "_simple_new" incompatible with supertype "NDArrayBacked" 

249 @classmethod 

250 def _simple_new( # type: ignore[override] 

251 cls, 

252 values: npt.NDArray[np.int64], 

253 dtype: PeriodDtype, 

254 ) -> Self: 

255 # alias for PeriodArray.__init__ 

256 assertion_msg = "Should be numpy array of type i8" 

257 assert isinstance(values, np.ndarray) and values.dtype == "i8", assertion_msg 

258 return cls(values, dtype=dtype) 

259 

260 @classmethod 

261 def _from_sequence( 

262 cls, 

263 scalars, 

264 *, 

265 dtype: Dtype | None = None, 

266 copy: bool = False, 

267 ) -> Self: 

268 if dtype is not None: 

269 dtype = pandas_dtype(dtype) 

270 

271 # Note: we can't do extract_array until after from_datetime64 path 

272 # bc of GH#64241 

273 data_dtype = getattr(scalars, "dtype", None) 

274 if lib.is_np_dtype(data_dtype, "M"): 

275 freq = dtype._freq if dtype is not None else None # type: ignore[union-attr] 

276 return cls._from_datetime64(scalars, freq) 

277 

278 scalars = extract_array(scalars, extract_numpy=True) 

279 if isinstance(scalars, cls): 

280 if dtype is not None: 

281 return scalars.astype(dtype, copy=copy) 

282 elif copy: 

283 return scalars.copy() 

284 return scalars 

285 

286 if not isinstance( 

287 scalars, (np.ndarray, list, tuple, ABCSeries, ABCIndex, ExtensionArray) 

288 ): 

289 # test_constructor_empty_special has a case with an iter object 

290 scalars = list(scalars) 

291 

292 if isinstance(scalars, ExtensionArray) and scalars.dtype.kind in "iu": 

293 # e.g. masked or arrow-backed integer array; np.asarray would cast 

294 # integers-with-NA to float and raise a misleading "floating point" 

295 # error below, so route through object dtype to keep the integers. 

296 scalars = scalars.to_numpy(dtype=object, na_value=NaT) 

297 

298 arrdata = np.asarray(scalars) 

299 if ( 

300 arrdata.dtype.kind == "u" 

301 and arrdata.size 

302 and arrdata.max() > np.iinfo(np.int64).max 

303 ): 

304 # GH#64231 the int64 cast below would wrap these silently; read 

305 # them through the object path, which rejects them the way the 

306 # Period(int) scalar constructor does. 

307 arrdata = arrdata.astype(object) 

308 

309 if arrdata.dtype.kind == "f" and len(arrdata) > 0: 

310 if not lib.all_nans(arrdata): 

311 raise TypeError( 

312 "PeriodArray does not allow floating point in construction" 

313 ) 

314 ordinals = np.full(arrdata.shape, iNaT, dtype=np.int64) 

315 return cls(ordinals, dtype=dtype) 

316 

317 elif arrdata.dtype.kind in "iu": 

318 # GH#64227 enforcing means dropping from_calendar_ordinals here and 

319 # reading arrdata as ordinals; the object-dtype and Period-scalar 

320 # paths in tslibs.period must be enforced at the same time or the 

321 # two interpretations diverge again. 

322 warnings.warn( 

323 INT_TO_PERIOD_DEPR_MSG, 

324 Pandas4Warning, 

325 stacklevel=find_stack_level(), 

326 ) 

327 arr = arrdata.astype(np.int64, copy=False) 

328 ordinals = libperiod.from_calendar_ordinals(arr, dtype) # type: ignore[arg-type] 

329 return cls(ordinals, dtype=dtype) 

330 else: 

331 periods = ensure_object(arrdata) 

332 

333 if dtype is None: 

334 dtype_base = libperiod.extract_period_unit(periods) 

335 dtype = PeriodDtype(dtype_base) 

336 ordinals = libperiod.extract_ordinals(periods, dtype) # type: ignore[arg-type] 

337 return cls(ordinals, dtype=dtype) 

338 

339 @classmethod 

340 def _from_sequence_of_strings( 

341 cls, strings, *, dtype: ExtensionDtype, copy: bool = False 

342 ) -> Self: 

343 return cls._from_sequence(strings, dtype=dtype, copy=copy) 

344 

345 @classmethod 

346 def _from_datetime64(cls, data, freq, tz=None) -> Self: 

347 """ 

348 Construct a PeriodArray from a datetime64 array 

349 

350 Parameters 

351 ---------- 

352 data : ndarray[datetime64[ns], datetime64[ns, tz]] 

353 freq : str or Tick 

354 tz : tzinfo, optional 

355 

356 Returns 

357 ------- 

358 PeriodArray[freq] 

359 """ 

360 if isinstance(freq, BaseOffset): 

361 freq = PeriodDtype(freq)._freqstr 

362 data, freq = dt64arr_to_periodarr(data, freq, tz) 

363 dtype = PeriodDtype(freq) 

364 return cls(data, dtype=dtype) 

365 

366 @classmethod 

367 def _generate_range(cls, start, end, periods, freq): 

368 periods = dtl.validate_periods(periods) 

369 

370 if freq is not None: 

371 freq = Period._maybe_convert_freq(freq) 

372 

373 if start is not None or end is not None: 

374 subarr, freq = _get_ordinal_range(start, end, periods, freq) 

375 else: 

376 raise ValueError("Not enough parameters to construct Period range") 

377 

378 return subarr, freq 

379 

380 @classmethod 

381 def _from_fields(cls, *, fields: dict[str, Any], freq) -> Self: 

382 subarr, freq = _range_from_fields(freq=freq, **fields) 

383 dtype = PeriodDtype(freq) 

384 return cls._simple_new(subarr, dtype=dtype) 

385 

386 # ----------------------------------------------------------------- 

387 # DatetimeLike Interface 

388 

389 def _unbox_scalar( 

390 self, 

391 # error: Argument 1 of "_unbox_scalar" is incompatible with supertype 

392 # "pandas.core.arrays.datetimelike.DatetimeLikeArrayMixin"; supertype 

393 # defines the argument type as "Period | Timestamp | Timedelta | NaTType" 

394 value: Period | NaTType, # type: ignore[override] 

395 ) -> np.int64: 

396 if value is NaT: 

397 return np.int64(value._value) 

398 elif isinstance(value, self._scalar_type): 

399 self._check_compatible_with(value) 

400 return np.int64(value.ordinal) 

401 else: 

402 raise ValueError(f"'value' should be a Period. Got '{value}' instead.") 

403 

404 def _scalar_from_string(self, value: str) -> Period: 

405 return Period(value, freq=self.freq) 

406 

407 def _check_compatible_with( 

408 self, 

409 # error: Argument 1 of "_check_compatible_with" is incompatible with 

410 # supertype "pandas.core.arrays.datetimelike.DatetimeLikeArrayMixin"; 

411 # supertype defines the argument type as "Period | Timestamp | Timedelta 

412 # | NaTType" 

413 other: Period | NaTType | PeriodArray, # type: ignore[override] 

414 ) -> None: 

415 if other is NaT: 

416 return 

417 elif isinstance(other, Period): 

418 self._require_matching_unit(other._dtype._freqstr) 

419 else: 

420 # error: Item "NaTType" of "NaTType | PeriodArray" has no 

421 # attribute "freq" 

422 self._require_matching_unit(other.dtype._freqstr) # type: ignore[union-attr] 

423 

424 # -------------------------------------------------------------------- 

425 # Data / Attributes 

426 

427 @cache_readonly 

428 def dtype(self) -> PeriodDtype: 

429 return self._dtype 

430 

431 @property 

432 def freq(self) -> BaseOffset: 

433 """ 

434 Return the frequency object for this PeriodArray. 

435 """ 

436 return self.dtype.freq 

437 

438 @property 

439 def freqstr(self) -> str: 

440 return PeriodDtype(self.freq)._freqstr 

441 

442 @property # NB: override with cache_readonly in immutable subclasses 

443 def _resolution_obj(self) -> Resolution: 

444 freqstr = self.freqstr 

445 try: 

446 return Resolution.get_reso_from_freqstr(freqstr) 

447 except KeyError: 

448 return None # type: ignore[return-value] 

449 

450 def __array__( 

451 self, dtype: NpDtype | None = None, copy: bool | None = None 

452 ) -> np.ndarray: 

453 if dtype == "i8": 

454 # For NumPy 1.x compatibility we cannot use copy=None. And 

455 # `copy=False` has the meaning of `copy=None` here: 

456 if not copy: 

457 result = np.asarray(self.asi8, dtype=dtype) 

458 if self._readonly: 

459 result = result.view() 

460 result.flags.writeable = False 

461 return result 

462 else: 

463 return np.array(self.asi8, dtype=dtype) 

464 

465 if copy is False: 

466 raise ValueError( 

467 "Unable to avoid copy while creating an array as requested." 

468 ) 

469 

470 if dtype == bool: 

471 return ~self._isnan 

472 

473 # This will raise TypeError for non-object dtypes 

474 return np.array(list(self), dtype=object) 

475 

476 def __arrow_array__(self, type=None): 

477 """ 

478 Convert myself into a pyarrow Array. 

479 """ 

480 import pyarrow 

481 

482 from pandas.core.arrays.arrow.extension_types import ArrowPeriodType 

483 

484 if type is not None: 

485 if pyarrow.types.is_integer(type): 

486 return pyarrow.array(self._ndarray, mask=self.isna(), type=type) 

487 elif isinstance(type, ArrowPeriodType): 

488 # ensure we have the same freq 

489 if self.freqstr != type.freq: 

490 raise TypeError( 

491 "Not supported to convert PeriodArray to array with different " 

492 f"'freq' ({self.freqstr} vs {type.freq})" 

493 ) 

494 else: 

495 raise TypeError( 

496 f"Not supported to convert PeriodArray to '{type}' type" 

497 ) 

498 

499 period_type = ArrowPeriodType(self.freqstr) 

500 storage_array = pyarrow.array(self._ndarray, mask=self.isna(), type="int64") 

501 return pyarrow.ExtensionArray.from_storage(period_type, storage_array) 

502 

503 # -------------------------------------------------------------------- 

504 # Vectorized analogues of Period properties 

505 

506 year = _field_accessor( 

507 "year", 

508 """ 

509 The year of the period. 

510 

511 Returns the year component for each period in the index. 

512 

513 See Also 

514 -------- 

515 PeriodIndex.day_of_year : The ordinal day of the year. 

516 PeriodIndex.dayofyear : The ordinal day of the year. 

517 PeriodIndex.is_leap_year : Logical indicating if the date belongs to a 

518 leap year. 

519 PeriodIndex.weekofyear : The week ordinal of the year. 

520 PeriodIndex.year : The year of the period. 

521 

522 Examples 

523 -------- 

524 >>> idx = pd.PeriodIndex(["2023", "2024", "2025"], freq="Y") 

525 >>> idx.year 

526 Index([2023, 2024, 2025], dtype='int64') 

527 """, 

528 ) 

529 month = _field_accessor( 

530 "month", 

531 """ 

532 The month as January=1, December=12. 

533 

534 Returns the month component for each period in the index as an 

535 integer, where January is 1 and December is 12. 

536 

537 See Also 

538 -------- 

539 PeriodIndex.days_in_month : The number of days in the month. 

540 PeriodIndex.daysinmonth : The number of days in the month. 

541 

542 Examples 

543 -------- 

544 >>> idx = pd.PeriodIndex(["2023-01", "2023-02", "2023-03"], freq="M") 

545 >>> idx.month 

546 Index([1, 2, 3], dtype='int64') 

547 """, 

548 ) 

549 day = _field_accessor( 

550 "day", 

551 """ 

552 The days of the period. 

553 

554 Returns the day-of-month component for each period in the index. 

555 

556 See Also 

557 -------- 

558 PeriodIndex.day_of_week : The day of the week with Monday=0, Sunday=6. 

559 PeriodIndex.day_of_year : The ordinal day of the year. 

560 PeriodIndex.dayofweek : The day of the week with Monday=0, Sunday=6. 

561 PeriodIndex.dayofyear : The ordinal day of the year. 

562 PeriodIndex.days_in_month : The number of days in the month. 

563 PeriodIndex.daysinmonth : The number of days in the month. 

564 PeriodIndex.weekday : The day of the week with Monday=0, Sunday=6. 

565 

566 Examples 

567 -------- 

568 >>> idx = pd.PeriodIndex(['2020-01-31', '2020-02-28'], freq='D') 

569 >>> idx.day 

570 Index([31, 28], dtype='int64') 

571 """, 

572 ) 

573 hour = _field_accessor( 

574 "hour", 

575 """ 

576 The hour of the period. 

577 

578 Returns the hour component for each period in the index. 

579 

580 See Also 

581 -------- 

582 PeriodIndex.minute : The minute of the period. 

583 PeriodIndex.second : The second of the period. 

584 PeriodIndex.to_timestamp : Cast to DatetimeArray/Index. 

585 

586 Examples 

587 -------- 

588 >>> idx = pd.PeriodIndex(["2023-01-01 10:00", "2023-01-01 11:00"], freq='h') 

589 >>> idx.hour 

590 Index([10, 11], dtype='int64') 

591 """, 

592 ) 

593 minute = _field_accessor( 

594 "minute", 

595 """ 

596 The minute of the period. 

597 

598 Returns the minute component for each period in the index. 

599 

600 See Also 

601 -------- 

602 PeriodIndex.hour : The hour of the period. 

603 PeriodIndex.second : The second of the period. 

604 PeriodIndex.to_timestamp : Cast to DatetimeArray/Index. 

605 

606 Examples 

607 -------- 

608 >>> idx = pd.PeriodIndex(["2023-01-01 10:30:00", 

609 ... "2023-01-01 11:50:00"], freq='min') 

610 >>> idx.minute 

611 Index([30, 50], dtype='int64') 

612 """, 

613 ) 

614 second = _field_accessor( 

615 "second", 

616 """ 

617 The second of the period. 

618 

619 Returns the second component for each period in the index. 

620 

621 See Also 

622 -------- 

623 PeriodIndex.hour : The hour of the period. 

624 PeriodIndex.minute : The minute of the period. 

625 PeriodIndex.to_timestamp : Cast to DatetimeArray/Index. 

626 

627 Examples 

628 -------- 

629 >>> idx = pd.PeriodIndex(["2023-01-01 10:00:30", 

630 ... "2023-01-01 10:00:31"], freq='s') 

631 >>> idx.second 

632 Index([30, 31], dtype='int64') 

633 """, 

634 ) 

635 weekofyear = _field_accessor( 

636 "week", 

637 """ 

638 The week ordinal of the year. 

639 

640 Returns the week number (1 through 53) for each period in the index. 

641 

642 See Also 

643 -------- 

644 PeriodIndex.day_of_week : The day of the week with Monday=0, Sunday=6. 

645 PeriodIndex.dayofweek : The day of the week with Monday=0, Sunday=6. 

646 PeriodIndex.week : The week ordinal of the year. 

647 PeriodIndex.weekday : The day of the week with Monday=0, Sunday=6. 

648 PeriodIndex.year : The year of the period. 

649 

650 Examples 

651 -------- 

652 >>> idx = pd.PeriodIndex(["2023-01", "2023-02", "2023-03"], freq="M") 

653 >>> idx.week # It can be written `weekofyear` 

654 Index([5, 9, 13], dtype='int64') 

655 """, 

656 ) 

657 week = weekofyear 

658 day_of_week = _field_accessor( 

659 "day_of_week", 

660 """ 

661 The day of the week with Monday=0, Sunday=6. 

662 

663 Returns the day-of-week component for each period, following the 

664 Python convention where Monday is 0 and Sunday is 6. 

665 

666 See Also 

667 -------- 

668 PeriodIndex.day : The days of the period. 

669 PeriodIndex.day_of_week : The day of the week with Monday=0, Sunday=6. 

670 PeriodIndex.day_of_year : The ordinal day of the year. 

671 PeriodIndex.dayofweek : The day of the week with Monday=0, Sunday=6. 

672 PeriodIndex.dayofyear : The ordinal day of the year. 

673 PeriodIndex.week : The week ordinal of the year. 

674 PeriodIndex.weekday : The day of the week with Monday=0, Sunday=6. 

675 PeriodIndex.weekofyear : The week ordinal of the year. 

676 

677 Examples 

678 -------- 

679 >>> idx = pd.PeriodIndex(["2023-01-01", "2023-01-02", "2023-01-03"], freq="D") 

680 >>> idx.day_of_week 

681 Index([6, 0, 1], dtype='int64') 

682 """, 

683 ) 

684 

685 @property 

686 def weekday(self): 

687 """ 

688 The day of the week with Monday=0, Sunday=6. 

689 

690 .. deprecated:: 3.1.0 

691 Use :attr:`PeriodArray.day_of_week` instead. 

692 """ 

693 # GH#12816 

694 warnings.warn( 

695 "PeriodArray.weekday is deprecated and will be removed " 

696 "in a future version. Use PeriodArray.day_of_week instead.", 

697 Pandas4Warning, 

698 stacklevel=find_stack_level(), 

699 ) 

700 return self.day_of_week 

701 

702 @property 

703 def dayofweek(self): 

704 """ 

705 The day of the week with Monday=0, Sunday=6. 

706 

707 .. deprecated:: 3.1.0 

708 Use :attr:`PeriodArray.day_of_week` instead. 

709 """ 

710 warnings.warn( 

711 "PeriodArray.dayofweek is deprecated and will be removed in a " 

712 "future version. Use PeriodArray.day_of_week instead.", 

713 Pandas4Warning, 

714 stacklevel=find_stack_level(), 

715 ) 

716 return self.day_of_week 

717 

718 day_of_year = _field_accessor( 

719 "day_of_year", 

720 """ 

721 The ordinal day of the year. 

722 

723 Returns the day-of-year component for each period, ranging from 

724 1 (January 1st) to 365 or 366 for leap years. 

725 

726 See Also 

727 -------- 

728 PeriodIndex.day : The days of the period. 

729 PeriodIndex.day_of_week : The day of the week with Monday=0, Sunday=6. 

730 PeriodIndex.day_of_year : The ordinal day of the year. 

731 PeriodIndex.dayofweek : The day of the week with Monday=0, Sunday=6. 

732 PeriodIndex.dayofyear : The ordinal day of the year. 

733 PeriodIndex.weekday : The day of the week with Monday=0, Sunday=6. 

734 PeriodIndex.weekofyear : The week ordinal of the year. 

735 PeriodIndex.year : The year of the period. 

736 

737 Examples 

738 -------- 

739 >>> idx = pd.PeriodIndex(["2023-01-10", "2023-02-01", "2023-03-01"], freq="D") 

740 >>> idx.day_of_year 

741 Index([10, 32, 60], dtype='int64') 

742 

743 >>> idx = pd.PeriodIndex(["2023", "2024", "2025"], freq="Y") 

744 >>> idx 

745 PeriodIndex(['2023', '2024', '2025'], dtype='period[Y-DEC]') 

746 >>> idx.day_of_year 

747 Index([365, 366, 365], dtype='int64') 

748 """, 

749 ) 

750 

751 @property 

752 def dayofyear(self): 

753 """ 

754 The ordinal day of the year. 

755 

756 .. deprecated:: 3.1.0 

757 Use :attr:`PeriodArray.day_of_year` instead. 

758 """ 

759 warnings.warn( 

760 "PeriodArray.dayofyear is deprecated and will be removed in a " 

761 "future version. Use PeriodArray.day_of_year instead.", 

762 Pandas4Warning, 

763 stacklevel=find_stack_level(), 

764 ) 

765 return self.day_of_year 

766 

767 quarter = _field_accessor( 

768 "quarter", 

769 """ 

770 The quarter of the date. 

771 

772 Returns the quarter (1 through 4) for each period in the index. 

773 

774 See Also 

775 -------- 

776 PeriodIndex.qyear : Fiscal year the Period lies in according to its 

777 starting-quarter. 

778 

779 Examples 

780 -------- 

781 >>> idx = pd.PeriodIndex(["2023-01", "2023-02", "2023-03"], freq="M") 

782 >>> idx.quarter 

783 Index([1, 1, 1], dtype='int64') 

784 """, 

785 ) 

786 qyear = _field_accessor( 

787 "qyear", 

788 """ 

789 Fiscal year the Period lies in according to its starting-quarter. 

790 

791 The `year` and the `qyear` of the period will be the same if the fiscal 

792 and calendar years are the same. When they are not, the fiscal year 

793 can be different from the calendar year of the period. 

794 

795 Returns 

796 ------- 

797 int 

798 The fiscal year of the period. 

799 

800 See Also 

801 -------- 

802 PeriodIndex.quarter : The quarter of the date. 

803 PeriodIndex.year : The year of the period. 

804 

805 Examples 

806 -------- 

807 If the natural and fiscal year are the same, `qyear` and `year` will 

808 be the same. 

809 

810 >>> per = pd.Period('2018Q1', freq='Q') 

811 >>> per.qyear 

812 2018 

813 >>> per.year 

814 2018 

815 

816 If the fiscal year starts in April (`Q-MAR`), the first quarter of 

817 2018 will start in April 2017. `year` will then be 2017, but `qyear` 

818 will be the fiscal year, 2018. 

819 

820 >>> per = pd.Period('2018Q1', freq='Q-MAR') 

821 >>> per.start_time 

822 Timestamp('2017-04-01 00:00:00') 

823 >>> per.qyear 

824 2018 

825 >>> per.year 

826 2017 

827 """, 

828 ) 

829 

830 days_in_month = _field_accessor( 

831 "days_in_month", 

832 """ 

833 The number of days in the month. 

834 

835 Returns the total number of days in the month of each period, 

836 accounting for leap years. 

837 

838 See Also 

839 -------- 

840 PeriodIndex.day : The days of the period. 

841 PeriodIndex.days_in_month : The number of days in the month. 

842 PeriodIndex.daysinmonth : The number of days in the month. 

843 PeriodIndex.month : The month as January=1, December=12. 

844 

845 Examples 

846 -------- 

847 For Series: 

848 

849 >>> period = pd.period_range('2020-1-1 00:00', '2020-3-1 00:00', freq='M') 

850 >>> s = pd.Series(period) 

851 >>> s 

852 0 2020-01 

853 1 2020-02 

854 2 2020-03 

855 dtype: period[M] 

856 >>> s.dt.days_in_month 

857 0 31 

858 1 29 

859 2 31 

860 dtype: int64 

861 

862 For PeriodIndex: 

863 

864 >>> idx = pd.PeriodIndex(["2023-01", "2023-02", "2023-03"], freq="M") 

865 >>> idx.days_in_month # It can be also entered as `daysinmonth` 

866 Index([31, 28, 31], dtype='int64') 

867 """, 

868 ) 

869 

870 @property 

871 def daysinmonth(self): 

872 """ 

873 The number of days in the month. 

874 

875 .. deprecated:: 3.1.0 

876 Use :attr:`PeriodArray.days_in_month` instead. 

877 """ 

878 warnings.warn( 

879 "PeriodArray.daysinmonth is deprecated and will be removed in a " 

880 "future version. Use PeriodArray.days_in_month instead.", 

881 Pandas4Warning, 

882 stacklevel=find_stack_level(), 

883 ) 

884 return self.days_in_month 

885 

886 @property 

887 def is_leap_year(self) -> npt.NDArray[np.bool_]: 

888 """ 

889 Logical indicating if the date belongs to a leap year. 

890 

891 Returns a boolean array where ``True`` indicates the period's year 

892 is a leap year. 

893 

894 See Also 

895 -------- 

896 PeriodIndex.qyear : Fiscal year the Period lies in according to its 

897 starting-quarter. 

898 PeriodIndex.year : The year of the period. 

899 

900 Examples 

901 -------- 

902 >>> idx = pd.PeriodIndex(["2023", "2024", "2025"], freq="Y") 

903 >>> idx.is_leap_year 

904 array([False, True, False]) 

905 """ 

906 # NaT gives year == -1, which the modulo below reports as not-leap 

907 year = np.asarray(self.year) 

908 return (year % 400 == 0) | ((year % 4 == 0) & (year % 100 > 0)) 

909 

910 def to_timestamp(self, freq=None, how: str = "start") -> DatetimeArray: 

911 """ 

912 Cast to DatetimeArray/Index. 

913 

914 If possible, gives microsecond-unit DatetimeArray/Index. Otherwise 

915 gives nanosecond unit. On the ``Series.dt`` accessor, this is only 

916 available for period-typed Series. 

917 

918 Parameters 

919 ---------- 

920 freq : str or DateOffset, optional 

921 Target frequency. The default is 'D' for week or longer, 

922 's' otherwise. 

923 how : {'start', 'end', 's', 'e'}, default 'start' 

924 Whether to use the start or end of the time period being converted. 

925 Case-insensitive. 

926 

927 Returns 

928 ------- 

929 DatetimeArray/Index 

930 Timestamp representation of given Period-like object. 

931 

932 See Also 

933 -------- 

934 PeriodIndex.day : The days of the period. 

935 PeriodIndex.from_fields : Construct a PeriodIndex from fields 

936 (year, month, day, etc.). 

937 PeriodIndex.from_ordinals : Construct a PeriodIndex from ordinals. 

938 PeriodIndex.hour : The hour of the period. 

939 PeriodIndex.minute : The minute of the period. 

940 PeriodIndex.month : The month as January=1, December=12. 

941 PeriodIndex.second : The second of the period. 

942 PeriodIndex.year : The year of the period. 

943 

944 Examples 

945 -------- 

946 >>> idx = pd.PeriodIndex(["2023-01", "2023-02", "2023-03"], freq="M") 

947 >>> idx.to_timestamp() 

948 DatetimeIndex(['2023-01-01', '2023-02-01', '2023-03-01'], 

949 dtype='datetime64[us]', freq='MS') 

950 

951 The frequency will not be inferred if the index contains less than 

952 three elements, or if the values of index are not strictly monotonic: 

953 

954 >>> idx = pd.PeriodIndex(["2023-01", "2023-02"], freq="M") 

955 >>> idx.to_timestamp() 

956 DatetimeIndex(['2023-01-01', '2023-02-01'], dtype='datetime64[us]', freq=None) 

957 

958 >>> idx = pd.PeriodIndex( 

959 ... ["2023-01", "2023-02", "2023-02", "2023-03"], freq="2M" 

960 ... ) 

961 >>> idx.to_timestamp() 

962 DatetimeIndex(['2023-01-01', '2023-02-01', '2023-02-01', '2023-03-01'], 

963 dtype='datetime64[us]', freq=None) 

964 """ 

965 from pandas.core.arrays import DatetimeArray 

966 

967 how = libperiod.validate_end_alias(how) 

968 

969 end = how == "E" 

970 if end: 

971 if freq is not None: 

972 # GH#63760 normalize so e.g. "1ns" is recognized as nanosecond 

973 freq = Period._maybe_convert_freq(freq) 

974 ns_target = freq is not None and freq.base == "ns" 

975 if freq == "B" or self.freq == "B": 

976 # roll forward to ensure we land on B date 

977 stamp = self.to_timestamp(how="start") 

978 unit = "ns" if ns_target else stamp.unit 

979 adjust = Timedelta(1, unit="D") - Timedelta(1, unit=unit) 

980 return stamp + adjust 

981 else: 

982 stamp = (self + self.freq).to_timestamp(how="start") 

983 unit = "ns" if ns_target else stamp.unit 

984 return stamp - Timedelta(1, unit=unit) 

985 

986 if freq is None: 

987 freq_code = self._dtype._get_to_timestamp_base() 

988 dtype = PeriodDtypeBase(freq_code, 1) 

989 freq = dtype._freqstr 

990 base = freq_code 

991 else: 

992 freq = Period._maybe_convert_freq(freq) 

993 base = freq._period_dtype_code 

994 

995 new_parr = self.asfreq(freq, how=how) 

996 

997 new_data = libperiod.periodarr_to_dt64arr(new_parr.asi8, base) 

998 # GH#63760 periodarr_to_dt64arr gives nanoseconds only for the 

999 # nanosecond target base and microseconds otherwise, so new_data 

1000 # already carries the correct resolution. 

1001 dta = DatetimeArray._from_sequence(new_data, dtype=new_data.dtype) 

1002 return dta 

1003 

1004 def _to_timestamp_freq( 

1005 self, dta: DatetimeArray, target_freq, how: str 

1006 ) -> BaseOffset | None: 

1007 """ 

1008 Determine the freq to stamp on the DatetimeArray returned by 

1009 ``self.to_timestamp(target_freq, how)``. 

1010 

1011 Called by ``PeriodIndex.to_timestamp`` so the array-level 

1012 ``to_timestamp`` can return a bare DatetimeArray. 

1013 """ 

1014 how = libperiod.validate_end_alias(how) 

1015 if how == "E": 

1016 # For the "end" case, to_timestamp routes through arithmetic that 

1017 # does not preserve freq; match that behavior here. 

1018 return None 

1019 

1020 if self.freq.rule_code == "B": 

1021 # See if we can retain BDay instead of Day in cases where 

1022 # len(self) is too small for infer_freq to distinguish between them 

1023 diffs = libalgos.unique_deltas(self.asi8) 

1024 if len(diffs) == 1: 

1025 diff = diffs[0] 

1026 if diff == self.dtype._n: 

1027 return self.freq 

1028 elif diff == 1: 

1029 return self.freq.base 

1030 # TODO: other cases? 

1031 return None 

1032 

1033 result_freq = to_offset(dta._inferred_freq_str) 

1034 if target_freq is not None: 

1035 # match the normalization applied in to_timestamp 

1036 target_freq = Period._maybe_convert_freq(target_freq) 

1037 if ( 

1038 isinstance(result_freq, Day) 

1039 and not isinstance(target_freq, Day) 

1040 and Timedelta(target_freq) == Timedelta(days=result_freq.n) 

1041 ): 

1042 result_freq = target_freq 

1043 return result_freq 

1044 

1045 # -------------------------------------------------------------------- 

1046 

1047 def _box_func(self, x) -> Period | NaTType: 

1048 return Period._from_ordinal(ordinal=x, dtype=self.dtype) 

1049 

1050 def asfreq(self, freq=None, how: str = "E") -> Self: 

1051 """ 

1052 Convert the PeriodArray to the specified frequency `freq`. 

1053 

1054 Equivalent to applying :meth:`pandas.Period.asfreq` with the given arguments 

1055 to each :class:`~pandas.Period` in this PeriodArray. 

1056 

1057 Parameters 

1058 ---------- 

1059 freq : str 

1060 A frequency. 

1061 how : {'end', 'start', 'e', 's'}, default 'end' 

1062 Whether the elements should be aligned to the end or start of 

1063 each period, e.g. January 31st vs. January 1st. Case-insensitive. 

1064 

1065 Returns 

1066 ------- 

1067 PeriodArray 

1068 The transformed PeriodArray with the new frequency. 

1069 

1070 See Also 

1071 -------- 

1072 PeriodIndex.asfreq: Convert each Period in a PeriodIndex to the given frequency. 

1073 Period.asfreq : Convert a :class:`~pandas.Period` object to the given frequency. 

1074 

1075 Examples 

1076 -------- 

1077 >>> pidx = pd.period_range("2010-01-01", "2015-01-01", freq="Y") 

1078 >>> pidx 

1079 PeriodIndex(['2010', '2011', '2012', '2013', '2014', '2015'], 

1080 dtype='period[Y-DEC]') 

1081 

1082 >>> pidx.asfreq("M") 

1083 PeriodIndex(['2010-12', '2011-12', '2012-12', '2013-12', '2014-12', 

1084 '2015-12'], dtype='period[M]') 

1085 

1086 >>> pidx.asfreq("M", how="S") 

1087 PeriodIndex(['2010-01', '2011-01', '2012-01', '2013-01', '2014-01', 

1088 '2015-01'], dtype='period[M]') 

1089 """ 

1090 how = libperiod.validate_end_alias(how) 

1091 if isinstance(freq, BaseOffset) and hasattr(freq, "_period_dtype_code"): 

1092 freq = PeriodDtype(freq)._freqstr 

1093 freq = Period._maybe_convert_freq(freq) 

1094 

1095 base1 = self._dtype._dtype_code 

1096 base2 = freq._period_dtype_code 

1097 

1098 asi8 = self.asi8 

1099 # self.freq.n can't be negative or 0 

1100 end = how == "E" 

1101 if end: 

1102 ordinal = asi8 + self.dtype._n - 1 

1103 else: 

1104 ordinal = asi8 

1105 

1106 new_data = period_asfreq_arr(ordinal, base1, base2, end) 

1107 

1108 if self._hasna: 

1109 new_data[self._isnan] = iNaT 

1110 

1111 dtype = PeriodDtype(freq) 

1112 return type(self)(new_data, dtype=dtype) 

1113 

1114 # ------------------------------------------------------------------ 

1115 # Rendering Methods 

1116 

1117 def _formatter(self, boxed: bool = False) -> Callable[[object], str]: 

1118 if boxed: 

1119 return str 

1120 return "'{}'".format 

1121 

1122 def _format_native_types( 

1123 self, *, na_rep: str | float = "NaT", date_format=None, **kwargs 

1124 ) -> npt.NDArray[np.object_]: 

1125 """ 

1126 actually format my specific types 

1127 """ 

1128 return libperiod.period_array_strftime( 

1129 self.asi8, self.dtype._dtype_code, na_rep, date_format 

1130 ) 

1131 

1132 # ------------------------------------------------------------------ 

1133 

1134 def astype(self, dtype, copy: bool = True): 

1135 # We handle Period[T] -> Period[U] 

1136 # Our parent handles everything else. 

1137 dtype = pandas_dtype(dtype) 

1138 if dtype == self._dtype: 

1139 if not copy: 

1140 return self 

1141 else: 

1142 return self.copy() 

1143 if isinstance(dtype, PeriodDtype): 

1144 return self.asfreq(dtype.freq) 

1145 

1146 if lib.is_np_dtype(dtype, "M") or isinstance(dtype, DatetimeTZDtype): 

1147 # GH#45038 match PeriodIndex behavior. 

1148 tz = getattr(dtype, "tz", None) 

1149 unit = dtl.dtype_to_unit(dtype) 

1150 # error: Argument 1 to "as_unit" of "TimelikeOps" has incompatible 

1151 # type "str"; expected "Literal['s', 'ms', 'us', 'ns']" [arg-type] 

1152 return self.to_timestamp().tz_localize(tz).as_unit(unit) # type: ignore[arg-type] 

1153 

1154 return super().astype(dtype, copy=copy) 

1155 

1156 def searchsorted( 

1157 self, 

1158 value: NumpyValueArrayLike | ExtensionArray, 

1159 side: Literal["left", "right"] = "left", 

1160 sorter: NumpySorter | None = None, 

1161 ) -> npt.NDArray[np.intp] | np.intp: 

1162 npvalue = self._validate_setitem_value(value).view("M8[ns]") 

1163 

1164 # Cast to M8 to get datetime-like NaT placement, 

1165 # similar to dtl._period_dispatch 

1166 m8arr = self._ndarray.view("M8[ns]") 

1167 return m8arr.searchsorted(npvalue, side=side, sorter=sorter) 

1168 

1169 def _pad_or_backfill( 

1170 self, 

1171 *, 

1172 method: FillnaOptions, 

1173 limit: int | None = None, 

1174 limit_area: Literal["inside", "outside"] | None = None, 

1175 copy: bool = True, 

1176 ) -> Self: 

1177 # view as dt64 so we get treated as timelike in core.missing, 

1178 # similar to dtl._period_dispatch 

1179 dta = self.view("M8[ns]") 

1180 result = dta._pad_or_backfill( 

1181 method=method, limit=limit, limit_area=limit_area, copy=copy 

1182 ) 

1183 if copy: 

1184 return cast("Self", result.view(self.dtype)) 

1185 else: 

1186 return self 

1187 

1188 # ------------------------------------------------------------------ 

1189 # Arithmetic Methods 

1190 

1191 def _addsub_int_array_or_scalar( 

1192 self, other: np.ndarray | int, op: Callable[[Any, Any], Any] 

1193 ) -> Self: 

1194 """ 

1195 Add or subtract array of integers. 

1196 

1197 Parameters 

1198 ---------- 

1199 other : np.ndarray[int64] or int 

1200 op : {operator.add, operator.sub} 

1201 

1202 Returns 

1203 ------- 

1204 result : PeriodArray 

1205 """ 

1206 assert op in [operator.add, operator.sub] 

1207 if op is operator.sub: 

1208 other = -other 

1209 res_values = add_overflowsafe(self.asi8, np.asarray(other, dtype="i8")) 

1210 return type(self)(res_values, dtype=self.dtype) 

1211 

1212 def _add_offset(self, other: BaseOffset): 

1213 assert not isinstance(other, Tick) 

1214 

1215 if isinstance(other, Day): 

1216 return self + np.timedelta64(other.n, "D") 

1217 

1218 self._require_matching_unit(other._period_unit, base=True) 

1219 return self._addsub_int_array_or_scalar(other.n, operator.add) 

1220 

1221 # TODO: can we de-duplicate with Period._add_timedeltalike_scalar? 

1222 def _add_timedeltalike_scalar(self, other): 

1223 """ 

1224 Parameters 

1225 ---------- 

1226 other : timedelta, Tick, np.timedelta64 

1227 

1228 Returns 

1229 ------- 

1230 PeriodArray 

1231 """ 

1232 if not isinstance(self.freq, (Tick, Day)): 

1233 # We cannot add timedelta-like to non-tick PeriodArray 

1234 raise raise_on_incompatible(self, other) 

1235 

1236 if isna(other): 

1237 # i.e. np.timedelta64("NaT") 

1238 return super()._add_timedeltalike_scalar(other) 

1239 

1240 if isinstance(other, Day): 

1241 td = np.asarray(Timedelta(days=other.n).asm8) 

1242 else: 

1243 td = np.asarray(Timedelta(other).asm8) 

1244 return self._add_timedelta_arraylike(td) 

1245 

1246 def _add_timedelta_arraylike( 

1247 self, other: TimedeltaArray | npt.NDArray[np.timedelta64] 

1248 ) -> Self: 

1249 """ 

1250 Parameters 

1251 ---------- 

1252 other : TimedeltaArray or ndarray[timedelta64] 

1253 

1254 Returns 

1255 ------- 

1256 PeriodArray 

1257 """ 

1258 if not self.dtype._is_tick_like(): 

1259 # We cannot add timedelta-like to non-tick PeriodArray 

1260 raise TypeError( 

1261 f"Cannot add or subtract timedelta64[ns] dtype from {self.dtype}" 

1262 ) 

1263 

1264 dtype = np.dtype(f"m8[{self.dtype._td64_unit}]") 

1265 

1266 # Similar to _check_timedeltalike_freq_compat, but we raise with a 

1267 # more specific exception message if necessary. 

1268 try: 

1269 delta = astype_overflowsafe( 

1270 np.asarray(other), dtype=dtype, copy=False, round_ok=False 

1271 ) 

1272 except ValueError as err: 

1273 # e.g. if we have minutes freq and try to add 30s 

1274 # "Cannot losslessly convert units" 

1275 raise IncompatibleFrequency( 

1276 "Cannot add/subtract timedelta-like from PeriodArray that is " 

1277 "not an integer multiple of the PeriodArray's freq." 

1278 ) from err 

1279 

1280 res_values = add_overflowsafe(self.asi8, np.asarray(delta.view("i8"))) 

1281 return type(self)(res_values, dtype=self.dtype) 

1282 

1283 def _check_timedeltalike_freq_compat(self, other): 

1284 """ 

1285 Arithmetic operations with timedelta-like scalars or array `other` 

1286 are only valid if `other` is an integer multiple of `self.freq`. 

1287 If the operation is valid, find that integer multiple. Otherwise, 

1288 raise because the operation is invalid. 

1289 

1290 Parameters 

1291 ---------- 

1292 other : timedelta, np.timedelta64, Tick, 

1293 ndarray[timedelta64], TimedeltaArray, TimedeltaIndex 

1294 

1295 Returns 

1296 ------- 

1297 multiple : int or ndarray[int64] 

1298 

1299 Raises 

1300 ------ 

1301 IncompatibleFrequency 

1302 """ 

1303 assert self.dtype._is_tick_like() # checked by calling function 

1304 

1305 dtype = np.dtype(f"m8[{self.dtype._td64_unit}]") 

1306 

1307 if isinstance(other, (timedelta, np.timedelta64, Tick)): 

1308 td = np.asarray(Timedelta(other).asm8) 

1309 else: 

1310 td = np.asarray(other) 

1311 

1312 try: 

1313 delta = astype_overflowsafe(td, dtype=dtype, copy=False, round_ok=False) 

1314 except ValueError as err: 

1315 raise raise_on_incompatible(self, other) from err 

1316 

1317 delta = delta.view("i8") 

1318 return lib.item_from_zerodim(delta) 

1319 

1320 # ------------------------------------------------------------------ 

1321 # Reductions 

1322 

1323 def _reduce( 

1324 self, name: str, *, skipna: bool = True, keepdims: bool = False, **kwargs 

1325 ): 

1326 result = super()._reduce(name, skipna=skipna, keepdims=keepdims, **kwargs) 

1327 if keepdims and isinstance(result, np.ndarray): 

1328 return self._from_sequence(result, dtype=self.dtype) 

1329 return result 

1330 

1331 

1332def raise_on_incompatible(left, right) -> IncompatibleFrequency: 

1333 """ 

1334 Helper function to render a consistent error message when raising 

1335 IncompatibleFrequency. 

1336 

1337 Parameters 

1338 ---------- 

1339 left : PeriodArray 

1340 right : None, DateOffset, Period, ndarray, or timedelta-like 

1341 

1342 Returns 

1343 ------- 

1344 IncompatibleFrequency 

1345 Exception to be raised by the caller. 

1346 """ 

1347 # GH#24283 error message format depends on whether right is scalar 

1348 if isinstance(right, (np.ndarray, ABCTimedeltaArray)) or right is None: 

1349 other_freq = None 

1350 elif isinstance(right, BaseOffset): 

1351 with warnings.catch_warnings(): 

1352 warnings.filterwarnings( 

1353 "ignore", r"PeriodDtype\[B\] is deprecated", category=FutureWarning 

1354 ) 

1355 other_freq = PeriodDtype(right)._freqstr 

1356 elif isinstance(right, (ABCPeriodIndex, PeriodArray, Period)): 

1357 other_freq = right.freqstr 

1358 else: 

1359 other_freq = delta_to_tick(Timedelta(right)).freqstr 

1360 

1361 own_freq = PeriodDtype(left.freq)._freqstr 

1362 msg = DIFFERENT_FREQ.format( 

1363 cls=type(left).__name__, own_freq=own_freq, other_freq=other_freq 

1364 ) 

1365 return IncompatibleFrequency(msg) 

1366 

1367 

1368# ------------------------------------------------------------------- 

1369# Constructor Helpers 

1370 

1371 

1372def period_array( 

1373 data: Sequence[Period | str | None] | AnyArrayLike, 

1374 dtype: PeriodDtype | None = None, 

1375) -> PeriodArray: 

1376 """ 

1377 Construct a new PeriodArray from a sequence of Period scalars. 

1378 

1379 Parameters 

1380 ---------- 

1381 data : Sequence of Period objects 

1382 A sequence of Period objects. These are required to all have 

1383 the same ``freq.`` Missing values can be indicated by ``None`` 

1384 or ``pandas.NaT``. 

1385 dtype : PeriodDtype or None, default None 

1386 The dtype for the array. If not specified, is inferred from the data. 

1387 

1388 Returns 

1389 ------- 

1390 PeriodArray 

1391 

1392 See Also 

1393 -------- 

1394 PeriodArray 

1395 pandas.PeriodIndex 

1396 

1397 Examples 

1398 -------- 

1399 >>> period_array([pd.Period("2017", freq="Y"), pd.Period("2018", freq="Y")]) 

1400 <PeriodArray> 

1401 ['2017', '2018'] 

1402 Length: 2, dtype: period[Y-DEC] 

1403 

1404 >>> period_array([pd.Period("2017", freq="Y"), pd.Period("2018", freq="Y"), pd.NaT]) 

1405 <PeriodArray> 

1406 ['2017', '2018', 'NaT'] 

1407 Length: 3, dtype: period[Y-DEC] 

1408 

1409 Datetime-like strings may also be passed 

1410 

1411 >>> period_array( 

1412 ... ["2000-Q1", "2000-Q2", "2000-Q3", "2000-Q4"], dtype=PeriodDtype("Q") 

1413 ... ) 

1414 <PeriodArray> 

1415 ['2000Q1', '2000Q2', '2000Q3', '2000Q4'] 

1416 Length: 4, dtype: period[Q-DEC] 

1417 """ 

1418 return PeriodArray._from_sequence(data, dtype=dtype) 

1419 

1420 

1421def validate_dtype_freq( 

1422 dtype: DtypeObj | None, dtype2: DtypeObj | None 

1423) -> PeriodDtype | None: 

1424 """ 

1425 If a dtype is specified both directly and indirectly via a `freq` (dtype2), 

1426 ensure they match. If only the latter is available, return the indirect dtype. 

1427 

1428 Parameters 

1429 ---------- 

1430 dtype : dtype 

1431 dtype2 : PeriodDtype or None 

1432 Dtype derived from a `freq` passed to the caller. 

1433 

1434 Returns 

1435 ------- 

1436 PeriodDtype or None 

1437 

1438 Raises 

1439 ------ 

1440 ValueError : non-period dtype 

1441 IncompatibleFrequency : mismatch between dtype and freq 

1442 """ 

1443 if dtype is not None: 

1444 if not isinstance(dtype, PeriodDtype): 

1445 raise ValueError("dtype must be PeriodDtype") 

1446 if dtype2 is not None and dtype != dtype2: 

1447 raise IncompatibleFrequency("specified freq and dtype are different") 

1448 

1449 elif dtype2 is not None: 

1450 if not isinstance(dtype2, PeriodDtype): 

1451 raise ValueError("dtype must be PeriodDtype") 

1452 dtype = dtype2 

1453 

1454 return dtype 

1455 

1456 

1457def dt64arr_to_periodarr( 

1458 data, freq, tz=None 

1459) -> tuple[npt.NDArray[np.int64], BaseOffset]: 

1460 """ 

1461 Convert a datetime-like array to values Period ordinals. 

1462 

1463 Parameters 

1464 ---------- 

1465 data : Union[Series[datetime64[ns]], DatetimeIndex, ndarray[datetime64ns]] 

1466 freq : Optional[Union[str, Tick]] 

1467 Must match the `freq` on the `data` if `data` is a DatetimeIndex 

1468 or Series. 

1469 tz : Optional[tzinfo] 

1470 

1471 Returns 

1472 ------- 

1473 ordinals : ndarray[int64] 

1474 freq : Tick 

1475 The frequency extracted from the Series or DatetimeIndex if that's 

1476 used. 

1477 

1478 """ 

1479 if not isinstance(data.dtype, np.dtype) or data.dtype.kind != "M": 

1480 raise ValueError(f"Wrong dtype: {data.dtype}") 

1481 

1482 if freq is None: 

1483 if isinstance(data, ABCIndex): 

1484 data, freq = data._values, data.freq 

1485 elif isinstance(data, ABCSeries): 

1486 # freq is always None for DatetimeArray inside a Series, so we 

1487 # fall back to the inferred freq. 

1488 inferred_freq = data._values._inferred_freq_str 

1489 if inferred_freq is not None: 

1490 warnings.warn( 

1491 "Constructing PeriodArray from a Series of datetime64 data " 

1492 "will stop inferring the frequency in a future version. " 

1493 "Pass `freq` explicitly instead.", 

1494 Pandas4Warning, 

1495 stacklevel=find_stack_level(), 

1496 ) 

1497 freq = inferred_freq 

1498 data = data._values 

1499 

1500 elif isinstance(data, (ABCIndex, ABCSeries)): 

1501 data = data._values 

1502 

1503 reso = get_unit_from_dtype(data.dtype) 

1504 freq = Period._maybe_convert_freq(freq) 

1505 base = freq._period_dtype_code 

1506 return c_dt64arr_to_periodarr(data.view("i8"), base, tz, reso=reso), freq 

1507 

1508 

1509def _get_ordinal_range(start, end, periods, freq, mult: int = 1): 

1510 if com.count_not_none(start, end, periods) != 2: 

1511 raise ValueError( 

1512 "Of the three parameters: start, end, and periods, " 

1513 "exactly two must be specified" 

1514 ) 

1515 

1516 if freq is not None: 

1517 freq = to_offset(freq, is_period=True) 

1518 mult = freq.n 

1519 

1520 if start is not None: 

1521 start = Period(start, freq) 

1522 if end is not None: 

1523 end = Period(end, freq) 

1524 

1525 is_start_per = isinstance(start, Period) 

1526 is_end_per = isinstance(end, Period) 

1527 

1528 if is_start_per and is_end_per and start.freq != end.freq: 

1529 raise ValueError("start and end must have same freq") 

1530 if start is NaT or end is NaT: 

1531 raise ValueError("start and end must not be NaT") 

1532 

1533 if freq is None: 

1534 if is_start_per: 

1535 freq = start.freq 

1536 elif is_end_per: 

1537 freq = end.freq 

1538 else: # pragma: no cover 

1539 raise ValueError("Could not infer freq from start/end") 

1540 mult = freq.n 

1541 

1542 if periods is not None: 

1543 periods = periods * mult 

1544 if start is None: 

1545 data = np.arange( 

1546 end.ordinal - periods + mult, end.ordinal + 1, mult, dtype=np.int64 

1547 ) 

1548 else: 

1549 data = np.arange( 

1550 start.ordinal, start.ordinal + periods, mult, dtype=np.int64 

1551 ) 

1552 else: 

1553 data = np.arange(start.ordinal, end.ordinal + 1, mult, dtype=np.int64) 

1554 

1555 return data, freq 

1556 

1557 

1558def _range_from_fields( 

1559 year=None, 

1560 month=None, 

1561 quarter=None, 

1562 day=None, 

1563 hour=None, 

1564 minute=None, 

1565 second=None, 

1566 freq=None, 

1567) -> tuple[np.ndarray, BaseOffset]: 

1568 if hour is None: 

1569 hour = 0 

1570 if minute is None: 

1571 minute = 0 

1572 if second is None: 

1573 second = 0 

1574 if day is None: 

1575 day = 1 

1576 

1577 if quarter is not None: 

1578 if freq is None: 

1579 freq = to_offset("Q", is_period=True) 

1580 base = FreqGroup.FR_QTR.value 

1581 else: 

1582 freq = to_offset(freq, is_period=True) 

1583 base = libperiod.freq_to_dtype_code(freq) 

1584 if FreqGroup.from_period_dtype_code(base) != FreqGroup.FR_QTR: 

1585 raise ValueError("freq must be a quarterly frequency") 

1586 

1587 freqstr = freq.freqstr 

1588 year, quarter = _make_field_arrays(year, quarter) 

1589 year = _field_to_int64(year) 

1590 quarter = _field_to_int64(quarter) 

1591 

1592 if (quarter < 1).any() or (quarter > 4).any(): 

1593 raise ValueError("Quarter must be 1 <= q <= 4") 

1594 

1595 # Vectorized quarter_to_myear 

1596 mnum = MONTH_NUMBERS[parsing.get_rule_month(freqstr)] + 1 

1597 months = (mnum + (quarter - 1) * 3) % 12 + 1 

1598 years = np.where(months > mnum, year - 1, year) 

1599 

1600 length = len(years) 

1601 ones = np.ones(length, dtype=np.int64) 

1602 zeros = np.zeros(length, dtype=np.int64) 

1603 ordinals = libperiod.period_ordinals_from_fields( 

1604 years, months, ones, zeros, zeros, zeros, base 

1605 ) 

1606 else: 

1607 freq = to_offset(freq, is_period=True) 

1608 base = libperiod.freq_to_dtype_code(freq) 

1609 arrays = _make_field_arrays(year, month, day, hour, minute, second) 

1610 ordinals = libperiod.period_ordinals_from_fields( 

1611 _field_to_int64(arrays[0]), 

1612 _field_to_int64(arrays[1]), 

1613 _field_to_int64(arrays[2]), 

1614 _field_to_int64(arrays[3]), 

1615 _field_to_int64(arrays[4]), 

1616 _field_to_int64(arrays[5]), 

1617 base, 

1618 ) 

1619 

1620 return ordinals, freq 

1621 

1622 

1623def _field_to_int64(values) -> np.ndarray: 

1624 values = np.asarray(values) 

1625 if values.dtype.kind == "f" and np.isnan(values).any(): 

1626 # Match the error raised by the scalar Period constructor; casting 

1627 # NaN to int64 would otherwise silently produce garbage ordinals. 

1628 raise ValueError("cannot convert float NaN to integer") 

1629 return values.astype(np.int64, copy=False) 

1630 

1631 

1632def _make_field_arrays(*fields) -> list[np.ndarray]: 

1633 length = None 

1634 for x in fields: 

1635 if isinstance(x, (list, tuple, np.ndarray, ABCSeries)): 

1636 if length is not None and len(x) != length: 

1637 raise ValueError("Mismatched Period array lengths") 

1638 if length is None: 

1639 length = len(x) 

1640 

1641 # error: Argument 2 to "repeat" has incompatible type "Optional[int]"; expected 

1642 # "Union[Union[int, integer[Any]], Union[bool, bool_], ndarray, Sequence[Union[int, 

1643 # integer[Any]]], Sequence[Union[bool, bool_]], Sequence[Sequence[Any]]]" 

1644 return [ 

1645 ( 

1646 np.asarray(x) 

1647 if isinstance(x, (np.ndarray, list, tuple, ABCSeries)) 

1648 else np.repeat(x, length) # type: ignore[arg-type] 

1649 ) 

1650 for x in fields 

1651 ]