|
1 | 1 | # -*- coding: utf-8 -*-
|
| 2 | +from datetime import datetime, timedelta |
2 | 3 | import operator
|
3 | 4 | import warnings
|
4 | 5 |
|
|
8 | 9 | from pandas._libs.tslibs import timezones
|
9 | 10 | from pandas._libs.tslibs.timedeltas import delta_to_nanoseconds, Timedelta
|
10 | 11 | from pandas._libs.tslibs.period import (
|
11 |
| - DIFFERENT_FREQ_INDEX, IncompatibleFrequency) |
| 12 | + Period, DIFFERENT_FREQ_INDEX, IncompatibleFrequency) |
12 | 13 |
|
13 | 14 | from pandas.errors import NullFrequencyError, PerformanceWarning
|
14 | 15 | from pandas import compat
|
|
19 | 20 | from pandas.core.dtypes.common import (
|
20 | 21 | needs_i8_conversion,
|
21 | 22 | is_list_like,
|
| 23 | + is_offsetlike, |
| 24 | + is_extension_array_dtype, |
| 25 | + is_datetime64_dtype, |
| 26 | + is_datetime64_any_dtype, |
| 27 | + is_datetime64tz_dtype, |
| 28 | + is_float_dtype, |
| 29 | + is_integer_dtype, |
22 | 30 | is_bool_dtype,
|
23 | 31 | is_period_dtype,
|
24 | 32 | is_timedelta64_dtype,
|
@@ -100,7 +108,7 @@ class DatetimeLikeArrayMixin(ExtensionOpsMixin, AttributesMixin):
|
100 | 108 | _freq
|
101 | 109 |
|
102 | 110 | and that the inheriting class has methods:
|
103 |
| - _validate_frequency |
| 111 | + _generate_range |
104 | 112 | """
|
105 | 113 |
|
106 | 114 | @property
|
@@ -132,6 +140,14 @@ def asi8(self):
|
132 | 140 | # ------------------------------------------------------------------
|
133 | 141 | # Array-like Methods
|
134 | 142 |
|
| 143 | + @property |
| 144 | + def shape(self): |
| 145 | + return (len(self),) |
| 146 | + |
| 147 | + @property |
| 148 | + def size(self): |
| 149 | + return np.prod(self.shape) |
| 150 | + |
135 | 151 | def __len__(self):
|
136 | 152 | return len(self._data)
|
137 | 153 |
|
@@ -296,6 +312,34 @@ def resolution(self):
|
296 | 312 | """
|
297 | 313 | return frequencies.Resolution.get_str(self._resolution)
|
298 | 314 |
|
| 315 | + @classmethod |
| 316 | + def _validate_frequency(cls, index, freq, **kwargs): |
| 317 | + """ |
| 318 | + Validate that a frequency is compatible with the values of a given |
| 319 | + Datetime Array/Index or Timedelta Array/Index |
| 320 | +
|
| 321 | + Parameters |
| 322 | + ---------- |
| 323 | + index : DatetimeIndex or TimedeltaIndex |
| 324 | + The index on which to determine if the given frequency is valid |
| 325 | + freq : DateOffset |
| 326 | + The frequency to validate |
| 327 | + """ |
| 328 | + if is_period_dtype(cls): |
| 329 | + # Frequency validation is not meaningful for Period Array/Index |
| 330 | + return None |
| 331 | + |
| 332 | + inferred = index.inferred_freq |
| 333 | + if index.size == 0 or inferred == freq.freqstr: |
| 334 | + return None |
| 335 | + |
| 336 | + on_freq = cls._generate_range(start=index[0], end=None, |
| 337 | + periods=len(index), freq=freq, **kwargs) |
| 338 | + if not np.array_equal(index.asi8, on_freq.asi8): |
| 339 | + raise ValueError('Inferred frequency {infer} from passed values ' |
| 340 | + 'does not conform to passed frequency {passed}' |
| 341 | + .format(infer=inferred, passed=freq.freqstr)) |
| 342 | + |
299 | 343 | # ------------------------------------------------------------------
|
300 | 344 | # Arithmetic Methods
|
301 | 345 |
|
@@ -477,6 +521,188 @@ def _addsub_offset_array(self, other, op):
|
477 | 521 | kwargs['freq'] = 'infer'
|
478 | 522 | return type(self)(res_values, **kwargs)
|
479 | 523 |
|
| 524 | + def shift(self, n, freq=None): |
| 525 | + """ |
| 526 | + Specialized shift which produces a Datetime/Timedelta Array/Index |
| 527 | +
|
| 528 | + Parameters |
| 529 | + ---------- |
| 530 | + n : int |
| 531 | + Periods to shift by |
| 532 | + freq : DateOffset or timedelta-like, optional |
| 533 | +
|
| 534 | + Returns |
| 535 | + ------- |
| 536 | + shifted : same type as self |
| 537 | + """ |
| 538 | + if freq is not None and freq != self.freq: |
| 539 | + if isinstance(freq, compat.string_types): |
| 540 | + freq = frequencies.to_offset(freq) |
| 541 | + offset = n * freq |
| 542 | + result = self + offset |
| 543 | + |
| 544 | + if hasattr(self, 'tz'): |
| 545 | + result._tz = self.tz |
| 546 | + |
| 547 | + return result |
| 548 | + |
| 549 | + if n == 0: |
| 550 | + # immutable so OK |
| 551 | + return self |
| 552 | + |
| 553 | + if self.freq is None: |
| 554 | + raise NullFrequencyError("Cannot shift with no freq") |
| 555 | + |
| 556 | + start = self[0] + n * self.freq |
| 557 | + end = self[-1] + n * self.freq |
| 558 | + attribs = self._get_attributes_dict() |
| 559 | + return self._generate_range(start=start, end=end, periods=None, |
| 560 | + **attribs) |
| 561 | + |
| 562 | + @classmethod |
| 563 | + def _add_datetimelike_methods(cls): |
| 564 | + """ |
| 565 | + add in the datetimelike methods (as we may have to override the |
| 566 | + superclass) |
| 567 | + """ |
| 568 | + |
| 569 | + def __add__(self, other): |
| 570 | + other = lib.item_from_zerodim(other) |
| 571 | + if isinstance(other, (ABCSeries, ABCDataFrame)): |
| 572 | + return NotImplemented |
| 573 | + |
| 574 | + # scalar others |
| 575 | + elif other is NaT: |
| 576 | + result = self._add_nat() |
| 577 | + elif isinstance(other, (Tick, timedelta, np.timedelta64)): |
| 578 | + result = self._add_delta(other) |
| 579 | + elif isinstance(other, DateOffset): |
| 580 | + # specifically _not_ a Tick |
| 581 | + result = self._add_offset(other) |
| 582 | + elif isinstance(other, (datetime, np.datetime64)): |
| 583 | + result = self._add_datelike(other) |
| 584 | + elif lib.is_integer(other): |
| 585 | + # This check must come after the check for np.timedelta64 |
| 586 | + # as is_integer returns True for these |
| 587 | + result = self.shift(other) |
| 588 | + |
| 589 | + # array-like others |
| 590 | + elif is_timedelta64_dtype(other): |
| 591 | + # TimedeltaIndex, ndarray[timedelta64] |
| 592 | + result = self._add_delta(other) |
| 593 | + elif is_offsetlike(other): |
| 594 | + # Array/Index of DateOffset objects |
| 595 | + result = self._addsub_offset_array(other, operator.add) |
| 596 | + elif is_datetime64_dtype(other) or is_datetime64tz_dtype(other): |
| 597 | + # DatetimeIndex, ndarray[datetime64] |
| 598 | + return self._add_datelike(other) |
| 599 | + elif is_integer_dtype(other): |
| 600 | + result = self._addsub_int_array(other, operator.add) |
| 601 | + elif is_float_dtype(other) or is_period_dtype(other): |
| 602 | + # Explicitly catch invalid dtypes |
| 603 | + raise TypeError("cannot add {dtype}-dtype to {cls}" |
| 604 | + .format(dtype=other.dtype, |
| 605 | + cls=type(self).__name__)) |
| 606 | + elif is_extension_array_dtype(other): |
| 607 | + # Categorical op will raise; defer explicitly |
| 608 | + return NotImplemented |
| 609 | + else: # pragma: no cover |
| 610 | + return NotImplemented |
| 611 | + |
| 612 | + return result |
| 613 | + |
| 614 | + cls.__add__ = __add__ |
| 615 | + |
| 616 | + def __radd__(self, other): |
| 617 | + # alias for __add__ |
| 618 | + return self.__add__(other) |
| 619 | + cls.__radd__ = __radd__ |
| 620 | + |
| 621 | + def __sub__(self, other): |
| 622 | + other = lib.item_from_zerodim(other) |
| 623 | + if isinstance(other, (ABCSeries, ABCDataFrame)): |
| 624 | + return NotImplemented |
| 625 | + |
| 626 | + # scalar others |
| 627 | + elif other is NaT: |
| 628 | + result = self._sub_nat() |
| 629 | + elif isinstance(other, (Tick, timedelta, np.timedelta64)): |
| 630 | + result = self._add_delta(-other) |
| 631 | + elif isinstance(other, DateOffset): |
| 632 | + # specifically _not_ a Tick |
| 633 | + result = self._add_offset(-other) |
| 634 | + elif isinstance(other, (datetime, np.datetime64)): |
| 635 | + result = self._sub_datelike(other) |
| 636 | + elif lib.is_integer(other): |
| 637 | + # This check must come after the check for np.timedelta64 |
| 638 | + # as is_integer returns True for these |
| 639 | + result = self.shift(-other) |
| 640 | + elif isinstance(other, Period): |
| 641 | + result = self._sub_period(other) |
| 642 | + |
| 643 | + # array-like others |
| 644 | + elif is_timedelta64_dtype(other): |
| 645 | + # TimedeltaIndex, ndarray[timedelta64] |
| 646 | + result = self._add_delta(-other) |
| 647 | + elif is_offsetlike(other): |
| 648 | + # Array/Index of DateOffset objects |
| 649 | + result = self._addsub_offset_array(other, operator.sub) |
| 650 | + elif is_datetime64_dtype(other) or is_datetime64tz_dtype(other): |
| 651 | + # DatetimeIndex, ndarray[datetime64] |
| 652 | + result = self._sub_datelike(other) |
| 653 | + elif is_period_dtype(other): |
| 654 | + # PeriodIndex |
| 655 | + result = self._sub_period_array(other) |
| 656 | + elif is_integer_dtype(other): |
| 657 | + result = self._addsub_int_array(other, operator.sub) |
| 658 | + elif isinstance(other, ABCIndexClass): |
| 659 | + raise TypeError("cannot subtract {cls} and {typ}" |
| 660 | + .format(cls=type(self).__name__, |
| 661 | + typ=type(other).__name__)) |
| 662 | + elif is_float_dtype(other): |
| 663 | + # Explicitly catch invalid dtypes |
| 664 | + raise TypeError("cannot subtract {dtype}-dtype from {cls}" |
| 665 | + .format(dtype=other.dtype, |
| 666 | + cls=type(self).__name__)) |
| 667 | + elif is_extension_array_dtype(other): |
| 668 | + # Categorical op will raise; defer explicitly |
| 669 | + return NotImplemented |
| 670 | + else: # pragma: no cover |
| 671 | + return NotImplemented |
| 672 | + |
| 673 | + return result |
| 674 | + |
| 675 | + cls.__sub__ = __sub__ |
| 676 | + |
| 677 | + def __rsub__(self, other): |
| 678 | + if is_datetime64_dtype(other) and is_timedelta64_dtype(self): |
| 679 | + # ndarray[datetime64] cannot be subtracted from self, so |
| 680 | + # we need to wrap in DatetimeArray/Index and flip the operation |
| 681 | + if not isinstance(other, DatetimeLikeArrayMixin): |
| 682 | + # Avoid down-casting DatetimeIndex |
| 683 | + from pandas.core.arrays import DatetimeArrayMixin |
| 684 | + other = DatetimeArrayMixin(other) |
| 685 | + return other - self |
| 686 | + elif (is_datetime64_any_dtype(self) and hasattr(other, 'dtype') and |
| 687 | + not is_datetime64_any_dtype(other)): |
| 688 | + # GH#19959 datetime - datetime is well-defined as timedelta, |
| 689 | + # but any other type - datetime is not well-defined. |
| 690 | + raise TypeError("cannot subtract {cls} from {typ}" |
| 691 | + .format(cls=type(self).__name__, |
| 692 | + typ=type(other).__name__)) |
| 693 | + return -(self - other) |
| 694 | + cls.__rsub__ = __rsub__ |
| 695 | + |
| 696 | + def __iadd__(self, other): |
| 697 | + # alias for __add__ |
| 698 | + return self.__add__(other) |
| 699 | + cls.__iadd__ = __iadd__ |
| 700 | + |
| 701 | + def __isub__(self, other): |
| 702 | + # alias for __sub__ |
| 703 | + return self.__sub__(other) |
| 704 | + cls.__isub__ = __isub__ |
| 705 | + |
480 | 706 | # --------------------------------------------------------------
|
481 | 707 | # Comparison Methods
|
482 | 708 |
|
|
0 commit comments