diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 95b997fae6b6c..cece1ea28203e 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -801,6 +801,25 @@ def _evaluate_compare(self, other, op): # ------------------------------------------------------------------- # Shared Constructor Helpers +def scalar_data_error(scalar, cls): + """ + Produce the error message to issue when raising a TypeError if a scalar + is passed to an array constructor. + + Parameters + ---------- + scalar : object + cls : class + + Returns + ------- + message : str + """ + return ('{cls}() must be called with a ' + 'collection of some kind, {data} was passed' + .format(cls=cls.__name__, data=repr(scalar))) + + def validate_periods(periods): """ If a `periods` argument is passed to the Datetime/Timedelta Array/Index @@ -888,6 +907,47 @@ def maybe_infer_freq(freq): return freq, freq_infer +def maybe_define_freq(freq_infer, result): + """ + If appropriate, infer the frequency of the given Datetime/Timedelta Array + and pin it to the object at the end of the construction. + + Parameters + ---------- + freq_infer : bool + result : DatetimeArray or TimedeltaArray + + Notes + ----- + This may alter `result` in-place, should only ever be called + from __new__/__init__. + """ + if freq_infer: + inferred = result.inferred_freq + if inferred: + result.freq = frequencies.to_offset(inferred) + + +def maybe_validate_freq(result, verify, freq, freq_infer, **kwargs): + """ + If a frequency was passed by the user and not inferred or extracted + from the underlying data, then validate that the data is consistent with + the user-provided frequency. + + Parameters + ---------- + result : DatetimeIndex or TimedeltaIndex + verify : bool + freq : DateOffset or None + freq_infer : bool + **kwargs : arguments to pass to `_validate_frequency` + For DatetimeIndex this is just "ambiguous", empty for TimedeltaIndex + """ + if verify and len(result) > 0: + if freq is not None and not freq_infer: + result._validate_frequency(result, freq, **kwargs) + + def validate_tz_from_dtype(dtype, tz): """ If the given dtype is a DatetimeTZDtype, extract the implied diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index 0258e1e6e5973..512f7c98aded1 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -119,7 +119,8 @@ def wrapper(self, other): if isinstance(other, list): # FIXME: This can break for object-dtype with mixed types other = type(self)(other) - elif not isinstance(other, (np.ndarray, ABCIndexClass, ABCSeries)): + elif not isinstance(other, (np.ndarray, ABCIndexClass, ABCSeries, + DatetimeArrayMixin)): # Following Timestamp convention, __eq__ is all-False # and __ne__ is all True, others raise TypeError. return ops.invalid_comparison(self, other, op) @@ -170,6 +171,8 @@ class DatetimeArrayMixin(dtl.DatetimeLikeArrayMixin): # Constructors _attributes = ["freq", "tz"] + _freq = None + _tz = None @classmethod def _simple_new(cls, values, freq=None, tz=None, **kwargs): @@ -193,11 +196,16 @@ def _simple_new(cls, values, freq=None, tz=None, **kwargs): result._tz = timezones.tz_standardize(tz) return result - def __new__(cls, values, freq=None, tz=None, dtype=None): + def __new__(cls, values, freq=None, tz=None, dtype=None, copy=False): + if isinstance(values, (list, tuple)) or is_object_dtype(values): + values = cls._from_sequence(values, copy=copy) + # TODO: Can we set copy=False here to avoid re-coping? + if tz is None and hasattr(values, 'tz'): - # e.g. DatetimeIndex + # e.g. DatetimeArray, DatetimeIndex tz = values.tz + # TODO: what about if freq == 'infer'? if freq is None and hasattr(values, "freq"): # i.e. DatetimeArray, DatetimeIndex freq = values.freq @@ -207,26 +215,46 @@ def __new__(cls, values, freq=None, tz=None, dtype=None): # if dtype has an embedded tz, capture it tz = dtl.validate_tz_from_dtype(dtype, tz) - if isinstance(values, DatetimeArrayMixin): + if lib.is_scalar(values): + raise TypeError(dtl.scalar_data_error(values, cls)) + elif isinstance(values, ABCSeries): + # extract nanosecond unix timestamps + if tz is None: + # TODO: Try to do this in just one place + tz = values.dt.tz + values = np.array(values.view('i8')) + elif isinstance(values, DatetimeArrayMixin): # extract nanosecond unix timestamps values = values.asi8 + if values.dtype == 'i8': values = values.view('M8[ns]') assert isinstance(values, np.ndarray), type(values) assert is_datetime64_dtype(values) # not yet assured nanosecond - values = conversion.ensure_datetime64ns(values, copy=False) + values = conversion.ensure_datetime64ns(values, copy=copy) result = cls._simple_new(values, freq=freq, tz=tz) - if freq_infer: - inferred = result.inferred_freq - if inferred: - result.freq = to_offset(inferred) + dtl.maybe_define_freq(freq_infer, result) # NB: Among other things not yet ported from the DatetimeIndex # constructor, this does not call _deepcopy_if_needed return result + @classmethod + def _from_sequence(cls, scalars, dtype=None, copy=False): + # list, tuple, or object-dtype ndarray/Index + values = np.array(scalars, dtype=np.object_, copy=copy) + if values.ndim != 1: + raise TypeError("Values must be 1-dimensional") + + # TODO: See if we can decrease circularity + from pandas.core.tools.datetimes import to_datetime + values = to_datetime(values) + + # pass dtype to constructor in order to convert timezone if necessary + return cls(values, dtype=dtype) + @classmethod def _generate_range(cls, start, end, periods, freq, tz=None, normalize=False, ambiguous='raise', closed=None): diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py index 90e7beac63427..0a64eda9c9551 100644 --- a/pandas/core/arrays/period.py +++ b/pandas/core/arrays/period.py @@ -165,7 +165,9 @@ class PeriodArray(dtl.DatetimeLikeArrayMixin, ExtensionArray): # -------------------------------------------------------------------- # Constructors - def __init__(self, values, freq=None, copy=False): + def __init__(self, values, freq=None, dtype=None, copy=False): + freq = dtl.validate_dtype_freq(dtype, freq) + if freq is not None: freq = Period._maybe_convert_freq(freq) diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index 9653121879c0d..1ce72a5ee8e4e 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -3,7 +3,7 @@ import numpy as np -from pandas._libs import tslibs +from pandas._libs import tslibs, lib, algos from pandas._libs.tslibs import Timedelta, Timestamp, NaT from pandas._libs.tslibs.fields import get_timedelta_field from pandas._libs.tslibs.timedeltas import array_to_timedelta64 @@ -11,7 +11,7 @@ from pandas import compat from pandas.core.dtypes.common import ( - _TD_DTYPE, is_list_like) + _TD_DTYPE, is_list_like, is_object_dtype, is_timedelta64_dtype) from pandas.core.dtypes.generic import ABCSeries from pandas.core.dtypes.missing import isna @@ -19,7 +19,6 @@ from pandas.core.algorithms import checked_add_with_arr from pandas.tseries.offsets import Tick -from pandas.tseries.frequencies import to_offset from . import datetimelike as dtl @@ -112,9 +111,7 @@ def dtype(self): @classmethod def _simple_new(cls, values, freq=None, dtype=_TD_DTYPE): - # `dtype` is passed by _shallow_copy in corner cases, should always - # be timedelta64[ns] if present - assert dtype == _TD_DTYPE + _require_m8ns_dtype(dtype) assert isinstance(values, np.ndarray), type(values) if values.dtype == 'i8': @@ -127,22 +124,48 @@ def _simple_new(cls, values, freq=None, dtype=_TD_DTYPE): result._freq = freq return result - def __new__(cls, values, freq=None): + def __new__(cls, values, freq=None, dtype=_TD_DTYPE, copy=False): + _require_m8ns_dtype(dtype) + + if isinstance(values, (list, tuple)) or is_object_dtype(values): + values = cls._from_sequence(values, copy=copy)._data + # TODO: can we set copy=False to avoid re-copying? freq, freq_infer = dtl.maybe_infer_freq(freq) - values = np.array(values, copy=False) - if values.dtype == np.object_: - values = array_to_timedelta64(values) + if lib.is_scalar(values): + raise TypeError(dtl.scalar_data_error(values, cls)) + elif isinstance(values, TimedeltaArrayMixin): + if freq is None and values.freq is not None: + freq = values.freq + freq_infer = False + values = values._data - result = cls._simple_new(values, freq=freq) - if freq_infer: - inferred = result.inferred_freq - if inferred: - result.freq = to_offset(inferred) + values = np.array(values, copy=copy) + + if values.dtype == 'i8': + pass + elif not is_timedelta64_dtype(values): + raise TypeError(values.dtype) + elif values.dtype != _TD_DTYPE: + # i.e. non-nano unit + # TODO: use tslibs.conversion func? watch out for overflows + values = values.astype(_TD_DTYPE) + result = cls._simple_new(values, freq=freq) + dtl.maybe_define_freq(freq_infer, result) return result + @classmethod + def _from_sequence(cls, scalars, dtype=_TD_DTYPE, copy=False): + # list, tuple, or object-dtype ndarray/Index + values = np.array(scalars, dtype=np.object_, copy=copy) + if values.ndim != 1: + raise TypeError("Values must be 1-dimensional") + + result = array_to_timedelta64(values) + return cls(result, dtype=dtype) + @classmethod def _generate_range(cls, start, end, periods, freq, closed=None): @@ -180,6 +203,23 @@ def _generate_range(cls, start, end, periods, freq, closed=None): return cls._simple_new(index, freq=freq) + # ---------------------------------------------------------------- + # Array-Like Methods + # NB: these are appreciably less efficient than the TimedeltaIndex versions + + @property + def is_monotonic_increasing(self): + return algos.is_monotonic(self.asi8, timelike=True)[0] + + @property + def is_monotonic_decreasing(self): + return algos.is_monotonic(self.asi8, timelike=True)[1] + + @property + def is_unique(self): + from pandas.core.algorithms import unique1d + return len(unique1d(self.asi8)) == len(self) + # ---------------------------------------------------------------- # Arithmetic Methods @@ -413,3 +453,21 @@ def _generate_regular_range(start, end, periods, offset): data = np.arange(b, e, stride, dtype=np.int64) return data + + +def _require_m8ns_dtype(dtype): + """ + `dtype` is included in the constructor signature for consistency with + DatetimeArray and PeriodArray, but only timedelta64[ns] is considered + valid. Raise if anything else is passed. + + Parameters + ---------- + dtype : dtype + + Raises + ------ + ValueError + """ + if dtype != _TD_DTYPE: + raise ValueError("Only timedelta64[ns] dtype is valid.", dtype) diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 210bdabbd9dd7..b4e38fd9f6bdf 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -182,7 +182,6 @@ class DatetimeIndex(DatetimeArrayMixin, DatelikeOps, TimelikeOps, """ _resolution = cache_readonly(DatetimeArrayMixin._resolution.fget) - _shallow_copy = Index._shallow_copy _typ = 'datetimeindex' _join_precedence = 10 @@ -199,10 +198,11 @@ def _join_i8_wrapper(joinf, **kwargs): _engine_type = libindex.DatetimeEngine - tz = None + _tz = None _freq = None _comparables = ['name', 'freqstr', 'tz'] _attributes = ['name', 'freq', 'tz'] + timetuple = None # define my properties & methods for delegation _bool_ops = ['is_month_start', 'is_month_end', @@ -226,6 +226,9 @@ def _join_i8_wrapper(joinf, **kwargs): _timezone = cache_readonly(DatetimeArrayMixin._timezone.fget) is_normalized = cache_readonly(DatetimeArrayMixin.is_normalized.fget) + # -------------------------------------------------------------------- + # Constructors + def __new__(cls, data=None, freq=None, start=None, end=None, periods=None, tz=None, normalize=False, closed=None, ambiguous='raise', @@ -254,13 +257,11 @@ def __new__(cls, data=None, result.name = name return result - if not isinstance(data, (np.ndarray, Index, ABCSeries, - DatetimeArrayMixin)): - if is_scalar(data): - raise ValueError('DatetimeIndex() must be called with a ' - 'collection of some kind, %s was passed' - % repr(data)) - # other iterable of some kind + if is_scalar(data): + raise TypeError(dtl.scalar_data_error(data, cls)) + + elif not isinstance(data, (np.ndarray, Index, ABCSeries, + DatetimeArrayMixin)): if not isinstance(data, (list, tuple)): data = list(data) data = np.asarray(data, dtype='O') @@ -280,16 +281,15 @@ def __new__(cls, data=None, data = data.tz_localize(tz, ambiguous=ambiguous) else: # the tz's must match - if str(tz) != str(data.tz): + if not timezones.tz_compare(tz, data.tz): msg = ('data is already tz-aware {0}, unable to ' 'set specified tz: {1}') raise TypeError(msg.format(data.tz, tz)) - subarr = data.values - if freq is None: freq = data.freq verify_integrity = False + data = data._data elif issubclass(data.dtype.type, np.datetime64): if data.dtype != _NS_DTYPE: data = conversion.ensure_datetime64ns(data) @@ -298,41 +298,22 @@ def __new__(cls, data=None, tz = timezones.maybe_get_tz(tz) data = conversion.tz_localize_to_utc(data.view('i8'), tz, ambiguous=ambiguous) - subarr = data.view(_NS_DTYPE) else: # must be integer dtype otherwise # assume this data are epoch timestamps if data.dtype != _INT64_DTYPE: data = data.astype(np.int64, copy=False) - subarr = data.view(_NS_DTYPE) + subarr = data.view(_NS_DTYPE) assert isinstance(subarr, np.ndarray), type(subarr) assert subarr.dtype == 'M8[ns]', subarr.dtype subarr = cls._simple_new(subarr, name=name, freq=freq, tz=tz) - if dtype is not None: - if not is_dtype_equal(subarr.dtype, dtype): - # dtype must be coerced to DatetimeTZDtype above - if subarr.tz is not None: - raise ValueError("cannot localize from non-UTC data") - - if verify_integrity and len(subarr) > 0: - if freq is not None and not freq_infer: - cls._validate_frequency(subarr, freq, ambiguous=ambiguous) - - if freq_infer: - inferred = subarr.inferred_freq - if inferred: - subarr.freq = to_offset(inferred) - + dtl.maybe_validate_freq(subarr, verify_integrity, freq, freq_infer, + ambiguous=ambiguous) + dtl.maybe_define_freq(freq_infer, subarr) return subarr._deepcopy_if_needed(ref_to_data, copy) - def _convert_for_op(self, value): - """ Convert value to be insertable to ndarray """ - if self._has_same_tz(value): - return _to_m8(value) - raise ValueError('Passed item and index have different timezone') - @classmethod def _simple_new(cls, values, name=None, freq=None, tz=None, dtype=None, **kwargs): @@ -349,6 +330,8 @@ def _simple_new(cls, values, name=None, freq=None, tz=None, result._reset_identity() return result + # -------------------------------------------------------------------- + @property def _values(self): # tz-naive -> ndarray @@ -400,6 +383,12 @@ def _is_dates_only(self): from pandas.io.formats.format import _is_dates_only return _is_dates_only(self.values) and self.tz is None + def _convert_for_op(self, value): + """ Convert value to be insertable to ndarray """ + if self._has_same_tz(value): + return _to_m8(value) + raise ValueError('Passed item and index have different timezone') + @property def _formatter_func(self): from pandas.io.formats.format import _get_format_datetime64 @@ -1104,6 +1093,9 @@ def slice_indexer(self, start=None, end=None, step=None, kind=None): else: raise + # ----------------------------------------------------------------- + # Wrapping DatetimeArray + year = wrap_field_accessor(DatetimeArrayMixin.year) month = wrap_field_accessor(DatetimeArrayMixin.month) day = wrap_field_accessor(DatetimeArrayMixin.day) @@ -1142,6 +1134,8 @@ def slice_indexer(self, start=None, end=None, step=None, kind=None): month_name = wrap_array_method(DatetimeArrayMixin.month_name, True) day_name = wrap_array_method(DatetimeArrayMixin.day_name, True) + # ----------------------------------------------------------------- + @Substitution(klass='DatetimeIndex') @Appender(_shared_docs['searchsorted']) def searchsorted(self, value, side='left', sorter=None): diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 22ecefae8cbe2..f4f844eb5c7f1 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -35,7 +35,6 @@ to_timedelta, _coerce_scalar_to_timedelta_type) from pandas._libs import (lib, index as libindex, join as libjoin, Timedelta, NaT) -from pandas._libs.tslibs.timedeltas import array_to_timedelta64 class TimedeltaIndex(TimedeltaArrayMixin, DatetimeIndexOpsMixin, @@ -157,9 +156,7 @@ def __new__(cls, data=None, unit=None, freq=None, start=None, end=None, data = to_timedelta(data, unit=unit, box=False) if is_scalar(data): - raise ValueError('TimedeltaIndex() must be called with a ' - 'collection of some kind, {data} was passed' - .format(data=repr(data))) + raise TypeError(dtl.scalar_data_error(data, cls)) # convert if not already if getattr(data, 'dtype', None) != _TD_DTYPE: @@ -167,30 +164,18 @@ def __new__(cls, data=None, unit=None, freq=None, start=None, end=None, elif copy: data = np.array(data, copy=True) - data = np.array(data, copy=False) - if data.dtype == np.object_: - data = array_to_timedelta64(data) - if data.dtype != _TD_DTYPE: - if is_timedelta64_dtype(data): - # non-nano unit - # TODO: watch out for overflows - data = data.astype(_TD_DTYPE) - else: - data = ensure_int64(data).view(_TD_DTYPE) + arr = TimedeltaArrayMixin(data, freq=freq) + if freq_infer and arr.freq is not None: + freq_infer = False + verify_integrity = False + freq = arr.freq + data = arr._data assert data.dtype == 'm8[ns]', data.dtype subarr = cls._simple_new(data, name=name, freq=freq) - # check that we are matching freqs - if verify_integrity and len(subarr) > 0: - if freq is not None and not freq_infer: - cls._validate_frequency(subarr, freq) - - if freq_infer: - inferred = subarr.inferred_freq - if inferred: - subarr.freq = to_offset(inferred) - + dtl.maybe_validate_freq(subarr, verify_integrity, freq, freq_infer) + dtl.maybe_define_freq(freq_infer, subarr) return subarr @classmethod @@ -209,8 +194,6 @@ def _simple_new(cls, values, name=None, freq=None, dtype=_TD_DTYPE): result._reset_identity() return result - _shallow_copy = Index._shallow_copy - @property def _formatter_func(self): from pandas.io.formats.format import _get_format_timedelta64 @@ -243,6 +226,9 @@ def _format_native_types(self, na_rep=u'NaT', date_format=None, **kwargs): nat_rep=na_rep, justify='all').get_result() + # ----------------------------------------------------------------- + # Wrapping TimedeltaArray + days = wrap_field_accessor(TimedeltaArrayMixin.days) seconds = wrap_field_accessor(TimedeltaArrayMixin.seconds) microseconds = wrap_field_accessor(TimedeltaArrayMixin.microseconds) @@ -250,6 +236,13 @@ def _format_native_types(self, na_rep=u'NaT', date_format=None, **kwargs): total_seconds = wrap_array_method(TimedeltaArrayMixin.total_seconds, True) + # override TimedeltaArray versions + is_monotonic_increasing = Index.is_monotonic_increasing + is_monotonic_decreasing = Index.is_monotonic_decreasing + is_unique = Index.is_unique + + # ----------------------------------------------------------------- + @Appender(_index_shared_docs['astype']) def astype(self, dtype, copy=True): dtype = pandas_dtype(dtype) diff --git a/pandas/tests/arrays/test_datetimes.py b/pandas/tests/arrays/test_datetimes.py new file mode 100644 index 0000000000000..a6fb22f93f5e0 --- /dev/null +++ b/pandas/tests/arrays/test_datetimes.py @@ -0,0 +1,96 @@ +""" +Tests for DatetimeArray +""" +import pytest +import numpy as np + +from pandas.core.arrays import DatetimeArrayMixin as DatetimeArray + +import pandas as pd +import pandas.util.testing as tm + + +class TestDatetimeArrayConstructors(object): + + def test_scalar_raises_type_error(self): + # GH#23493 + with pytest.raises(TypeError): + DatetimeArray(2) + + with pytest.raises(TypeError): + pd.DatetimeIndex(pd.Timestamp.now()) + + def test_from_sequence_requires_1dim(self): + arr2d = np.arange(10).view('M8[s]').astype(object).reshape(2, 5) + with pytest.raises(TypeError): + DatetimeArray(arr2d) + + with pytest.raises(TypeError): + pd.DatetimeIndex(arr2d) + + arr0d = np.array(pd.Timestamp.now()) + with pytest.raises(TypeError): + DatetimeArray(arr0d) + + with pytest.raises(TypeError): + pd.DatetimeIndex(arr0d) + + def test_init_from_object_dtype(self, tz_naive_fixture): + # GH#23493 + tz = tz_naive_fixture + if tz is not None: + pytest.xfail(reason="Casting DatetimeIndex to object-dtype raises " + "for pd.Index and is incorrect for np.array; " + "GH#24391") + + # arbitrary DatetimeIndex; this should work for any DatetimeIndex + # with non-None freq + dti = pd.date_range('2016-01-1', freq='MS', periods=9, tz=tz) + + # Fails because np.array(dti, dtype=object) incorrectly returns Longs + result = DatetimeArray(np.array(dti, dtype=object), freq='infer') + tm.assert_equal(pd.DatetimeIndex(result), dti) + + # Fails because `pd.Index(dti, dtype=object) raises incorrectly + result = DatetimeArray(pd.Index(dti, dtype=object), freq='infer') + tm.assert_equal(pd.DatetimeIndex(result), dti) + + # NB: for now we re-wrap in DatetimeIndex to use assert_index_equal + # once assert_datetime_array_equal is in place, this will be changed + def test_init_only_freq_infer(self, tz_naive_fixture): + # GH#23493 + # just pass data and freq='infer' if relevant; no other kwargs + tz = tz_naive_fixture + + # arbitrary DatetimeIndex; this should work for any DatetimeIndex + # with non-None freq + dti = pd.date_range('2016-01-1', freq='MS', periods=9, tz=tz) + expected = DatetimeArray(dti) + assert expected.freq == dti.freq + assert expected.tz == dti.tz + + # broken until ABCDatetimeArray and isna is fixed + # assert (dti == expected).all() + # assert (expected == dti).all() + + result = DatetimeArray(expected) + tm.assert_equal(pd.DatetimeIndex(result), dti) + + result = DatetimeArray(list(dti), freq='infer') + tm.assert_equal(pd.DatetimeIndex(result), dti) + + result = DatetimeArray(tuple(dti), freq='infer') + tm.assert_equal(pd.DatetimeIndex(result), dti) + + if tz is None: + result = DatetimeArray(np.array(dti), freq='infer') + tm.assert_equal(pd.DatetimeIndex(result), dti) + + result = DatetimeArray(np.array(dti).astype('M8[s]'), freq='infer') + tm.assert_equal(pd.DatetimeIndex(result), dti) + + result = DatetimeArray(pd.Series(dti), freq='infer') + tm.assert_equal(pd.DatetimeIndex(result), dti) + + result = DatetimeArray(pd.Series(dti, dtype=object), freq='infer') + tm.assert_equal(pd.DatetimeIndex(result), dti) diff --git a/pandas/tests/arrays/test_timedeltas.py b/pandas/tests/arrays/test_timedeltas.py new file mode 100644 index 0000000000000..0ec9432e61bbc --- /dev/null +++ b/pandas/tests/arrays/test_timedeltas.py @@ -0,0 +1,89 @@ +""" +Tests for TimedeltaArray +""" + +import numpy as np +import pytest + +from pandas.core.arrays import TimedeltaArrayMixin as TimedeltaArray + +import pandas as pd +import pandas.util.testing as tm + + +# TODO: Many of these tests are mirrored in test_datetimes; see if these +# can be shared +class TestTimedeltaArrayConstructors(object): + def test_scalar_raises_type_error(self): + # GH#23493 + with pytest.raises(TypeError): + TimedeltaArray(2) + + with pytest.raises(TypeError): + pd.TimedeltaIndex(pd.Timedelta(days=4)) + + def test_from_sequence_requires_1dim(self): + arr2d = np.arange(10).view('m8[s]').astype(object).reshape(2, 5) + with pytest.raises(TypeError): + TimedeltaArray(arr2d) + + with pytest.raises(TypeError): + pd.TimedeltaIndex(arr2d) + + arr0d = np.array(pd.Timedelta('49 days')) + with pytest.raises(TypeError): + TimedeltaArray(arr0d) + + with pytest.raises(TypeError): + pd.TimedeltaIndex(arr0d) + + def test_init_from_object_dtype(self): + # GH#23493 + + # arbitrary TimedeltaIndex; this should work for any TimedeltaIndex + # with non-None freq + tdi = pd.timedelta_range('3 Days', freq='ms', periods=9) + + # Fails because np.array(tdi, dtype=object) incorrectly returns Longs + result = TimedeltaArray(np.array(tdi, dtype=object), freq='infer') + tm.assert_equal(pd.TimedeltaIndex(result), tdi) + + # Fails because `pd.Index(tdi, dtype=object) raises incorrectly + result = TimedeltaArray(pd.Index(tdi, dtype=object), freq='infer') + tm.assert_equal(pd.TimedeltaIndex(result), tdi) + + # NB: for now we re-wrap in TimedeltaIndex to use assert_index_equal + # once assert_timedelta_array_equal is in place, this will be changed + def test_init_only_freq_infer(self): + # GH#23493 + # just pass data and freq='infer' if relevant; no other kwargs + + # arbitrary TimedeltaIndex; this should work for any TimedeltaIndex + # with non-None freq + tdi = pd.timedelta_range('3 Days', freq='H', periods=9) + expected = TimedeltaArray(tdi) + assert expected.freq == tdi.freq + + assert (tdi == expected).all() + assert (expected == tdi).all() + + result = TimedeltaArray(expected) + tm.assert_equal(pd.TimedeltaIndex(result), tdi) + + result = TimedeltaArray(list(tdi), freq='infer') + tm.assert_equal(pd.TimedeltaIndex(result), tdi) + + result = TimedeltaArray(tuple(tdi), freq='infer') + tm.assert_equal(pd.TimedeltaIndex(result), tdi) + + result = TimedeltaArray(np.array(tdi), freq='infer') + tm.assert_equal(pd.TimedeltaIndex(result), tdi) + + result = TimedeltaArray(np.array(tdi).astype('m8[s]'), freq='infer') + tm.assert_equal(pd.TimedeltaIndex(result), tdi) + + result = TimedeltaArray(pd.Series(tdi), freq='infer') + tm.assert_equal(pd.TimedeltaIndex(result), tdi) + + result = TimedeltaArray(pd.Series(tdi, dtype=object), freq='infer') + tm.assert_equal(pd.TimedeltaIndex(result), tdi) diff --git a/pandas/tests/indexes/datetimes/test_construction.py b/pandas/tests/indexes/datetimes/test_construction.py index 7a251a8ecfb28..3cecd024cb85e 100644 --- a/pandas/tests/indexes/datetimes/test_construction.py +++ b/pandas/tests/indexes/datetimes/test_construction.py @@ -320,7 +320,9 @@ def test_constructor_coverage(self): pytest.raises(ValueError, DatetimeIndex, start='1/1/2000', end='1/10/2000') - pytest.raises(ValueError, DatetimeIndex, '1/1/2000') + with pytest.raises(TypeError): + # GH#24393 + DatetimeIndex('1/1/2000') # generator expression gen = (datetime(2000, 1, 1) + timedelta(i) for i in range(10)) diff --git a/pandas/tests/indexes/timedeltas/test_construction.py b/pandas/tests/indexes/timedeltas/test_construction.py index a5cfad98b31c1..84a4800a9b61f 100644 --- a/pandas/tests/indexes/timedeltas/test_construction.py +++ b/pandas/tests/indexes/timedeltas/test_construction.py @@ -63,7 +63,9 @@ def test_constructor_coverage(self): pytest.raises(ValueError, TimedeltaIndex, start='1 days', end='10 days') - pytest.raises(ValueError, TimedeltaIndex, '1 days') + with pytest.raises(TypeError): + # GH#23493 + TimedeltaIndex('1 days') # generator expression gen = (timedelta(i) for i in range(10)) diff --git a/pandas/tseries/frequencies.py b/pandas/tseries/frequencies.py index d6e4824575468..ac9a87b258056 100644 --- a/pandas/tseries/frequencies.py +++ b/pandas/tseries/frequencies.py @@ -292,7 +292,7 @@ class _FrequencyInferer(object): def __init__(self, index, warn=True): self.index = index - self.values = np.asarray(index).view('i8') + self.values = index.asi8 # This moves the values, which are implicitly in UTC, to the # the timezone so they are in local time