Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/pandas/core/arrays/period.py: 18%
428 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1from __future__ import annotations
3from datetime import timedelta
4import operator
5from typing import (
6 TYPE_CHECKING,
7 Any,
8 Callable,
9 Literal,
10 Sequence,
11 TypeVar,
12 overload,
13)
15import numpy as np
17from pandas._libs import (
18 algos as libalgos,
19 lib,
20)
21from pandas._libs.arrays import NDArrayBacked
22from pandas._libs.tslibs import (
23 BaseOffset,
24 NaT,
25 NaTType,
26 Timedelta,
27 astype_overflowsafe,
28 dt64arr_to_periodarr as c_dt64arr_to_periodarr,
29 get_unit_from_dtype,
30 iNaT,
31 parsing,
32 period as libperiod,
33 to_offset,
34)
35from pandas._libs.tslibs.dtypes import FreqGroup
36from pandas._libs.tslibs.fields import isleapyear_arr
37from pandas._libs.tslibs.offsets import (
38 Tick,
39 delta_to_tick,
40)
41from pandas._libs.tslibs.period import (
42 DIFFERENT_FREQ,
43 IncompatibleFrequency,
44 Period,
45 get_period_field_arr,
46 period_asfreq_arr,
47)
48from pandas._typing import (
49 AnyArrayLike,
50 Dtype,
51 NpDtype,
52 npt,
53)
54from pandas.util._decorators import (
55 cache_readonly,
56 doc,
57)
59from pandas.core.dtypes.common import (
60 ensure_object,
61 is_datetime64_any_dtype,
62 is_datetime64_dtype,
63 is_dtype_equal,
64 is_float_dtype,
65 is_integer_dtype,
66 is_period_dtype,
67 pandas_dtype,
68)
69from pandas.core.dtypes.dtypes import PeriodDtype
70from pandas.core.dtypes.generic import (
71 ABCIndex,
72 ABCPeriodIndex,
73 ABCSeries,
74 ABCTimedeltaArray,
75)
76from pandas.core.dtypes.missing import isna
78import pandas.core.algorithms as algos
79from pandas.core.arrays import datetimelike as dtl
80import pandas.core.common as com
82if TYPE_CHECKING: 82 ↛ 84line 82 didn't jump to line 84, because the condition on line 82 was never true
84 from pandas._typing import (
85 NumpySorter,
86 NumpyValueArrayLike,
87 )
89 from pandas.core.arrays import (
90 DatetimeArray,
91 TimedeltaArray,
92 )
93 from pandas.core.arrays.base import ExtensionArray
96BaseOffsetT = TypeVar("BaseOffsetT", bound=BaseOffset)
99_shared_doc_kwargs = {
100 "klass": "PeriodArray",
101}
104def _field_accessor(name: str, docstring=None):
105 def f(self):
106 base = self.freq._period_dtype_code
107 result = get_period_field_arr(name, self.asi8, base)
108 return result
110 f.__name__ = name
111 f.__doc__ = docstring
112 return property(f)
115class PeriodArray(dtl.DatelikeOps, libperiod.PeriodMixin):
116 """
117 Pandas ExtensionArray for storing Period data.
119 Users should use :func:`~pandas.period_array` to create new instances.
120 Alternatively, :func:`~pandas.array` can be used to create new instances
121 from a sequence of Period scalars.
123 Parameters
124 ----------
125 values : Union[PeriodArray, Series[period], ndarray[int], PeriodIndex]
126 The data to store. These should be arrays that can be directly
127 converted to ordinals without inference or copy (PeriodArray,
128 ndarray[int64]), or a box around such an array (Series[period],
129 PeriodIndex).
130 dtype : PeriodDtype, optional
131 A PeriodDtype instance from which to extract a `freq`. If both
132 `freq` and `dtype` are specified, then the frequencies must match.
133 freq : str or DateOffset
134 The `freq` to use for the array. Mostly applicable when `values`
135 is an ndarray of integers, when `freq` is required. When `values`
136 is a PeriodArray (or box around), it's checked that ``values.freq``
137 matches `freq`.
138 copy : bool, default False
139 Whether to copy the ordinals before storing.
141 Attributes
142 ----------
143 None
145 Methods
146 -------
147 None
149 See Also
150 --------
151 Period: Represents a period of time.
152 PeriodIndex : Immutable Index for period data.
153 period_range: Create a fixed-frequency PeriodArray.
154 array: Construct a pandas array.
156 Notes
157 -----
158 There are two components to a PeriodArray
160 - ordinals : integer ndarray
161 - freq : pd.tseries.offsets.Offset
163 The values are physically stored as a 1-D ndarray of integers. These are
164 called "ordinals" and represent some kind of offset from a base.
166 The `freq` indicates the span covered by each element of the array.
167 All elements in the PeriodArray have the same `freq`.
168 """
170 # array priority higher than numpy scalars
171 __array_priority__ = 1000
172 _typ = "periodarray" # ABCPeriodArray
173 _internal_fill_value = np.int64(iNaT)
174 _recognized_scalars = (Period,)
175 _is_recognized_dtype = is_period_dtype
176 _infer_matches = ("period",)
178 @property
179 def _scalar_type(self) -> type[Period]:
180 return Period
182 # Names others delegate to us
183 _other_ops: list[str] = []
184 _bool_ops: list[str] = ["is_leap_year"]
185 _object_ops: list[str] = ["start_time", "end_time", "freq"]
186 _field_ops: list[str] = [
187 "year",
188 "month",
189 "day",
190 "hour",
191 "minute",
192 "second",
193 "weekofyear",
194 "weekday",
195 "week",
196 "dayofweek",
197 "day_of_week",
198 "dayofyear",
199 "day_of_year",
200 "quarter",
201 "qyear",
202 "days_in_month",
203 "daysinmonth",
204 ]
205 _datetimelike_ops: list[str] = _field_ops + _object_ops + _bool_ops
206 _datetimelike_methods: list[str] = ["strftime", "to_timestamp", "asfreq"]
208 _dtype: PeriodDtype
210 # --------------------------------------------------------------------
211 # Constructors
213 def __init__(
214 self, values, dtype: Dtype | None = None, freq=None, copy: bool = False
215 ) -> None:
216 freq = validate_dtype_freq(dtype, freq)
218 if freq is not None:
219 freq = Period._maybe_convert_freq(freq)
221 if isinstance(values, ABCSeries):
222 values = values._values
223 if not isinstance(values, type(self)):
224 raise TypeError("Incorrect dtype")
226 elif isinstance(values, ABCPeriodIndex):
227 values = values._values
229 if isinstance(values, type(self)):
230 if freq is not None and freq != values.freq:
231 raise raise_on_incompatible(values, freq)
232 values, freq = values._ndarray, values.freq
234 values = np.array(values, dtype="int64", copy=copy)
235 if freq is None:
236 raise ValueError("freq is not specified and cannot be inferred")
237 NDArrayBacked.__init__(self, values, PeriodDtype(freq))
239 # error: Signature of "_simple_new" incompatible with supertype "NDArrayBacked"
240 @classmethod
241 def _simple_new( # type: ignore[override]
242 cls,
243 values: np.ndarray,
244 freq: BaseOffset | None = None,
245 dtype: Dtype | None = None,
246 ) -> PeriodArray:
247 # alias for PeriodArray.__init__
248 assertion_msg = "Should be numpy array of type i8"
249 assert isinstance(values, np.ndarray) and values.dtype == "i8", assertion_msg
250 return cls(values, freq=freq, dtype=dtype)
252 @classmethod
253 def _from_sequence(
254 cls: type[PeriodArray],
255 scalars: Sequence[Period | None] | AnyArrayLike,
256 *,
257 dtype: Dtype | None = None,
258 copy: bool = False,
259 ) -> PeriodArray:
260 if dtype and isinstance(dtype, PeriodDtype):
261 freq = dtype.freq
262 else:
263 freq = None
265 if isinstance(scalars, cls):
266 validate_dtype_freq(scalars.dtype, freq)
267 if copy:
268 scalars = scalars.copy()
269 return scalars
271 periods = np.asarray(scalars, dtype=object)
273 freq = freq or libperiod.extract_freq(periods)
274 ordinals = libperiod.extract_ordinals(periods, freq)
275 return cls(ordinals, freq=freq)
277 @classmethod
278 def _from_sequence_of_strings(
279 cls, strings, *, dtype: Dtype | None = None, copy: bool = False
280 ) -> PeriodArray:
281 return cls._from_sequence(strings, dtype=dtype, copy=copy)
283 @classmethod
284 def _from_datetime64(cls, data, freq, tz=None) -> PeriodArray:
285 """
286 Construct a PeriodArray from a datetime64 array
288 Parameters
289 ----------
290 data : ndarray[datetime64[ns], datetime64[ns, tz]]
291 freq : str or Tick
292 tz : tzinfo, optional
294 Returns
295 -------
296 PeriodArray[freq]
297 """
298 data, freq = dt64arr_to_periodarr(data, freq, tz)
299 return cls(data, freq=freq)
301 @classmethod
302 def _generate_range(cls, start, end, periods, freq, fields):
303 periods = dtl.validate_periods(periods)
305 if freq is not None:
306 freq = Period._maybe_convert_freq(freq)
308 field_count = len(fields)
309 if start is not None or end is not None:
310 if field_count > 0:
311 raise ValueError(
312 "Can either instantiate from fields or endpoints, but not both"
313 )
314 subarr, freq = _get_ordinal_range(start, end, periods, freq)
315 elif field_count > 0:
316 subarr, freq = _range_from_fields(freq=freq, **fields)
317 else:
318 raise ValueError("Not enough parameters to construct Period range")
320 return subarr, freq
322 # -----------------------------------------------------------------
323 # DatetimeLike Interface
325 # error: Argument 1 of "_unbox_scalar" is incompatible with supertype
326 # "DatetimeLikeArrayMixin"; supertype defines the argument type as
327 # "Union[Union[Period, Any, Timedelta], NaTType]"
328 def _unbox_scalar( # type: ignore[override]
329 self,
330 value: Period | NaTType,
331 setitem: bool = False,
332 ) -> np.int64:
333 if value is NaT:
334 # error: Item "Period" of "Union[Period, NaTType]" has no attribute "value"
335 return np.int64(value.value) # type: ignore[union-attr]
336 elif isinstance(value, self._scalar_type):
337 self._check_compatible_with(value, setitem=setitem)
338 return np.int64(value.ordinal)
339 else:
340 raise ValueError(f"'value' should be a Period. Got '{value}' instead.")
342 def _scalar_from_string(self, value: str) -> Period:
343 return Period(value, freq=self.freq)
345 def _check_compatible_with(self, other, setitem: bool = False):
346 if other is NaT:
347 return
348 self._require_matching_freq(other)
350 # --------------------------------------------------------------------
351 # Data / Attributes
353 @cache_readonly
354 def dtype(self) -> PeriodDtype:
355 return self._dtype
357 # error: Read-only property cannot override read-write property
358 @property # type: ignore[misc]
359 def freq(self) -> BaseOffset:
360 """
361 Return the frequency object for this PeriodArray.
362 """
363 return self.dtype.freq
365 def __array__(self, dtype: NpDtype | None = None) -> np.ndarray:
366 if dtype == "i8":
367 return self.asi8
368 elif dtype == bool:
369 return ~self._isnan
371 # This will raise TypeError for non-object dtypes
372 return np.array(list(self), dtype=object)
374 def __arrow_array__(self, type=None):
375 """
376 Convert myself into a pyarrow Array.
377 """
378 import pyarrow
380 from pandas.core.arrays.arrow.extension_types import ArrowPeriodType
382 if type is not None:
383 if pyarrow.types.is_integer(type):
384 return pyarrow.array(self._ndarray, mask=self.isna(), type=type)
385 elif isinstance(type, ArrowPeriodType):
386 # ensure we have the same freq
387 if self.freqstr != type.freq:
388 raise TypeError(
389 "Not supported to convert PeriodArray to array with different "
390 f"'freq' ({self.freqstr} vs {type.freq})"
391 )
392 else:
393 raise TypeError(
394 f"Not supported to convert PeriodArray to '{type}' type"
395 )
397 period_type = ArrowPeriodType(self.freqstr)
398 storage_array = pyarrow.array(self._ndarray, mask=self.isna(), type="int64")
399 return pyarrow.ExtensionArray.from_storage(period_type, storage_array)
401 # --------------------------------------------------------------------
402 # Vectorized analogues of Period properties
404 year = _field_accessor(
405 "year",
406 """
407 The year of the period.
408 """,
409 )
410 month = _field_accessor(
411 "month",
412 """
413 The month as January=1, December=12.
414 """,
415 )
416 day = _field_accessor(
417 "day",
418 """
419 The days of the period.
420 """,
421 )
422 hour = _field_accessor(
423 "hour",
424 """
425 The hour of the period.
426 """,
427 )
428 minute = _field_accessor(
429 "minute",
430 """
431 The minute of the period.
432 """,
433 )
434 second = _field_accessor(
435 "second",
436 """
437 The second of the period.
438 """,
439 )
440 weekofyear = _field_accessor(
441 "week",
442 """
443 The week ordinal of the year.
444 """,
445 )
446 week = weekofyear
447 day_of_week = _field_accessor(
448 "day_of_week",
449 """
450 The day of the week with Monday=0, Sunday=6.
451 """,
452 )
453 dayofweek = day_of_week
454 weekday = dayofweek
455 dayofyear = day_of_year = _field_accessor(
456 "day_of_year",
457 """
458 The ordinal day of the year.
459 """,
460 )
461 quarter = _field_accessor(
462 "quarter",
463 """
464 The quarter of the date.
465 """,
466 )
467 qyear = _field_accessor("qyear")
468 days_in_month = _field_accessor(
469 "days_in_month",
470 """
471 The number of days in the month.
472 """,
473 )
474 daysinmonth = days_in_month
476 @property
477 def is_leap_year(self) -> np.ndarray:
478 """
479 Logical indicating if the date belongs to a leap year.
480 """
481 return isleapyear_arr(np.asarray(self.year))
483 def to_timestamp(self, freq=None, how: str = "start") -> DatetimeArray:
484 """
485 Cast to DatetimeArray/Index.
487 Parameters
488 ----------
489 freq : str or DateOffset, optional
490 Target frequency. The default is 'D' for week or longer,
491 'S' otherwise.
492 how : {'s', 'e', 'start', 'end'}
493 Whether to use the start or end of the time period being converted.
495 Returns
496 -------
497 DatetimeArray/Index
498 """
499 from pandas.core.arrays import DatetimeArray
501 how = libperiod.validate_end_alias(how)
503 end = how == "E"
504 if end:
505 if freq == "B" or self.freq == "B":
506 # roll forward to ensure we land on B date
507 adjust = Timedelta(1, "D") - Timedelta(1, "ns")
508 return self.to_timestamp(how="start") + adjust
509 else:
510 adjust = Timedelta(1, "ns")
511 return (self + self.freq).to_timestamp(how="start") - adjust
513 if freq is None:
514 freq = self._dtype._get_to_timestamp_base()
515 base = freq
516 else:
517 freq = Period._maybe_convert_freq(freq)
518 base = freq._period_dtype_code
520 new_parr = self.asfreq(freq, how=how)
522 new_data = libperiod.periodarr_to_dt64arr(new_parr.asi8, base)
523 dta = DatetimeArray(new_data)
525 if self.freq.name == "B":
526 # See if we can retain BDay instead of Day in cases where
527 # len(self) is too small for infer_freq to distinguish between them
528 diffs = libalgos.unique_deltas(self.asi8)
529 if len(diffs) == 1:
530 diff = diffs[0]
531 if diff == self.freq.n:
532 dta._freq = self.freq
533 elif diff == 1:
534 dta._freq = self.freq.base
535 # TODO: other cases?
536 return dta
537 else:
538 return dta._with_freq("infer")
540 # --------------------------------------------------------------------
542 def _time_shift(self, periods: int, freq=None) -> PeriodArray:
543 """
544 Shift each value by `periods`.
546 Note this is different from ExtensionArray.shift, which
547 shifts the *position* of each element, padding the end with
548 missing values.
550 Parameters
551 ----------
552 periods : int
553 Number of periods to shift by.
554 freq : pandas.DateOffset, pandas.Timedelta, or str
555 Frequency increment to shift by.
556 """
557 if freq is not None:
558 raise TypeError(
559 "`freq` argument is not supported for "
560 f"{type(self).__name__}._time_shift"
561 )
562 return self + periods
564 def _box_func(self, x) -> Period | NaTType:
565 return Period._from_ordinal(ordinal=x, freq=self.freq)
567 @doc(**_shared_doc_kwargs, other="PeriodIndex", other_name="PeriodIndex")
568 def asfreq(self, freq=None, how: str = "E") -> PeriodArray:
569 """
570 Convert the {klass} to the specified frequency `freq`.
572 Equivalent to applying :meth:`pandas.Period.asfreq` with the given arguments
573 to each :class:`~pandas.Period` in this {klass}.
575 Parameters
576 ----------
577 freq : str
578 A frequency.
579 how : str {{'E', 'S'}}, default 'E'
580 Whether the elements should be aligned to the end
581 or start within pa period.
583 * 'E', 'END', or 'FINISH' for end,
584 * 'S', 'START', or 'BEGIN' for start.
586 January 31st ('END') vs. January 1st ('START') for example.
588 Returns
589 -------
590 {klass}
591 The transformed {klass} with the new frequency.
593 See Also
594 --------
595 {other}.asfreq: Convert each Period in a {other_name} to the given frequency.
596 Period.asfreq : Convert a :class:`~pandas.Period` object to the given frequency.
598 Examples
599 --------
600 >>> pidx = pd.period_range('2010-01-01', '2015-01-01', freq='A')
601 >>> pidx
602 PeriodIndex(['2010', '2011', '2012', '2013', '2014', '2015'],
603 dtype='period[A-DEC]')
605 >>> pidx.asfreq('M')
606 PeriodIndex(['2010-12', '2011-12', '2012-12', '2013-12', '2014-12',
607 '2015-12'], dtype='period[M]')
609 >>> pidx.asfreq('M', how='S')
610 PeriodIndex(['2010-01', '2011-01', '2012-01', '2013-01', '2014-01',
611 '2015-01'], dtype='period[M]')
612 """
613 how = libperiod.validate_end_alias(how)
615 freq = Period._maybe_convert_freq(freq)
617 base1 = self._dtype._dtype_code
618 base2 = freq._period_dtype_code
620 asi8 = self.asi8
621 # self.freq.n can't be negative or 0
622 end = how == "E"
623 if end:
624 ordinal = asi8 + self.freq.n - 1
625 else:
626 ordinal = asi8
628 new_data = period_asfreq_arr(ordinal, base1, base2, end)
630 if self._hasna:
631 new_data[self._isnan] = iNaT
633 return type(self)(new_data, freq=freq)
635 # ------------------------------------------------------------------
636 # Rendering Methods
638 def _formatter(self, boxed: bool = False):
639 if boxed:
640 return str
641 return "'{}'".format
643 @dtl.ravel_compat
644 def _format_native_types(
645 self, *, na_rep="NaT", date_format=None, **kwargs
646 ) -> npt.NDArray[np.object_]:
647 """
648 actually format my specific types
649 """
650 values = self.astype(object)
652 # Create the formatter function
653 if date_format:
654 formatter = lambda per: per.strftime(date_format)
655 else:
656 # Uses `_Period.str` which in turn uses `format_period`
657 formatter = lambda per: str(per)
659 # Apply the formatter to all values in the array, possibly with a mask
660 if self._hasna:
661 mask = self._isnan
662 values[mask] = na_rep
663 imask = ~mask
664 values[imask] = np.array([formatter(per) for per in values[imask]])
665 else:
666 values = np.array([formatter(per) for per in values])
667 return values
669 # ------------------------------------------------------------------
671 def astype(self, dtype, copy: bool = True):
672 # We handle Period[T] -> Period[U]
673 # Our parent handles everything else.
674 dtype = pandas_dtype(dtype)
675 if is_dtype_equal(dtype, self._dtype):
676 if not copy:
677 return self
678 else:
679 return self.copy()
680 if is_period_dtype(dtype):
681 return self.asfreq(dtype.freq)
683 if is_datetime64_any_dtype(dtype):
684 # GH#45038 match PeriodIndex behavior.
685 tz = getattr(dtype, "tz", None)
686 return self.to_timestamp().tz_localize(tz)
688 return super().astype(dtype, copy=copy)
690 def searchsorted(
691 self,
692 value: NumpyValueArrayLike | ExtensionArray,
693 side: Literal["left", "right"] = "left",
694 sorter: NumpySorter = None,
695 ) -> npt.NDArray[np.intp] | np.intp:
696 npvalue = self._validate_searchsorted_value(value).view("M8[ns]")
698 # Cast to M8 to get datetime-like NaT placement
699 m8arr = self._ndarray.view("M8[ns]")
700 return m8arr.searchsorted(npvalue, side=side, sorter=sorter)
702 def fillna(self, value=None, method=None, limit=None) -> PeriodArray:
703 if method is not None:
704 # view as dt64 so we get treated as timelike in core.missing
705 dta = self.view("M8[ns]")
706 result = dta.fillna(value=value, method=method, limit=limit)
707 # error: Incompatible return value type (got "Union[ExtensionArray,
708 # ndarray[Any, Any]]", expected "PeriodArray")
709 return result.view(self.dtype) # type: ignore[return-value]
710 return super().fillna(value=value, method=method, limit=limit)
712 def _quantile(
713 self: PeriodArray,
714 qs: npt.NDArray[np.float64],
715 interpolation: str,
716 ) -> PeriodArray:
717 # dispatch to DatetimeArray implementation
718 dtres = self.view("M8[ns]")._quantile(qs, interpolation)
719 # error: Incompatible return value type (got "Union[ExtensionArray,
720 # ndarray[Any, Any]]", expected "PeriodArray")
721 return dtres.view(self.dtype) # type: ignore[return-value]
723 # ------------------------------------------------------------------
724 # Arithmetic Methods
726 def _addsub_int_array_or_scalar(
727 self, other: np.ndarray | int, op: Callable[[Any, Any], Any]
728 ) -> PeriodArray:
729 """
730 Add or subtract array of integers; equivalent to applying
731 `_time_shift` pointwise.
733 Parameters
734 ----------
735 other : np.ndarray[int64] or int
736 op : {operator.add, operator.sub}
738 Returns
739 -------
740 result : PeriodArray
741 """
742 assert op in [operator.add, operator.sub]
743 if op is operator.sub:
744 other = -other
745 res_values = algos.checked_add_with_arr(self.asi8, other, arr_mask=self._isnan)
746 return type(self)(res_values, freq=self.freq)
748 def _add_offset(self, other: BaseOffset):
749 assert not isinstance(other, Tick)
751 self._require_matching_freq(other, base=True)
752 return self._addsub_int_array_or_scalar(other.n, operator.add)
754 # TODO: can we de-duplicate with Period._add_timedeltalike_scalar?
755 def _add_timedeltalike_scalar(self, other):
756 """
757 Parameters
758 ----------
759 other : timedelta, Tick, np.timedelta64
761 Returns
762 -------
763 PeriodArray
764 """
765 if not isinstance(self.freq, Tick):
766 # We cannot add timedelta-like to non-tick PeriodArray
767 raise raise_on_incompatible(self, other)
769 if isna(other):
770 # i.e. np.timedelta64("NaT")
771 return super()._add_timedeltalike_scalar(other)
773 td = np.asarray(Timedelta(other).asm8)
774 return self._add_timedelta_arraylike(td)
776 def _add_timedelta_arraylike(
777 self, other: TimedeltaArray | npt.NDArray[np.timedelta64]
778 ) -> PeriodArray:
779 """
780 Parameters
781 ----------
782 other : TimedeltaArray or ndarray[timedelta64]
784 Returns
785 -------
786 PeriodArray
787 """
788 freq = self.freq
789 if not isinstance(freq, Tick):
790 # We cannot add timedelta-like to non-tick PeriodArray
791 raise TypeError(
792 f"Cannot add or subtract timedelta64[ns] dtype from {self.dtype}"
793 )
795 dtype = np.dtype(f"m8[{freq._td64_unit}]")
797 try:
798 delta = astype_overflowsafe(
799 np.asarray(other), dtype=dtype, copy=False, round_ok=False
800 )
801 except ValueError as err:
802 # e.g. if we have minutes freq and try to add 30s
803 # "Cannot losslessly convert units"
804 raise IncompatibleFrequency(
805 "Cannot add/subtract timedelta-like from PeriodArray that is "
806 "not an integer multiple of the PeriodArray's freq."
807 ) from err
809 b_mask = np.isnat(delta)
811 res_values = algos.checked_add_with_arr(
812 self.asi8, delta.view("i8"), arr_mask=self._isnan, b_mask=b_mask
813 )
814 np.putmask(res_values, self._isnan | b_mask, iNaT)
815 return type(self)(res_values, freq=self.freq)
817 def _check_timedeltalike_freq_compat(self, other):
818 """
819 Arithmetic operations with timedelta-like scalars or array `other`
820 are only valid if `other` is an integer multiple of `self.freq`.
821 If the operation is valid, find that integer multiple. Otherwise,
822 raise because the operation is invalid.
824 Parameters
825 ----------
826 other : timedelta, np.timedelta64, Tick,
827 ndarray[timedelta64], TimedeltaArray, TimedeltaIndex
829 Returns
830 -------
831 multiple : int or ndarray[int64]
833 Raises
834 ------
835 IncompatibleFrequency
836 """
837 assert isinstance(self.freq, Tick) # checked by calling function
839 dtype = np.dtype(f"m8[{self.freq._td64_unit}]")
841 if isinstance(other, (timedelta, np.timedelta64, Tick)):
842 td = np.asarray(Timedelta(other).asm8)
843 else:
844 td = np.asarray(other)
846 try:
847 delta = astype_overflowsafe(td, dtype=dtype, copy=False, round_ok=False)
848 except ValueError as err:
849 raise raise_on_incompatible(self, other) from err
851 delta = delta.view("i8")
852 return lib.item_from_zerodim(delta)
855def raise_on_incompatible(left, right):
856 """
857 Helper function to render a consistent error message when raising
858 IncompatibleFrequency.
860 Parameters
861 ----------
862 left : PeriodArray
863 right : None, DateOffset, Period, ndarray, or timedelta-like
865 Returns
866 -------
867 IncompatibleFrequency
868 Exception to be raised by the caller.
869 """
870 # GH#24283 error message format depends on whether right is scalar
871 if isinstance(right, (np.ndarray, ABCTimedeltaArray)) or right is None:
872 other_freq = None
873 elif isinstance(right, (ABCPeriodIndex, PeriodArray, Period, BaseOffset)):
874 other_freq = right.freqstr
875 else:
876 other_freq = delta_to_tick(Timedelta(right)).freqstr
878 msg = DIFFERENT_FREQ.format(
879 cls=type(left).__name__, own_freq=left.freqstr, other_freq=other_freq
880 )
881 return IncompatibleFrequency(msg)
884# -------------------------------------------------------------------
885# Constructor Helpers
888def period_array(
889 data: Sequence[Period | str | None] | AnyArrayLike,
890 freq: str | Tick | None = None,
891 copy: bool = False,
892) -> PeriodArray:
893 """
894 Construct a new PeriodArray from a sequence of Period scalars.
896 Parameters
897 ----------
898 data : Sequence of Period objects
899 A sequence of Period objects. These are required to all have
900 the same ``freq.`` Missing values can be indicated by ``None``
901 or ``pandas.NaT``.
902 freq : str, Tick, or Offset
903 The frequency of every element of the array. This can be specified
904 to avoid inferring the `freq` from `data`.
905 copy : bool, default False
906 Whether to ensure a copy of the data is made.
908 Returns
909 -------
910 PeriodArray
912 See Also
913 --------
914 PeriodArray
915 pandas.PeriodIndex
917 Examples
918 --------
919 >>> period_array([pd.Period('2017', freq='A'),
920 ... pd.Period('2018', freq='A')])
921 <PeriodArray>
922 ['2017', '2018']
923 Length: 2, dtype: period[A-DEC]
925 >>> period_array([pd.Period('2017', freq='A'),
926 ... pd.Period('2018', freq='A'),
927 ... pd.NaT])
928 <PeriodArray>
929 ['2017', '2018', 'NaT']
930 Length: 3, dtype: period[A-DEC]
932 Integers that look like years are handled
934 >>> period_array([2000, 2001, 2002], freq='D')
935 <PeriodArray>
936 ['2000-01-01', '2001-01-01', '2002-01-01']
937 Length: 3, dtype: period[D]
939 Datetime-like strings may also be passed
941 >>> period_array(['2000-Q1', '2000-Q2', '2000-Q3', '2000-Q4'], freq='Q')
942 <PeriodArray>
943 ['2000Q1', '2000Q2', '2000Q3', '2000Q4']
944 Length: 4, dtype: period[Q-DEC]
945 """
946 data_dtype = getattr(data, "dtype", None)
948 if is_datetime64_dtype(data_dtype):
949 return PeriodArray._from_datetime64(data, freq)
950 if is_period_dtype(data_dtype):
951 return PeriodArray(data, freq=freq)
953 # other iterable of some kind
954 if not isinstance(data, (np.ndarray, list, tuple, ABCSeries)):
955 data = list(data)
957 arrdata = np.asarray(data)
959 dtype: PeriodDtype | None
960 if freq:
961 dtype = PeriodDtype(freq)
962 else:
963 dtype = None
965 if is_float_dtype(arrdata) and len(arrdata) > 0:
966 raise TypeError("PeriodIndex does not allow floating point in construction")
968 if is_integer_dtype(arrdata.dtype):
969 arr = arrdata.astype(np.int64, copy=False)
970 # error: Argument 2 to "from_ordinals" has incompatible type "Union[str,
971 # Tick, None]"; expected "Union[timedelta, BaseOffset, str]"
972 ordinals = libperiod.from_ordinals(arr, freq) # type: ignore[arg-type]
973 return PeriodArray(ordinals, dtype=dtype)
975 data = ensure_object(arrdata)
977 return PeriodArray._from_sequence(data, dtype=dtype)
980@overload
981def validate_dtype_freq(dtype, freq: BaseOffsetT) -> BaseOffsetT:
982 ...
985@overload
986def validate_dtype_freq(dtype, freq: timedelta | str | None) -> BaseOffset:
987 ...
990def validate_dtype_freq(
991 dtype, freq: BaseOffsetT | timedelta | str | None
992) -> BaseOffsetT:
993 """
994 If both a dtype and a freq are available, ensure they match. If only
995 dtype is available, extract the implied freq.
997 Parameters
998 ----------
999 dtype : dtype
1000 freq : DateOffset or None
1002 Returns
1003 -------
1004 freq : DateOffset
1006 Raises
1007 ------
1008 ValueError : non-period dtype
1009 IncompatibleFrequency : mismatch between dtype and freq
1010 """
1011 if freq is not None:
1012 # error: Incompatible types in assignment (expression has type
1013 # "BaseOffset", variable has type "Union[BaseOffsetT, timedelta,
1014 # str, None]")
1015 freq = to_offset(freq) # type: ignore[assignment]
1017 if dtype is not None:
1018 dtype = pandas_dtype(dtype)
1019 if not is_period_dtype(dtype):
1020 raise ValueError("dtype must be PeriodDtype")
1021 if freq is None:
1022 freq = dtype.freq
1023 elif freq != dtype.freq:
1024 raise IncompatibleFrequency("specified freq and dtype are different")
1025 # error: Incompatible return value type (got "Union[BaseOffset, Any, None]",
1026 # expected "BaseOffset")
1027 return freq # type: ignore[return-value]
1030def dt64arr_to_periodarr(
1031 data, freq, tz=None
1032) -> tuple[npt.NDArray[np.int64], BaseOffset]:
1033 """
1034 Convert an datetime-like array to values Period ordinals.
1036 Parameters
1037 ----------
1038 data : Union[Series[datetime64[ns]], DatetimeIndex, ndarray[datetime64ns]]
1039 freq : Optional[Union[str, Tick]]
1040 Must match the `freq` on the `data` if `data` is a DatetimeIndex
1041 or Series.
1042 tz : Optional[tzinfo]
1044 Returns
1045 -------
1046 ordinals : ndarray[int64]
1047 freq : Tick
1048 The frequency extracted from the Series or DatetimeIndex if that's
1049 used.
1051 """
1052 if not isinstance(data.dtype, np.dtype) or data.dtype.kind != "M":
1053 raise ValueError(f"Wrong dtype: {data.dtype}")
1055 if freq is None:
1056 if isinstance(data, ABCIndex):
1057 data, freq = data._values, data.freq
1058 elif isinstance(data, ABCSeries):
1059 data, freq = data._values, data.dt.freq
1061 elif isinstance(data, (ABCIndex, ABCSeries)):
1062 data = data._values
1064 reso = get_unit_from_dtype(data.dtype)
1065 freq = Period._maybe_convert_freq(freq)
1066 base = freq._period_dtype_code
1067 return c_dt64arr_to_periodarr(data.view("i8"), base, tz, reso=reso), freq
1070def _get_ordinal_range(start, end, periods, freq, mult=1):
1071 if com.count_not_none(start, end, periods) != 2:
1072 raise ValueError(
1073 "Of the three parameters: start, end, and periods, "
1074 "exactly two must be specified"
1075 )
1077 if freq is not None:
1078 freq = to_offset(freq)
1079 mult = freq.n
1081 if start is not None:
1082 start = Period(start, freq)
1083 if end is not None:
1084 end = Period(end, freq)
1086 is_start_per = isinstance(start, Period)
1087 is_end_per = isinstance(end, Period)
1089 if is_start_per and is_end_per and start.freq != end.freq:
1090 raise ValueError("start and end must have same freq")
1091 if start is NaT or end is NaT:
1092 raise ValueError("start and end must not be NaT")
1094 if freq is None:
1095 if is_start_per:
1096 freq = start.freq
1097 elif is_end_per:
1098 freq = end.freq
1099 else: # pragma: no cover
1100 raise ValueError("Could not infer freq from start/end")
1102 if periods is not None:
1103 periods = periods * mult
1104 if start is None:
1105 data = np.arange(
1106 end.ordinal - periods + mult, end.ordinal + 1, mult, dtype=np.int64
1107 )
1108 else:
1109 data = np.arange(
1110 start.ordinal, start.ordinal + periods, mult, dtype=np.int64
1111 )
1112 else:
1113 data = np.arange(start.ordinal, end.ordinal + 1, mult, dtype=np.int64)
1115 return data, freq
1118def _range_from_fields(
1119 year=None,
1120 month=None,
1121 quarter=None,
1122 day=None,
1123 hour=None,
1124 minute=None,
1125 second=None,
1126 freq=None,
1127) -> tuple[np.ndarray, BaseOffset]:
1128 if hour is None:
1129 hour = 0
1130 if minute is None:
1131 minute = 0
1132 if second is None:
1133 second = 0
1134 if day is None:
1135 day = 1
1137 ordinals = []
1139 if quarter is not None:
1140 if freq is None:
1141 freq = to_offset("Q")
1142 base = FreqGroup.FR_QTR.value
1143 else:
1144 freq = to_offset(freq)
1145 base = libperiod.freq_to_dtype_code(freq)
1146 if base != FreqGroup.FR_QTR.value:
1147 raise AssertionError("base must equal FR_QTR")
1149 freqstr = freq.freqstr
1150 year, quarter = _make_field_arrays(year, quarter)
1151 for y, q in zip(year, quarter):
1152 y, m = parsing.quarter_to_myear(y, q, freqstr)
1153 val = libperiod.period_ordinal(y, m, 1, 1, 1, 1, 0, 0, base)
1154 ordinals.append(val)
1155 else:
1156 freq = to_offset(freq)
1157 base = libperiod.freq_to_dtype_code(freq)
1158 arrays = _make_field_arrays(year, month, day, hour, minute, second)
1159 for y, mth, d, h, mn, s in zip(*arrays):
1160 ordinals.append(libperiod.period_ordinal(y, mth, d, h, mn, s, 0, 0, base))
1162 return np.array(ordinals, dtype=np.int64), freq
1165def _make_field_arrays(*fields) -> list[np.ndarray]:
1166 length = None
1167 for x in fields:
1168 if isinstance(x, (list, np.ndarray, ABCSeries)):
1169 if length is not None and len(x) != length:
1170 raise ValueError("Mismatched Period array lengths")
1171 elif length is None:
1172 length = len(x)
1174 # error: Argument 2 to "repeat" has incompatible type "Optional[int]"; expected
1175 # "Union[Union[int, integer[Any]], Union[bool, bool_], ndarray, Sequence[Union[int,
1176 # integer[Any]]], Sequence[Union[bool, bool_]], Sequence[Sequence[Any]]]"
1177 return [
1178 np.asarray(x)
1179 if isinstance(x, (np.ndarray, list, ABCSeries))
1180 else np.repeat(x, length) # type: ignore[arg-type]
1181 for x in fields
1182 ]