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 ]