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 overload,
13)
14import warnings
15
16import numpy as np
17
18from pandas._libs import (
19 algos as libalgos,
20 lib,
21)
22from pandas._libs.arrays import NDArrayBacked
23from pandas._libs.tslibs import (
24 BaseOffset,
25 Day,
26 NaT,
27 NaTType,
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.dtypes import (
39 FreqGroup,
40 PeriodDtypeBase,
41)
42from pandas._libs.tslibs.fields import isleapyear_arr
43from pandas._libs.tslibs.offsets import (
44 Tick,
45 delta_to_tick,
46)
47from pandas._libs.tslibs.period import (
48 DIFFERENT_FREQ,
49 IncompatibleFrequency,
50 Period,
51 get_period_field_arr,
52 period_asfreq_arr,
53)
54from pandas.util._decorators import (
55 cache_readonly,
56 doc,
57 set_module,
58)
59
60from pandas.core.dtypes.common import (
61 ensure_object,
62 pandas_dtype,
63)
64from pandas.core.dtypes.dtypes import (
65 DatetimeTZDtype,
66 PeriodDtype,
67)
68from pandas.core.dtypes.generic import (
69 ABCIndex,
70 ABCPeriodIndex,
71 ABCSeries,
72 ABCTimedeltaArray,
73)
74from pandas.core.dtypes.missing import isna
75
76from pandas.core.arrays import datetimelike as dtl
77import pandas.core.common as com
78
79if TYPE_CHECKING:
80 from collections.abc import (
81 Callable,
82 Sequence,
83 )
84
85 from pandas._typing import (
86 AnyArrayLike,
87 Dtype,
88 DtypeObj,
89 FillnaOptions,
90 NpDtype,
91 NumpySorter,
92 NumpyValueArrayLike,
93 npt,
94 )
95
96 from pandas.core.dtypes.dtypes import ExtensionDtype
97
98 from pandas.core.arrays import (
99 DatetimeArray,
100 TimedeltaArray,
101 )
102 from pandas.core.arrays.base import ExtensionArray
103
104
105BaseOffsetT = TypeVar("BaseOffsetT", bound=BaseOffset)
106
107
108_shared_doc_kwargs = {
109 "klass": "PeriodArray",
110}
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")
125# error: Definition of "_concat_same_type" in base class "NDArrayBacked" is
126# incompatible with definition in base class "ExtensionArray"
127class PeriodArray(dtl.DatelikeOps, libperiod.PeriodMixin): # type: ignore[misc]
128 """
129 Pandas ExtensionArray for storing Period data.
130
131 Users should use :func:`~pandas.array` to create new instances.
132
133 Parameters
134 ----------
135 values : Union[PeriodArray, Series[period], ndarray[int], PeriodIndex]
136 The data to store. These should be arrays that can be directly
137 converted to ordinals without inference or copy (PeriodArray,
138 ndarray[int64]), or a box around such an array (Series[period],
139 PeriodIndex).
140 dtype : PeriodDtype, optional
141 A PeriodDtype instance from which to extract a `freq`. If both
142 `freq` and `dtype` are specified, then the frequencies must match.
143 copy : bool, default False
144 Whether to copy the ordinals before storing.
145
146 Attributes
147 ----------
148 None
149
150 Methods
151 -------
152 None
153
154 See Also
155 --------
156 Period: Represents a period of time.
157 PeriodIndex : Immutable Index for period data.
158 period_range: Create a fixed-frequency PeriodArray.
159 array: Construct a pandas array.
160
161 Notes
162 -----
163 There are two components to a PeriodArray
164
165 - ordinals : integer ndarray
166 - freq : pd.tseries.offsets.Offset
167
168 The values are physically stored as a 1-D ndarray of integers. These are
169 called "ordinals" and represent some kind of offset from a base.
170
171 The `freq` indicates the span covered by each element of the array.
172 All elements in the PeriodArray have the same `freq`.
173
174 Examples
175 --------
176 >>> pd.arrays.PeriodArray(pd.PeriodIndex(["2023-01-01", "2023-01-02"], freq="D"))
177 <PeriodArray>
178 ['2023-01-01', '2023-01-02']
179 Length: 2, dtype: period[D]
180 """
181
182 # array priority higher than numpy scalars
183 __array_priority__ = 1000
184 _typ = "periodarray" # ABCPeriodArray
185 _internal_fill_value = np.int64(iNaT)
186 _recognized_scalars = (Period,)
187 _is_recognized_dtype: Callable[[DtypeObj], bool] = lambda x: isinstance(
188 x, PeriodDtype
189 ) # check_compatible_with checks freq match
190 _infer_matches = ("period",)
191
192 @property
193 def _scalar_type(self) -> type[Period]:
194 return Period
195
196 # Names others delegate to us
197 _other_ops: list[str] = []
198 _bool_ops: list[str] = ["is_leap_year"]
199 _object_ops: list[str] = ["start_time", "end_time", "freq"]
200 _field_ops: list[str] = [
201 "year",
202 "month",
203 "day",
204 "hour",
205 "minute",
206 "second",
207 "weekofyear",
208 "weekday",
209 "week",
210 "dayofweek",
211 "day_of_week",
212 "dayofyear",
213 "day_of_year",
214 "quarter",
215 "qyear",
216 "days_in_month",
217 "daysinmonth",
218 ]
219 _datetimelike_ops: list[str] = _field_ops + _object_ops + _bool_ops
220 _datetimelike_methods: list[str] = ["strftime", "to_timestamp", "asfreq"]
221
222 _dtype: PeriodDtype
223
224 # --------------------------------------------------------------------
225 # Constructors
226
227 def __init__(self, values, dtype: Dtype | None = None, copy: bool = False) -> None:
228 if dtype is not None:
229 dtype = pandas_dtype(dtype)
230 if not isinstance(dtype, PeriodDtype):
231 raise ValueError(f"Invalid dtype {dtype} for PeriodArray")
232
233 if isinstance(values, ABCSeries):
234 values = values._values
235 if not isinstance(values, type(self)):
236 raise TypeError("Incorrect dtype")
237
238 elif isinstance(values, ABCPeriodIndex):
239 values = values._values
240
241 if isinstance(values, type(self)):
242 if dtype is not None and dtype != values.dtype:
243 raise raise_on_incompatible(values, dtype.freq)
244 values, dtype = values._ndarray, values.dtype
245
246 if not copy:
247 values = np.asarray(values, dtype="int64")
248 else:
249 values = np.array(values, dtype="int64", copy=copy)
250 if dtype is None:
251 raise ValueError("dtype is not specified and cannot be inferred")
252 dtype = cast(PeriodDtype, dtype)
253 NDArrayBacked.__init__(self, values, dtype)
254
255 # error: Signature of "_simple_new" incompatible with supertype "NDArrayBacked"
256 @classmethod
257 def _simple_new( # type: ignore[override]
258 cls,
259 values: npt.NDArray[np.int64],
260 dtype: PeriodDtype,
261 ) -> Self:
262 # alias for PeriodArray.__init__
263 assertion_msg = "Should be numpy array of type i8"
264 assert isinstance(values, np.ndarray) and values.dtype == "i8", assertion_msg
265 return cls(values, dtype=dtype)
266
267 @classmethod
268 def _from_sequence(
269 cls,
270 scalars,
271 *,
272 dtype: Dtype | None = None,
273 copy: bool = False,
274 ) -> Self:
275 if dtype is not None:
276 dtype = pandas_dtype(dtype)
277 if dtype and isinstance(dtype, PeriodDtype):
278 freq = dtype.freq
279 else:
280 freq = None
281
282 if isinstance(scalars, cls):
283 validate_dtype_freq(scalars.dtype, freq)
284 if copy:
285 scalars = scalars.copy()
286 return scalars
287
288 periods = np.asarray(scalars, dtype=object)
289
290 freq = freq or libperiod.extract_freq(periods)
291 ordinals = libperiod.extract_ordinals(periods, freq)
292 dtype = PeriodDtype(freq)
293 return cls(ordinals, dtype=dtype)
294
295 @classmethod
296 def _from_sequence_of_strings(
297 cls, strings, *, dtype: ExtensionDtype, copy: bool = False
298 ) -> Self:
299 return cls._from_sequence(strings, dtype=dtype, copy=copy)
300
301 @classmethod
302 def _from_datetime64(cls, data, freq, tz=None) -> Self:
303 """
304 Construct a PeriodArray from a datetime64 array
305
306 Parameters
307 ----------
308 data : ndarray[datetime64[ns], datetime64[ns, tz]]
309 freq : str or Tick
310 tz : tzinfo, optional
311
312 Returns
313 -------
314 PeriodArray[freq]
315 """
316 if isinstance(freq, BaseOffset):
317 freq = PeriodDtype(freq)._freqstr
318 data, freq = dt64arr_to_periodarr(data, freq, tz)
319 dtype = PeriodDtype(freq)
320 return cls(data, dtype=dtype)
321
322 @classmethod
323 def _generate_range(cls, start, end, periods, freq):
324 periods = dtl.validate_periods(periods)
325
326 if freq is not None:
327 freq = Period._maybe_convert_freq(freq)
328
329 if start is not None or end is not None:
330 subarr, freq = _get_ordinal_range(start, end, periods, freq)
331 else:
332 raise ValueError("Not enough parameters to construct Period range")
333
334 return subarr, freq
335
336 @classmethod
337 def _from_fields(cls, *, fields: dict, freq) -> Self:
338 subarr, freq = _range_from_fields(freq=freq, **fields)
339 dtype = PeriodDtype(freq)
340 return cls._simple_new(subarr, dtype=dtype)
341
342 # -----------------------------------------------------------------
343 # DatetimeLike Interface
344
345 # error: Argument 1 of "_unbox_scalar" is incompatible with supertype
346 # "DatetimeLikeArrayMixin"; supertype defines the argument type as
347 # "Union[Union[Period, Any, Timedelta], NaTType]"
348 def _unbox_scalar( # type: ignore[override]
349 self,
350 value: Period | NaTType,
351 ) -> np.int64:
352 if value is NaT:
353 # error: Item "Period" of "Union[Period, NaTType]" has no attribute "value"
354 return np.int64(value._value) # type: ignore[union-attr]
355 elif isinstance(value, self._scalar_type):
356 self._check_compatible_with(value)
357 return np.int64(value.ordinal)
358 else:
359 raise ValueError(f"'value' should be a Period. Got '{value}' instead.")
360
361 def _scalar_from_string(self, value: str) -> Period:
362 return Period(value, freq=self.freq)
363
364 # error: Argument 1 of "_check_compatible_with" is incompatible with
365 # supertype "DatetimeLikeArrayMixin"; supertype defines the argument type
366 # as "Period | Timestamp | Timedelta | NaTType"
367 def _check_compatible_with(self, other: Period | NaTType | PeriodArray) -> None: # type: ignore[override]
368 if other is NaT:
369 return
370 # error: Item "NaTType" of "Period | NaTType | PeriodArray" has no
371 # attribute "freq"
372 self._require_matching_freq(other.freq) # type: ignore[union-attr]
373
374 # --------------------------------------------------------------------
375 # Data / Attributes
376
377 @cache_readonly
378 def dtype(self) -> PeriodDtype:
379 return self._dtype
380
381 # error: Cannot override writeable attribute with read-only property
382 @property
383 def freq(self) -> BaseOffset: # type: ignore[override]
384 """
385 Return the frequency object for this PeriodArray.
386 """
387 return self.dtype.freq
388
389 @property
390 def freqstr(self) -> str:
391 return PeriodDtype(self.freq)._freqstr
392
393 def __array__(
394 self, dtype: NpDtype | None = None, copy: bool | None = None
395 ) -> np.ndarray:
396 if dtype == "i8":
397 # For NumPy 1.x compatibility we cannot use copy=None. And
398 # `copy=False` has the meaning of `copy=None` here:
399 if not copy:
400 result = np.asarray(self.asi8, dtype=dtype)
401 if self._readonly:
402 result = result.view()
403 result.flags.writeable = False
404 return result
405 else:
406 return np.array(self.asi8, dtype=dtype)
407
408 if copy is False:
409 raise ValueError(
410 "Unable to avoid copy while creating an array as requested."
411 )
412
413 if dtype == bool:
414 return ~self._isnan
415
416 # This will raise TypeError for non-object dtypes
417 return np.array(list(self), dtype=object)
418
419 def __arrow_array__(self, type=None):
420 """
421 Convert myself into a pyarrow Array.
422 """
423 import pyarrow
424
425 from pandas.core.arrays.arrow.extension_types import ArrowPeriodType
426
427 if type is not None:
428 if pyarrow.types.is_integer(type):
429 return pyarrow.array(self._ndarray, mask=self.isna(), type=type)
430 elif isinstance(type, ArrowPeriodType):
431 # ensure we have the same freq
432 if self.freqstr != type.freq:
433 raise TypeError(
434 "Not supported to convert PeriodArray to array with different "
435 f"'freq' ({self.freqstr} vs {type.freq})"
436 )
437 else:
438 raise TypeError(
439 f"Not supported to convert PeriodArray to '{type}' type"
440 )
441
442 period_type = ArrowPeriodType(self.freqstr)
443 storage_array = pyarrow.array(self._ndarray, mask=self.isna(), type="int64")
444 return pyarrow.ExtensionArray.from_storage(period_type, storage_array)
445
446 # --------------------------------------------------------------------
447 # Vectorized analogues of Period properties
448
449 year = _field_accessor(
450 "year",
451 """
452 The year of the period.
453
454 See Also
455 --------
456 PeriodIndex.day_of_year : The ordinal day of the year.
457 PeriodIndex.dayofyear : The ordinal day of the year.
458 PeriodIndex.is_leap_year : Logical indicating if the date belongs to a
459 leap year.
460 PeriodIndex.weekofyear : The week ordinal of the year.
461 PeriodIndex.year : The year of the period.
462
463 Examples
464 --------
465 >>> idx = pd.PeriodIndex(["2023", "2024", "2025"], freq="Y")
466 >>> idx.year
467 Index([2023, 2024, 2025], dtype='int64')
468 """,
469 )
470 month = _field_accessor(
471 "month",
472 """
473 The month as January=1, December=12.
474
475 See Also
476 --------
477 PeriodIndex.days_in_month : The number of days in the month.
478 PeriodIndex.daysinmonth : The number of days in the month.
479
480 Examples
481 --------
482 >>> idx = pd.PeriodIndex(["2023-01", "2023-02", "2023-03"], freq="M")
483 >>> idx.month
484 Index([1, 2, 3], dtype='int64')
485 """,
486 )
487 day = _field_accessor(
488 "day",
489 """
490 The days of the period.
491
492 See Also
493 --------
494 PeriodIndex.day_of_week : The day of the week with Monday=0, Sunday=6.
495 PeriodIndex.day_of_year : The ordinal day of the year.
496 PeriodIndex.dayofweek : The day of the week with Monday=0, Sunday=6.
497 PeriodIndex.dayofyear : The ordinal day of the year.
498 PeriodIndex.days_in_month : The number of days in the month.
499 PeriodIndex.daysinmonth : The number of days in the month.
500 PeriodIndex.weekday : The day of the week with Monday=0, Sunday=6.
501
502 Examples
503 --------
504 >>> idx = pd.PeriodIndex(['2020-01-31', '2020-02-28'], freq='D')
505 >>> idx.day
506 Index([31, 28], dtype='int64')
507 """,
508 )
509 hour = _field_accessor(
510 "hour",
511 """
512 The hour of the period.
513
514 See Also
515 --------
516 PeriodIndex.minute : The minute of the period.
517 PeriodIndex.second : The second of the period.
518 PeriodIndex.to_timestamp : Cast to DatetimeArray/Index.
519
520 Examples
521 --------
522 >>> idx = pd.PeriodIndex(["2023-01-01 10:00", "2023-01-01 11:00"], freq='h')
523 >>> idx.hour
524 Index([10, 11], dtype='int64')
525 """,
526 )
527 minute = _field_accessor(
528 "minute",
529 """
530 The minute of the period.
531
532 See Also
533 --------
534 PeriodIndex.hour : The hour of the period.
535 PeriodIndex.second : The second of the period.
536 PeriodIndex.to_timestamp : Cast to DatetimeArray/Index.
537
538 Examples
539 --------
540 >>> idx = pd.PeriodIndex(["2023-01-01 10:30:00",
541 ... "2023-01-01 11:50:00"], freq='min')
542 >>> idx.minute
543 Index([30, 50], dtype='int64')
544 """,
545 )
546 second = _field_accessor(
547 "second",
548 """
549 The second of the period.
550
551 See Also
552 --------
553 PeriodIndex.hour : The hour of the period.
554 PeriodIndex.minute : The minute of the period.
555 PeriodIndex.to_timestamp : Cast to DatetimeArray/Index.
556
557 Examples
558 --------
559 >>> idx = pd.PeriodIndex(["2023-01-01 10:00:30",
560 ... "2023-01-01 10:00:31"], freq='s')
561 >>> idx.second
562 Index([30, 31], dtype='int64')
563 """,
564 )
565 weekofyear = _field_accessor(
566 "week",
567 """
568 The week ordinal of the year.
569
570 See Also
571 --------
572 PeriodIndex.day_of_week : The day of the week with Monday=0, Sunday=6.
573 PeriodIndex.dayofweek : The day of the week with Monday=0, Sunday=6.
574 PeriodIndex.week : The week ordinal of the year.
575 PeriodIndex.weekday : The day of the week with Monday=0, Sunday=6.
576 PeriodIndex.year : The year of the period.
577
578 Examples
579 --------
580 >>> idx = pd.PeriodIndex(["2023-01", "2023-02", "2023-03"], freq="M")
581 >>> idx.week # It can be written `weekofyear`
582 Index([5, 9, 13], dtype='int64')
583 """,
584 )
585 week = weekofyear
586 day_of_week = _field_accessor(
587 "day_of_week",
588 """
589 The day of the week with Monday=0, Sunday=6.
590
591 See Also
592 --------
593 PeriodIndex.day : The days of the period.
594 PeriodIndex.day_of_week : The day of the week with Monday=0, Sunday=6.
595 PeriodIndex.day_of_year : The ordinal day of the year.
596 PeriodIndex.dayofweek : The day of the week with Monday=0, Sunday=6.
597 PeriodIndex.dayofyear : The ordinal day of the year.
598 PeriodIndex.week : The week ordinal of the year.
599 PeriodIndex.weekday : The day of the week with Monday=0, Sunday=6.
600 PeriodIndex.weekofyear : The week ordinal of the year.
601
602 Examples
603 --------
604 >>> idx = pd.PeriodIndex(["2023-01-01", "2023-01-02", "2023-01-03"], freq="D")
605 >>> idx.weekday
606 Index([6, 0, 1], dtype='int64')
607 """,
608 )
609 dayofweek = day_of_week
610 weekday = dayofweek
611 dayofyear = day_of_year = _field_accessor(
612 "day_of_year",
613 """
614 The ordinal day of the year.
615
616 See Also
617 --------
618 PeriodIndex.day : The days of the period.
619 PeriodIndex.day_of_week : The day of the week with Monday=0, Sunday=6.
620 PeriodIndex.day_of_year : The ordinal day of the year.
621 PeriodIndex.dayofweek : The day of the week with Monday=0, Sunday=6.
622 PeriodIndex.dayofyear : The ordinal day of the year.
623 PeriodIndex.weekday : The day of the week with Monday=0, Sunday=6.
624 PeriodIndex.weekofyear : The week ordinal of the year.
625 PeriodIndex.year : The year of the period.
626
627 Examples
628 --------
629 >>> idx = pd.PeriodIndex(["2023-01-10", "2023-02-01", "2023-03-01"], freq="D")
630 >>> idx.dayofyear
631 Index([10, 32, 60], dtype='int64')
632
633 >>> idx = pd.PeriodIndex(["2023", "2024", "2025"], freq="Y")
634 >>> idx
635 PeriodIndex(['2023', '2024', '2025'], dtype='period[Y-DEC]')
636 >>> idx.dayofyear
637 Index([365, 366, 365], dtype='int64')
638 """,
639 )
640 quarter = _field_accessor(
641 "quarter",
642 """
643 The quarter of the date.
644
645 See Also
646 --------
647 PeriodIndex.qyear : Fiscal year the Period lies in according to its
648 starting-quarter.
649
650 Examples
651 --------
652 >>> idx = pd.PeriodIndex(["2023-01", "2023-02", "2023-03"], freq="M")
653 >>> idx.quarter
654 Index([1, 1, 1], dtype='int64')
655 """,
656 )
657 qyear = _field_accessor(
658 "qyear",
659 """
660 Fiscal year the Period lies in according to its starting-quarter.
661
662 The `year` and the `qyear` of the period will be the same if the fiscal
663 and calendar years are the same. When they are not, the fiscal year
664 can be different from the calendar year of the period.
665
666 Returns
667 -------
668 int
669 The fiscal year of the period.
670
671 See Also
672 --------
673 PeriodIndex.quarter : The quarter of the date.
674 PeriodIndex.year : The year of the period.
675
676 Examples
677 --------
678 If the natural and fiscal year are the same, `qyear` and `year` will
679 be the same.
680
681 >>> per = pd.Period('2018Q1', freq='Q')
682 >>> per.qyear
683 2018
684 >>> per.year
685 2018
686
687 If the fiscal year starts in April (`Q-MAR`), the first quarter of
688 2018 will start in April 2017. `year` will then be 2017, but `qyear`
689 will be the fiscal year, 2018.
690
691 >>> per = pd.Period('2018Q1', freq='Q-MAR')
692 >>> per.start_time
693 Timestamp('2017-04-01 00:00:00')
694 >>> per.qyear
695 2018
696 >>> per.year
697 2017
698 """,
699 )
700
701 days_in_month = _field_accessor(
702 "days_in_month",
703 """
704 The number of days in the month.
705
706 See Also
707 --------
708 PeriodIndex.day : The days of the period.
709 PeriodIndex.days_in_month : The number of days in the month.
710 PeriodIndex.daysinmonth : The number of days in the month.
711 PeriodIndex.month : The month as January=1, December=12.
712
713 Examples
714 --------
715 For Series:
716
717 >>> period = pd.period_range('2020-1-1 00:00', '2020-3-1 00:00', freq='M')
718 >>> s = pd.Series(period)
719 >>> s
720 0 2020-01
721 1 2020-02
722 2 2020-03
723 dtype: period[M]
724 >>> s.dt.days_in_month
725 0 31
726 1 29
727 2 31
728 dtype: int64
729
730 For PeriodIndex:
731
732 >>> idx = pd.PeriodIndex(["2023-01", "2023-02", "2023-03"], freq="M")
733 >>> idx.days_in_month # It can be also entered as `daysinmonth`
734 Index([31, 28, 31], dtype='int64')
735 """,
736 )
737 daysinmonth = days_in_month
738
739 @property
740 def is_leap_year(self) -> npt.NDArray[np.bool_]:
741 """
742 Logical indicating if the date belongs to a leap year.
743
744 See Also
745 --------
746 PeriodIndex.qyear : Fiscal year the Period lies in according to its
747 starting-quarter.
748 PeriodIndex.year : The year of the period.
749
750 Examples
751 --------
752 >>> idx = pd.PeriodIndex(["2023", "2024", "2025"], freq="Y")
753 >>> idx.is_leap_year
754 array([False, True, False])
755 """
756 return isleapyear_arr(np.asarray(self.year))
757
758 def to_timestamp(self, freq=None, how: str = "start") -> DatetimeArray:
759 """
760 Cast to DatetimeArray/Index.
761
762 If possible, gives microsecond-unit DatetimeArray/Index. Otherwise
763 gives nanosecond unit.
764
765 Parameters
766 ----------
767 freq : str or DateOffset, optional
768 Target frequency. The default is 'D' for week or longer,
769 's' otherwise.
770 how : {'s', 'e', 'start', 'end'}
771 Whether to use the start or end of the time period being converted.
772
773 Returns
774 -------
775 DatetimeArray/Index
776 Timestamp representation of given Period-like object.
777
778 See Also
779 --------
780 PeriodIndex.day : The days of the period.
781 PeriodIndex.from_fields : Construct a PeriodIndex from fields
782 (year, month, day, etc.).
783 PeriodIndex.from_ordinals : Construct a PeriodIndex from ordinals.
784 PeriodIndex.hour : The hour of the period.
785 PeriodIndex.minute : The minute of the period.
786 PeriodIndex.month : The month as January=1, December=12.
787 PeriodIndex.second : The second of the period.
788 PeriodIndex.year : The year of the period.
789
790 Examples
791 --------
792 >>> idx = pd.PeriodIndex(["2023-01", "2023-02", "2023-03"], freq="M")
793 >>> idx.to_timestamp()
794 DatetimeIndex(['2023-01-01', '2023-02-01', '2023-03-01'],
795 dtype='datetime64[us]', freq='MS')
796
797 The frequency will not be inferred if the index contains less than
798 three elements, or if the values of index are not strictly monotonic:
799
800 >>> idx = pd.PeriodIndex(["2023-01", "2023-02"], freq="M")
801 >>> idx.to_timestamp()
802 DatetimeIndex(['2023-01-01', '2023-02-01'], dtype='datetime64[us]', freq=None)
803
804 >>> idx = pd.PeriodIndex(
805 ... ["2023-01", "2023-02", "2023-02", "2023-03"], freq="2M"
806 ... )
807 >>> idx.to_timestamp()
808 DatetimeIndex(['2023-01-01', '2023-02-01', '2023-02-01', '2023-03-01'],
809 dtype='datetime64[us]', freq=None)
810 """
811 from pandas.core.arrays import DatetimeArray
812
813 how = libperiod.validate_end_alias(how)
814
815 if self.freq.base == "ns" or freq == "ns":
816 unit = "ns"
817 else:
818 unit = "us"
819
820 end = how == "E"
821 if end:
822 if freq == "B" or self.freq == "B":
823 # roll forward to ensure we land on B date
824 adjust = Timedelta(1, unit="D") - Timedelta(1, unit=unit)
825 return self.to_timestamp(how="start") + adjust
826 else:
827 adjust = Timedelta(1, unit=unit)
828 return (self + self.freq).to_timestamp(how="start") - adjust
829
830 if freq is None:
831 freq_code = self._dtype._get_to_timestamp_base()
832 dtype = PeriodDtypeBase(freq_code, 1)
833 freq = dtype._freqstr
834 base = freq_code
835 else:
836 freq = Period._maybe_convert_freq(freq)
837 base = freq._period_dtype_code
838
839 new_parr = self.asfreq(freq, how=how)
840
841 new_data = libperiod.periodarr_to_dt64arr(new_parr.asi8, base)
842 dta = DatetimeArray._from_sequence(new_data, dtype=new_data.dtype)
843 assert dta.unit == unit
844
845 if self.freq.name == "B":
846 # See if we can retain BDay instead of Day in cases where
847 # len(self) is too small for infer_freq to distinguish between them
848 diffs = libalgos.unique_deltas(self.asi8)
849 if len(diffs) == 1:
850 diff = diffs[0]
851 if diff == self.dtype._n:
852 dta._freq = self.freq
853 elif diff == 1:
854 dta._freq = self.freq.base
855 # TODO: other cases?
856 return dta
857 else:
858 dta = dta._with_freq("infer")
859 if freq is not None:
860 freq = to_offset(freq)
861 if (
862 isinstance(dta.freq, Day)
863 and not isinstance(freq, Day)
864 and Timedelta(freq) == Timedelta(days=dta.freq.n)
865 ):
866 dta._freq = freq
867 return dta
868
869 # --------------------------------------------------------------------
870
871 def _box_func(self, x) -> Period | NaTType:
872 return Period._from_ordinal(ordinal=x, freq=self.freq)
873
874 @doc(**_shared_doc_kwargs, other="PeriodIndex", other_name="PeriodIndex")
875 def asfreq(self, freq=None, how: str = "E") -> Self:
876 """
877 Convert the {klass} to the specified frequency `freq`.
878
879 Equivalent to applying :meth:`pandas.Period.asfreq` with the given arguments
880 to each :class:`~pandas.Period` in this {klass}.
881
882 Parameters
883 ----------
884 freq : str
885 A frequency.
886 how : str {{'E', 'S'}}, default 'E'
887 Whether the elements should be aligned to the end
888 or start within pa period.
889
890 * 'E', 'END', or 'FINISH' for end,
891 * 'S', 'START', or 'BEGIN' for start.
892
893 January 31st ('END') vs. January 1st ('START') for example.
894
895 Returns
896 -------
897 {klass}
898 The transformed {klass} with the new frequency.
899
900 See Also
901 --------
902 {other}.asfreq: Convert each Period in a {other_name} to the given frequency.
903 Period.asfreq : Convert a :class:`~pandas.Period` object to the given frequency.
904
905 Examples
906 --------
907 >>> pidx = pd.period_range("2010-01-01", "2015-01-01", freq="Y")
908 >>> pidx
909 PeriodIndex(['2010', '2011', '2012', '2013', '2014', '2015'],
910 dtype='period[Y-DEC]')
911
912 >>> pidx.asfreq("M")
913 PeriodIndex(['2010-12', '2011-12', '2012-12', '2013-12', '2014-12',
914 '2015-12'], dtype='period[M]')
915
916 >>> pidx.asfreq("M", how="S")
917 PeriodIndex(['2010-01', '2011-01', '2012-01', '2013-01', '2014-01',
918 '2015-01'], dtype='period[M]')
919 """
920 how = libperiod.validate_end_alias(how)
921 if isinstance(freq, BaseOffset) and hasattr(freq, "_period_dtype_code"):
922 freq = PeriodDtype(freq)._freqstr
923 freq = Period._maybe_convert_freq(freq)
924
925 base1 = self._dtype._dtype_code
926 base2 = freq._period_dtype_code
927
928 asi8 = self.asi8
929 # self.freq.n can't be negative or 0
930 end = how == "E"
931 if end:
932 ordinal = asi8 + self.dtype._n - 1
933 else:
934 ordinal = asi8
935
936 new_data = period_asfreq_arr(ordinal, base1, base2, end)
937
938 if self._hasna:
939 new_data[self._isnan] = iNaT
940
941 dtype = PeriodDtype(freq)
942 return type(self)(new_data, dtype=dtype)
943
944 # ------------------------------------------------------------------
945 # Rendering Methods
946
947 def _formatter(self, boxed: bool = False) -> Callable[[object], str]:
948 if boxed:
949 return str
950 return "'{}'".format
951
952 def _format_native_types(
953 self, *, na_rep: str | float = "NaT", date_format=None, **kwargs
954 ) -> npt.NDArray[np.object_]:
955 """
956 actually format my specific types
957 """
958 return libperiod.period_array_strftime(
959 self.asi8, self.dtype._dtype_code, na_rep, date_format
960 )
961
962 # ------------------------------------------------------------------
963
964 def astype(self, dtype, copy: bool = True):
965 # We handle Period[T] -> Period[U]
966 # Our parent handles everything else.
967 dtype = pandas_dtype(dtype)
968 if dtype == self._dtype:
969 if not copy:
970 return self
971 else:
972 return self.copy()
973 if isinstance(dtype, PeriodDtype):
974 return self.asfreq(dtype.freq)
975
976 if lib.is_np_dtype(dtype, "M") or isinstance(dtype, DatetimeTZDtype):
977 # GH#45038 match PeriodIndex behavior.
978 tz = getattr(dtype, "tz", None)
979 unit = dtl.dtype_to_unit(dtype)
980 # error: Argument 1 to "as_unit" of "TimelikeOps" has incompatible
981 # type "str"; expected "Literal['s', 'ms', 'us', 'ns']" [arg-type]
982 return self.to_timestamp().tz_localize(tz).as_unit(unit) # type: ignore[arg-type]
983
984 return super().astype(dtype, copy=copy)
985
986 def searchsorted(
987 self,
988 value: NumpyValueArrayLike | ExtensionArray,
989 side: Literal["left", "right"] = "left",
990 sorter: NumpySorter | None = None,
991 ) -> npt.NDArray[np.intp] | np.intp:
992 npvalue = self._validate_setitem_value(value).view("M8[ns]")
993
994 # Cast to M8 to get datetime-like NaT placement,
995 # similar to dtl._period_dispatch
996 m8arr = self._ndarray.view("M8[ns]")
997 return m8arr.searchsorted(npvalue, side=side, sorter=sorter)
998
999 def _pad_or_backfill(
1000 self,
1001 *,
1002 method: FillnaOptions,
1003 limit: int | None = None,
1004 limit_area: Literal["inside", "outside"] | None = None,
1005 copy: bool = True,
1006 ) -> Self:
1007 # view as dt64 so we get treated as timelike in core.missing,
1008 # similar to dtl._period_dispatch
1009 dta = self.view("M8[ns]")
1010 result = dta._pad_or_backfill(
1011 method=method, limit=limit, limit_area=limit_area, copy=copy
1012 )
1013 if copy:
1014 return cast("Self", result.view(self.dtype))
1015 else:
1016 return self
1017
1018 # ------------------------------------------------------------------
1019 # Arithmetic Methods
1020
1021 def _addsub_int_array_or_scalar(
1022 self, other: np.ndarray | int, op: Callable[[Any, Any], Any]
1023 ) -> Self:
1024 """
1025 Add or subtract array of integers.
1026
1027 Parameters
1028 ----------
1029 other : np.ndarray[int64] or int
1030 op : {operator.add, operator.sub}
1031
1032 Returns
1033 -------
1034 result : PeriodArray
1035 """
1036 assert op in [operator.add, operator.sub]
1037 if op is operator.sub:
1038 other = -other
1039 res_values = add_overflowsafe(self.asi8, np.asarray(other, dtype="i8"))
1040 return type(self)(res_values, dtype=self.dtype)
1041
1042 def _add_offset(self, other: BaseOffset):
1043 assert not isinstance(other, Tick)
1044
1045 if isinstance(other, Day):
1046 return self + np.timedelta64(other.n, "D")
1047
1048 self._require_matching_freq(other, base=True)
1049 return self._addsub_int_array_or_scalar(other.n, operator.add)
1050
1051 # TODO: can we de-duplicate with Period._add_timedeltalike_scalar?
1052 def _add_timedeltalike_scalar(self, other):
1053 """
1054 Parameters
1055 ----------
1056 other : timedelta, Tick, np.timedelta64
1057
1058 Returns
1059 -------
1060 PeriodArray
1061 """
1062 if not isinstance(self.freq, (Tick, Day)):
1063 # We cannot add timedelta-like to non-tick PeriodArray
1064 raise raise_on_incompatible(self, other)
1065
1066 if isna(other):
1067 # i.e. np.timedelta64("NaT")
1068 return super()._add_timedeltalike_scalar(other)
1069
1070 if isinstance(other, Day):
1071 td = np.asarray(Timedelta(days=other.n).asm8)
1072 else:
1073 td = np.asarray(Timedelta(other).asm8)
1074 return self._add_timedelta_arraylike(td)
1075
1076 def _add_timedelta_arraylike(
1077 self, other: TimedeltaArray | npt.NDArray[np.timedelta64]
1078 ) -> Self:
1079 """
1080 Parameters
1081 ----------
1082 other : TimedeltaArray or ndarray[timedelta64]
1083
1084 Returns
1085 -------
1086 PeriodArray
1087 """
1088 if not self.dtype._is_tick_like():
1089 # We cannot add timedelta-like to non-tick PeriodArray
1090 raise TypeError(
1091 f"Cannot add or subtract timedelta64[ns] dtype from {self.dtype}"
1092 )
1093
1094 dtype = np.dtype(f"m8[{self.dtype._td64_unit}]")
1095
1096 # Similar to _check_timedeltalike_freq_compat, but we raise with a
1097 # more specific exception message if necessary.
1098 try:
1099 delta = astype_overflowsafe(
1100 np.asarray(other), dtype=dtype, copy=False, round_ok=False
1101 )
1102 except ValueError as err:
1103 # e.g. if we have minutes freq and try to add 30s
1104 # "Cannot losslessly convert units"
1105 raise IncompatibleFrequency(
1106 "Cannot add/subtract timedelta-like from PeriodArray that is "
1107 "not an integer multiple of the PeriodArray's freq."
1108 ) from err
1109
1110 res_values = add_overflowsafe(self.asi8, np.asarray(delta.view("i8")))
1111 return type(self)(res_values, dtype=self.dtype)
1112
1113 def _check_timedeltalike_freq_compat(self, other):
1114 """
1115 Arithmetic operations with timedelta-like scalars or array `other`
1116 are only valid if `other` is an integer multiple of `self.freq`.
1117 If the operation is valid, find that integer multiple. Otherwise,
1118 raise because the operation is invalid.
1119
1120 Parameters
1121 ----------
1122 other : timedelta, np.timedelta64, Tick,
1123 ndarray[timedelta64], TimedeltaArray, TimedeltaIndex
1124
1125 Returns
1126 -------
1127 multiple : int or ndarray[int64]
1128
1129 Raises
1130 ------
1131 IncompatibleFrequency
1132 """
1133 assert self.dtype._is_tick_like() # checked by calling function
1134
1135 dtype = np.dtype(f"m8[{self.dtype._td64_unit}]")
1136
1137 if isinstance(other, (timedelta, np.timedelta64, Tick)):
1138 td = np.asarray(Timedelta(other).asm8)
1139 else:
1140 td = np.asarray(other)
1141
1142 try:
1143 delta = astype_overflowsafe(td, dtype=dtype, copy=False, round_ok=False)
1144 except ValueError as err:
1145 raise raise_on_incompatible(self, other) from err
1146
1147 delta = delta.view("i8")
1148 return lib.item_from_zerodim(delta)
1149
1150 # ------------------------------------------------------------------
1151 # Reductions
1152
1153 def _reduce(
1154 self, name: str, *, skipna: bool = True, keepdims: bool = False, **kwargs
1155 ):
1156 result = super()._reduce(name, skipna=skipna, keepdims=keepdims, **kwargs)
1157 if keepdims and isinstance(result, np.ndarray):
1158 return self._from_sequence(result, dtype=self.dtype)
1159 return result
1160
1161
1162def raise_on_incompatible(left, right) -> IncompatibleFrequency:
1163 """
1164 Helper function to render a consistent error message when raising
1165 IncompatibleFrequency.
1166
1167 Parameters
1168 ----------
1169 left : PeriodArray
1170 right : None, DateOffset, Period, ndarray, or timedelta-like
1171
1172 Returns
1173 -------
1174 IncompatibleFrequency
1175 Exception to be raised by the caller.
1176 """
1177 # GH#24283 error message format depends on whether right is scalar
1178 if isinstance(right, (np.ndarray, ABCTimedeltaArray)) or right is None:
1179 other_freq = None
1180 elif isinstance(right, BaseOffset):
1181 with warnings.catch_warnings():
1182 warnings.filterwarnings(
1183 "ignore", r"PeriodDtype\[B\] is deprecated", category=FutureWarning
1184 )
1185 other_freq = PeriodDtype(right)._freqstr
1186 elif isinstance(right, (ABCPeriodIndex, PeriodArray, Period)):
1187 other_freq = right.freqstr
1188 else:
1189 other_freq = delta_to_tick(Timedelta(right)).freqstr
1190
1191 own_freq = PeriodDtype(left.freq)._freqstr
1192 msg = DIFFERENT_FREQ.format(
1193 cls=type(left).__name__, own_freq=own_freq, other_freq=other_freq
1194 )
1195 return IncompatibleFrequency(msg)
1196
1197
1198# -------------------------------------------------------------------
1199# Constructor Helpers
1200
1201
1202def period_array(
1203 data: Sequence[Period | str | None] | AnyArrayLike,
1204 freq: str | Tick | BaseOffset | None = None,
1205 copy: bool = False,
1206) -> PeriodArray:
1207 """
1208 Construct a new PeriodArray from a sequence of Period scalars.
1209
1210 Parameters
1211 ----------
1212 data : Sequence of Period objects
1213 A sequence of Period objects. These are required to all have
1214 the same ``freq.`` Missing values can be indicated by ``None``
1215 or ``pandas.NaT``.
1216 freq : str, Tick, or Offset
1217 The frequency of every element of the array. This can be specified
1218 to avoid inferring the `freq` from `data`.
1219 copy : bool, default False
1220 Whether to ensure a copy of the data is made.
1221
1222 Returns
1223 -------
1224 PeriodArray
1225
1226 See Also
1227 --------
1228 PeriodArray
1229 pandas.PeriodIndex
1230
1231 Examples
1232 --------
1233 >>> period_array([pd.Period("2017", freq="Y"), pd.Period("2018", freq="Y")])
1234 <PeriodArray>
1235 ['2017', '2018']
1236 Length: 2, dtype: period[Y-DEC]
1237
1238 >>> period_array([pd.Period("2017", freq="Y"), pd.Period("2018", freq="Y"), pd.NaT])
1239 <PeriodArray>
1240 ['2017', '2018', 'NaT']
1241 Length: 3, dtype: period[Y-DEC]
1242
1243 Integers that look like years are handled
1244
1245 >>> period_array([2000, 2001, 2002], freq="D")
1246 <PeriodArray>
1247 ['2000-01-01', '2001-01-01', '2002-01-01']
1248 Length: 3, dtype: period[D]
1249
1250 Datetime-like strings may also be passed
1251
1252 >>> period_array(["2000-Q1", "2000-Q2", "2000-Q3", "2000-Q4"], freq="Q")
1253 <PeriodArray>
1254 ['2000Q1', '2000Q2', '2000Q3', '2000Q4']
1255 Length: 4, dtype: period[Q-DEC]
1256 """
1257 data_dtype = getattr(data, "dtype", None)
1258
1259 if lib.is_np_dtype(data_dtype, "M"):
1260 return PeriodArray._from_datetime64(data, freq)
1261 if isinstance(data_dtype, PeriodDtype):
1262 out = PeriodArray(data)
1263 if freq is not None:
1264 if freq == data_dtype.freq:
1265 return out
1266 return out.asfreq(freq)
1267 return out
1268
1269 # other iterable of some kind
1270 if not isinstance(data, (np.ndarray, list, tuple, ABCSeries)):
1271 data = list(data)
1272
1273 arrdata = np.asarray(data)
1274
1275 dtype: PeriodDtype | None
1276 if freq:
1277 dtype = PeriodDtype(freq)
1278 else:
1279 dtype = None
1280
1281 if arrdata.dtype.kind == "f" and len(arrdata) > 0:
1282 raise TypeError("PeriodIndex does not allow floating point in construction")
1283
1284 if arrdata.dtype.kind in "iu":
1285 arr = arrdata.astype(np.int64, copy=False)
1286 # error: Argument 2 to "from_ordinals" has incompatible type "Union[str,
1287 # Tick, None]"; expected "Union[timedelta, BaseOffset, str]"
1288 ordinals = libperiod.from_ordinals(arr, freq) # type: ignore[arg-type]
1289 return PeriodArray(ordinals, dtype=dtype)
1290
1291 data = ensure_object(arrdata)
1292 if freq is None:
1293 freq = libperiod.extract_freq(data)
1294 dtype = PeriodDtype(freq)
1295 return PeriodArray._from_sequence(data, dtype=dtype)
1296
1297
1298@overload
1299def validate_dtype_freq(dtype, freq: BaseOffsetT) -> BaseOffsetT: ...
1300
1301
1302@overload
1303def validate_dtype_freq(dtype, freq: timedelta | str | None) -> BaseOffset: ...
1304
1305
1306def validate_dtype_freq(
1307 dtype, freq: BaseOffsetT | BaseOffset | timedelta | str | None
1308) -> BaseOffsetT:
1309 """
1310 If both a dtype and a freq are available, ensure they match. If only
1311 dtype is available, extract the implied freq.
1312
1313 Parameters
1314 ----------
1315 dtype : dtype
1316 freq : DateOffset or None
1317
1318 Returns
1319 -------
1320 freq : DateOffset
1321
1322 Raises
1323 ------
1324 ValueError : non-period dtype
1325 IncompatibleFrequency : mismatch between dtype and freq
1326 """
1327 if freq is not None:
1328 freq = to_offset(freq, is_period=True)
1329
1330 if dtype is not None:
1331 dtype = pandas_dtype(dtype)
1332 if not isinstance(dtype, PeriodDtype):
1333 raise ValueError("dtype must be PeriodDtype")
1334 if freq is None:
1335 freq = dtype.freq
1336 elif freq != dtype.freq:
1337 raise IncompatibleFrequency("specified freq and dtype are different")
1338 # error: Incompatible return value type (got "Union[BaseOffset, Any, None]",
1339 # expected "BaseOffset")
1340 return freq # type: ignore[return-value]
1341
1342
1343def dt64arr_to_periodarr(
1344 data, freq, tz=None
1345) -> tuple[npt.NDArray[np.int64], BaseOffset]:
1346 """
1347 Convert a datetime-like array to values Period ordinals.
1348
1349 Parameters
1350 ----------
1351 data : Union[Series[datetime64[ns]], DatetimeIndex, ndarray[datetime64ns]]
1352 freq : Optional[Union[str, Tick]]
1353 Must match the `freq` on the `data` if `data` is a DatetimeIndex
1354 or Series.
1355 tz : Optional[tzinfo]
1356
1357 Returns
1358 -------
1359 ordinals : ndarray[int64]
1360 freq : Tick
1361 The frequency extracted from the Series or DatetimeIndex if that's
1362 used.
1363
1364 """
1365 if not isinstance(data.dtype, np.dtype) or data.dtype.kind != "M":
1366 raise ValueError(f"Wrong dtype: {data.dtype}")
1367
1368 if freq is None:
1369 if isinstance(data, ABCIndex):
1370 data, freq = data._values, data.freq
1371 elif isinstance(data, ABCSeries):
1372 data, freq = data._values, data.dt.freq
1373
1374 elif isinstance(data, (ABCIndex, ABCSeries)):
1375 data = data._values
1376
1377 reso = get_unit_from_dtype(data.dtype)
1378 freq = Period._maybe_convert_freq(freq)
1379 base = freq._period_dtype_code
1380 return c_dt64arr_to_periodarr(data.view("i8"), base, tz, reso=reso), freq
1381
1382
1383def _get_ordinal_range(start, end, periods, freq, mult: int = 1):
1384 if com.count_not_none(start, end, periods) != 2:
1385 raise ValueError(
1386 "Of the three parameters: start, end, and periods, "
1387 "exactly two must be specified"
1388 )
1389
1390 if freq is not None:
1391 freq = to_offset(freq, is_period=True)
1392 mult = freq.n
1393
1394 if start is not None:
1395 start = Period(start, freq)
1396 if end is not None:
1397 end = Period(end, freq)
1398
1399 is_start_per = isinstance(start, Period)
1400 is_end_per = isinstance(end, Period)
1401
1402 if is_start_per and is_end_per and start.freq != end.freq:
1403 raise ValueError("start and end must have same freq")
1404 if start is NaT or end is NaT:
1405 raise ValueError("start and end must not be NaT")
1406
1407 if freq is None:
1408 if is_start_per:
1409 freq = start.freq
1410 elif is_end_per:
1411 freq = end.freq
1412 else: # pragma: no cover
1413 raise ValueError("Could not infer freq from start/end")
1414 mult = freq.n
1415
1416 if periods is not None:
1417 periods = periods * mult
1418 if start is None:
1419 data = np.arange(
1420 end.ordinal - periods + mult, end.ordinal + 1, mult, dtype=np.int64
1421 )
1422 else:
1423 data = np.arange(
1424 start.ordinal, start.ordinal + periods, mult, dtype=np.int64
1425 )
1426 else:
1427 data = np.arange(start.ordinal, end.ordinal + 1, mult, dtype=np.int64)
1428
1429 return data, freq
1430
1431
1432def _range_from_fields(
1433 year=None,
1434 month=None,
1435 quarter=None,
1436 day=None,
1437 hour=None,
1438 minute=None,
1439 second=None,
1440 freq=None,
1441) -> tuple[np.ndarray, BaseOffset]:
1442 if hour is None:
1443 hour = 0
1444 if minute is None:
1445 minute = 0
1446 if second is None:
1447 second = 0
1448 if day is None:
1449 day = 1
1450
1451 ordinals = []
1452
1453 if quarter is not None:
1454 if freq is None:
1455 freq = to_offset("Q", is_period=True)
1456 base = cast(int, FreqGroup.FR_QTR.value)
1457 else:
1458 freq = to_offset(freq, is_period=True)
1459 base = libperiod.freq_to_dtype_code(freq)
1460 if base != cast(int, FreqGroup.FR_QTR.value):
1461 raise AssertionError("base must equal FR_QTR")
1462
1463 freqstr = freq.freqstr
1464 year, quarter = _make_field_arrays(year, quarter)
1465 for y, q in zip(year, quarter, strict=True):
1466 calendar_year, calendar_month = parsing.quarter_to_myear(y, q, freqstr)
1467 val = libperiod.period_ordinal(
1468 calendar_year, calendar_month, 1, 1, 1, 1, 0, 0, base
1469 )
1470 ordinals.append(val)
1471 else:
1472 freq = to_offset(freq, is_period=True)
1473 base = libperiod.freq_to_dtype_code(freq)
1474 arrays = _make_field_arrays(year, month, day, hour, minute, second)
1475 for y, mth, d, h, mn, s in zip(*arrays, strict=True):
1476 ordinals.append(libperiod.period_ordinal(y, mth, d, h, mn, s, 0, 0, base))
1477
1478 return np.array(ordinals, dtype=np.int64), freq
1479
1480
1481def _make_field_arrays(*fields) -> list[np.ndarray]:
1482 length = None
1483 for x in fields:
1484 if isinstance(x, (list, np.ndarray, ABCSeries)):
1485 if length is not None and len(x) != length:
1486 raise ValueError("Mismatched Period array lengths")
1487 if length is None:
1488 length = len(x)
1489
1490 # error: Argument 2 to "repeat" has incompatible type "Optional[int]"; expected
1491 # "Union[Union[int, integer[Any]], Union[bool, bool_], ndarray, Sequence[Union[int,
1492 # integer[Any]]], Sequence[Union[bool, bool_]], Sequence[Sequence[Any]]]"
1493 return [
1494 (
1495 np.asarray(x)
1496 if isinstance(x, (np.ndarray, list, ABCSeries))
1497 else np.repeat(x, length) # type: ignore[arg-type]
1498 )
1499 for x in fields
1500 ]