Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/pandas/core/arrays/period.py: 18%

428 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1from __future__ import annotations 

2 

3from datetime import timedelta 

4import operator 

5from typing import ( 

6 TYPE_CHECKING, 

7 Any, 

8 Callable, 

9 Literal, 

10 Sequence, 

11 TypeVar, 

12 overload, 

13) 

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 NaT, 

25 NaTType, 

26 Timedelta, 

27 astype_overflowsafe, 

28 dt64arr_to_periodarr as c_dt64arr_to_periodarr, 

29 get_unit_from_dtype, 

30 iNaT, 

31 parsing, 

32 period as libperiod, 

33 to_offset, 

34) 

35from pandas._libs.tslibs.dtypes import FreqGroup 

36from pandas._libs.tslibs.fields import isleapyear_arr 

37from pandas._libs.tslibs.offsets import ( 

38 Tick, 

39 delta_to_tick, 

40) 

41from pandas._libs.tslibs.period import ( 

42 DIFFERENT_FREQ, 

43 IncompatibleFrequency, 

44 Period, 

45 get_period_field_arr, 

46 period_asfreq_arr, 

47) 

48from pandas._typing import ( 

49 AnyArrayLike, 

50 Dtype, 

51 NpDtype, 

52 npt, 

53) 

54from pandas.util._decorators import ( 

55 cache_readonly, 

56 doc, 

57) 

58 

59from pandas.core.dtypes.common import ( 

60 ensure_object, 

61 is_datetime64_any_dtype, 

62 is_datetime64_dtype, 

63 is_dtype_equal, 

64 is_float_dtype, 

65 is_integer_dtype, 

66 is_period_dtype, 

67 pandas_dtype, 

68) 

69from pandas.core.dtypes.dtypes import PeriodDtype 

70from pandas.core.dtypes.generic import ( 

71 ABCIndex, 

72 ABCPeriodIndex, 

73 ABCSeries, 

74 ABCTimedeltaArray, 

75) 

76from pandas.core.dtypes.missing import isna 

77 

78import pandas.core.algorithms as algos 

79from pandas.core.arrays import datetimelike as dtl 

80import pandas.core.common as com 

81 

82if TYPE_CHECKING: 82 ↛ 84line 82 didn't jump to line 84, because the condition on line 82 was never true

83 

84 from pandas._typing import ( 

85 NumpySorter, 

86 NumpyValueArrayLike, 

87 ) 

88 

89 from pandas.core.arrays import ( 

90 DatetimeArray, 

91 TimedeltaArray, 

92 ) 

93 from pandas.core.arrays.base import ExtensionArray 

94 

95 

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

97 

98 

99_shared_doc_kwargs = { 

100 "klass": "PeriodArray", 

101} 

102 

103 

104def _field_accessor(name: str, docstring=None): 

105 def f(self): 

106 base = self.freq._period_dtype_code 

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

108 return result 

109 

110 f.__name__ = name 

111 f.__doc__ = docstring 

112 return property(f) 

113 

114 

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

116 """ 

117 Pandas ExtensionArray for storing Period data. 

118 

119 Users should use :func:`~pandas.period_array` to create new instances. 

120 Alternatively, :func:`~pandas.array` can be used to create new instances 

121 from a sequence of Period scalars. 

122 

123 Parameters 

124 ---------- 

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

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

127 converted to ordinals without inference or copy (PeriodArray, 

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

129 PeriodIndex). 

130 dtype : PeriodDtype, optional 

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

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

133 freq : str or DateOffset 

134 The `freq` to use for the array. Mostly applicable when `values` 

135 is an ndarray of integers, when `freq` is required. When `values` 

136 is a PeriodArray (or box around), it's checked that ``values.freq`` 

137 matches `freq`. 

138 copy : bool, default False 

139 Whether to copy the ordinals before storing. 

140 

141 Attributes 

142 ---------- 

143 None 

144 

145 Methods 

146 ------- 

147 None 

148 

149 See Also 

150 -------- 

151 Period: Represents a period of time. 

152 PeriodIndex : Immutable Index for period data. 

153 period_range: Create a fixed-frequency PeriodArray. 

154 array: Construct a pandas array. 

155 

156 Notes 

157 ----- 

158 There are two components to a PeriodArray 

159 

160 - ordinals : integer ndarray 

161 - freq : pd.tseries.offsets.Offset 

162 

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

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

165 

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

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

168 """ 

169 

170 # array priority higher than numpy scalars 

171 __array_priority__ = 1000 

172 _typ = "periodarray" # ABCPeriodArray 

173 _internal_fill_value = np.int64(iNaT) 

174 _recognized_scalars = (Period,) 

175 _is_recognized_dtype = is_period_dtype 

176 _infer_matches = ("period",) 

177 

178 @property 

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

180 return Period 

181 

182 # Names others delegate to us 

183 _other_ops: list[str] = [] 

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

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

186 _field_ops: list[str] = [ 

187 "year", 

188 "month", 

189 "day", 

190 "hour", 

191 "minute", 

192 "second", 

193 "weekofyear", 

194 "weekday", 

195 "week", 

196 "dayofweek", 

197 "day_of_week", 

198 "dayofyear", 

199 "day_of_year", 

200 "quarter", 

201 "qyear", 

202 "days_in_month", 

203 "daysinmonth", 

204 ] 

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

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

207 

208 _dtype: PeriodDtype 

209 

210 # -------------------------------------------------------------------- 

211 # Constructors 

212 

213 def __init__( 

214 self, values, dtype: Dtype | None = None, freq=None, copy: bool = False 

215 ) -> None: 

216 freq = validate_dtype_freq(dtype, freq) 

217 

218 if freq is not None: 

219 freq = Period._maybe_convert_freq(freq) 

220 

221 if isinstance(values, ABCSeries): 

222 values = values._values 

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

224 raise TypeError("Incorrect dtype") 

225 

226 elif isinstance(values, ABCPeriodIndex): 

227 values = values._values 

228 

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

230 if freq is not None and freq != values.freq: 

231 raise raise_on_incompatible(values, freq) 

232 values, freq = values._ndarray, values.freq 

233 

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

235 if freq is None: 

236 raise ValueError("freq is not specified and cannot be inferred") 

237 NDArrayBacked.__init__(self, values, PeriodDtype(freq)) 

238 

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

240 @classmethod 

241 def _simple_new( # type: ignore[override] 

242 cls, 

243 values: np.ndarray, 

244 freq: BaseOffset | None = None, 

245 dtype: Dtype | None = None, 

246 ) -> PeriodArray: 

247 # alias for PeriodArray.__init__ 

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

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

250 return cls(values, freq=freq, dtype=dtype) 

251 

252 @classmethod 

253 def _from_sequence( 

254 cls: type[PeriodArray], 

255 scalars: Sequence[Period | None] | AnyArrayLike, 

256 *, 

257 dtype: Dtype | None = None, 

258 copy: bool = False, 

259 ) -> PeriodArray: 

260 if dtype and isinstance(dtype, PeriodDtype): 

261 freq = dtype.freq 

262 else: 

263 freq = None 

264 

265 if isinstance(scalars, cls): 

266 validate_dtype_freq(scalars.dtype, freq) 

267 if copy: 

268 scalars = scalars.copy() 

269 return scalars 

270 

271 periods = np.asarray(scalars, dtype=object) 

272 

273 freq = freq or libperiod.extract_freq(periods) 

274 ordinals = libperiod.extract_ordinals(periods, freq) 

275 return cls(ordinals, freq=freq) 

276 

277 @classmethod 

278 def _from_sequence_of_strings( 

279 cls, strings, *, dtype: Dtype | None = None, copy: bool = False 

280 ) -> PeriodArray: 

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

282 

283 @classmethod 

284 def _from_datetime64(cls, data, freq, tz=None) -> PeriodArray: 

285 """ 

286 Construct a PeriodArray from a datetime64 array 

287 

288 Parameters 

289 ---------- 

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

291 freq : str or Tick 

292 tz : tzinfo, optional 

293 

294 Returns 

295 ------- 

296 PeriodArray[freq] 

297 """ 

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

299 return cls(data, freq=freq) 

300 

301 @classmethod 

302 def _generate_range(cls, start, end, periods, freq, fields): 

303 periods = dtl.validate_periods(periods) 

304 

305 if freq is not None: 

306 freq = Period._maybe_convert_freq(freq) 

307 

308 field_count = len(fields) 

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

310 if field_count > 0: 

311 raise ValueError( 

312 "Can either instantiate from fields or endpoints, but not both" 

313 ) 

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

315 elif field_count > 0: 

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

317 else: 

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

319 

320 return subarr, freq 

321 

322 # ----------------------------------------------------------------- 

323 # DatetimeLike Interface 

324 

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

326 # "DatetimeLikeArrayMixin"; supertype defines the argument type as 

327 # "Union[Union[Period, Any, Timedelta], NaTType]" 

328 def _unbox_scalar( # type: ignore[override] 

329 self, 

330 value: Period | NaTType, 

331 setitem: bool = False, 

332 ) -> np.int64: 

333 if value is NaT: 

334 # error: Item "Period" of "Union[Period, NaTType]" has no attribute "value" 

335 return np.int64(value.value) # type: ignore[union-attr] 

336 elif isinstance(value, self._scalar_type): 

337 self._check_compatible_with(value, setitem=setitem) 

338 return np.int64(value.ordinal) 

339 else: 

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

341 

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

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

344 

345 def _check_compatible_with(self, other, setitem: bool = False): 

346 if other is NaT: 

347 return 

348 self._require_matching_freq(other) 

349 

350 # -------------------------------------------------------------------- 

351 # Data / Attributes 

352 

353 @cache_readonly 

354 def dtype(self) -> PeriodDtype: 

355 return self._dtype 

356 

357 # error: Read-only property cannot override read-write property 

358 @property # type: ignore[misc] 

359 def freq(self) -> BaseOffset: 

360 """ 

361 Return the frequency object for this PeriodArray. 

362 """ 

363 return self.dtype.freq 

364 

365 def __array__(self, dtype: NpDtype | None = None) -> np.ndarray: 

366 if dtype == "i8": 

367 return self.asi8 

368 elif dtype == bool: 

369 return ~self._isnan 

370 

371 # This will raise TypeError for non-object dtypes 

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

373 

374 def __arrow_array__(self, type=None): 

375 """ 

376 Convert myself into a pyarrow Array. 

377 """ 

378 import pyarrow 

379 

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

381 

382 if type is not None: 

383 if pyarrow.types.is_integer(type): 

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

385 elif isinstance(type, ArrowPeriodType): 

386 # ensure we have the same freq 

387 if self.freqstr != type.freq: 

388 raise TypeError( 

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

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

391 ) 

392 else: 

393 raise TypeError( 

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

395 ) 

396 

397 period_type = ArrowPeriodType(self.freqstr) 

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

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

400 

401 # -------------------------------------------------------------------- 

402 # Vectorized analogues of Period properties 

403 

404 year = _field_accessor( 

405 "year", 

406 """ 

407 The year of the period. 

408 """, 

409 ) 

410 month = _field_accessor( 

411 "month", 

412 """ 

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

414 """, 

415 ) 

416 day = _field_accessor( 

417 "day", 

418 """ 

419 The days of the period. 

420 """, 

421 ) 

422 hour = _field_accessor( 

423 "hour", 

424 """ 

425 The hour of the period. 

426 """, 

427 ) 

428 minute = _field_accessor( 

429 "minute", 

430 """ 

431 The minute of the period. 

432 """, 

433 ) 

434 second = _field_accessor( 

435 "second", 

436 """ 

437 The second of the period. 

438 """, 

439 ) 

440 weekofyear = _field_accessor( 

441 "week", 

442 """ 

443 The week ordinal of the year. 

444 """, 

445 ) 

446 week = weekofyear 

447 day_of_week = _field_accessor( 

448 "day_of_week", 

449 """ 

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

451 """, 

452 ) 

453 dayofweek = day_of_week 

454 weekday = dayofweek 

455 dayofyear = day_of_year = _field_accessor( 

456 "day_of_year", 

457 """ 

458 The ordinal day of the year. 

459 """, 

460 ) 

461 quarter = _field_accessor( 

462 "quarter", 

463 """ 

464 The quarter of the date. 

465 """, 

466 ) 

467 qyear = _field_accessor("qyear") 

468 days_in_month = _field_accessor( 

469 "days_in_month", 

470 """ 

471 The number of days in the month. 

472 """, 

473 ) 

474 daysinmonth = days_in_month 

475 

476 @property 

477 def is_leap_year(self) -> np.ndarray: 

478 """ 

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

480 """ 

481 return isleapyear_arr(np.asarray(self.year)) 

482 

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

484 """ 

485 Cast to DatetimeArray/Index. 

486 

487 Parameters 

488 ---------- 

489 freq : str or DateOffset, optional 

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

491 'S' otherwise. 

492 how : {'s', 'e', 'start', 'end'} 

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

494 

495 Returns 

496 ------- 

497 DatetimeArray/Index 

498 """ 

499 from pandas.core.arrays import DatetimeArray 

500 

501 how = libperiod.validate_end_alias(how) 

502 

503 end = how == "E" 

504 if end: 

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

506 # roll forward to ensure we land on B date 

507 adjust = Timedelta(1, "D") - Timedelta(1, "ns") 

508 return self.to_timestamp(how="start") + adjust 

509 else: 

510 adjust = Timedelta(1, "ns") 

511 return (self + self.freq).to_timestamp(how="start") - adjust 

512 

513 if freq is None: 

514 freq = self._dtype._get_to_timestamp_base() 

515 base = freq 

516 else: 

517 freq = Period._maybe_convert_freq(freq) 

518 base = freq._period_dtype_code 

519 

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

521 

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

523 dta = DatetimeArray(new_data) 

524 

525 if self.freq.name == "B": 

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

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

528 diffs = libalgos.unique_deltas(self.asi8) 

529 if len(diffs) == 1: 

530 diff = diffs[0] 

531 if diff == self.freq.n: 

532 dta._freq = self.freq 

533 elif diff == 1: 

534 dta._freq = self.freq.base 

535 # TODO: other cases? 

536 return dta 

537 else: 

538 return dta._with_freq("infer") 

539 

540 # -------------------------------------------------------------------- 

541 

542 def _time_shift(self, periods: int, freq=None) -> PeriodArray: 

543 """ 

544 Shift each value by `periods`. 

545 

546 Note this is different from ExtensionArray.shift, which 

547 shifts the *position* of each element, padding the end with 

548 missing values. 

549 

550 Parameters 

551 ---------- 

552 periods : int 

553 Number of periods to shift by. 

554 freq : pandas.DateOffset, pandas.Timedelta, or str 

555 Frequency increment to shift by. 

556 """ 

557 if freq is not None: 

558 raise TypeError( 

559 "`freq` argument is not supported for " 

560 f"{type(self).__name__}._time_shift" 

561 ) 

562 return self + periods 

563 

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

565 return Period._from_ordinal(ordinal=x, freq=self.freq) 

566 

567 @doc(**_shared_doc_kwargs, other="PeriodIndex", other_name="PeriodIndex") 

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

569 """ 

570 Convert the {klass} to the specified frequency `freq`. 

571 

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

573 to each :class:`~pandas.Period` in this {klass}. 

574 

575 Parameters 

576 ---------- 

577 freq : str 

578 A frequency. 

579 how : str {{'E', 'S'}}, default 'E' 

580 Whether the elements should be aligned to the end 

581 or start within pa period. 

582 

583 * 'E', 'END', or 'FINISH' for end, 

584 * 'S', 'START', or 'BEGIN' for start. 

585 

586 January 31st ('END') vs. January 1st ('START') for example. 

587 

588 Returns 

589 ------- 

590 {klass} 

591 The transformed {klass} with the new frequency. 

592 

593 See Also 

594 -------- 

595 {other}.asfreq: Convert each Period in a {other_name} to the given frequency. 

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

597 

598 Examples 

599 -------- 

600 >>> pidx = pd.period_range('2010-01-01', '2015-01-01', freq='A') 

601 >>> pidx 

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

603 dtype='period[A-DEC]') 

604 

605 >>> pidx.asfreq('M') 

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

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

608 

609 >>> pidx.asfreq('M', how='S') 

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

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

612 """ 

613 how = libperiod.validate_end_alias(how) 

614 

615 freq = Period._maybe_convert_freq(freq) 

616 

617 base1 = self._dtype._dtype_code 

618 base2 = freq._period_dtype_code 

619 

620 asi8 = self.asi8 

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

622 end = how == "E" 

623 if end: 

624 ordinal = asi8 + self.freq.n - 1 

625 else: 

626 ordinal = asi8 

627 

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

629 

630 if self._hasna: 

631 new_data[self._isnan] = iNaT 

632 

633 return type(self)(new_data, freq=freq) 

634 

635 # ------------------------------------------------------------------ 

636 # Rendering Methods 

637 

638 def _formatter(self, boxed: bool = False): 

639 if boxed: 

640 return str 

641 return "'{}'".format 

642 

643 @dtl.ravel_compat 

644 def _format_native_types( 

645 self, *, na_rep="NaT", date_format=None, **kwargs 

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

647 """ 

648 actually format my specific types 

649 """ 

650 values = self.astype(object) 

651 

652 # Create the formatter function 

653 if date_format: 

654 formatter = lambda per: per.strftime(date_format) 

655 else: 

656 # Uses `_Period.str` which in turn uses `format_period` 

657 formatter = lambda per: str(per) 

658 

659 # Apply the formatter to all values in the array, possibly with a mask 

660 if self._hasna: 

661 mask = self._isnan 

662 values[mask] = na_rep 

663 imask = ~mask 

664 values[imask] = np.array([formatter(per) for per in values[imask]]) 

665 else: 

666 values = np.array([formatter(per) for per in values]) 

667 return values 

668 

669 # ------------------------------------------------------------------ 

670 

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

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

673 # Our parent handles everything else. 

674 dtype = pandas_dtype(dtype) 

675 if is_dtype_equal(dtype, self._dtype): 

676 if not copy: 

677 return self 

678 else: 

679 return self.copy() 

680 if is_period_dtype(dtype): 

681 return self.asfreq(dtype.freq) 

682 

683 if is_datetime64_any_dtype(dtype): 

684 # GH#45038 match PeriodIndex behavior. 

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

686 return self.to_timestamp().tz_localize(tz) 

687 

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

689 

690 def searchsorted( 

691 self, 

692 value: NumpyValueArrayLike | ExtensionArray, 

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

694 sorter: NumpySorter = None, 

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

696 npvalue = self._validate_searchsorted_value(value).view("M8[ns]") 

697 

698 # Cast to M8 to get datetime-like NaT placement 

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

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

701 

702 def fillna(self, value=None, method=None, limit=None) -> PeriodArray: 

703 if method is not None: 

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

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

706 result = dta.fillna(value=value, method=method, limit=limit) 

707 # error: Incompatible return value type (got "Union[ExtensionArray, 

708 # ndarray[Any, Any]]", expected "PeriodArray") 

709 return result.view(self.dtype) # type: ignore[return-value] 

710 return super().fillna(value=value, method=method, limit=limit) 

711 

712 def _quantile( 

713 self: PeriodArray, 

714 qs: npt.NDArray[np.float64], 

715 interpolation: str, 

716 ) -> PeriodArray: 

717 # dispatch to DatetimeArray implementation 

718 dtres = self.view("M8[ns]")._quantile(qs, interpolation) 

719 # error: Incompatible return value type (got "Union[ExtensionArray, 

720 # ndarray[Any, Any]]", expected "PeriodArray") 

721 return dtres.view(self.dtype) # type: ignore[return-value] 

722 

723 # ------------------------------------------------------------------ 

724 # Arithmetic Methods 

725 

726 def _addsub_int_array_or_scalar( 

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

728 ) -> PeriodArray: 

729 """ 

730 Add or subtract array of integers; equivalent to applying 

731 `_time_shift` pointwise. 

732 

733 Parameters 

734 ---------- 

735 other : np.ndarray[int64] or int 

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

737 

738 Returns 

739 ------- 

740 result : PeriodArray 

741 """ 

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

743 if op is operator.sub: 

744 other = -other 

745 res_values = algos.checked_add_with_arr(self.asi8, other, arr_mask=self._isnan) 

746 return type(self)(res_values, freq=self.freq) 

747 

748 def _add_offset(self, other: BaseOffset): 

749 assert not isinstance(other, Tick) 

750 

751 self._require_matching_freq(other, base=True) 

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

753 

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

755 def _add_timedeltalike_scalar(self, other): 

756 """ 

757 Parameters 

758 ---------- 

759 other : timedelta, Tick, np.timedelta64 

760 

761 Returns 

762 ------- 

763 PeriodArray 

764 """ 

765 if not isinstance(self.freq, Tick): 

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

767 raise raise_on_incompatible(self, other) 

768 

769 if isna(other): 

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

771 return super()._add_timedeltalike_scalar(other) 

772 

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

774 return self._add_timedelta_arraylike(td) 

775 

776 def _add_timedelta_arraylike( 

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

778 ) -> PeriodArray: 

779 """ 

780 Parameters 

781 ---------- 

782 other : TimedeltaArray or ndarray[timedelta64] 

783 

784 Returns 

785 ------- 

786 PeriodArray 

787 """ 

788 freq = self.freq 

789 if not isinstance(freq, Tick): 

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

791 raise TypeError( 

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

793 ) 

794 

795 dtype = np.dtype(f"m8[{freq._td64_unit}]") 

796 

797 try: 

798 delta = astype_overflowsafe( 

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

800 ) 

801 except ValueError as err: 

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

803 # "Cannot losslessly convert units" 

804 raise IncompatibleFrequency( 

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

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

807 ) from err 

808 

809 b_mask = np.isnat(delta) 

810 

811 res_values = algos.checked_add_with_arr( 

812 self.asi8, delta.view("i8"), arr_mask=self._isnan, b_mask=b_mask 

813 ) 

814 np.putmask(res_values, self._isnan | b_mask, iNaT) 

815 return type(self)(res_values, freq=self.freq) 

816 

817 def _check_timedeltalike_freq_compat(self, other): 

818 """ 

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

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

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

822 raise because the operation is invalid. 

823 

824 Parameters 

825 ---------- 

826 other : timedelta, np.timedelta64, Tick, 

827 ndarray[timedelta64], TimedeltaArray, TimedeltaIndex 

828 

829 Returns 

830 ------- 

831 multiple : int or ndarray[int64] 

832 

833 Raises 

834 ------ 

835 IncompatibleFrequency 

836 """ 

837 assert isinstance(self.freq, Tick) # checked by calling function 

838 

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

840 

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

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

843 else: 

844 td = np.asarray(other) 

845 

846 try: 

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

848 except ValueError as err: 

849 raise raise_on_incompatible(self, other) from err 

850 

851 delta = delta.view("i8") 

852 return lib.item_from_zerodim(delta) 

853 

854 

855def raise_on_incompatible(left, right): 

856 """ 

857 Helper function to render a consistent error message when raising 

858 IncompatibleFrequency. 

859 

860 Parameters 

861 ---------- 

862 left : PeriodArray 

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

864 

865 Returns 

866 ------- 

867 IncompatibleFrequency 

868 Exception to be raised by the caller. 

869 """ 

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

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

872 other_freq = None 

873 elif isinstance(right, (ABCPeriodIndex, PeriodArray, Period, BaseOffset)): 

874 other_freq = right.freqstr 

875 else: 

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

877 

878 msg = DIFFERENT_FREQ.format( 

879 cls=type(left).__name__, own_freq=left.freqstr, other_freq=other_freq 

880 ) 

881 return IncompatibleFrequency(msg) 

882 

883 

884# ------------------------------------------------------------------- 

885# Constructor Helpers 

886 

887 

888def period_array( 

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

890 freq: str | Tick | None = None, 

891 copy: bool = False, 

892) -> PeriodArray: 

893 """ 

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

895 

896 Parameters 

897 ---------- 

898 data : Sequence of Period objects 

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

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

901 or ``pandas.NaT``. 

902 freq : str, Tick, or Offset 

903 The frequency of every element of the array. This can be specified 

904 to avoid inferring the `freq` from `data`. 

905 copy : bool, default False 

906 Whether to ensure a copy of the data is made. 

907 

908 Returns 

909 ------- 

910 PeriodArray 

911 

912 See Also 

913 -------- 

914 PeriodArray 

915 pandas.PeriodIndex 

916 

917 Examples 

918 -------- 

919 >>> period_array([pd.Period('2017', freq='A'), 

920 ... pd.Period('2018', freq='A')]) 

921 <PeriodArray> 

922 ['2017', '2018'] 

923 Length: 2, dtype: period[A-DEC] 

924 

925 >>> period_array([pd.Period('2017', freq='A'), 

926 ... pd.Period('2018', freq='A'), 

927 ... pd.NaT]) 

928 <PeriodArray> 

929 ['2017', '2018', 'NaT'] 

930 Length: 3, dtype: period[A-DEC] 

931 

932 Integers that look like years are handled 

933 

934 >>> period_array([2000, 2001, 2002], freq='D') 

935 <PeriodArray> 

936 ['2000-01-01', '2001-01-01', '2002-01-01'] 

937 Length: 3, dtype: period[D] 

938 

939 Datetime-like strings may also be passed 

940 

941 >>> period_array(['2000-Q1', '2000-Q2', '2000-Q3', '2000-Q4'], freq='Q') 

942 <PeriodArray> 

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

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

945 """ 

946 data_dtype = getattr(data, "dtype", None) 

947 

948 if is_datetime64_dtype(data_dtype): 

949 return PeriodArray._from_datetime64(data, freq) 

950 if is_period_dtype(data_dtype): 

951 return PeriodArray(data, freq=freq) 

952 

953 # other iterable of some kind 

954 if not isinstance(data, (np.ndarray, list, tuple, ABCSeries)): 

955 data = list(data) 

956 

957 arrdata = np.asarray(data) 

958 

959 dtype: PeriodDtype | None 

960 if freq: 

961 dtype = PeriodDtype(freq) 

962 else: 

963 dtype = None 

964 

965 if is_float_dtype(arrdata) and len(arrdata) > 0: 

966 raise TypeError("PeriodIndex does not allow floating point in construction") 

967 

968 if is_integer_dtype(arrdata.dtype): 

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

970 # error: Argument 2 to "from_ordinals" has incompatible type "Union[str, 

971 # Tick, None]"; expected "Union[timedelta, BaseOffset, str]" 

972 ordinals = libperiod.from_ordinals(arr, freq) # type: ignore[arg-type] 

973 return PeriodArray(ordinals, dtype=dtype) 

974 

975 data = ensure_object(arrdata) 

976 

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

978 

979 

980@overload 

981def validate_dtype_freq(dtype, freq: BaseOffsetT) -> BaseOffsetT: 

982 ... 

983 

984 

985@overload 

986def validate_dtype_freq(dtype, freq: timedelta | str | None) -> BaseOffset: 

987 ... 

988 

989 

990def validate_dtype_freq( 

991 dtype, freq: BaseOffsetT | timedelta | str | None 

992) -> BaseOffsetT: 

993 """ 

994 If both a dtype and a freq are available, ensure they match. If only 

995 dtype is available, extract the implied freq. 

996 

997 Parameters 

998 ---------- 

999 dtype : dtype 

1000 freq : DateOffset or None 

1001 

1002 Returns 

1003 ------- 

1004 freq : DateOffset 

1005 

1006 Raises 

1007 ------ 

1008 ValueError : non-period dtype 

1009 IncompatibleFrequency : mismatch between dtype and freq 

1010 """ 

1011 if freq is not None: 

1012 # error: Incompatible types in assignment (expression has type 

1013 # "BaseOffset", variable has type "Union[BaseOffsetT, timedelta, 

1014 # str, None]") 

1015 freq = to_offset(freq) # type: ignore[assignment] 

1016 

1017 if dtype is not None: 

1018 dtype = pandas_dtype(dtype) 

1019 if not is_period_dtype(dtype): 

1020 raise ValueError("dtype must be PeriodDtype") 

1021 if freq is None: 

1022 freq = dtype.freq 

1023 elif freq != dtype.freq: 

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

1025 # error: Incompatible return value type (got "Union[BaseOffset, Any, None]", 

1026 # expected "BaseOffset") 

1027 return freq # type: ignore[return-value] 

1028 

1029 

1030def dt64arr_to_periodarr( 

1031 data, freq, tz=None 

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

1033 """ 

1034 Convert an datetime-like array to values Period ordinals. 

1035 

1036 Parameters 

1037 ---------- 

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

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

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

1041 or Series. 

1042 tz : Optional[tzinfo] 

1043 

1044 Returns 

1045 ------- 

1046 ordinals : ndarray[int64] 

1047 freq : Tick 

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

1049 used. 

1050 

1051 """ 

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

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

1054 

1055 if freq is None: 

1056 if isinstance(data, ABCIndex): 

1057 data, freq = data._values, data.freq 

1058 elif isinstance(data, ABCSeries): 

1059 data, freq = data._values, data.dt.freq 

1060 

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

1062 data = data._values 

1063 

1064 reso = get_unit_from_dtype(data.dtype) 

1065 freq = Period._maybe_convert_freq(freq) 

1066 base = freq._period_dtype_code 

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

1068 

1069 

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

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

1072 raise ValueError( 

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

1074 "exactly two must be specified" 

1075 ) 

1076 

1077 if freq is not None: 

1078 freq = to_offset(freq) 

1079 mult = freq.n 

1080 

1081 if start is not None: 

1082 start = Period(start, freq) 

1083 if end is not None: 

1084 end = Period(end, freq) 

1085 

1086 is_start_per = isinstance(start, Period) 

1087 is_end_per = isinstance(end, Period) 

1088 

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

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

1091 if start is NaT or end is NaT: 

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

1093 

1094 if freq is None: 

1095 if is_start_per: 

1096 freq = start.freq 

1097 elif is_end_per: 

1098 freq = end.freq 

1099 else: # pragma: no cover 

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

1101 

1102 if periods is not None: 

1103 periods = periods * mult 

1104 if start is None: 

1105 data = np.arange( 

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

1107 ) 

1108 else: 

1109 data = np.arange( 

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

1111 ) 

1112 else: 

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

1114 

1115 return data, freq 

1116 

1117 

1118def _range_from_fields( 

1119 year=None, 

1120 month=None, 

1121 quarter=None, 

1122 day=None, 

1123 hour=None, 

1124 minute=None, 

1125 second=None, 

1126 freq=None, 

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

1128 if hour is None: 

1129 hour = 0 

1130 if minute is None: 

1131 minute = 0 

1132 if second is None: 

1133 second = 0 

1134 if day is None: 

1135 day = 1 

1136 

1137 ordinals = [] 

1138 

1139 if quarter is not None: 

1140 if freq is None: 

1141 freq = to_offset("Q") 

1142 base = FreqGroup.FR_QTR.value 

1143 else: 

1144 freq = to_offset(freq) 

1145 base = libperiod.freq_to_dtype_code(freq) 

1146 if base != FreqGroup.FR_QTR.value: 

1147 raise AssertionError("base must equal FR_QTR") 

1148 

1149 freqstr = freq.freqstr 

1150 year, quarter = _make_field_arrays(year, quarter) 

1151 for y, q in zip(year, quarter): 

1152 y, m = parsing.quarter_to_myear(y, q, freqstr) 

1153 val = libperiod.period_ordinal(y, m, 1, 1, 1, 1, 0, 0, base) 

1154 ordinals.append(val) 

1155 else: 

1156 freq = to_offset(freq) 

1157 base = libperiod.freq_to_dtype_code(freq) 

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

1159 for y, mth, d, h, mn, s in zip(*arrays): 

1160 ordinals.append(libperiod.period_ordinal(y, mth, d, h, mn, s, 0, 0, base)) 

1161 

1162 return np.array(ordinals, dtype=np.int64), freq 

1163 

1164 

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

1166 length = None 

1167 for x in fields: 

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

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

1170 raise ValueError("Mismatched Period array lengths") 

1171 elif length is None: 

1172 length = len(x) 

1173 

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

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

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

1177 return [ 

1178 np.asarray(x) 

1179 if isinstance(x, (np.ndarray, list, ABCSeries)) 

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

1181 for x in fields 

1182 ]