diff --git a/cf/data/data.py b/cf/data/data.py index ffc1c03c65..4709ea636c 100644 --- a/cf/data/data.py +++ b/cf/data/data.py @@ -10294,31 +10294,71 @@ def outerproduct(self, e, inplace=False, i=False): return d + @daskified(_DASKIFIED_VERBOSE) @_deprecated_kwarg_check("i") @_inplace_enabled(default=False) def change_calendar(self, calendar, inplace=False, i=False): - """Change the calendar of the data array elements. + """Change the calendar of date-time array elements. + + Reinterprets the existing date-times for the new calendar by + adjusting the underlying numerical values relative to the + reference date-time defined by the units. + + If a date-time value is not allowed in the new calendar then + an exception is raised when the data array is accessed. + + .. seealso:: `override_calendar`, `Units` + + :Parameters: + + calendar: `str` + The new calendar, as recognised by the CF conventions. + + *Parameter example:* + ``'proleptic_gregorian'`` - Changing the calendar could result in a change of reference time - data array values. + {{inplace: `bool`, optional}} - Not to be confused with using the `override_calendar` method or - resetting `d.Units`. `override_calendar` is different because the - new calendar need not be equivalent to the original ones and the - data array elements will not be changed to reflect the new - units. Resetting `d.Units` will + {{i: deprecated at version 3.0.0}} + + :Returns: + + `Data` or `None` + The new data with updated calendar, or `None` if the + operation was in-place. + + **Examples** + + >>> d = cf.Data([0, 1, 2, 3, 4], 'days since 2004-02-27') + >>> print(d.array) + [0 1 2 3 4] + >>> print(d.datetime_as_string) + ['2004-02-27 00:00:00' '2004-02-28 00:00:00' '2004-02-29 00:00:00' + '2004-03-01 00:00:00' '2004-03-02 00:00:00'] + >>> e = d.change_calendar('360_day') + >>> print(e.array) + [0 1 2 4 5] + >>> print(e.datetime_as_string) + ['2004-02-27 00:00:00' '2004-02-28 00:00:00' '2004-02-29 00:00:00' + '2004-03-01 00:00:00' '2004-03-02 00:00:00'] + + >>> d.change_calendar('noleap').array + Traceback (most recent call last): + ... + ValueError: invalid day number provided in cftime.DatetimeNoLeap(2004, 2, 29, 0, 0, 0, 0, has_year_zero=True) """ d = _inplace_enabled_define_and_cleanup(self) - if not self.Units.isreftime: + units = self.Units + if not units.isreftime: raise ValueError( "Can't change calendar of non-reference time " - "units: {!r}".format(self.Units) + f"units: {units!r}" ) d._asdatetime(inplace=True) - d.override_units(Units(self.Units.units, calendar), inplace=True) + d.override_calendar(calendar, inplace=True) d._asreftime(inplace=True) return d diff --git a/cf/test/test_Data.py b/cf/test/test_Data.py index a68ab17555..0a2ca27ecc 100644 --- a/cf/test/test_Data.py +++ b/cf/test/test_Data.py @@ -3842,6 +3842,20 @@ def test_Data__bool__(self): with self.assertRaises(ValueError): bool(cf.Data([1, 2])) + def test_Data_change_calendar(self): + d = cf.Data( + [0, 1, 2, 3, 4], "days since 2004-02-27", calendar="standard" + ) + e = d.change_calendar("360_day") + self.assertTrue(np.allclose(e.array, [0, 1, 2, 4, 5])) + self.assertEqual(e.Units, cf.Units("days since 2004-02-27", "360_day")) + + # An Exception should be raised when a date is stored that is + # invalid to the calendar (e.g. 29th of February in the noleap + # calendar). + with self.assertRaises(ValueError): + e = d.change_calendar("noleap").array + if __name__ == "__main__": print("Run date:", datetime.datetime.now())