from __future__ import annotations import warnings from collections.abc import Callable from itertools import product from typing import Literal import numpy as np import pandas as pd import pytest from xarray import CFTimeIndex from xarray.coding.cftime_offsets import ( _MONTH_ABBREVIATIONS, BaseCFTimeOffset, Day, Hour, Microsecond, Millisecond, Minute, MonthBegin, MonthEnd, QuarterBegin, QuarterEnd, Second, Tick, YearBegin, YearEnd, _legacy_to_new_freq, _new_to_legacy_freq, cftime_range, date_range, date_range_like, get_date_type, to_cftime_datetime, to_offset, ) from xarray.coding.frequencies import infer_freq from xarray.core.dataarray import DataArray from xarray.tests import ( _CFTIME_CALENDARS, assert_no_warnings, has_cftime, has_pandas_ge_2_2, requires_cftime, requires_pandas_3, ) cftime = pytest.importorskip("cftime") def _id_func(param): """Called on each parameter passed to pytest.mark.parametrize""" return str(param) @pytest.fixture(params=_CFTIME_CALENDARS) def calendar(request): return request.param @pytest.mark.parametrize( ("offset", "expected_n"), [ (BaseCFTimeOffset(), 1), (YearBegin(), 1), (YearEnd(), 1), (QuarterBegin(), 1), (QuarterEnd(), 1), (Tick(), 1), (Day(), 1), (Hour(), 1), (Minute(), 1), (Second(), 1), (Millisecond(), 1), (Microsecond(), 1), (BaseCFTimeOffset(n=2), 2), (YearBegin(n=2), 2), (YearEnd(n=2), 2), (QuarterBegin(n=2), 2), (QuarterEnd(n=2), 2), (Tick(n=2), 2), (Day(n=2), 2), (Hour(n=2), 2), (Minute(n=2), 2), (Second(n=2), 2), (Millisecond(n=2), 2), (Microsecond(n=2), 2), ], ids=_id_func, ) def test_cftime_offset_constructor_valid_n(offset, expected_n): assert offset.n == expected_n @pytest.mark.parametrize( ("offset", "invalid_n"), [ (BaseCFTimeOffset, 1.5), (YearBegin, 1.5), (YearEnd, 1.5), (QuarterBegin, 1.5), (QuarterEnd, 1.5), (MonthBegin, 1.5), (MonthEnd, 1.5), (Tick, 1.5), (Day, 1.5), (Hour, 1.5), (Minute, 1.5), (Second, 1.5), (Millisecond, 1.5), (Microsecond, 1.5), ], ids=_id_func, ) def test_cftime_offset_constructor_invalid_n(offset, invalid_n): with pytest.raises(TypeError): offset(n=invalid_n) @pytest.mark.parametrize( ("offset", "expected_month"), [ (YearBegin(), 1), (YearEnd(), 12), (YearBegin(month=5), 5), (YearEnd(month=5), 5), (QuarterBegin(), 3), (QuarterEnd(), 3), (QuarterBegin(month=5), 5), (QuarterEnd(month=5), 5), ], ids=_id_func, ) def test_year_offset_constructor_valid_month(offset, expected_month): assert offset.month == expected_month @pytest.mark.parametrize( ("offset", "invalid_month", "exception"), [ (YearBegin, 0, ValueError), (YearEnd, 0, ValueError), (YearBegin, 13, ValueError), (YearEnd, 13, ValueError), (YearBegin, 1.5, TypeError), (YearEnd, 1.5, TypeError), (QuarterBegin, 0, ValueError), (QuarterEnd, 0, ValueError), (QuarterBegin, 1.5, TypeError), (QuarterEnd, 1.5, TypeError), (QuarterBegin, 13, ValueError), (QuarterEnd, 13, ValueError), ], ids=_id_func, ) def test_year_offset_constructor_invalid_month(offset, invalid_month, exception): with pytest.raises(exception): offset(month=invalid_month) @pytest.mark.parametrize( ("offset", "expected"), [ (BaseCFTimeOffset(), None), (MonthBegin(), "MS"), (MonthEnd(), "ME"), (YearBegin(), "YS-JAN"), (YearEnd(), "YE-DEC"), (QuarterBegin(), "QS-MAR"), (QuarterEnd(), "QE-MAR"), (Day(), "D"), (Hour(), "h"), (Minute(), "min"), (Second(), "s"), (Millisecond(), "ms"), (Microsecond(), "us"), ], ids=_id_func, ) def test_rule_code(offset, expected): assert offset.rule_code() == expected @pytest.mark.parametrize( ("offset", "expected"), [ (BaseCFTimeOffset(), ""), (YearBegin(), ""), (QuarterBegin(), ""), ], ids=_id_func, ) def test_str_and_repr(offset, expected): assert str(offset) == expected assert repr(offset) == expected @pytest.mark.parametrize( "offset", [BaseCFTimeOffset(), MonthBegin(), QuarterBegin(), YearBegin()], ids=_id_func, ) def test_to_offset_offset_input(offset): assert to_offset(offset) == offset @pytest.mark.parametrize( ("freq", "expected"), [ ("M", MonthEnd()), ("2M", MonthEnd(n=2)), ("ME", MonthEnd()), ("2ME", MonthEnd(n=2)), ("MS", MonthBegin()), ("2MS", MonthBegin(n=2)), ("D", Day()), ("2D", Day(n=2)), ("H", Hour()), ("2H", Hour(n=2)), ("h", Hour()), ("2h", Hour(n=2)), ("T", Minute()), ("2T", Minute(n=2)), ("min", Minute()), ("2min", Minute(n=2)), ("S", Second()), ("2S", Second(n=2)), ("L", Millisecond(n=1)), ("2L", Millisecond(n=2)), ("ms", Millisecond(n=1)), ("2ms", Millisecond(n=2)), ("U", Microsecond(n=1)), ("2U", Microsecond(n=2)), ("us", Microsecond(n=1)), ("2us", Microsecond(n=2)), # negative ("-2M", MonthEnd(n=-2)), ("-2ME", MonthEnd(n=-2)), ("-2MS", MonthBegin(n=-2)), ("-2D", Day(n=-2)), ("-2H", Hour(n=-2)), ("-2h", Hour(n=-2)), ("-2T", Minute(n=-2)), ("-2min", Minute(n=-2)), ("-2S", Second(n=-2)), ("-2L", Millisecond(n=-2)), ("-2ms", Millisecond(n=-2)), ("-2U", Microsecond(n=-2)), ("-2us", Microsecond(n=-2)), ], ids=_id_func, ) @pytest.mark.filterwarnings("ignore::FutureWarning") # Deprecation of "M" etc. def test_to_offset_sub_annual(freq, expected): assert to_offset(freq) == expected _ANNUAL_OFFSET_TYPES = { "A": YearEnd, "AS": YearBegin, "Y": YearEnd, "YS": YearBegin, "YE": YearEnd, } @pytest.mark.parametrize( ("month_int", "month_label"), list(_MONTH_ABBREVIATIONS.items()) + [(0, "")] ) @pytest.mark.parametrize("multiple", [None, 2, -1]) @pytest.mark.parametrize("offset_str", ["AS", "A", "YS", "Y"]) @pytest.mark.filterwarnings("ignore::FutureWarning") # Deprecation of "A" etc. def test_to_offset_annual(month_label, month_int, multiple, offset_str): freq = offset_str offset_type = _ANNUAL_OFFSET_TYPES[offset_str] if month_label: freq = "-".join([freq, month_label]) if multiple: freq = f"{multiple}{freq}" result = to_offset(freq) if multiple and month_int: expected = offset_type(n=multiple, month=month_int) elif multiple: expected = offset_type(n=multiple) elif month_int: expected = offset_type(month=month_int) else: expected = offset_type() assert result == expected _QUARTER_OFFSET_TYPES = {"Q": QuarterEnd, "QS": QuarterBegin, "QE": QuarterEnd} @pytest.mark.parametrize( ("month_int", "month_label"), list(_MONTH_ABBREVIATIONS.items()) + [(0, "")] ) @pytest.mark.parametrize("multiple", [None, 2, -1]) @pytest.mark.parametrize("offset_str", ["QS", "Q", "QE"]) @pytest.mark.filterwarnings("ignore::FutureWarning") # Deprecation of "Q" etc. def test_to_offset_quarter(month_label, month_int, multiple, offset_str): freq = offset_str offset_type = _QUARTER_OFFSET_TYPES[offset_str] if month_label: freq = "-".join([freq, month_label]) if multiple: freq = f"{multiple}{freq}" result = to_offset(freq) if multiple and month_int: expected = offset_type(n=multiple, month=month_int) elif multiple: if month_int: expected = offset_type(n=multiple) else: if offset_type == QuarterBegin: expected = offset_type(n=multiple, month=1) elif offset_type == QuarterEnd: expected = offset_type(n=multiple, month=12) elif month_int: expected = offset_type(month=month_int) else: if offset_type == QuarterBegin: expected = offset_type(month=1) elif offset_type == QuarterEnd: expected = offset_type(month=12) assert result == expected @pytest.mark.parametrize("freq", ["Z", "7min2", "AM", "M-", "AS-", "QS-", "1H1min"]) def test_invalid_to_offset_str(freq): with pytest.raises(ValueError): to_offset(freq) @pytest.mark.parametrize( ("argument", "expected_date_args"), [("2000-01-01", (2000, 1, 1)), ((2000, 1, 1), (2000, 1, 1))], ids=_id_func, ) def test_to_cftime_datetime(calendar, argument, expected_date_args): date_type = get_date_type(calendar) expected = date_type(*expected_date_args) if isinstance(argument, tuple): argument = date_type(*argument) result = to_cftime_datetime(argument, calendar=calendar) assert result == expected def test_to_cftime_datetime_error_no_calendar(): with pytest.raises(ValueError): to_cftime_datetime("2000") def test_to_cftime_datetime_error_type_error(): with pytest.raises(TypeError): to_cftime_datetime(1) _EQ_TESTS_A = [ BaseCFTimeOffset(), YearBegin(), YearEnd(), YearBegin(month=2), YearEnd(month=2), QuarterBegin(), QuarterEnd(), QuarterBegin(month=2), QuarterEnd(month=2), MonthBegin(), MonthEnd(), Day(), Hour(), Minute(), Second(), Millisecond(), Microsecond(), ] _EQ_TESTS_B = [ BaseCFTimeOffset(n=2), YearBegin(n=2), YearEnd(n=2), YearBegin(n=2, month=2), YearEnd(n=2, month=2), QuarterBegin(n=2), QuarterEnd(n=2), QuarterBegin(n=2, month=2), QuarterEnd(n=2, month=2), MonthBegin(n=2), MonthEnd(n=2), Day(n=2), Hour(n=2), Minute(n=2), Second(n=2), Millisecond(n=2), Microsecond(n=2), ] @pytest.mark.parametrize(("a", "b"), product(_EQ_TESTS_A, _EQ_TESTS_B), ids=_id_func) def test_neq(a, b): assert a != b _EQ_TESTS_B_COPY = [ BaseCFTimeOffset(n=2), YearBegin(n=2), YearEnd(n=2), YearBegin(n=2, month=2), YearEnd(n=2, month=2), QuarterBegin(n=2), QuarterEnd(n=2), QuarterBegin(n=2, month=2), QuarterEnd(n=2, month=2), MonthBegin(n=2), MonthEnd(n=2), Day(n=2), Hour(n=2), Minute(n=2), Second(n=2), Millisecond(n=2), Microsecond(n=2), ] @pytest.mark.parametrize( ("a", "b"), zip(_EQ_TESTS_B, _EQ_TESTS_B_COPY, strict=True), ids=_id_func ) def test_eq(a, b): assert a == b _MUL_TESTS = [ (BaseCFTimeOffset(), 3, BaseCFTimeOffset(n=3)), (BaseCFTimeOffset(), -3, BaseCFTimeOffset(n=-3)), (YearEnd(), 3, YearEnd(n=3)), (YearBegin(), 3, YearBegin(n=3)), (QuarterEnd(), 3, QuarterEnd(n=3)), (QuarterBegin(), 3, QuarterBegin(n=3)), (MonthEnd(), 3, MonthEnd(n=3)), (MonthBegin(), 3, MonthBegin(n=3)), (Tick(), 3, Tick(n=3)), (Day(), 3, Day(n=3)), (Hour(), 3, Hour(n=3)), (Minute(), 3, Minute(n=3)), (Second(), 3, Second(n=3)), (Millisecond(), 3, Millisecond(n=3)), (Microsecond(), 3, Microsecond(n=3)), (Day(), 0.5, Hour(n=12)), (Hour(), 0.5, Minute(n=30)), (Hour(), -0.5, Minute(n=-30)), (Minute(), 0.5, Second(n=30)), (Second(), 0.5, Millisecond(n=500)), (Millisecond(), 0.5, Microsecond(n=500)), ] @pytest.mark.parametrize(("offset", "multiple", "expected"), _MUL_TESTS, ids=_id_func) def test_mul(offset, multiple, expected): assert offset * multiple == expected @pytest.mark.parametrize(("offset", "multiple", "expected"), _MUL_TESTS, ids=_id_func) def test_rmul(offset, multiple, expected): assert multiple * offset == expected def test_mul_float_multiple_next_higher_resolution(): """Test more than one iteration through _next_higher_resolution is required.""" assert 1e-6 * Second() == Microsecond() assert 1e-6 / 60 * Minute() == Microsecond() @pytest.mark.parametrize( "offset", [YearBegin(), YearEnd(), QuarterBegin(), QuarterEnd(), MonthBegin(), MonthEnd()], ids=_id_func, ) def test_nonTick_offset_multiplied_float_error(offset): """Test that the appropriate error is raised if a non-Tick offset is multiplied by a float.""" with pytest.raises(TypeError, match="unsupported operand type"): offset * 0.5 def test_Microsecond_multiplied_float_error(): """Test that the appropriate error is raised if a Tick offset is multiplied by a float which causes it not to be representable by a microsecond-precision timedelta.""" with pytest.raises( ValueError, match="Could not convert to integer offset at any resolution" ): Microsecond() * 0.5 @pytest.mark.parametrize( ("offset", "expected"), [ (BaseCFTimeOffset(), BaseCFTimeOffset(n=-1)), (YearEnd(), YearEnd(n=-1)), (YearBegin(), YearBegin(n=-1)), (QuarterEnd(), QuarterEnd(n=-1)), (QuarterBegin(), QuarterBegin(n=-1)), (MonthEnd(), MonthEnd(n=-1)), (MonthBegin(), MonthBegin(n=-1)), (Day(), Day(n=-1)), (Hour(), Hour(n=-1)), (Minute(), Minute(n=-1)), (Second(), Second(n=-1)), (Millisecond(), Millisecond(n=-1)), (Microsecond(), Microsecond(n=-1)), ], ids=_id_func, ) def test_neg(offset: BaseCFTimeOffset, expected: BaseCFTimeOffset) -> None: assert -offset == expected _ADD_TESTS = [ (Day(n=2), (1, 1, 3)), (Hour(n=2), (1, 1, 1, 2)), (Minute(n=2), (1, 1, 1, 0, 2)), (Second(n=2), (1, 1, 1, 0, 0, 2)), (Millisecond(n=2), (1, 1, 1, 0, 0, 0, 2000)), (Microsecond(n=2), (1, 1, 1, 0, 0, 0, 2)), ] @pytest.mark.parametrize(("offset", "expected_date_args"), _ADD_TESTS, ids=_id_func) def test_add_sub_monthly(offset, expected_date_args, calendar): date_type = get_date_type(calendar) initial = date_type(1, 1, 1) expected = date_type(*expected_date_args) result = offset + initial assert result == expected @pytest.mark.parametrize(("offset", "expected_date_args"), _ADD_TESTS, ids=_id_func) def test_radd_sub_monthly(offset, expected_date_args, calendar): date_type = get_date_type(calendar) initial = date_type(1, 1, 1) expected = date_type(*expected_date_args) result = initial + offset assert result == expected @pytest.mark.parametrize( ("offset", "expected_date_args"), [ (Day(n=2), (1, 1, 1)), (Hour(n=2), (1, 1, 2, 22)), (Minute(n=2), (1, 1, 2, 23, 58)), (Second(n=2), (1, 1, 2, 23, 59, 58)), (Millisecond(n=2), (1, 1, 2, 23, 59, 59, 998000)), (Microsecond(n=2), (1, 1, 2, 23, 59, 59, 999998)), ], ids=_id_func, ) def test_rsub_sub_monthly(offset, expected_date_args, calendar): date_type = get_date_type(calendar) initial = date_type(1, 1, 3) expected = date_type(*expected_date_args) result = initial - offset assert result == expected @pytest.mark.parametrize("offset", _EQ_TESTS_A, ids=_id_func) def test_sub_error(offset, calendar): date_type = get_date_type(calendar) initial = date_type(1, 1, 1) with pytest.raises(TypeError): offset - initial @pytest.mark.parametrize( ("a", "b"), zip(_EQ_TESTS_A, _EQ_TESTS_B, strict=True), ids=_id_func ) def test_minus_offset(a, b): result = b - a expected = a assert result == expected @pytest.mark.parametrize( ("a", "b"), list(zip(np.roll(_EQ_TESTS_A, 1), _EQ_TESTS_B, strict=True)) # type: ignore[arg-type] + [(YearEnd(month=1), YearEnd(month=2))], ids=_id_func, ) def test_minus_offset_error(a, b): with pytest.raises(TypeError): b - a @pytest.mark.parametrize( ("initial_date_args", "offset", "expected_date_args"), [ ((1, 1, 1), MonthBegin(), (1, 2, 1)), ((1, 1, 1), MonthBegin(n=2), (1, 3, 1)), ((1, 1, 7), MonthBegin(), (1, 2, 1)), ((1, 1, 7), MonthBegin(n=2), (1, 3, 1)), ((1, 3, 1), MonthBegin(n=-1), (1, 2, 1)), ((1, 3, 1), MonthBegin(n=-2), (1, 1, 1)), ((1, 3, 3), MonthBegin(n=-1), (1, 3, 1)), ((1, 3, 3), MonthBegin(n=-2), (1, 2, 1)), ((1, 2, 1), MonthBegin(n=14), (2, 4, 1)), ((2, 4, 1), MonthBegin(n=-14), (1, 2, 1)), ((1, 1, 1, 5, 5, 5, 5), MonthBegin(), (1, 2, 1, 5, 5, 5, 5)), ((1, 1, 3, 5, 5, 5, 5), MonthBegin(), (1, 2, 1, 5, 5, 5, 5)), ((1, 1, 3, 5, 5, 5, 5), MonthBegin(n=-1), (1, 1, 1, 5, 5, 5, 5)), ], ids=_id_func, ) def test_add_month_begin(calendar, initial_date_args, offset, expected_date_args): date_type = get_date_type(calendar) initial = date_type(*initial_date_args) result = initial + offset expected = date_type(*expected_date_args) assert result == expected @pytest.mark.parametrize( ("initial_date_args", "offset", "expected_year_month", "expected_sub_day"), [ ((1, 1, 1), MonthEnd(), (1, 1), ()), ((1, 1, 1), MonthEnd(n=2), (1, 2), ()), ((1, 3, 1), MonthEnd(n=-1), (1, 2), ()), ((1, 3, 1), MonthEnd(n=-2), (1, 1), ()), ((1, 2, 1), MonthEnd(n=14), (2, 3), ()), ((2, 4, 1), MonthEnd(n=-14), (1, 2), ()), ((1, 1, 1, 5, 5, 5, 5), MonthEnd(), (1, 1), (5, 5, 5, 5)), ((1, 2, 1, 5, 5, 5, 5), MonthEnd(n=-1), (1, 1), (5, 5, 5, 5)), ], ids=_id_func, ) def test_add_month_end( calendar, initial_date_args, offset, expected_year_month, expected_sub_day ): date_type = get_date_type(calendar) initial = date_type(*initial_date_args) result = initial + offset reference_args = expected_year_month + (1,) reference = date_type(*reference_args) # Here the days at the end of each month varies based on the calendar used expected_date_args = ( expected_year_month + (reference.daysinmonth,) + expected_sub_day ) expected = date_type(*expected_date_args) assert result == expected @pytest.mark.parametrize( ( "initial_year_month", "initial_sub_day", "offset", "expected_year_month", "expected_sub_day", ), [ ((1, 1), (), MonthEnd(), (1, 2), ()), ((1, 1), (), MonthEnd(n=2), (1, 3), ()), ((1, 3), (), MonthEnd(n=-1), (1, 2), ()), ((1, 3), (), MonthEnd(n=-2), (1, 1), ()), ((1, 2), (), MonthEnd(n=14), (2, 4), ()), ((2, 4), (), MonthEnd(n=-14), (1, 2), ()), ((1, 1), (5, 5, 5, 5), MonthEnd(), (1, 2), (5, 5, 5, 5)), ((1, 2), (5, 5, 5, 5), MonthEnd(n=-1), (1, 1), (5, 5, 5, 5)), ], ids=_id_func, ) def test_add_month_end_onOffset( calendar, initial_year_month, initial_sub_day, offset, expected_year_month, expected_sub_day, ): date_type = get_date_type(calendar) reference_args = initial_year_month + (1,) reference = date_type(*reference_args) initial_date_args = initial_year_month + (reference.daysinmonth,) + initial_sub_day initial = date_type(*initial_date_args) result = initial + offset reference_args = expected_year_month + (1,) reference = date_type(*reference_args) # Here the days at the end of each month varies based on the calendar used expected_date_args = ( expected_year_month + (reference.daysinmonth,) + expected_sub_day ) expected = date_type(*expected_date_args) assert result == expected @pytest.mark.parametrize( ("initial_date_args", "offset", "expected_date_args"), [ ((1, 1, 1), YearBegin(), (2, 1, 1)), ((1, 1, 1), YearBegin(n=2), (3, 1, 1)), ((1, 1, 1), YearBegin(month=2), (1, 2, 1)), ((1, 1, 7), YearBegin(n=2), (3, 1, 1)), ((2, 2, 1), YearBegin(n=-1), (2, 1, 1)), ((1, 1, 2), YearBegin(n=-1), (1, 1, 1)), ((1, 1, 1, 5, 5, 5, 5), YearBegin(), (2, 1, 1, 5, 5, 5, 5)), ((2, 1, 1, 5, 5, 5, 5), YearBegin(n=-1), (1, 1, 1, 5, 5, 5, 5)), ], ids=_id_func, ) def test_add_year_begin(calendar, initial_date_args, offset, expected_date_args): date_type = get_date_type(calendar) initial = date_type(*initial_date_args) result = initial + offset expected = date_type(*expected_date_args) assert result == expected @pytest.mark.parametrize( ("initial_date_args", "offset", "expected_year_month", "expected_sub_day"), [ ((1, 1, 1), YearEnd(), (1, 12), ()), ((1, 1, 1), YearEnd(n=2), (2, 12), ()), ((1, 1, 1), YearEnd(month=1), (1, 1), ()), ((2, 3, 1), YearEnd(n=-1), (1, 12), ()), ((1, 3, 1), YearEnd(n=-1, month=2), (1, 2), ()), ((1, 1, 1, 5, 5, 5, 5), YearEnd(), (1, 12), (5, 5, 5, 5)), ((1, 1, 1, 5, 5, 5, 5), YearEnd(n=2), (2, 12), (5, 5, 5, 5)), ], ids=_id_func, ) def test_add_year_end( calendar, initial_date_args, offset, expected_year_month, expected_sub_day ): date_type = get_date_type(calendar) initial = date_type(*initial_date_args) result = initial + offset reference_args = expected_year_month + (1,) reference = date_type(*reference_args) # Here the days at the end of each month varies based on the calendar used expected_date_args = ( expected_year_month + (reference.daysinmonth,) + expected_sub_day ) expected = date_type(*expected_date_args) assert result == expected @pytest.mark.parametrize( ( "initial_year_month", "initial_sub_day", "offset", "expected_year_month", "expected_sub_day", ), [ ((1, 12), (), YearEnd(), (2, 12), ()), ((1, 12), (), YearEnd(n=2), (3, 12), ()), ((2, 12), (), YearEnd(n=-1), (1, 12), ()), ((3, 12), (), YearEnd(n=-2), (1, 12), ()), ((1, 1), (), YearEnd(month=2), (1, 2), ()), ((1, 12), (5, 5, 5, 5), YearEnd(), (2, 12), (5, 5, 5, 5)), ((2, 12), (5, 5, 5, 5), YearEnd(n=-1), (1, 12), (5, 5, 5, 5)), ], ids=_id_func, ) def test_add_year_end_onOffset( calendar, initial_year_month, initial_sub_day, offset, expected_year_month, expected_sub_day, ): date_type = get_date_type(calendar) reference_args = initial_year_month + (1,) reference = date_type(*reference_args) initial_date_args = initial_year_month + (reference.daysinmonth,) + initial_sub_day initial = date_type(*initial_date_args) result = initial + offset reference_args = expected_year_month + (1,) reference = date_type(*reference_args) # Here the days at the end of each month varies based on the calendar used expected_date_args = ( expected_year_month + (reference.daysinmonth,) + expected_sub_day ) expected = date_type(*expected_date_args) assert result == expected @pytest.mark.parametrize( ("initial_date_args", "offset", "expected_date_args"), [ ((1, 1, 1), QuarterBegin(), (1, 3, 1)), ((1, 1, 1), QuarterBegin(n=2), (1, 6, 1)), ((1, 1, 1), QuarterBegin(month=2), (1, 2, 1)), ((1, 1, 7), QuarterBegin(n=2), (1, 6, 1)), ((2, 2, 1), QuarterBegin(n=-1), (1, 12, 1)), ((1, 3, 2), QuarterBegin(n=-1), (1, 3, 1)), ((1, 1, 1, 5, 5, 5, 5), QuarterBegin(), (1, 3, 1, 5, 5, 5, 5)), ((2, 1, 1, 5, 5, 5, 5), QuarterBegin(n=-1), (1, 12, 1, 5, 5, 5, 5)), ], ids=_id_func, ) def test_add_quarter_begin(calendar, initial_date_args, offset, expected_date_args): date_type = get_date_type(calendar) initial = date_type(*initial_date_args) result = initial + offset expected = date_type(*expected_date_args) assert result == expected @pytest.mark.parametrize( ("initial_date_args", "offset", "expected_year_month", "expected_sub_day"), [ ((1, 1, 1), QuarterEnd(), (1, 3), ()), ((1, 1, 1), QuarterEnd(n=2), (1, 6), ()), ((1, 1, 1), QuarterEnd(month=1), (1, 1), ()), ((2, 3, 1), QuarterEnd(n=-1), (1, 12), ()), ((1, 3, 1), QuarterEnd(n=-1, month=2), (1, 2), ()), ((1, 1, 1, 5, 5, 5, 5), QuarterEnd(), (1, 3), (5, 5, 5, 5)), ((1, 1, 1, 5, 5, 5, 5), QuarterEnd(n=2), (1, 6), (5, 5, 5, 5)), ], ids=_id_func, ) def test_add_quarter_end( calendar, initial_date_args, offset, expected_year_month, expected_sub_day ): date_type = get_date_type(calendar) initial = date_type(*initial_date_args) result = initial + offset reference_args = expected_year_month + (1,) reference = date_type(*reference_args) # Here the days at the end of each month varies based on the calendar used expected_date_args = ( expected_year_month + (reference.daysinmonth,) + expected_sub_day ) expected = date_type(*expected_date_args) assert result == expected @pytest.mark.parametrize( ( "initial_year_month", "initial_sub_day", "offset", "expected_year_month", "expected_sub_day", ), [ ((1, 12), (), QuarterEnd(), (2, 3), ()), ((1, 12), (), QuarterEnd(n=2), (2, 6), ()), ((1, 12), (), QuarterEnd(n=-1), (1, 9), ()), ((1, 12), (), QuarterEnd(n=-2), (1, 6), ()), ((1, 1), (), QuarterEnd(month=2), (1, 2), ()), ((1, 12), (5, 5, 5, 5), QuarterEnd(), (2, 3), (5, 5, 5, 5)), ((1, 12), (5, 5, 5, 5), QuarterEnd(n=-1), (1, 9), (5, 5, 5, 5)), ], ids=_id_func, ) def test_add_quarter_end_onOffset( calendar, initial_year_month, initial_sub_day, offset, expected_year_month, expected_sub_day, ): date_type = get_date_type(calendar) reference_args = initial_year_month + (1,) reference = date_type(*reference_args) initial_date_args = initial_year_month + (reference.daysinmonth,) + initial_sub_day initial = date_type(*initial_date_args) result = initial + offset reference_args = expected_year_month + (1,) reference = date_type(*reference_args) # Here the days at the end of each month varies based on the calendar used expected_date_args = ( expected_year_month + (reference.daysinmonth,) + expected_sub_day ) expected = date_type(*expected_date_args) assert result == expected # Note for all sub-monthly offsets, pandas always returns True for onOffset @pytest.mark.parametrize( ("date_args", "offset", "expected"), [ ((1, 1, 1), MonthBegin(), True), ((1, 1, 1, 1), MonthBegin(), True), ((1, 1, 5), MonthBegin(), False), ((1, 1, 5), MonthEnd(), False), ((1, 3, 1), QuarterBegin(), True), ((1, 3, 1, 1), QuarterBegin(), True), ((1, 3, 5), QuarterBegin(), False), ((1, 12, 1), QuarterEnd(), False), ((1, 1, 1), YearBegin(), True), ((1, 1, 1, 1), YearBegin(), True), ((1, 1, 5), YearBegin(), False), ((1, 12, 1), YearEnd(), False), ((1, 1, 1), Day(), True), ((1, 1, 1, 1), Day(), True), ((1, 1, 1), Hour(), True), ((1, 1, 1), Minute(), True), ((1, 1, 1), Second(), True), ((1, 1, 1), Millisecond(), True), ((1, 1, 1), Microsecond(), True), ], ids=_id_func, ) def test_onOffset(calendar, date_args, offset, expected): date_type = get_date_type(calendar) date = date_type(*date_args) result = offset.onOffset(date) assert result == expected @pytest.mark.parametrize( ("year_month_args", "sub_day_args", "offset"), [ ((1, 1), (), MonthEnd()), ((1, 1), (1,), MonthEnd()), ((1, 12), (), QuarterEnd()), ((1, 1), (), QuarterEnd(month=1)), ((1, 12), (), YearEnd()), ((1, 1), (), YearEnd(month=1)), ], ids=_id_func, ) def test_onOffset_month_or_quarter_or_year_end( calendar, year_month_args, sub_day_args, offset ): date_type = get_date_type(calendar) reference_args = year_month_args + (1,) reference = date_type(*reference_args) date_args = year_month_args + (reference.daysinmonth,) + sub_day_args date = date_type(*date_args) result = offset.onOffset(date) assert result @pytest.mark.parametrize( ("offset", "initial_date_args", "partial_expected_date_args"), [ (YearBegin(), (1, 3, 1), (2, 1)), (YearBegin(), (1, 1, 1), (1, 1)), (YearBegin(n=2), (1, 3, 1), (2, 1)), (YearBegin(n=2, month=2), (1, 3, 1), (2, 2)), (YearEnd(), (1, 3, 1), (1, 12)), (YearEnd(n=2), (1, 3, 1), (1, 12)), (YearEnd(n=2, month=2), (1, 3, 1), (2, 2)), (YearEnd(n=2, month=4), (1, 4, 30), (1, 4)), (QuarterBegin(), (1, 3, 2), (1, 6)), (QuarterBegin(), (1, 4, 1), (1, 6)), (QuarterBegin(n=2), (1, 4, 1), (1, 6)), (QuarterBegin(n=2, month=2), (1, 4, 1), (1, 5)), (QuarterEnd(), (1, 3, 1), (1, 3)), (QuarterEnd(n=2), (1, 3, 1), (1, 3)), (QuarterEnd(n=2, month=2), (1, 3, 1), (1, 5)), (QuarterEnd(n=2, month=4), (1, 4, 30), (1, 4)), (MonthBegin(), (1, 3, 2), (1, 4)), (MonthBegin(), (1, 3, 1), (1, 3)), (MonthBegin(n=2), (1, 3, 2), (1, 4)), (MonthEnd(), (1, 3, 2), (1, 3)), (MonthEnd(), (1, 4, 30), (1, 4)), (MonthEnd(n=2), (1, 3, 2), (1, 3)), (Day(), (1, 3, 2, 1), (1, 3, 2, 1)), (Hour(), (1, 3, 2, 1, 1), (1, 3, 2, 1, 1)), (Minute(), (1, 3, 2, 1, 1, 1), (1, 3, 2, 1, 1, 1)), (Second(), (1, 3, 2, 1, 1, 1, 1), (1, 3, 2, 1, 1, 1, 1)), (Millisecond(), (1, 3, 2, 1, 1, 1, 1000), (1, 3, 2, 1, 1, 1, 1000)), (Microsecond(), (1, 3, 2, 1, 1, 1, 1), (1, 3, 2, 1, 1, 1, 1)), ], ids=_id_func, ) def test_rollforward(calendar, offset, initial_date_args, partial_expected_date_args): date_type = get_date_type(calendar) initial = date_type(*initial_date_args) if isinstance(offset, MonthBegin | QuarterBegin | YearBegin): expected_date_args = partial_expected_date_args + (1,) elif isinstance(offset, MonthEnd | QuarterEnd | YearEnd): reference_args = partial_expected_date_args + (1,) reference = date_type(*reference_args) expected_date_args = partial_expected_date_args + (reference.daysinmonth,) else: expected_date_args = partial_expected_date_args expected = date_type(*expected_date_args) result = offset.rollforward(initial) assert result == expected @pytest.mark.parametrize( ("offset", "initial_date_args", "partial_expected_date_args"), [ (YearBegin(), (1, 3, 1), (1, 1)), (YearBegin(n=2), (1, 3, 1), (1, 1)), (YearBegin(n=2, month=2), (1, 3, 1), (1, 2)), (YearBegin(), (1, 1, 1), (1, 1)), (YearBegin(n=2, month=2), (1, 2, 1), (1, 2)), (YearEnd(), (2, 3, 1), (1, 12)), (YearEnd(n=2), (2, 3, 1), (1, 12)), (YearEnd(n=2, month=2), (2, 3, 1), (2, 2)), (YearEnd(month=4), (1, 4, 30), (1, 4)), (QuarterBegin(), (1, 3, 2), (1, 3)), (QuarterBegin(), (1, 4, 1), (1, 3)), (QuarterBegin(n=2), (1, 4, 1), (1, 3)), (QuarterBegin(n=2, month=2), (1, 4, 1), (1, 2)), (QuarterEnd(), (2, 3, 1), (1, 12)), (QuarterEnd(n=2), (2, 3, 1), (1, 12)), (QuarterEnd(n=2, month=2), (2, 3, 1), (2, 2)), (QuarterEnd(n=2, month=4), (1, 4, 30), (1, 4)), (MonthBegin(), (1, 3, 2), (1, 3)), (MonthBegin(n=2), (1, 3, 2), (1, 3)), (MonthBegin(), (1, 3, 1), (1, 3)), (MonthEnd(), (1, 3, 2), (1, 2)), (MonthEnd(n=2), (1, 3, 2), (1, 2)), (MonthEnd(), (1, 4, 30), (1, 4)), (Day(), (1, 3, 2, 1), (1, 3, 2, 1)), (Hour(), (1, 3, 2, 1, 1), (1, 3, 2, 1, 1)), (Minute(), (1, 3, 2, 1, 1, 1), (1, 3, 2, 1, 1, 1)), (Second(), (1, 3, 2, 1, 1, 1, 1), (1, 3, 2, 1, 1, 1, 1)), (Millisecond(), (1, 3, 2, 1, 1, 1, 1000), (1, 3, 2, 1, 1, 1, 1000)), (Microsecond(), (1, 3, 2, 1, 1, 1, 1), (1, 3, 2, 1, 1, 1, 1)), ], ids=_id_func, ) def test_rollback(calendar, offset, initial_date_args, partial_expected_date_args): date_type = get_date_type(calendar) initial = date_type(*initial_date_args) if isinstance(offset, MonthBegin | QuarterBegin | YearBegin): expected_date_args = partial_expected_date_args + (1,) elif isinstance(offset, MonthEnd | QuarterEnd | YearEnd): reference_args = partial_expected_date_args + (1,) reference = date_type(*reference_args) expected_date_args = partial_expected_date_args + (reference.daysinmonth,) else: expected_date_args = partial_expected_date_args expected = date_type(*expected_date_args) result = offset.rollback(initial) assert result == expected _CFTIME_RANGE_TESTS = [ ( "0001-01-01", "0001-01-04", None, "D", "neither", False, [(1, 1, 2), (1, 1, 3)], ), ( "0001-01-01", "0001-01-04", None, "D", "both", False, [(1, 1, 1), (1, 1, 2), (1, 1, 3), (1, 1, 4)], ), ( "0001-01-01", "0001-01-04", None, "D", "left", False, [(1, 1, 1), (1, 1, 2), (1, 1, 3)], ), ( "0001-01-01", "0001-01-04", None, "D", "right", False, [(1, 1, 2), (1, 1, 3), (1, 1, 4)], ), ( "0001-01-01T01:00:00", "0001-01-04", None, "D", "both", False, [(1, 1, 1, 1), (1, 1, 2, 1), (1, 1, 3, 1)], ), ( "0001-01-01 01:00:00", "0001-01-04", None, "D", "both", False, [(1, 1, 1, 1), (1, 1, 2, 1), (1, 1, 3, 1)], ), ( "0001-01-01T01:00:00", "0001-01-04", None, "D", "both", True, [(1, 1, 1), (1, 1, 2), (1, 1, 3), (1, 1, 4)], ), ( "0001-01-01", None, 4, "D", "both", False, [(1, 1, 1), (1, 1, 2), (1, 1, 3), (1, 1, 4)], ), ( None, "0001-01-04", 4, "D", "both", False, [(1, 1, 1), (1, 1, 2), (1, 1, 3), (1, 1, 4)], ), ( (1, 1, 1), "0001-01-04", None, "D", "both", False, [(1, 1, 1), (1, 1, 2), (1, 1, 3), (1, 1, 4)], ), ( (1, 1, 1), (1, 1, 4), None, "D", "both", False, [(1, 1, 1), (1, 1, 2), (1, 1, 3), (1, 1, 4)], ), ( "0001-01-30", "0011-02-01", None, "3YS-JUN", "both", False, [(1, 6, 1), (4, 6, 1), (7, 6, 1), (10, 6, 1)], ), ("0001-01-04", "0001-01-01", None, "D", "both", False, []), ( "0010", None, 4, YearBegin(n=-2), "both", False, [(10, 1, 1), (8, 1, 1), (6, 1, 1), (4, 1, 1)], ), ( "0010", None, 4, "-2YS", "both", False, [(10, 1, 1), (8, 1, 1), (6, 1, 1), (4, 1, 1)], ), ( "0001-01-01", "0001-01-04", 4, None, "both", False, [(1, 1, 1), (1, 1, 2), (1, 1, 3), (1, 1, 4)], ), ( "0001-06-01", None, 4, "3QS-JUN", "both", False, [(1, 6, 1), (2, 3, 1), (2, 12, 1), (3, 9, 1)], ), ( "0001-06-01", None, 4, "-1MS", "both", False, [(1, 6, 1), (1, 5, 1), (1, 4, 1), (1, 3, 1)], ), ( "0001-01-30", None, 4, "-1D", "both", False, [(1, 1, 30), (1, 1, 29), (1, 1, 28), (1, 1, 27)], ), ] @pytest.mark.parametrize( ("start", "end", "periods", "freq", "inclusive", "normalize", "expected_date_args"), _CFTIME_RANGE_TESTS, ids=_id_func, ) def test_cftime_range( start, end, periods, freq, inclusive, normalize, calendar, expected_date_args ): date_type = get_date_type(calendar) expected_dates = [date_type(*args) for args in expected_date_args] if isinstance(start, tuple): start = date_type(*start) if isinstance(end, tuple): end = date_type(*end) result = cftime_range( start=start, end=end, periods=periods, freq=freq, inclusive=inclusive, normalize=normalize, calendar=calendar, ) resulting_dates = result.values assert isinstance(result, CFTimeIndex) if freq is not None: np.testing.assert_equal(resulting_dates, expected_dates) else: # If we create a linear range of dates using cftime.num2date # we will not get exact round number dates. This is because # datetime arithmetic in cftime is accurate approximately to # 1 millisecond (see https://unidata.github.io/cftime/api.html). deltas = resulting_dates - expected_dates deltas = np.array([delta.total_seconds() for delta in deltas]) assert np.max(np.abs(deltas)) < 0.001 def test_cftime_range_name(): result = cftime_range(start="2000", periods=4, name="foo") assert result.name == "foo" result = cftime_range(start="2000", periods=4) assert result.name is None @pytest.mark.parametrize( ("start", "end", "periods", "freq", "inclusive"), [ (None, None, 5, "YE", None), ("2000", None, None, "YE", None), (None, "2000", None, "YE", None), (None, None, None, None, None), ("2000", "2001", None, "YE", "up"), ("2000", "2001", 5, "YE", None), ], ) def test_invalid_cftime_range_inputs( start: str | None, end: str | None, periods: int | None, freq: str | None, inclusive: Literal["up", None], ) -> None: with pytest.raises(ValueError): cftime_range(start, end, periods, freq, inclusive=inclusive) # type: ignore[arg-type] _CALENDAR_SPECIFIC_MONTH_END_TESTS = [ ("noleap", [(2, 28), (4, 30), (6, 30), (8, 31), (10, 31), (12, 31)]), ("all_leap", [(2, 29), (4, 30), (6, 30), (8, 31), (10, 31), (12, 31)]), ("360_day", [(2, 30), (4, 30), (6, 30), (8, 30), (10, 30), (12, 30)]), ("standard", [(2, 29), (4, 30), (6, 30), (8, 31), (10, 31), (12, 31)]), ("gregorian", [(2, 29), (4, 30), (6, 30), (8, 31), (10, 31), (12, 31)]), ("julian", [(2, 29), (4, 30), (6, 30), (8, 31), (10, 31), (12, 31)]), ] @pytest.mark.parametrize( ("calendar", "expected_month_day"), _CALENDAR_SPECIFIC_MONTH_END_TESTS, ids=_id_func, ) def test_calendar_specific_month_end( calendar: str, expected_month_day: list[tuple[int, int]] ) -> None: year = 2000 # Use a leap-year to highlight calendar differences result = cftime_range( start="2000-02", end="2001", freq="2ME", calendar=calendar ).values date_type = get_date_type(calendar) expected = [date_type(year, *args) for args in expected_month_day] np.testing.assert_equal(result, expected) @pytest.mark.parametrize( ("calendar", "expected_month_day"), _CALENDAR_SPECIFIC_MONTH_END_TESTS, ids=_id_func, ) def test_calendar_specific_month_end_negative_freq( calendar: str, expected_month_day: list[tuple[int, int]] ) -> None: year = 2000 # Use a leap-year to highlight calendar differences result = cftime_range( start="2001", end="2000", freq="-2ME", calendar=calendar, ).values date_type = get_date_type(calendar) expected = [date_type(year, *args) for args in expected_month_day[::-1]] np.testing.assert_equal(result, expected) @pytest.mark.parametrize( ("calendar", "start", "end", "expected_number_of_days"), [ ("noleap", "2000", "2001", 365), ("all_leap", "2000", "2001", 366), ("360_day", "2000", "2001", 360), ("standard", "2000", "2001", 366), ("gregorian", "2000", "2001", 366), ("julian", "2000", "2001", 366), ("noleap", "2001", "2002", 365), ("all_leap", "2001", "2002", 366), ("360_day", "2001", "2002", 360), ("standard", "2001", "2002", 365), ("gregorian", "2001", "2002", 365), ("julian", "2001", "2002", 365), ], ) def test_calendar_year_length( calendar: str, start: str, end: str, expected_number_of_days: int ) -> None: result = cftime_range(start, end, freq="D", inclusive="left", calendar=calendar) assert len(result) == expected_number_of_days @pytest.mark.parametrize("freq", ["YE", "ME", "D"]) def test_dayofweek_after_cftime_range(freq: str) -> None: result = cftime_range("2000-02-01", periods=3, freq=freq).dayofweek # TODO: remove once requiring pandas 2.2+ freq = _new_to_legacy_freq(freq) expected = pd.date_range("2000-02-01", periods=3, freq=freq).dayofweek np.testing.assert_array_equal(result, expected) @pytest.mark.parametrize("freq", ["YE", "ME", "D"]) def test_dayofyear_after_cftime_range(freq: str) -> None: result = cftime_range("2000-02-01", periods=3, freq=freq).dayofyear # TODO: remove once requiring pandas 2.2+ freq = _new_to_legacy_freq(freq) expected = pd.date_range("2000-02-01", periods=3, freq=freq).dayofyear np.testing.assert_array_equal(result, expected) def test_cftime_range_standard_calendar_refers_to_gregorian() -> None: from cftime import DatetimeGregorian (result,) = cftime_range("2000", periods=1) assert isinstance(result, DatetimeGregorian) @pytest.mark.parametrize( "start,calendar,use_cftime,expected_type", [ ("1990-01-01", "standard", None, pd.DatetimeIndex), ("1990-01-01", "proleptic_gregorian", True, CFTimeIndex), ("1990-01-01", "noleap", None, CFTimeIndex), ("1990-01-01", "gregorian", False, pd.DatetimeIndex), ("1400-01-01", "standard", None, CFTimeIndex), ("3400-01-01", "standard", None, CFTimeIndex), ], ) def test_date_range( start: str, calendar: str, use_cftime: bool | None, expected_type ) -> None: dr = date_range( start, periods=14, freq="D", calendar=calendar, use_cftime=use_cftime ) assert isinstance(dr, expected_type) def test_date_range_errors() -> None: with pytest.raises(ValueError, match="Date range is invalid"): date_range( "1400-01-01", periods=1, freq="D", calendar="standard", use_cftime=False ) with pytest.raises(ValueError, match="Date range is invalid"): date_range( "2480-01-01", periods=1, freq="D", calendar="proleptic_gregorian", use_cftime=False, ) with pytest.raises(ValueError, match="Invalid calendar "): date_range( "1900-01-01", periods=1, freq="D", calendar="noleap", use_cftime=False ) @requires_cftime @pytest.mark.parametrize( "start,freq,cal_src,cal_tgt,use_cftime,exp0,exp_pd", [ ("2020-02-01", "4ME", "standard", "noleap", None, "2020-02-28", False), ("2020-02-01", "ME", "noleap", "gregorian", True, "2020-02-29", True), ("2020-02-01", "QE-DEC", "noleap", "gregorian", True, "2020-03-31", True), ("2020-02-01", "YS-FEB", "noleap", "gregorian", True, "2020-02-01", True), ("2020-02-01", "YE-FEB", "noleap", "gregorian", True, "2020-02-29", True), ("2020-02-01", "-1YE-FEB", "noleap", "gregorian", True, "2019-02-28", True), ("2020-02-28", "3h", "all_leap", "gregorian", False, "2020-02-28", True), ("2020-03-30", "ME", "360_day", "gregorian", False, "2020-03-31", True), ("2020-03-31", "ME", "gregorian", "360_day", None, "2020-03-30", False), ("2020-03-31", "-1ME", "gregorian", "360_day", None, "2020-03-30", False), ], ) def test_date_range_like(start, freq, cal_src, cal_tgt, use_cftime, exp0, exp_pd): expected_freq = freq source = date_range(start, periods=12, freq=freq, calendar=cal_src) out = date_range_like(source, cal_tgt, use_cftime=use_cftime) assert len(out) == 12 assert infer_freq(out) == expected_freq assert out[0].isoformat().startswith(exp0) if exp_pd: assert isinstance(out, pd.DatetimeIndex) else: assert isinstance(out, CFTimeIndex) assert out.calendar == cal_tgt @requires_cftime @pytest.mark.parametrize( "freq", ("YE", "YS", "YE-MAY", "MS", "ME", "QS", "h", "min", "s") ) @pytest.mark.parametrize("use_cftime", (True, False)) def test_date_range_like_no_deprecation(freq, use_cftime): # ensure no internal warnings # TODO: remove once freq string deprecation is finished source = date_range("2000", periods=3, freq=freq, use_cftime=False) with assert_no_warnings(): date_range_like(source, "standard", use_cftime=use_cftime) def test_date_range_like_same_calendar(): src = date_range("2000-01-01", periods=12, freq="6h", use_cftime=False) out = date_range_like(src, "standard", use_cftime=False) assert src is out @pytest.mark.filterwarnings("ignore:Converting non-default") def test_date_range_like_errors(): src = date_range("1899-02-03", periods=20, freq="D", use_cftime=False) src = src[np.arange(20) != 10] # Remove 1 day so the frequency is not inferable. with pytest.raises( ValueError, match="`date_range_like` was unable to generate a range as the source frequency was not inferable.", ): date_range_like(src, "gregorian") src = DataArray( np.array( [["1999-01-01", "1999-01-02"], ["1999-01-03", "1999-01-04"]], dtype=np.datetime64, ), dims=("x", "y"), ) with pytest.raises( ValueError, match="'source' must be a 1D array of datetime objects for inferring its range.", ): date_range_like(src, "noleap") da = DataArray([1, 2, 3, 4], dims=("time",)) with pytest.raises( ValueError, match="'source' must be a 1D array of datetime objects for inferring its range.", ): date_range_like(da, "noleap") def as_timedelta_not_implemented_error(): tick = Tick() with pytest.raises(NotImplementedError): tick.as_timedelta() @pytest.mark.parametrize("function", [cftime_range, date_range]) def test_cftime_or_date_range_invalid_inclusive_value(function: Callable) -> None: if function == cftime_range and not has_cftime: pytest.skip("requires cftime") with pytest.raises(ValueError, match="nclusive"): function("2000", periods=3, inclusive="foo") @pytest.mark.parametrize("function", [cftime_range, date_range]) def test_cftime_or_date_range_inclusive_None(function) -> None: if function == cftime_range and not has_cftime: pytest.skip("requires cftime") result_None = function("2000-01-01", "2000-01-04") result_both = function("2000-01-01", "2000-01-04", inclusive="both") np.testing.assert_equal(result_None.values, result_both.values) @pytest.mark.parametrize( "freq", ["A", "AS", "Q", "M", "H", "T", "S", "L", "U", "Y", "A-MAY"] ) def test_to_offset_deprecation_warning(freq): # Test for deprecations outlined in GitHub issue #8394 with pytest.warns(FutureWarning, match="is deprecated"): to_offset(freq) @pytest.mark.skipif(has_pandas_ge_2_2, reason="only relevant for pandas lt 2.2") @pytest.mark.parametrize( "freq, expected", ( ["Y", "YE"], ["A", "YE"], ["Q", "QE"], ["M", "ME"], ["AS", "YS"], ["YE", "YE"], ["QE", "QE"], ["ME", "ME"], ["YS", "YS"], ), ) @pytest.mark.parametrize("n", ("", "2")) def test_legacy_to_new_freq(freq, expected, n): freq = f"{n}{freq}" result = _legacy_to_new_freq(freq) expected = f"{n}{expected}" assert result == expected @pytest.mark.skipif(has_pandas_ge_2_2, reason="only relevant for pandas lt 2.2") @pytest.mark.parametrize("year_alias", ("YE", "Y", "A")) @pytest.mark.parametrize("n", ("", "2")) def test_legacy_to_new_freq_anchored(year_alias, n): for month in _MONTH_ABBREVIATIONS.values(): freq = f"{n}{year_alias}-{month}" result = _legacy_to_new_freq(freq) expected = f"{n}YE-{month}" assert result == expected @pytest.mark.skipif(has_pandas_ge_2_2, reason="only relevant for pandas lt 2.2") @pytest.mark.filterwarnings("ignore:'[AY]' is deprecated") @pytest.mark.parametrize( "freq, expected", (["A", "A"], ["YE", "A"], ["Y", "A"], ["QE", "Q"], ["ME", "M"], ["YS", "AS"]), ) @pytest.mark.parametrize("n", ("", "2")) def test_new_to_legacy_freq(freq, expected, n): freq = f"{n}{freq}" result = _new_to_legacy_freq(freq) expected = f"{n}{expected}" assert result == expected @pytest.mark.skipif(has_pandas_ge_2_2, reason="only relevant for pandas lt 2.2") @pytest.mark.filterwarnings("ignore:'[AY]-.{3}' is deprecated") @pytest.mark.parametrize("year_alias", ("A", "Y", "YE")) @pytest.mark.parametrize("n", ("", "2")) def test_new_to_legacy_freq_anchored(year_alias, n): for month in _MONTH_ABBREVIATIONS.values(): freq = f"{n}{year_alias}-{month}" result = _new_to_legacy_freq(freq) expected = f"{n}A-{month}" assert result == expected @pytest.mark.skipif(has_pandas_ge_2_2, reason="only for pandas lt 2.2") @pytest.mark.parametrize( "freq, expected", ( # pandas-only freq strings are passed through ("BH", "BH"), ("CBH", "CBH"), ("N", "N"), ), ) def test_legacy_to_new_freq_pd_freq_passthrough(freq, expected): result = _legacy_to_new_freq(freq) assert result == expected @pytest.mark.filterwarnings("ignore:'.' is deprecated ") @pytest.mark.skipif(has_pandas_ge_2_2, reason="only for pandas lt 2.2") @pytest.mark.parametrize( "freq, expected", ( # these are each valid in pandas lt 2.2 ("T", "T"), ("min", "min"), ("S", "S"), ("s", "s"), ("L", "L"), ("ms", "ms"), ("U", "U"), ("us", "us"), # pandas-only freq strings are passed through ("bh", "bh"), ("cbh", "cbh"), ("ns", "ns"), ), ) def test_new_to_legacy_freq_pd_freq_passthrough(freq, expected): result = _new_to_legacy_freq(freq) assert result == expected @pytest.mark.filterwarnings("ignore:Converting a CFTimeIndex with:") @pytest.mark.parametrize("start", ("2000", "2001")) @pytest.mark.parametrize("end", ("2000", "2001")) @pytest.mark.parametrize( "freq", ( "MS", pytest.param("-1MS", marks=requires_pandas_3), "YS", pytest.param("-1YS", marks=requires_pandas_3), "ME", pytest.param("-1ME", marks=requires_pandas_3), "YE", pytest.param("-1YE", marks=requires_pandas_3), ), ) def test_cftime_range_same_as_pandas(start, end, freq) -> None: result = date_range(start, end, freq=freq, calendar="standard", use_cftime=True) result = result.to_datetimeindex(time_unit="ns") expected = date_range(start, end, freq=freq, use_cftime=False) np.testing.assert_array_equal(result, expected) @pytest.mark.filterwarnings("ignore:Converting a CFTimeIndex with:") @pytest.mark.parametrize( "start, end, periods", [ ("2022-01-01", "2022-01-10", 2), ("2022-03-01", "2022-03-31", 2), ("2022-01-01", "2022-01-10", None), ("2022-03-01", "2022-03-31", None), ], ) def test_cftime_range_no_freq(start, end, periods): """ Test whether cftime_range produces the same result as Pandas when freq is not provided, but start, end and periods are. """ # Generate date ranges using cftime_range cftimeindex = cftime_range(start=start, end=end, periods=periods) result = cftimeindex.to_datetimeindex(time_unit="ns") expected = pd.date_range(start=start, end=end, periods=periods) np.testing.assert_array_equal(result, expected) @pytest.mark.parametrize( "start, end, periods", [ ("2022-01-01", "2022-01-10", 2), ("2022-03-01", "2022-03-31", 2), ("2022-01-01", "2022-01-10", None), ("2022-03-01", "2022-03-31", None), ], ) def test_date_range_no_freq(start, end, periods): """ Test whether date_range produces the same result as Pandas when freq is not provided, but start, end and periods are. """ # Generate date ranges using date_range result = date_range(start=start, end=end, periods=periods) expected = pd.date_range(start=start, end=end, periods=periods) np.testing.assert_array_equal(result, expected) @pytest.mark.parametrize( "offset", [ MonthBegin(n=1), MonthEnd(n=1), QuarterBegin(n=1), QuarterEnd(n=1), YearBegin(n=1), YearEnd(n=1), ], ids=lambda x: f"{x}", ) @pytest.mark.parametrize("has_year_zero", [False, True]) def test_offset_addition_preserves_has_year_zero(offset, has_year_zero): with warnings.catch_warnings(): warnings.filterwarnings("ignore", message="this date/calendar/year zero") datetime = cftime.DatetimeGregorian(-1, 12, 31, has_year_zero=has_year_zero) result = datetime + offset assert result.has_year_zero == datetime.has_year_zero if has_year_zero: assert result.year == 0 else: assert result.year == 1 @pytest.mark.parametrize( "offset", [ MonthBegin(n=1), MonthEnd(n=1), QuarterBegin(n=1), QuarterEnd(n=1), YearBegin(n=1), YearEnd(n=1), ], ids=lambda x: f"{x}", ) @pytest.mark.parametrize("has_year_zero", [False, True]) def test_offset_subtraction_preserves_has_year_zero(offset, has_year_zero): datetime = cftime.DatetimeGregorian(1, 1, 1, has_year_zero=has_year_zero) result = datetime - offset assert result.has_year_zero == datetime.has_year_zero if has_year_zero: assert result.year == 0 else: assert result.year == -1 @pytest.mark.parametrize("has_year_zero", [False, True]) def test_offset_day_option_end_accounts_for_has_year_zero(has_year_zero): offset = MonthEnd(n=1) with warnings.catch_warnings(): warnings.filterwarnings("ignore", message="this date/calendar/year zero") datetime = cftime.DatetimeGregorian(-1, 1, 31, has_year_zero=has_year_zero) result = datetime + offset assert result.has_year_zero == datetime.has_year_zero if has_year_zero: assert result.day == 28 else: assert result.day == 29