• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mcuntz / pyjams / 10243642634

05 Aug 2024 06:18AM UTC coverage: 94.71% (+0.04%) from 94.666%
10243642634

push

github

mcuntz
dt -> dt2, small bug in test of datetime

4297 of 4537 relevant lines covered (94.71%)

11.31 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.45
/src/pyjams/class_datetime.py
1
#!/usr/bin/env python
2
"""
3
Conversion of datetime formats
4

5
The module enhances cftime by non-CF date formats.
6

7
This module was written by Matthias Cuntz while at Institut National de
8
Recherche pour l'Agriculture, l'Alimentation et l'Environnement (INRAE), Nancy,
9
France.
10

11
:copyright: Copyright 2022- Matthias Cuntz, see AUTHORS.rst for details.
12
:license: MIT License, see LICENSE for details.
13

14
.. moduleauthor:: Matthias Cuntz
15

16
The following functions are provided:
17

18
.. autosummary::
19
   date2dec
20
   date2num
21
   dec2date
22
   num2date
23
   datetime
24

25
History
26
    * Written date2dec and dec2date Jun 2010, Arndt Piayda
27
    * Input can be scalar or array_like, Feb 2012, Matthias Cuntz
28
    * fulldate=True default in dec2date, Feb 2012, Matthias Cuntz
29
    * Added calendars decimal and decimal360, Feb 2012, Matthias Cuntz
30
    * Rename units to refdate and add units as in netcdftime in dec2date,
31
      Jun 2012, Matthias Cuntz
32
    * Add units='day as %Y%m%d.%f' in dec2date, Jun 2012, Matthias Cuntz
33
    * Change units of proleptic_gregorian from 'days since 0001-01-01 00:00:00'
34
      to 'days since 0001-01-00 00:00:00' in date2dec, Dec 2012, Matthias Cuntz
35
    * Bug in Excel and leap years, Feb 2013
36
    * Ported to Python 3, Feb 2013
37
    * Bug in 'eng' output of dec2date, May 2013, Arndt Piayda
38
    * Times with keywords ascii and eng default to 00:00:00 in date2dec,
39
      Jul 2013, Matthias Cuntz
40
    * Corrected that Excel year starts as 1 not at 0, Oct 2013, Matthias Cuntz
41
    * But in units keyword and Julian calendar, day was subtracted even if
42
      units were given, Oct 2013, Matthias Cuntz
43
    * Removed remnant of time treatment before time check in eng keyword in
44
      date2dec, Nov 2013, Matthias Cuntz
45
    * Adapted date2dec to new netCDF4/netcdftime (>=v1.0) and Python datetime
46
      (>v2.7.9), Jun 2015, Matthias Cuntz
47
    * Add units=='month as %Y%m.%f' and units=='year as %Y.%f' in dec2date,
48
      May 2016, Matthias Cuntz
49
    * Now possible to pass array_like to date2num instead of single
50
      netCDF4.datetime objects in date2dec, Oct 2016, Matthias Cuntz
51
    * Provide netcdftime even with netCDF4 > v1.0.0, Oct 2016, Matthias Cuntz
52
    * mo is always integer in date2dec, Oct 2016, Matthias Cuntz
53
    * leap is always integer in dec2date, Oct 2016, Matthias Cuntz
54
    * Corrected 00, 01, etc. in date2dec, which are not accepted as integer
55
      constants by Python 3, Nov 2016, Matthias Cuntz
56
    * numpydoc docstring format, May 2020, Matthias Cuntz
57
    * Renamed eng keword to en, Jul 2020, Matthias Cuntz
58
    * Use proleptic_gregorian calendar for Excel dates,
59
      Jul 2020, Matthias Cuntz
60
    * Change all np.int, np.float, etc. to Python equivalents,
61
      May 2021, Matthias Cuntz
62
    * flake8 compatible, May 2021, Matthias Cuntz
63
    * Written class_datetime, Jun 2022, Matthias Cuntz
64
      Complete rewrite from scratch following closely cftime but for
65
      non-CF-conform calendars such as decimal, Excel, and the cdo
66
      absolute time formats (e.g. units='day as %Y%m%d.%f').
67
      Provides its own datetime class.
68
      Use cftime notation now, i.e. date2num and num2date but provide
69
      date2dec and dec2date wrappers for backward compatibility (almost).
70
      Provide microsecond resolution with all supported calendars no matter
71
      of the units, which means that date2num returns np.longdouble values.
72
      date2num works together with date2date and can have formatted date
73
      strings as input.
74
    * calendar keyword takes precedence on calendar attribute of
75
      datetime objects in date2num, Jul 2022, Matthias Cuntz
76
    * return_arrays keyword in date2num, Jul 2022, Matthias Cuntz
77
    * round_microseconds method for datetime, Jul 2022, Matthias Cuntz
78
    * only_use_pyjams_datetimes keyword in num2date, Jan 2022, Matthias Cuntz
79
    * Also CF-calendars in datetime class, Jan 2023, Matthias Cuntz
80
    * Use longdouble keyword with date2num if cftime > v1.6.1,
81
      Aug 2024, Matthias Cuntz
82
    * Filter UserWarning from cftime, Aug 2024, Matthias Cuntz
83
    * ensure_seconds keyword in date2num, Aug 2024, Matthias Cuntz
84

85
ToDo
86
    * Check why datetime + timedelta but not timedelta + datetime
87
    * add date2index
88
    * add time2index
89
    * implement fromordinal
90
    * implement change_calendar
91
    * strptime
92

93
"""
94
from datetime import datetime as datetime_python
12✔
95
from datetime import timedelta
12✔
96
import re
12✔
97
import time as ptime
12✔
98
import warnings
12✔
99
import numpy as np
12✔
100
import cftime as cf
12✔
101
from .helper import input2array, array2input
12✔
102
from .date2date import date2date
12✔
103
# from pyjams.helper import input2array, array2input
104
# from pyjams.date2date import date2date
105

106
__all__ = ['date2dec', 'date2num', 'dec2date', 'num2date', 'datetime']
12✔
107

108

109
# supported calendars. Includes synonyms ('excel'=='excel1900')
110
_excelcalendars = ['excel', 'excel1900', 'excel1904']
12✔
111
_decimalcalendars = ['decimal', 'decimal360', 'decimal365', 'decimal366']
12✔
112
_noncfcalendars = _excelcalendars + _decimalcalendars
12✔
113
_cfcalendars = ['standard', 'gregorian', 'proleptic_gregorian',
12✔
114
                'noleap', 'julian', 'all_leap', '365_day', '366_day',
115
                '360_day']
116
_idealized_cfcalendars = ['all_leap', 'noleap', '366_day', '365_day',
12✔
117
                          '360_day']
118

119
# number of days in year
120
_dayspermonth      = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
12✔
121
_dayspermonth_leap = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
12✔
122
_dayspermonth_360  = [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]
12✔
123
_cumdayspermonth      = [0, 31, 59, 90, 120, 151, 181,
12✔
124
                         212, 243, 273, 304, 334, 365]
125
_cumdayspermonth_leap = [0, 31, 60, 91, 121, 152, 182,
12✔
126
                         213, 244, 274, 305, 335, 366]
127
_cumdayspermonth_360  = [0, 30, 60, 90, 120, 150, 180,
12✔
128
                         210, 240, 270, 300, 330, 360]
129
# feps = np.finfo(np.float64).eps
130
deps = np.finfo(np.longdouble).eps
12✔
131

132
#
133
# Exact copies of private cftime routines
134
# (changed Python time to ptime)
135
#
136

137
_illegal_s = re.compile(r"((^|[^%])(%%)*%s)")
12✔
138

139

140
def _findall(text, substr):
12✔
141
    # Also finds overlaps
142
    sites = []
12✔
143
    i = 0
12✔
144
    while 1:
9✔
145
        j = text.find(substr, i)
12✔
146
        if j == -1:
12✔
147
            break
12✔
148
        sites.append(j)
12✔
149
        i = j + 1
12✔
150
    return sites
12✔
151

152

153
def to_tuple(dt):
12✔
154
    """
155
    Turn a datetime instance into a tuple of integers. Elements go
156
    in the order of decreasing significance, making it easy to compare
157
    datetime instances. Parts of the state that don't affect ordering
158
    are omitted. Compare to timetuple().
159

160
    """
161
    return (dt.year, dt.month, dt.day, dt.hour, dt.minute,
12✔
162
            dt.second, dt.microsecond)
163

164

165
# factory function without optional kwargs that can be used in
166
# datetime.__reduce_
167
def _create_datetime(date_type, args, kwargs):
12✔
168
    return date_type(*args, **kwargs)
×
169

170

171
#
172
# Adapted cftime routines
173
#
174

175
# Every 28 years the calendar repeats, except through century leap
176
# years where it's 6 years. But only if you're using the Gregorian
177
# calendar. ;-)
178
# Make also 4-digit negative years
179
# Allow .%f for microseconds
180
def _strftime(dt, fmt):
12✔
181
    if _illegal_s.search(fmt):
12✔
182
        raise TypeError("This strftime implementation does not handle %s")
12✔
183
    if '%f' in fmt:
12✔
184
        if not fmt.endswith('.%f'):
12✔
185
            raise TypeError('If %f is used for microseconds it must be the'
12✔
186
                            ' at the end as .%f')
187
        else:
188
            ihavems = True
×
189
            fmt1 = fmt[:-3]
×
190
    else:
191
        ihavems = False
12✔
192
        fmt1 = fmt
12✔
193

194
    # don't use strftime method at all.
195
    # if dt.year > 1900:
196
    #    return dt.strftime(fmt)
197

198
    year = dt.year
12✔
199
    # For every non-leap year century, advance by
200
    # 6 years to get into the 28-year repeat cycle
201
    delta = 2000 - year
12✔
202
    off = 6 * (delta // 100 + delta // 400)
12✔
203
    year = year + off
12✔
204

205
    # Move to around the year 2000
206
    year = year + ((2000 - year) // 28) * 28
12✔
207
    # timetuple does not include microseconds
208
    timetuple = dt.timetuple()
12✔
209
    # time.strftime does hence not treat microseconds. i.e. format code %f
210
    s1 = ptime.strftime(fmt1, (year,) + timetuple[1:])
12✔
211
    sites1 = _findall(s1, str(year))
12✔
212

213
    s2 = ptime.strftime(fmt1, (year + 28,) + timetuple[1:])
12✔
214
    sites2 = _findall(s2, str(year + 28))
12✔
215

216
    sites = []
12✔
217
    for site in sites1:
12✔
218
        if site in sites2:
12✔
219
            sites.append(site)
12✔
220

221
    s = s1
12✔
222
    if dt.year < 0:
12✔
223
        syear = "%05d" % (dt.year,)
12✔
224
    else:
225
        syear = "%04d" % (dt.year,)
12✔
226
    for site in sites:
12✔
227
        s = s[:site] + syear + s[site + 4:]
12✔
228
    if ihavems:
12✔
229
        s = s + '.{:06d}'.format(dt.microsecond)
×
230
    return s
12✔
231

232

233
def _datesplit(timestr):
12✔
234
    """
235
    Split a unit string for time into its three components:
236
    unit, string 'since' or 'as', and the remainder
237

238
    """
239
    try:
12✔
240
        (units, sincestring, remainder) = timestr.split(None, 2)
12✔
241
    except ValueError:
12✔
242
        raise ValueError(f'Incorrectly formatted date-time unit_string:'
12✔
243
                         f' {timestr}')
244

245
    if sincestring.lower() not in ['since', 'as']:
12✔
246
        raise ValueError(f"No 'since' or 'as' in unit_string:"
12✔
247
                         f" {timestr}")
248

249
    return units.lower(), sincestring.lower(), remainder
12✔
250

251

252
def _year_zero_defaults(calendar):
12✔
253
    """
254
    Set calendar specific defaults for having year 0 or not
255

256
    Excel calendars *excel*, *excel1900*, *excel1904* start only 1900
257
    or above, i.e. no year 0.
258

259
    Decimal calendars *decimal*, *decimal360*, *decimal365*, *decimal366*
260
    have year 0 by default but might also omit it.
261

262
    Real-world calendars *standard*, *gregorian*, *julian* have no year 0.
263

264
    *proleptic_gregorian* (ISO 8601) and the idealized calendars
265
    *noleap*/*365_day*, *360_day*, *366_day*/*all_leap* have by default
266
    a year 0.
267

268
    Parameters
269
    ----------
270
    calendar : str
271
        One of the supported calendar names in *_noncfcalendars*
272

273
    Returns
274
    -------
275
    bool
276
       True if calendar includes year 0 by default
277

278
    Examples
279
    --------
280
    >>> print(_year_zero_defaults('Excel'))
281
    False
282
    >>> print(_year_zero_defaults('decimal'))
283
    True
284

285
    """
286
    calendar = calendar.lower()
12✔
287
    if calendar in ['standard', 'gregorian', 'julian']:
12✔
288
        return False
12✔
289
    elif calendar in ['proleptic_gregorian']:
12✔
290
        return True  # ISO 8601 year zero=1 BC
12✔
291
    elif calendar in _idealized_cfcalendars:
12✔
292
        return True
×
293
    elif calendar in _excelcalendars:
12✔
294
        return False
12✔
295
    elif calendar in _decimalcalendars:
12✔
296
        return True
12✔
297
    else:
298
        raise ValueError(f'Unknown calendar: {calendar}')
12✔
299

300

301
def _is_leap(year, calendar, has_year_zero=None):
12✔
302
    """
303
    Determines if a specific year in a given calendar is a leap year
304

305
    *has_year_zero* controls whether astronomical year numbering
306
    is used and the year zero exists. If not specified,
307
    calendar-specific default is assumed.
308

309
    Excel calendars *excel*, *excel1900*, *excel1904* start only in 1900
310
    or above, i.e. no year 0 allowed.
311

312
    Decimal calendars *decimal*, *decimal360*, *decimal365*, *decimal366*
313
    have year 0 by default but might omit it by setting *has_year_zero=False*.
314

315
    Parameters
316
    ----------
317
    year : int or array_like of int
318
        Year(s) to check if leap year
319
    calendar : str
320
        One of the supported calendar names in *_noncfcalendars*
321
    has_year_zero : bool, optional
322
        Astronomical year numbering is used, i.e. year zero exists, if True
323
        and possible for the given *calendar*. If *None* (default),
324
        calendar-specific defaults are assumed.
325

326
    Returns
327
    -------
328
    bool or array of bool
329
       True if year is a leap year
330

331
    Notes
332
    -----
333
    If there is no year 0 in a calendar, years -1, -5, -9, etc. are leap years.
334
    Year 0 is a leap year if it exists.
335

336
    Examples
337
    --------
338
    >>> years = [1900, 1904]
339
    >>> print(_is_leap(years, 'decimal'))
340
    [False, True]
341
    >>> print(_is_leap(years, 'Excel'))
342
    [True, True]
343

344
    """
345
    myear = input2array(year, default=1990)
12✔
346

347
    # set calendar-specific defaults for has_year_zero
348
    if has_year_zero is None:
12✔
349
        has_year_zero = _year_zero_defaults(calendar)
12✔
350

351
    if has_year_zero and (calendar in _excelcalendars):
12✔
352
        raise ValueError('year 0 not allowed with Excel calendars')
×
353
    if np.any(myear == 0) and (not has_year_zero):
12✔
354
        raise ValueError(f'year 0 does not exist in the calendar {calendar}')
×
355

356
    if calendar in _cfcalendars:
12✔
357
        leap = [ cf.is_leap_year(yy, calendar, has_year_zero) for yy in myear ]
12✔
358
    else:
359
        # If there is no year 0 in the calendar, years -1, -5, -9, etc.
360
        # are leap years. year 0 is a leap year if it exists.
361
        if not has_year_zero:
12✔
362
            myear = np.where(myear < 0, myear + 1, myear)
12✔
363

364
        if calendar in _excelcalendars:
12✔
365
            # Excel calendars are supposedly Julian calendars
366
            leap = (myear % 4) == 0
12✔
367
        elif calendar == 'decimal':
12✔
368
            leap = ( (((myear % 4) == 0) & ((myear % 100) != 0)) |
12✔
369
                     ((myear % 400) == 0) )
370
        elif calendar in ['decimal360', 'decimal365']:
12✔
371
            leap = np.zeros_like(myear, dtype=bool)
12✔
372
        elif calendar == 'decimal366':
12✔
373
            leap = np.ones_like(myear, dtype=bool)
12✔
374
        else:
375
            raise ValueError(f'Calendar not known: {calendar}')
×
376

377
    oleap = array2input(leap, year)
12✔
378

379
    return oleap
12✔
380

381

382
def _month_lengths(year, calendar, has_year_zero=None):
12✔
383
    """
384
    Number of days of the 12 months in specific year for a given calendar
385

386
    Parameters
387
    ----------
388
    year : int
389
        Year to inquire
390
    calendar : str
391
        One of the supported calendar names in *_noncfcalendars*
392
    has_year_zero : bool, optional
393
        Astronomical year numbering is used, i.e. year zero exists, if True
394
        and possible for the given *calendar*. If *None* (default),
395
        calendar-specific defaults are assumed.
396

397
    Returns
398
    -------
399
    list
400
       Lengths of the 12 months in specified *year*
401

402
    Examples
403
    --------
404
    >>> print(_month_lengths(1990, 'decimal'))
405
    [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
406
    >>> print(_month_lengths(1990, 'decimal360'))
407
    [30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]
408

409
    """
410
    leap = _is_leap(year, calendar, has_year_zero)
12✔
411
    if calendar == 'decimal360':
12✔
412
        return _dayspermonth_360
12✔
413
    else:
414
        if leap:
12✔
415
            return _dayspermonth_leap
12✔
416
        else:
417
            return _dayspermonth
12✔
418

419

420
def _int_julian_day_from_date(year, month, day, calendar,
12✔
421
                              skip_transition=False, has_year_zero=None):
422
    """
423
    Compute integer Julian Day from year, month, day, and calendar
424

425
    Integer julian day is number of days since noon UTC -4713-1-1
426
    in the julian or mixed julian/gregorian calendar, or noon UTC
427
    -4714-11-24 in the proleptic_gregorian calendar (without year zero).
428
    Reference date is noon UTC 0000-01-01 for other calendars.
429

430
    Excel calendars are supposedly Julian calendars with other reference dates.
431
    Julian calendar is hence used for the Excel calendars *excel*, *excel1900*,
432
    *excel1904*, i.e. integer julian day is the same number of days since
433
    -4713-01-01 12:00:00 in the Julian calendar.
434

435
    Integer julian day in the decimal calendars *decimal*, *decimal360*,
436
    *decimal365*, *decimal366* is the number of days after 0000-01-01 12:00:00.
437

438
    If the has_year_zero kwarg is set to True, astronomical year numbering
439
    is used and the year zero exists for the decimal calendars.
440
    If set to False, then historical year numbering is used and the year 1 is
441
    preceded by year -1 and no year zero exists.
442
    The defaults (has_year_zero=None) uses astronomical year numbering
443
    for the decimal calendars.
444
    CF version 1.9 conventions are:
445
    False for 'julian', 'gregorian'/'standard',
446
    True for 'proleptic_gregorian' (ISO 8601), and
447
    True for the idealized calendars 'noleap'/'365_day', '360_day',
448
    366_day'/'all_leap'
449

450
    'skip_transition': When True, leave a 10-day
451
    gap in Julian day numbers between Oct 4 and Oct 15 1582 (the transition
452
    from Julian to Gregorian calendars). Default: False,
453
    ignored unless calendar = 'standard' or 'gregorian'.
454

455
    """
456
    if calendar:
12✔
457
        calendar = calendar.lower()
12✔
458
    if has_year_zero is None:
12✔
459
        has_year_zero = _year_zero_defaults(calendar)
×
460
    if (calendar == 'decimal360') or (calendar == '360_day'):
12✔
461
        # return year * 360 + (month - 1) * 30 + day - 1
462
        return year * 360 + _cumdayspermonth_360[month - 1] + day - 1
12✔
463
    elif ( (calendar == 'decimal365') or (calendar == '365_day') or
12✔
464
           (calendar == 'noleap')):
465
        return year * 365 + _cumdayspermonth[month - 1] + day - 1
12✔
466
    elif ( (calendar == 'decimal366') or (calendar == '366_day') or
12✔
467
           (calendar == 'all_leap')):
468
        return year * 366 + _cumdayspermonth_leap[month - 1] + day - 1
12✔
469
    else:
470
        leap = _is_leap(year, calendar, has_year_zero=has_year_zero)
12✔
471
        if leap:
12✔
472
            jday = day + _cumdayspermonth_leap[month - 1]
12✔
473
        else:
474
            jday = day + _cumdayspermonth[month - 1]
12✔
475
        # If there is no year 0, years -1, -5, -9, etc,
476
        # are leap years. year zero is a leap year if it exists.
477
        if (year < 0) and (not has_year_zero):
12✔
478
            year += 1
12✔
479
        if calendar == 'decimal':
12✔
480
            # 1st term is the number of days in the last year
481
            # 2nd term is the number of days in each preceding non-leap year
482
            # last terms are the number of preceding leap years
483
            jday_greg = (jday + 365 * (year - 1) +
12✔
484
                         (year - 1) // 4 - (year - 1) // 100 +
485
                         (year - 1) // 400)
486
            return jday_greg
12✔
487
        elif (calendar in _excelcalendars) or (calendar == 'julian'):
12✔
488
            year += 4800  # add offset so -4800 is year 0.
12✔
489
            # 1st term is the number of days in the last year
490
            # 2nd term is the number of days in each preceding non-leap year
491
            # last terms are the number of preceding leap years
492
            jday_jul = jday + 365 * (year - 1) + (year - 1) // 4
12✔
493
            # remove offset for 87 years before -4713 (including leap days)
494
            jday_jul -= 31777
12✔
495
            return jday_jul
12✔
496
        elif ( (calendar == 'standard') or (calendar == 'gregorian') or
12✔
497
               (calendar == 'proleptic_gregorian') ):
498
            year += 4800  # add offset so -4800 is year 0.
12✔
499
            # 1st term is the number of days in the last year
500
            # 2nd term is the number of days in each preceding non-leap year
501
            # last terms are the number of preceding leap years since -4800
502
            jday_jul = jday + 365 * (year - 1) + (year - 1) // 4
12✔
503
            # remove offset for 87 years before -4713 (including leap days)
504
            jday_jul -= 31777
12✔
505
            jday_greg = (jday + 365 * (year - 1) +
12✔
506
                         (year - 1) // 4 - (year - 1) // 100 +
507
                         (year - 1) // 400)
508
            # remove offset, and account for the fact that -4713/1/1 is jday=38
509
            # in gregorian calendar.
510
            jday_greg -= 31739
12✔
511
            if calendar == 'proleptic_gregorian':
12✔
512
                return jday_greg
12✔
513
            else:
514
                # check for invalid days in mixed calendar
515
                # (there are 10 missing)
516
                if jday_jul >= 2299161 and jday_jul < 2299171:
12✔
517
                    raise ValueError('invalid date in mixed calendar')
×
518
                if jday_jul < 2299161:  # 1582 October 15
12✔
519
                    return jday_jul
12✔
520
                else:
521
                    if skip_transition:
12✔
522
                        return jday_greg + 10
×
523
                    else:
524
                        return jday_greg
12✔
525
        else:
526
            raise ValueError(f'Unknown calendar: {calendar}')
×
527

528

529
# Add a datetime.timedelta to a pyjams.datetime instance. Uses
530
# integer arithmetic to avoid rounding errors and preserve
531
# microsecond accuracy.
532
def _add_timedelta(dt, delta):
12✔
533
    # extract these inputs here to avoid type conversion in the code below
534
    delta_microseconds = delta.microseconds
12✔
535
    delta_seconds = delta.seconds
12✔
536
    delta_days = delta.days
12✔
537

538
    # shift microseconds, seconds, days
539
    calendar = dt.calendar
12✔
540
    has_year_zero = dt.has_year_zero
12✔
541
    microsecond = dt.microsecond + delta_microseconds
12✔
542
    second = dt.second + delta_seconds
12✔
543
    minute = dt.minute
12✔
544
    hour = dt.hour
12✔
545
    day = dt.day
12✔
546
    month = dt.month
12✔
547
    year = dt.year
12✔
548

549
    month_length = _month_lengths(year, calendar, has_year_zero)
12✔
550

551
    # Normalize microseconds, seconds, minutes, hours.
552
    second += microsecond // 1000000
12✔
553
    microsecond = microsecond % 1000000
12✔
554
    minute += second // 60
12✔
555
    second = second % 60
12✔
556
    hour += minute // 60
12✔
557
    minute = minute % 60
12✔
558
    extra_days = hour // 24
12✔
559
    hour = hour % 24
12✔
560

561
    delta_days += extra_days
12✔
562

563
    while delta_days < 0:
12✔
564
        # not done compared to cftime because Excel dates > 1900 and
565
        # decimal dates include 1582-10-05 to 1582-10-14
566
        # if (year == 1582 and month == 10 and day > 14 and
567
        #     day + delta_days < 15):
568
        #     delta_days -= n_invalid_dates    # skip over invalid dates
569
        if (day + delta_days) < 1:
12✔
570
            delta_days += day
12✔
571
            # decrement month
572
            month -= 1
12✔
573
            if month < 1:
12✔
574
                month = 12
×
575
                year -= 1
×
576
                if (year == 0) and (not has_year_zero):
×
577
                    year = -1
×
578
                month_length = _month_lengths(year, calendar, has_year_zero)
×
579
            day = month_length[month - 1]
12✔
580
        else:
581
            day += delta_days
12✔
582
            delta_days = 0
12✔
583

584
    while delta_days > 0:
12✔
585
        # not done compared to cftime because Excel dates > 1900 and
586
        # decimal dates include 1582-10-05 to 1582-10-14
587
        # if (year == 1582 and month == 10 and day < 5 and
588
        #     day + delta_days > 4):
589
        #     delta_days += n_invalid_dates    # skip over invalid dates
590
        if (day + delta_days) > month_length[month - 1]:
12✔
591
            delta_days -= month_length[month - 1] - (day - 1)
12✔
592
            # increment month
593
            month += 1
12✔
594
            if month > 12:
12✔
595
                month = 1
12✔
596
                year += 1
12✔
597
                if (year == 0) and (not has_year_zero):
12✔
598
                    year = 1
×
599
                month_length = _month_lengths(year, calendar, has_year_zero)
12✔
600
            day = 1
12✔
601
        else:
602
            day += delta_days
12✔
603
            delta_days = 0
12✔
604

605
    return (year, month, day, hour, minute, second, microsecond)
12✔
606

607

608
#
609
# New routines
610
#
611

612
def _units_defaults(calendar, has_year_zero=None):
12✔
613
    """
614
    Set calendar specific default units as 'days since reference_date'
615

616
    Day 0 of *excel* and *excel1900* starts at 1899-12-31 00:00:00.
617

618
    Day 0 of *excel1904* starts at 1903-12-31 00:00:00.
619

620
    Decimal calendars *decimal*, *decimal360*, *decimal365*, and
621
    *decimal366* do not need units so 0001-01-01 00:00:00 is taken.
622

623
    Day 0 of *julian*, *gregorian* and *standard* starts at
624
    -4713-01-01 12:00:00 if not has_year_zero, and at
625
    -4712-01-01 12:00:00 if has_year_zero.
626

627
    Day 0 of *proleptic_gregorian* starts at
628
    -4714-11-24 12:00:00 if not has_year_zero, and at
629
    -4713-11-24 12:00:00 if has_year_zero.
630

631
    Day 0 of *360_day*, *365_day*, *366_day*, *all_leap*, and
632
    *noleap* starts at 0000-01-01 12:00:00.
633

634
    Parameters
635
    ----------
636
    calendar : str
637
        One of the supported calendar names in *_cfcalendars* and
638
        *_noncfcalendars*
639
    has_year_zero : bool, optional
640
        Astronomical year numbering is used, i.e. year zero exists, if True
641
        and possible for the given *calendar*. If *None* (default),
642
        calendar-specific defaults are assumed.
643

644
    Returns
645
    -------
646
    str
647
       'days since reference_date' with calendar-specific reference_date
648

649
    Examples
650
    --------
651
    >>> print(_units_defaults('Excel'))
652
    'days since 1899-12-31 00:00:00'
653

654
    """
655
    calendar = calendar.lower()
12✔
656
    if has_year_zero is None:
12✔
657
        has_year_zero = _year_zero_defaults(calendar)
12✔
658
    if calendar in ['standard', 'gregorian', 'julian']:
12✔
659
        if has_year_zero:
12✔
660
            return 'days since -4712-01-01 12:00:00'
×
661
        else:
662
            return 'days since -4713-01-01 12:00:00'
12✔
663
    elif calendar in ['proleptic_gregorian']:
12✔
664
        if has_year_zero:
12✔
665
            return 'days since -4713-11-24 12:00:00'
12✔
666
        else:
667
            return 'days since -4714-11-24 12:00:00'
×
668
    elif calendar in _idealized_cfcalendars:
12✔
669
        return 'days since 0000-01-01 12:00:00'
×
670
    elif calendar in ['excel', 'excel1900']:
12✔
671
        return 'days since 1899-12-31 00:00:00'
12✔
672
    elif calendar in ['excel1904']:
12✔
673
        return 'days since 1903-12-31 00:00:00'
12✔
674
    elif calendar in _decimalcalendars:
12✔
675
        return 'days since 0001-01-01 00:00:00'
12✔
676
    else:
677
        raise ValueError(f'Unknown calendar: {calendar}')
×
678

679

680
def _date2decimal(date, calendar):
12✔
681
    """
682
    Decimal date from datetime object
683

684
    Parameters
685
    ----------
686
    date : datetime instance
687
        Instance of pyjams.datetime class
688
    calendar : str
689
        One of the decimal calendar names *decimal*, *decimal360*,
690
        *decimal365*, or *decimal366*
691

692
    Returns
693
    -------
694
    longdouble
695
       decimal date
696

697
    Examples
698
    --------
699
    >>> dt = datetime(1990, 1, 1)
700
    >>> dec = _date2decimal(dt, 'decimal')
701
    >>> print(dec)
702
    1990.
703

704
    """
705
    year     = date.year
12✔
706
    month    = date.month
12✔
707
    day      = np.longdouble(date.day)
12✔
708
    hour     = np.longdouble(date.hour)
12✔
709
    minute   = np.longdouble(date.minute)
12✔
710
    second   = np.longdouble(date.second)
12✔
711
    msecond  = np.longdouble(date.microsecond)
12✔
712
    calendar = calendar.lower()
12✔
713

714
    days_year = np.longdouble(365)
12✔
715
    diy = np.array([ [-9] + _cumdayspermonth,
12✔
716
                     [-9] + _cumdayspermonth_leap ], dtype=np.longdouble)
717
    if calendar == 'decimal':
12✔
718
        leap = int( (((year % 4) == 0) & ((year % 100) != 0)) |
12✔
719
                    ((year % 400) == 0) )
720
    elif calendar == 'decimal360':
12✔
721
        leap = 0
12✔
722
        days_year = np.longdouble(360)
12✔
723
        diy  = np.array([ [-9] + _cumdayspermonth_360,
12✔
724
                          [-9] + _cumdayspermonth_360 ], dtype=np.longdouble)
725
    elif calendar == 'decimal365':
12✔
726
        leap = 0
12✔
727
    elif calendar == 'decimal366':
12✔
728
        leap = 1
12✔
729
    fleap = np.longdouble(leap)
12✔
730
    tday  = diy[leap, month] + day
12✔
731
    thour = ( (tday - 1.) * 24. +
12✔
732
              hour +
733
              minute / 60. +
734
              second / 3600. +
735
              msecond / 3600000000. )
736
    out = np.longdouble(year) + thour / ((days_year + fleap) * 24.)
12✔
737

738
    return out
12✔
739

740

741
def _dates2decimal(dates, calendar):
12✔
742
    """
743
    Decimal dates from datetime objects
744

745
    Parameters
746
    ----------
747
    dates : datetime instance or array_like of datetime instances
748
        Instances of pyjams.datetime class
749
    calendar : str
750
        One of the decimal calendar names *decimal*, *decimal360*,
751
        *decimal365*, or *decimal366*
752

753
    Returns
754
    -------
755
    array_like of longdouble
756
       decimal dates
757

758
    Examples
759
    --------
760
    >>> dt = [datetime(1990, 1, 1), datetime(1991, 1, 1)]
761
    >>> dec = _dates2decimal(dt, 'decimal')
762
    >>> print(dec)
763
    [1990., 1991.]
764

765
    """
766
    mdates = input2array(dates, default=datetime(1990, 1, 1))
12✔
767

768
    # wrapper might be slow
769
    out = [ _date2decimal(dd, calendar) for dd in mdates ]
12✔
770

771
    out = array2input(out, dates)
12✔
772
    return out
12✔
773

774

775
def _date2absolute(date, units):
12✔
776
    """
777
    Absolute date from datetime object
778

779
    Parameters
780
    ----------
781
    date : datetime instance
782
        Instances of pyjams.datetime class
783
    units : str
784
        'day as %Y%m%d.%f', 'month as %Y%m.%f', or 'year as %Y.%f'
785

786
    Returns
787
    -------
788
    longdouble
789
       absolute date
790

791
    Examples
792
    --------
793
    >>> dt = datetime(1990, 1, 1)
794
    >>> dec = _date2absolute(dt, 'day as %Y%m%d.%f')
795
    >>> print(np.around(dec, 1))
796
    19900101.0
797

798
    """
799
    year     = date.year
12✔
800
    month    = date.month
12✔
801
    day      = np.longdouble(date.day)
12✔
802
    hour     = np.longdouble(date.hour)
12✔
803
    minute   = np.longdouble(date.minute)
12✔
804
    second   = np.longdouble(date.second)
12✔
805
    msecond  = np.longdouble(date.microsecond)
12✔
806

807
    if units == 'day as %Y%m%d.%f':
12✔
808
        tday  = (np.longdouble(year) * 10000. +
12✔
809
                 np.longdouble(month) * 100. +
810
                 day)
811
        thour = (hour +
12✔
812
                 minute / 60. +
813
                 second / 3600. +
814
                 msecond / 3600000000.)
815
        out = tday + thour / 24.
12✔
816
    elif units == 'month as %Y%m.%f':
12✔
817
        leap = int( (((year % 4) == 0) & ((year % 100) != 0)) |
12✔
818
                    ((year % 400) == 0) )
819
        dim = np.array([ [-9] + _dayspermonth,
12✔
820
                         [-9] + _dayspermonth_leap ], dtype=np.longdouble)
821
        tmonth = (np.longdouble(year) * 100. +
12✔
822
                  np.longdouble(month))
823
        thour = (day * 24. +
12✔
824
                 hour +
825
                 minute / 60. +
826
                 second / 3600. +
827
                 msecond / 3600000000.)
828
        out = tmonth + thour / (dim[leap, month] * 24.)
12✔
829
    elif units == 'year as %Y.%f':
12✔
830
        # same as decimal date
831
        out = _date2decimal(date, 'decimal')
12✔
832
    else:
833
        raise ValueError(f'Unknown absolute units: {units}')
×
834

835
    return out
12✔
836

837

838
def _dates2absolute(dates, units):
12✔
839
    """
840
    Absolute dates from datetime object
841

842
    Parameters
843
    ----------
844
    dates : datetime instance or array_like of datetime instances
845
        Instances of pyjams.datetime class
846
    units : str
847
        'day as %Y%m%d.%f', 'month as %Y%m.%f', or 'year as %Y.%f'
848

849
    Returns
850
    -------
851
    longdouble or array_like of longdouble
852
       absolute dates
853

854
    Examples
855
    --------
856
    >>> dt = [datetime(1990, 1, 1), datetime(1991, 1, 1)]
857
    >>> dec = _dates2absolute(dt, 'day as %Y%m%d.%f')
858
    >>> print(np.around(dec, 1))
859
    [19900101.0, 19910101.0]
860

861
    """
862
    mdates = input2array(dates, default=datetime(1990, 1, 1))
12✔
863

864
    # wrapper might be slow
865
    out = [ _date2absolute(dd, units) for dd in mdates ]
12✔
866

867
    out = array2input(out, dates)
12✔
868
    return out
12✔
869

870

871
def _decimal2date(times, calendar):
12✔
872
    """
873
    Split decimal dates into year, month, day, hour, minute, second,
874
    microsecond
875

876
    Parameters
877
    ----------
878
    times : float or array_like
879
        Decimal dates such as 1990.5102
880
    calendar : str
881
        One of the decimal calendar names *decimal*, *decimal360*,
882
        *decimal365*, or *decimal366*
883

884
    Returns
885
    -------
886
    tuple
887
       arrays of year, month, day, hour, minute, second, microsecond
888

889
    Examples
890
    --------
891
    >>> dec = [1990., 1991.5]
892
    >>> yr, mo, dy, hr, mi, sc, ms = _decimal2date(dec, 'decimal')
893
    >>> print(yr)
894
    [1990, 1991]
895
    >>> print(mo)
896
    [1, 7]
897
    >>> print(dy)
898
    [1, 3]
899

900
    """
901
    # old algorithm does decomposition on all array elements
902
    # should try similar to decode_dates_from_array for speed
903
    # where decomposition is done for the first (oldest) element only
904
    # and then timedeltas are added subsequently
905
    mtimes = input2array(times, default=1.)
12✔
906
    mtimes = np.array(mtimes, dtype=np.longdouble)
12✔
907
    calendar = calendar.lower()
12✔
908

909
    # year
910
    fyear = np.trunc(mtimes)
12✔
911
    fyear = np.where(mtimes < 0., fyear - 1., fyear)
12✔
912
    year = fyear.astype(np.int64)
12✔
913
    frac_year = mtimes - fyear
12✔
914
    days_year = np.longdouble(365)
12✔
915
    diy = np.array([ [-9] + _cumdayspermonth,
12✔
916
                     [-9] + _cumdayspermonth_leap ])
917
    if calendar == 'decimal':
12✔
918
        leap = ( (((year % 4) == 0) & ((year % 100) != 0)) |
12✔
919
                 ((year % 400) == 0) ).astype(int)
920
        fleap = leap.astype(np.longdouble)
12✔
921
    elif calendar == 'decimal360':
12✔
922
        leap  = np.zeros_like(mtimes, dtype=int)
12✔
923
        fleap = np.zeros_like(mtimes, dtype=np.longdouble)
12✔
924
        days_year = np.longdouble(360)
12✔
925
        diy  = np.array([ [-9] + _cumdayspermonth_360,
12✔
926
                          [-9] + _cumdayspermonth_360 ])
927
    elif calendar == 'decimal365':
12✔
928
        leap  = np.zeros_like(mtimes, dtype=int)
12✔
929
        fleap = np.zeros_like(mtimes, dtype=np.longdouble)
12✔
930
    elif calendar == 'decimal366':
12✔
931
        leap  = np.ones_like(mtimes, dtype=int)
12✔
932
        fleap = np.ones_like(mtimes, dtype=np.longdouble)
12✔
933
    else:
934
        raise ValueError(f'Unknown decimal calendar: {calendar}')
×
935
    # change to microseconds to catch round-off errors,
936
    # i.e. cases 1 microsec less or greater than a second
937
    # cf. issue #187 of cftime
938
    # day of year in microseconds
939
    fhoy = frac_year * (days_year + fleap) * 86400000000.
12✔
940
    ihoy = np.rint(fhoy).astype(np.int64)
12✔
941
    # Done in cftime for issue #187
942
    # ihoy = np.where(ihoy%1000000 == 1,
943
    #                 np.floor(fhoy).astype(np.int64), ihoy)
944
    # ihoy = np.where(ihoy%1000000 == 999999,
945
    #                 np.ceil(fhoy).astype(np.int64), ihoy)
946
    # microsecond
947
    msecond = ihoy % 1000000
12✔
948
    ihoy    = ihoy // 1000000
12✔
949
    # second
950
    second = ihoy % 60
12✔
951
    ihoy   = ihoy // 60
12✔
952
    # minute
953
    minute = ihoy % 60
12✔
954
    ihoy   = ihoy // 60
12✔
955
    # hour
956
    hour = ihoy % 24
12✔
957
    ihoy = ihoy // 24
12✔
958
    # day and month
959
    idoy = ihoy + 1
12✔
960
    month = np.zeros_like(mtimes, dtype=np.int64)
12✔
961
    day   = np.zeros_like(mtimes, dtype=np.int64)
12✔
962
    for i in range(mtimes.size):
12✔
963
        ii = np.where(idoy[i] > diy[leap[i], :])[0]
12✔
964
        month[i] = ii[-1]
12✔
965
        day[i]   = idoy[i] - diy[leap[i], month[i]]
12✔
966

967
    return year, month, day, hour, minute, second, msecond
12✔
968

969

970
def _absolute2date(times, units):
12✔
971
    """
972
    Split date(s) in absolute date format into
973
    year, month, day, hour, minute, second, microsecond
974

975
    Parameters
976
    ----------
977
    times : float or array_like
978
        Absolute dates such as 20070102.0034722
979
    units : str
980
        'day as %Y%m%d.%f', 'month as %Y%m.%f', or 'year as %Y.%f'
981

982
    Returns
983
    -------
984
    tuple
985
       arrays of year, month, day, hour, minute, second, microsecond
986

987
    Examples
988
    --------
989
    >>> absolut = [20070102.0034722, 20070102.0069444]
990
    >>> yr, mo, dy, hr, mi, sc, ms = _absolute2date(absolut,
991
    ...                                             'day as %Y%m%d.%f')
992
    >>> print(yr)
993
    [2007, 2007]
994
    >>> print(mi)
995
    [5, 10]
996

997
    """
998
    # old algorithm does decomposition on all array elements
999
    # should try similar to decode_dates_from_array for speed
1000
    # where decomposition is done for the first (oldest) element only
1001
    # and then timedeltas are added subsequently
1002
    mtimes = input2array(times, default=10101.)
12✔
1003
    mtimes = np.array(mtimes, dtype=np.longdouble)
12✔
1004

1005
    if units == 'day as %Y%m%d.%f':
12✔
1006
        # change to microseconds to catch round-off errors,
1007
        # i.e. cases 1 microsec less or greater than a second
1008
        # cf. issue #187 of cftime
1009
        # day of year in microseconds
1010
        fhoy = mtimes * 86400000000.
12✔
1011
        ihoy = np.rint(fhoy).astype(np.int64)
12✔
1012
        # Done in cftime for issue #187
1013
        # ihoy = np.where(ihoy%1000000 == 1,
1014
        #                 np.floor(fhoy).astype(np.int64), ihoy)
1015
        # ihoy = np.where(ihoy%1000000 == 999999,
1016
        #                 np.ceil(fhoy).astype(np.int64), ihoy)
1017
        # microsecond
1018
        msecond = ihoy % 1000000
12✔
1019
        ihoy    = ihoy // 1000000
12✔
1020
        # second
1021
        second = ihoy % 60
12✔
1022
        ihoy   = ihoy // 60
12✔
1023
        # minute
1024
        minute = ihoy % 60
12✔
1025
        ihoy   = ihoy // 60
12✔
1026
        # hour
1027
        hour = ihoy % 24
12✔
1028
        ihoy = ihoy // 24
12✔
1029
        # day
1030
        day = ihoy % 100
12✔
1031
        ihoy = ihoy // 100
12✔
1032
        # month
1033
        month = ihoy % 100
12✔
1034
        ihoy = ihoy // 100
12✔
1035
        # year
1036
        year = ihoy
12✔
1037
    elif units == 'month as %Y%m.%f':
12✔
1038
        fmo     = mtimes % 1.  # month fraction
12✔
1039
        # month
1040
        mtimes -= fmo
12✔
1041
        month   = np.rint(mtimes % 100.).astype(np.int64)
12✔
1042
        # year
1043
        mtimes -= month
12✔
1044
        year    = np.rint(mtimes / 100.).astype(np.int64)
12✔
1045
        # day of month in microseconds
1046
        leap    = np.where((((year % 4) == 0) & ((year % 100) != 0)) |
12✔
1047
                           ((year % 400) == 0), 1, 0)
1048
        dim     = np.array([ [-9] + _dayspermonth,
12✔
1049
                             [-9] + _dayspermonth_leap ])
1050
        fhoy = dim[(leap, month)] * fmo * 86400000000.
12✔
1051
        ihoy = np.rint(fhoy).astype(np.int64)
12✔
1052
        # Done in cftime for issue #187
1053
        # ihoy = np.where(ihoy%1000000 == 1,
1054
        #                 np.floor(fhoy).astype(np.int64), ihoy)
1055
        # ihoy = np.where(ihoy%1000000 == 999999,
1056
        #                 np.ceil(fhoy).astype(np.int64), ihoy)
1057
        # microsecond
1058
        msecond = ihoy % 1000000
12✔
1059
        ihoy    = ihoy // 1000000
12✔
1060
        # second
1061
        second = ihoy % 60
12✔
1062
        ihoy   = ihoy // 60
12✔
1063
        # minute
1064
        minute = ihoy % 60
12✔
1065
        ihoy   = ihoy // 60
12✔
1066
        # hour
1067
        hour = ihoy % 24
12✔
1068
        ihoy = ihoy // 24
12✔
1069
        # day
1070
        day = ihoy
12✔
1071
        # mtimes  = dim[(leap, month)] * fmo
1072
        # fdy     = mtimes % 1.  # day fraction
1073
        # mtimes -= fdy
1074
        # day     = np.rint(mtimes % 100.).astype(np.int64)
1075
        # # hour
1076
        # secs    = fdy * 86400.
1077
        # fsecs   = secs % 1.  # second fraction
1078
        # secs    = np.rint(secs)
1079
        # hour    = np.floor(secs / 3600.).astype(np.int64)
1080
        # # minute
1081
        # secs   -= 3600. * hour
1082
        # minute  = np.floor(secs / 60.).astype(np.int64)
1083
        # # second
1084
        # secs   -= 60. * minute
1085
        # second  = np.rint(secs).astype(np.int64)
1086
        # # millisecond
1087
        # msecond  = np.rint(fsecs * 1000000.).astype(np.int64)
1088
    elif units == 'year as %Y.%f':
12✔
1089
        # same as decimal date
1090
        year, month, day, hour, minute, second, msecond = _decimal2date(
12✔
1091
            times, 'decimal')
1092
    else:
1093
        raise ValueError(f'Unknown absolute units: {units}')
×
1094

1095
    return year, month, day, hour, minute, second, msecond
12✔
1096

1097

1098
#
1099
# Main routines and class
1100
#
1101

1102
def date2num(dates, units='', calendar=None, has_year_zero=None,
12✔
1103
             format='', timesep=' ', fr=False, return_arrays=False,
1104
             ensure_seconds=False):
1105
    """Return numeric time values given datetime objects or strings
1106

1107
    The units of the numeric time values are described by the
1108
    *units* and *calendar* keywords for CF-conform calendars, i.e.
1109
    *standard*, *gregorian*, *julian*, *proleptic_gregorian*,
1110
    *360_day*, *365_day*, *366_day*, *noleap*, *all_leap*.
1111
    See http://cfconventions.org/cf-conventions/cf-conventions#calendar
1112
    These times will be passed to cftime.num2date.
1113

1114
    Standard *units* are used for the non-CF-conform calendars
1115
    *excel*, *excel1900*, *excel1904*, and
1116
    *decimal*, *decimal360*, *decimal365*, *decimal366*,
1117
    and given *units* will hence be ignored.
1118

1119
    Parameters
1120
    ----------
1121
    dates : datetime instance or str, or array_like of datetime or str
1122
        datetime objects or strings with string representations.
1123
        datetime objects can either be Python datetime.datetime,
1124
        cf.datetime, or pyjams.datetime objects. If *dates* are strings,
1125
        then *format* keyword is relevant.
1126
    units : str, optional
1127
        Units string such as 'seconds since 1900-01-01 00:00:00' or
1128
        'day as %Y%m%d.%f'. Standard units corresponding to days after
1129
        day 0 of a given calendar will be used if omitted, i.e. assuming
1130
        Julian day ordinals.
1131

1132
        In the form *time_units since reference_time*,
1133
        *time_units* can be days, hours, minutes, seconds, milliseconds,
1134
        or microseconds. *reference_time* is the time origin.
1135
        *months since* is allowed only for the *360_day* calendar
1136
        and *common_years since* is allowed only for the *365_day* calendar.
1137

1138
        There are currently only three valid forms for *time_units as format*:
1139
        'day as %Y%m%d.%f', 'month as %Y%m.%f', 'year as %Y.%f'. The latter is
1140
        the same as 'calendar=decimal'. *calendar='decimal' will be set in
1141
        case units *time_units as format*.
1142
    calendar : str, optional
1143
        One of the supported calendars, i.e. the CF-conform calendars
1144
        *standard*, *gregorian*, *julian*, *proleptic_gregorian*,
1145
        *360_day*, *365_day*, *366_day*, *noleap*, *all_leap*,
1146
        as well as the non-CF-conform calendars
1147
        *excel*, *excel1900*, *excel1904*, and
1148
        *decimal*, *decimal360*, *decimal365*, *decimal366*.
1149
        *standard* will be taken by default, which is a mixed
1150
        Julian/Gregorian calendar.
1151
        The keyword takes precedence on calendar in datetime objects.
1152
    has_year_zero : bool, optional
1153
        Astronomical year numbering is used and the year zero exists, if set to
1154
        True. If set to False for real-world calendars, then historical year
1155
        numbering is used and the year 1 is preceded by year -1 and no year
1156
        zero exists.
1157
        The defaults are set to conform with CF version 1.9 conventions, i.e.
1158
        False for 'standard', 'gregorian', and 'julian', True
1159
        for 'proleptic_gregorian' (ISO 8601), and True for the idealized
1160
        calendars 'noleap'/'365_day', '360_day', 366_day'/'all_leap'.
1161
        Excel calendars *excel*, *excel1900*, *excel1904* start only 1900
1162
        or above so *has_year_zero* is always False.
1163
        Decimal calendars *decimal*, *decimal360*, *decimal365*, *decimal366*
1164
        have always year 0, i.e. *has_year_zero* is True.
1165
    format : str, optional
1166
        If *dates* are strings, then *format* is the Python
1167
        datetime.strftime/strptime format string if given. If empty (default),
1168
        then the routine pyjams.date2date will be used, which converts between
1169
        formats '%Y-%m-%d %H:%M:%S', '%d.%m.%Y %H:%M:%S', and '%m/%d/%Y
1170
        %H:%M:%S' (called English *YYYY-MM-DD hh:mm:ss*, standard
1171
        *DD.MM.YYYY hh:mm:ss*, and American *MM/DD/YYYY hh:mm:ss* in
1172
        pyjams.date2date), where times can be partial or missing.
1173
    timesep : str, optional
1174
        Separator string between date and time used by pyjams.date2date if
1175
        if *dates* are strings and *format* is empty (default: ' ')
1176
    fr : bool, optional
1177
        If True, pyjams.date2date will interpret input dates with '/'
1178
        separators not as the American format '%m/%d/%Y %H:%M:%S' but the
1179
        French way as '%d/%m/%Y %H:%M:%S', if *dates* are strings and
1180
        *format* is empty
1181
    return_arrays : bool, optional
1182
        If True, then return a tuple with individual arrays for
1183
        year, month, day, hour, minute, second, microsecond
1184
    ensure_seconds : bool, optional
1185
        If True, add small number (< 20 microseconds) to results to
1186
        ensure that back-conversion (`num2date`) gives the same results
1187
        up to seconds.
1188

1189
        Results should have an accuracy of approximately 1 microseconds.
1190
        This is however only possible with 64-bits if the discretization
1191
        of the time variable is an integer multiple of the units.
1192
        `cftime` introduced the longdouble keyword in `cftime.date2num`
1193
        to get microseconds accuracy. The datatype is, however, not available
1194
        on all platforms. The algorithm has the tendency to give
1195
        1-3 microseconds less at back-conversion in this case, which gives
1196
        times such as "11:59:59" instead of "12:00:00". `ensure_seconds`
1197
        toggles back-conversion above the next second to ensure that the
1198
        seconds are correct.
1199

1200
    Returns
1201
    -------
1202
    array_like
1203
       numeric time values
1204

1205
    Examples
1206
    --------
1207
    >>> idates = ['2000-01-05 12:30:15', '1810-04-24 16:15:10',
1208
    ...           '1630-07-15 10:20:40', '1510-09-20 14:35:50',
1209
    ...           '1271-03-18 19:41:34', '0619-08-27 11:08:37',
1210
    ...           '0001-01-01 12:00:00']
1211
    >>> decimal = date2num(idates, calendar='decimal')
1212
    >>> num2date(decimal, calendar='decimal', format='%Y-%m-%d %H:%M:%S')
1213
    ['2000-01-05 12:30:15',
1214
     '1810-04-24 16:15:10',
1215
     '1630-07-15 10:20:40',
1216
     '1510-09-20 14:35:50',
1217
     '1271-03-18 19:41:34',
1218
     '0619-08-27 11:08:37',
1219
     '0001-01-01 12:00:00']
1220

1221
    """
1222
    date0 = np.ravel(dates)[0]
12✔
1223
    if isinstance(date0, str):
12✔
1224
        if format:
12✔
1225
            iform = format
12✔
1226
        else:
1227
            iform = '%Y-%m-%d %H:%M:%S'
12✔
1228
        default = cf.real_datetime(1990, 1, 1).strftime(iform)
12✔
1229
    else:
1230
        default = cf.real_datetime(1990, 1, 1)
12✔
1231
    mdates = input2array(dates, default=default)
12✔
1232

1233
    # datetime with calendar
1234
    date0 = mdates[0]
12✔
1235
    if calendar is not None:
12✔
1236
        icalendar = calendar
12✔
1237
    else:
1238
        try:
12✔
1239
            icalendar = date0.calendar
12✔
1240
        except AttributeError:
12✔
1241
            # take standard otherwise
1242
            icalendar = 'standard'
12✔
1243

1244
    if icalendar:
12✔
1245
        icalendar = icalendar.lower()
12✔
1246
    else:
1247
        icalendar = 'standard'
12✔
1248
    if (icalendar not in _cfcalendars) and (icalendar not in _noncfcalendars):
12✔
1249
        raise ValueError(f'Unknown calendar: {icalendar}')
12✔
1250
    if not units:
12✔
1251
        units = _units_defaults(icalendar)
12✔
1252

1253
    # transform strings to datetime objects
1254
    # only possible for year > 0
1255
    isstr = all([ isinstance(dd, str) for dd in mdates ])
12✔
1256
    if isstr:
12✔
1257
        if format:
12✔
1258
            iform = format
12✔
1259
        else:
1260
            mdates = date2date(mdates, format='en', full=True,
12✔
1261
                               timesep=timesep, fr=fr)
1262
            iform = '%Y-%m-%d %H:%M:%S'
12✔
1263
        mmdates = [ cf.real_datetime.strptime(dd, iform)
12✔
1264
                    for dd in mdates ]
1265

1266
        if icalendar in _cfcalendars:
12✔
1267
            mmdates = [ cf.datetime(*to_tuple(dt), calendar=icalendar,
12✔
1268
                                    has_year_zero=has_year_zero)
1269
                        for dt in mmdates ]
1270
        else:
1271
            mmdates = [ datetime(*to_tuple(dt), calendar=icalendar,
12✔
1272
                                 has_year_zero=has_year_zero)
1273
                        for dt in mmdates ]
1274
        mdates = input2array(mmdates, default=cf.datetime(1990, 1, 1))
12✔
1275
    else:
1276
        mdates = input2array(dates, default=cf.real_datetime(1990, 1, 1))
12✔
1277

1278
    # if year, month, ... wanted, no need to go further
1279
    if return_arrays:
12✔
1280
        out = np.array([ to_tuple(dt) for dt in mdates ])
12✔
1281
        year        = array2input(out[:, 0], dates)
12✔
1282
        month       = array2input(out[:, 1], dates)
12✔
1283
        day         = array2input(out[:, 2], dates)
12✔
1284
        hour        = array2input(out[:, 3], dates)
12✔
1285
        minute      = array2input(out[:, 4], dates)
12✔
1286
        second      = array2input(out[:, 5], dates)
12✔
1287
        microsecond = array2input(out[:, 6], dates)
12✔
1288
        return year, month, day, hour, minute, second, microsecond
12✔
1289

1290
    # check if we can parse to cftime
1291
    if icalendar in _cfcalendars:
12✔
1292
        iscf = True
12✔
1293
    elif icalendar in _noncfcalendars:
12✔
1294
        iscf = False
12✔
1295
    else:  # pragma: no cover
1296
        # should be impossible to reach
1297
        raise ValueError(f'Unknown calendar: {icalendar}')
1298

1299
    unit, sincestr, remainder = _datesplit(units)
12✔
1300
    if sincestr == 'as':
12✔
1301
        iscf = False
12✔
1302
        icalendar = ''
12✔
1303

1304
    # use cftime.date2num if possible
1305
    if iscf:
12✔
1306
        if not remainder.startswith('-'):
12✔
1307
            if int(remainder.split('-')[0]) == 0:
12✔
1308
                if has_year_zero is not None:
×
1309
                    has_year_zero = True
×
1310
        if cf.__version__ > '1.6.1':
12✔
1311
            out = cf.date2num(mdates, units, calendar=icalendar,
12✔
1312
                              has_year_zero=has_year_zero,
1313
                              longdouble=True)
1314
        else:
1315
            out = cf.date2num(mdates, units, calendar=icalendar,
×
1316
                              has_year_zero=has_year_zero)
1317

1318
    if sincestr == 'as':
12✔
1319
        if units not in ['day as %Y%m%d.%f', 'month as %Y%m.%f',
12✔
1320
                         'year as %Y.%f']:
1321
            raise ValueError(f'Absolute date format unknown: {units}')
12✔
1322
        out = _dates2absolute(mdates, units)
12✔
1323

1324
    # use cftime.date2num with Excel
1325
    if icalendar in _excelcalendars:
12✔
1326
        cfcalendar = 'julian'
12✔
1327
        cfdates = [ cf.datetime(*to_tuple(dt), calendar=cfcalendar)
12✔
1328
                    for dt in mdates ]
1329
        if cf.__version__ > '1.6.1':
12✔
1330
            out = cf.date2num(cfdates, units, calendar=cfcalendar,
12✔
1331
                              has_year_zero=has_year_zero,
1332
                              longdouble=True)
1333
        else:
1334
            out = cf.date2num(cfdates, units, calendar=cfcalendar,
×
1335
                              has_year_zero=has_year_zero)
1336

1337
    # no cftime.num2date possible
1338
    if icalendar in _decimalcalendars:
12✔
1339
        out = _dates2decimal(mdates, icalendar)
12✔
1340

1341
    # toggle back-conversion above the second
1342
    if ensure_seconds:
12✔
1343
        out += np.abs(out) * deps
12✔
1344

1345
    out = array2input(out, dates)
12✔
1346
    return out
12✔
1347

1348

1349
def date2dec(*args, **kwargs):
12✔
1350
    """
1351
    Wrapper for :func:`date2num`
1352
    """
1353
    return date2num(*args, **kwargs)
12✔
1354

1355

1356
def num2date(times, units='', calendar='standard',
12✔
1357
             only_use_pyjams_datetimes=True,
1358
             only_use_cftime_datetimes=True,
1359
             only_use_python_datetimes=False,
1360
             has_year_zero=None,
1361
             format='', return_arrays=False):
1362
    """
1363
    Return datetime objects given numeric time values
1364

1365
    The units of the numeric time values are described by the
1366
    *units* and *calendar* keywords for CF-conform calendars, i.e.
1367
    *standard*, *gregorian*, *julian*, *proleptic_gregorian*,
1368
    *360_day*, *365_day*, *366_day*, *noleap*, *all_leap*.
1369
    See http://cfconventions.org/cf-conventions/cf-conventions#calendar
1370
    These times will be passed to cftime.num2date.
1371

1372
    Standard *units* are used for the non-CF-conform calendars
1373
    *excel*, *excel1900*, *excel1904*, and
1374
    *decimal*, *decimal360*, *decimal365*, *decimal366*,
1375
    and given *units* will hence be ignored.
1376

1377
    Parameters
1378
    ----------
1379
    times : float or array_like
1380
        Numeric time values
1381
    units : str, optional
1382
        Units string such as 'seconds since 1900-01-01 00:00:00' or
1383
        'day as %Y%m%d.%f'. Standard units corresponding to days after
1384
        day 0 of a given calendar will be used if omitted, i.e. assuming
1385
        Julian day ordinals.
1386

1387
        In the form *time_units since reference_time*,
1388
        *time_units* can be days, hours, minutes, seconds, milliseconds,
1389
        or microseconds. *reference_time* is the time origin.
1390
        *months since* is allowed only for the *360_day* calendar
1391
        and *common_years since* is allowed only for the *365_day* calendar.
1392

1393
        There are currently only three valid forms for *time_units as format*:
1394
        'day as %Y%m%d.%f', 'month as %Y%m.%f', 'year as %Y.%f'. The latter is
1395
        the same as 'calendar=decimal'. *calendar='decimal' will be set in
1396
        case units *time_units as format*.
1397
    calendar : str, optional
1398
        One of the support calendars, i.e. the CF-conform calendars
1399
        *standard*, *gregorian*, *julian*, *proleptic_gregorian*,
1400
        *360_day*, *365_day*, *366_day*, *noleap*, *all_leap*,
1401
        as well as the non-CF-conform calendars
1402
        *excel*, *excel1900*, *excel1904*, and
1403
        *decimal*, *decimal360*, *decimal365*, *decimal366*.
1404
        *standard* will be taken by default, which is a mixed
1405
        Julian/Gregorian calendar.
1406
    only_use_pyjams_datetimes : bool, optional
1407
        pyjams.datetime objects are returned by default.
1408
        Only if only_use_pyjams_datetimes is set to False (default: True) and
1409
        only_use_cftime_datetimes is set to True then cftime.datetime objects
1410
        will be returned where possible.
1411
        Only if only_use_pyjams_datetimes and only_use_cftime_datetimes are set
1412
        to False and only_use_python_datetimes is set to True then Python
1413
        datetime.datetime objects will be returned where possible.
1414
    only_use_cftime_datetimes : bool, optional
1415
        pyjams.datetime objects are returned by default.
1416
        Only if only_use_pyjams_datetimes is set to False and
1417
        only_use_cftime_datetimes is set to True (default: False) then
1418
        cftime.datetime objects will be returned where possible.
1419
        Only if only_use_pyjams_datetimes and only_use_cftime_datetimes are set
1420
        to False and only_use_python_datetimes is set to True then Python
1421
        datetime.datetime objects will be returned where possible.
1422
    only_use_python_datetimes : bool, optional
1423
        pyjams.datetime objects are returned by default.
1424
        Only if only_use_pyjams_datetimes is set to False and
1425
        only_use_cftime_datetimes is set to True then cftime.datetime objects
1426
        will be returned where possible.
1427
        Only if only_use_pyjams_datetimes and only_use_cftime_datetimes are set
1428
        to False and only_use_python_datetimes is set to True (default: False)
1429
        then Python datetime.datetime objects will be returned where possible.
1430
    has_year_zero : bool, optional
1431
        Astronomical year numbering is used and the year zero exists, if set to
1432
        True. If set to False for real-world calendars, then historical year
1433
        numbering is used and the year 1 is preceded by year -1 and no year
1434
        zero exists.
1435
        The defaults are set to conform with CF version 1.9 conventions, i.e.
1436
        False for 'standard', 'gregorian', and 'julian', True
1437
        for 'proleptic_gregorian' (ISO 8601), and True for the idealized
1438
        calendars 'noleap'/'365_day', '360_day', 366_day'/'all_leap'.
1439
        Excel calendars *excel*, *excel1900*, *excel1904* start only 1900
1440
        or above so *has_year_zero* is always False.
1441
        Decimal calendars *decimal*, *decimal360*, *decimal365*, *decimal366*
1442
        have always year 0, i.e. *has_year_zero* is True.
1443
    format : str, optional
1444
        If format string is given than a string representation of the
1445
        datetime objects will be returned.
1446
    return_arrays : bool, optional
1447
        If True, then return a tuple with individual arrays for
1448
        year, month, day, hour, minute, second, microsecond
1449

1450
    Returns
1451
    -------
1452
    array_like
1453
       datetime instances or string representations of datetime objects, or
1454
       tuple with individual arrays for year, month, day, hour, minute,
1455
       second, microsecond
1456

1457
    Examples
1458
    --------
1459
    >>> idates = ['2000-01-05 12:30:15', '1810-04-24 16:15:10',
1460
    ...           '1630-07-15 10:20:40', '1510-09-20 14:35:50',
1461
    ...           '1271-03-18 19:41:34', '0619-08-27 11:08:37',
1462
    ...           '0001-01-01 12:00:00']
1463
    >>> decimal = date2num(idates, calendar='decimal')
1464
    >>> num2date(decimal, calendar='decimal', format='%Y-%m-%d %H:%M:%S')
1465
    ['2000-01-05 12:30:15',
1466
     '1810-04-24 16:15:10',
1467
     '1630-07-15 10:20:40',
1468
     '1510-09-20 14:35:50',
1469
     '1271-03-18 19:41:34',
1470
     '0619-08-27 11:08:37',
1471
     '0001-01-01 12:00:00']
1472

1473
    """
1474
    if format and return_arrays:
12✔
1475
        raise ValueError('Keywords format and return_arrays mutually'
12✔
1476
                         ' exclusive')
1477
    if calendar:
12✔
1478
        calendar = calendar.lower()
12✔
1479
    else:
1480
        calendar = 'standard'
12✔
1481
    # check if we can parse to cftime
1482
    if calendar in _cfcalendars:
12✔
1483
        iscf = True
12✔
1484
    elif calendar in _noncfcalendars:
12✔
1485
        iscf = False
12✔
1486
    else:
1487
        raise ValueError(f'Unknown calendar: {calendar}')
12✔
1488
    if not units:
12✔
1489
        units = _units_defaults(calendar)
12✔
1490
    unit, sincestr, remainder = _datesplit(units)
12✔
1491
    if sincestr == 'as':
12✔
1492
        iscf = False
12✔
1493
        calendar = ''
12✔
1494

1495
    mtimes = input2array(times, default=1)
12✔
1496

1497
    # use cftime.num2date if possible
1498
    if iscf:
12✔
1499
        if remainder.startswith('-'):
12✔
1500
            # negative years
1501
            only_use_python_datetimes = False
12✔
1502
        else:
1503
            if int(remainder.split('-')[0]) == 0:
12✔
1504
                # reference year is year 0
1505
                only_use_python_datetimes = False
×
1506
                if has_year_zero is not None:
×
1507
                    has_year_zero = True
×
1508
        out = cf.num2date(
12✔
1509
            mtimes, units, calendar=calendar,
1510
            only_use_cftime_datetimes=only_use_cftime_datetimes,
1511
            only_use_python_datetimes=only_use_python_datetimes,
1512
            has_year_zero=has_year_zero)
1513
        if only_use_pyjams_datetimes:
12✔
1514
            out = [ datetime(*to_tuple(dt), calendar=calendar)
12✔
1515
                    for dt in out ]
1516

1517
    # cdo absolute time format
1518
    if sincestr == 'as':
12✔
1519
        if units not in ['day as %Y%m%d.%f', 'month as %Y%m.%f',
12✔
1520
                         'year as %Y.%f']:
1521
            raise ValueError(f'Absolute date format unknown: {units}')
12✔
1522
        # old algorithm does decomposition on all array elements
1523
        # should try similar to decode_dates_from_array for speed
1524
        # where decomposition is done for the first (oldest) element only
1525
        # and then timedeltas are added subsequently
1526
        year, month, day, hour, minute, second, microsecond = (
12✔
1527
            _absolute2date(mtimes, units))
1528

1529
        # shortcut
1530
        if return_arrays:
12✔
1531
            year        = array2input(year, times)
12✔
1532
            month       = array2input(month, times)
12✔
1533
            day         = array2input(day, times)
12✔
1534
            hour        = array2input(hour, times)
12✔
1535
            minute      = array2input(minute, times)
12✔
1536
            second      = array2input(second, times)
12✔
1537
            microsecond = array2input(microsecond, times)
12✔
1538
            return year, month, day, hour, minute, second, microsecond
12✔
1539

1540
        out = np.empty_like(year, dtype=object)
12✔
1541
        if ( (not only_use_pyjams_datetimes) and
12✔
1542
             (not only_use_cftime_datetimes) and
1543
             only_use_python_datetimes and
1544
             (year.min() > 0) ):
1545
            for i in range(year.size):
12✔
1546
                out[i] = cf.real_datetime(year[i], month[i], day[i],
12✔
1547
                                          hour[i], minute[i], second[i],
1548
                                          microsecond[i])
1549
        elif ( (not only_use_pyjams_datetimes) and
12✔
1550
               only_use_cftime_datetimes ):
1551
            for i in range(year.size):
12✔
1552
                out[i] = cf.datetime(year[i], month[i], day[i],
12✔
1553
                                     hour[i], minute[i], second[i],
1554
                                     microsecond[i])
1555
        else:
1556
            for i in range(year.size):
12✔
1557
                out[i] = datetime(year[i], month[i], day[i],
12✔
1558
                                  hour[i], minute[i], second[i],
1559
                                  microsecond[i],
1560
                                  calendar='decimal',
1561
                                  has_year_zero=has_year_zero)
1562

1563
    # use cftime.num2date for Excel but return pyjams.datetime
1564
    if calendar in _excelcalendars:
12✔
1565
        cfcalendar = 'julian'
12✔
1566
        cfunits = _units_defaults(calendar)
12✔
1567
        cfdates = cf.num2date(
12✔
1568
            mtimes, cfunits, calendar=cfcalendar,
1569
            only_use_cftime_datetimes=only_use_cftime_datetimes,
1570
            only_use_python_datetimes=False,
1571
            has_year_zero=has_year_zero)
1572

1573
        # shortcut
1574
        if format:
12✔
1575
            # Assure 4 digit years on all platforms
1576
            # see https://github.com/python/cpython/issues/76376
1577
            iform = format
×
1578
            if '%Y' in format:
×
1579
                format04 = format.replace('%Y', '%04Y')
×
1580
                try:
×
1581
                    dttest = cfdates[0].strftime(format04)
×
1582
                    if '4Y' in dttest:
×
1583
                        iform = format
×
1584
                    else:
1585
                        iform = format04
×
1586
                except ValueError:
×
1587
                    iform = format
×
1588
            out = [ dt.strftime(iform) for dt in cfdates ]
×
1589
            out = array2input(out, times)
×
1590
            return out
×
1591

1592
        out = [ datetime(*to_tuple(dt), calendar=calendar)
12✔
1593
                for dt in cfdates ]
1594

1595
    # no cftime.num2date possible
1596
    if calendar in _decimalcalendars:
12✔
1597
        # old algorithm does decomposition on all array elements
1598
        # should try similar to decode_dates_from_array for speed
1599
        # where decomposition is done for the first (oldest) element only
1600
        # and then timedeltas are added subsequently
1601
        year, month, day, hour, minute, second, microsecond = (
12✔
1602
            _decimal2date(mtimes, calendar))
1603

1604
        # shortcut
1605
        if return_arrays:
12✔
1606
            year        = array2input(year, times)
12✔
1607
            month       = array2input(month, times)
12✔
1608
            day         = array2input(day, times)
12✔
1609
            hour        = array2input(hour, times)
12✔
1610
            minute      = array2input(minute, times)
12✔
1611
            second      = array2input(second, times)
12✔
1612
            microsecond = array2input(microsecond, times)
12✔
1613
            return year, month, day, hour, minute, second, microsecond
12✔
1614

1615
        out = np.empty_like(year, dtype=object)
12✔
1616
        if ( (not only_use_pyjams_datetimes) and
12✔
1617
             (not only_use_cftime_datetimes) and
1618
             only_use_python_datetimes and (year.min() > 0) ):
1619
            for i in range(year.size):
12✔
1620
                out[i] = cf.real_datetime(year[i], month[i], day[i],
12✔
1621
                                          hour[i], minute[i], second[i],
1622
                                          microsecond[i])
1623
        elif ( (not only_use_pyjams_datetimes) and
12✔
1624
               only_use_cftime_datetimes ):
1625
            for i in range(year.size):
12✔
1626
                out[i] = cf.datetime(year[i], month[i], day[i],
12✔
1627
                                     hour[i], minute[i], second[i],
1628
                                     microsecond[i])
1629
        else:
1630
            for i in range(year.size):
12✔
1631
                out[i] = datetime(year[i], month[i], day[i],
12✔
1632
                                  hour[i], minute[i], second[i],
1633
                                  microsecond[i],
1634
                                  calendar=calendar,
1635
                                  has_year_zero=has_year_zero)
1636

1637
    if return_arrays:
12✔
1638
        out = np.array([ to_tuple(dt) for dt in out ])
×
1639
        year        = array2input(out[:, 0], times)
×
1640
        month       = array2input(out[:, 1], times)
×
1641
        day         = array2input(out[:, 2], times)
×
1642
        hour        = array2input(out[:, 3], times)
×
1643
        minute      = array2input(out[:, 4], times)
×
1644
        second      = array2input(out[:, 5], times)
×
1645
        microsecond = array2input(out[:, 6], times)
×
1646
        return year, month, day, hour, minute, second, microsecond
×
1647
    else:
1648
        if format:
12✔
1649
            # Assure 4 digit years on all platforms
1650
            # see https://github.com/python/cpython/issues/76376
1651
            iform = format
12✔
1652
            if '%Y' in format:
12✔
1653
                years0 = [ dd.year < 0 for dd in out ]
12✔
1654
                if any(years0):
12✔
1655
                    y4 = '%05Y'
×
1656
                else:
1657
                    y4 = '%04Y'
12✔
1658
                format04 = format.replace('%Y', y4)
12✔
1659
                try:
12✔
1660
                    dttest = out[0].strftime(format04)
12✔
1661
                    if ('4Y' in dttest) or ('5Y' in dttest):
8✔
1662
                        iform = format
4✔
1663
                    else:
1664
                        iform = format04
4✔
1665
                except ValueError:
4✔
1666
                    iform = format
4✔
1667
            out = [ dt.strftime(iform) for dt in out ]
12✔
1668

1669
        out = array2input(out, times)
12✔
1670
        return out
12✔
1671

1672

1673
def dec2date(*args, **kwargs):
12✔
1674
    """
1675
    Wrapper for :func:`num2date`
1676
    """
1677
    return num2date(*args, **kwargs)
12✔
1678

1679

1680
# Could not inherit from cf.datetime because all methods are read-only,
1681
# so had to re-code all methods, even identical ones
1682
class datetime(object):
12✔
1683
    """
1684
    This class mimics cftime.datetime but for non-CF-conform calendars
1685

1686
    The cftime.datetime class mimics itself datetime.datetime but
1687
    supports calendars other than the proleptic Gregorian calendar.
1688

1689
    This class supports timedelta operations by overloading +/-, and
1690
    comparisons with other instances using the same calendar.
1691

1692
    Current supported calendars are *excel*, *excel1900*, *excel1904*,
1693
    and *decimal*, *decimal360*, *decimal365*, *decimal366*. Excel
1694
    calendars are supposedly Julian calendars with other reference dates.
1695
    Day 0 of *excel*/*excel1900* starts at 1899-12-31 00:00:00 and day 0
1696
    of *excel1904* (old Lotus date) starts at 1903-12-31 00:00:00.
1697
    Decimal calendars have the form "%Y.%f", where the fractional year
1698
    assumes leap years as the proleptic Gregorian calendar, or fixed 360,
1699
    365, or 366 days per year.
1700

1701
    If the has_year_zero keyword argument is set to True, astronomical year
1702
    numbering is used and the year zero exists, which is the default for the
1703
    decimal calendars. The keyword will be ignored for Excel calendars.
1704

1705
    The class has the methods isoformat, strftime, timetuple, replace,
1706
    dayofwk, dayofyr, daysinmonth, __repr__, __format__, __add__, __sub__,
1707
    __str__, and comparison methods.
1708

1709
    The default format of the string produced by strftime is controlled by
1710
    self.format (default %Y-%m-%d %H:%M:%S).
1711

1712
    """
1713
    # Python's datetime.datetime uses the proleptic Gregorian
1714
    # calendar. This boolean is used to decide whether a
1715
    # cftime.datetime instance can be converted to
1716
    # datetime.datetime.
1717
    def __init__(self, year, month, day,
12✔
1718
                 hour=0, minute=0, second=0, microsecond=0,
1719
                 dayofwk=-1, dayofyr=-1,
1720
                 calendar='decimal', has_year_zero=None):
1721
        """
1722
        Initialise new datetime instance
1723

1724
        """
1725
        self.year = year
12✔
1726
        self.month = month
12✔
1727
        self.day = day
12✔
1728
        self.hour = hour
12✔
1729
        self.minute = minute
12✔
1730
        self.second = second
12✔
1731
        self.microsecond = microsecond
12✔
1732
        self._dayofwk = dayofwk
12✔
1733
        self._dayofyr = dayofyr
12✔
1734
        self.tzinfo = None
12✔
1735
        self.cf = None
12✔
1736
        if calendar:
12✔
1737
            self.calendar = calendar.lower()
12✔
1738
        else:
1739
            self.calendar = 'decimal'
×
1740
        # if self.calendar in _cfcalendars:
1741
        #     raise ValueError(f'Use cftime.datetime for CF-conform'
1742
        #                      f' calendars: {self.calendar}')
1743
        if has_year_zero is None:
12✔
1744
            self.has_year_zero = _year_zero_defaults(self.calendar)
12✔
1745
        else:
1746
            self.has_year_zero = has_year_zero
12✔
1747
        # if self.calendar and (self.calendar not in _noncfcalendars):
1748
        #     raise ValueError(f'Unknown calendar: {self.calendar}')
1749
        if ( self.calendar and (self.calendar not in _cfcalendars) and
12✔
1750
             (self.calendar not in _noncfcalendars) ):
1751
            raise ValueError(f'Unknown calendar: {self.calendar}')
×
1752
        if self.calendar in _cfcalendars:
12✔
1753
            self.cf = cf.datetime(self.year, self.month, self.day,
12✔
1754
                                  self.hour, self.minute, self.second,
1755
                                  self.microsecond,
1756
                                  calendar=self.calendar,
1757
                                  has_year_zero=self.has_year_zero)
1758
            self.datetime_compatible = self.cf.datetime_compatible
12✔
1759
        elif self.calendar in _excelcalendars:
12✔
1760
            self.cf = cf.datetime(self.year, self.month, self.day,
12✔
1761
                                  self.hour, self.minute, self.second,
1762
                                  self.microsecond,
1763
                                  calendar='julian',
1764
                                  has_year_zero=self.has_year_zero)
1765
            self.datetime_compatible = self.cf.datetime_compatible
12✔
1766
        else:
1767
            self.datetime_compatible = False
12✔
1768
        self.assert_valid_date()
12✔
1769

1770
    def assert_valid_date(self):
12✔
1771
        """
1772
        Check that datetime is a valid date for given calendar
1773

1774
        """
1775
        # year
1776
        if not self.has_year_zero:
12✔
1777
            if self.year == 0:
12✔
1778
                raise ValueError("Invalid year provided in {0!r}".format(self))
12✔
1779
        # Comment next block to allow negative days with Excel calendars,
1780
        # which does not exist in Excel
1781
        # if ( ((self.calendar == 'excel') or (self.calendar == 'excel1900'))
1782
        #      and self.year < 1900):
1783
        #     raise ValueError('Year must be >= 1900 for Excel dates')
1784
        # if ( (self.calendar == 'excel1904') and self.year < 1904):
1785
        #     raise ValueError('Year must be >= 1904 for Excel1904 dates')
1786

1787
        # month
1788
        if (self.month < 1) or (self.month > 12):
12✔
1789
            raise ValueError("Invalid month provided in {0!r}".format(self))
12✔
1790

1791
        # day
1792
        month_length = _month_lengths(self.year, self.calendar,
12✔
1793
                                      self.has_year_zero)
1794
        if (self.day < 1) or (self.day > month_length[self.month - 1]):
12✔
1795
            raise ValueError(
12✔
1796
                "Invalid day number provided in {0!r}".format(self))
1797

1798
        # hour
1799
        if (self.hour < 0) or (self.hour > 23):
12✔
1800
            raise ValueError("Invalid hour provided in {0!r}".format(self))
12✔
1801

1802
        # minute
1803
        if (self.minute < 0) or (self.minute > 59):
12✔
1804
            raise ValueError("Invalid minute provided in {0!r}".format(self))
12✔
1805

1806
        # second
1807
        if (self.second < 0) or (self.second > 59):
12✔
1808
            raise ValueError("Invalid second provided in {0!r}".format(self))
12✔
1809

1810
        # microsecond
1811
        if (self.microsecond) < 0 or (self.microsecond > 999999):
12✔
1812
            raise ValueError(
12✔
1813
                "Invalid microsecond provided in {0!r}".format(self))
1814

1815
    def change_calendar(self, calendar, has_year_zero=None):
12✔
1816
        return NotImplemented
×
1817

1818
    def dayofwk(self):
12✔
1819
        """
1820
        Day of the week
1821

1822
        Identical to cftime.datetime
1823

1824
        """
1825
        if (self._dayofwk < 0) and self.calendar:
12✔
1826
            ord0 = 0
12✔
1827
            if self.calendar == 'decimal':
12✔
1828
                ord0 = 1721425
12✔
1829
            jd = self.toordinal() + ord0
12✔
1830
            dayofwk = (jd + 1) % 7
12✔
1831
            # convert to ISO 8601 (0 = Monday, 6 = Sunday), like python
1832
            # datetime
1833
            dayofwk -= 1
12✔
1834
            if dayofwk == -1:
12✔
1835
                dayofwk = 6
12✔
1836
            # cache results for dayofwk
1837
            self._dayofwk = dayofwk
12✔
1838
            return dayofwk
12✔
1839
        else:
1840
            return self._dayofwk
12✔
1841

1842
    def dayofyr(self):
12✔
1843
        """
1844
        Day of year
1845

1846
        """
1847
        if (self._dayofyr < 0) and self.calendar:
12✔
1848
            if self.calendar == 'decimal360':
12✔
1849
                # dayofyr = (self.month - 1) * 30 + self.day
1850
                dayofyr = _cumdayspermonth_360[self.month - 1] + self.day
12✔
1851
            else:
1852
                if _is_leap(self.year, self.calendar,
12✔
1853
                            has_year_zero=self.has_year_zero):
1854
                    dayofyr = _cumdayspermonth_leap[self.month - 1] + self.day
12✔
1855
                else:
1856
                    dayofyr = _cumdayspermonth[self.month - 1] + self.day
12✔
1857
            # cache results for dayofyr
1858
            self._dayofyr = dayofyr
12✔
1859
            return dayofyr
12✔
1860
        else:
1861
            return self._dayofyr
12✔
1862

1863
    def daysinmonth(self):
12✔
1864
        """
1865
        Number of days in current month
1866

1867
        """
1868
        if self.calendar == 'decimal360':
12✔
1869
            # return 30
1870
            return _dayspermonth_360[self.month - 1]
12✔
1871
        else:
1872
            if _is_leap(self.year, self.calendar,
12✔
1873
                        has_year_zero=self.has_year_zero):
1874
                return _dayspermonth_leap[self.month - 1]
12✔
1875
            else:
1876
                return _dayspermonth[self.month - 1]
12✔
1877

1878
    def format(self):
12✔
1879
        """
1880
        Standard date representation
1881

1882
        Identical to cftime.datetime
1883

1884
        """
1885
        return '%Y-%m-%d %H:%M:%S'
12✔
1886

1887
    def fromordinal(jday, calendar='decimal', has_year_zero=None):
12✔
1888
        return NotImplemented
×
1889

1890
    def isoformat(self, sep='T', timespec='auto'):
12✔
1891
        """
1892
        ISO date representation
1893

1894
        """
1895
        if self.year < 0:
12✔
1896
            form0 = '{:05d}-{:02d}-{:02d}'
12✔
1897
        else:
1898
            form0 = '{:04d}-{:02d}-{:02d}'
12✔
1899
        if timespec == 'days':
12✔
1900
            form = form0
12✔
1901
            return form.format(self.year, self.month, self.day)
12✔
1902
        elif timespec == 'hours':
12✔
1903
            form = form0 + '{:s}{:02d}'
12✔
1904
            return form.format(self.year, self.month, self.day, sep,
12✔
1905
                               self.hour)
1906
        elif timespec == 'minutes':
12✔
1907
            form = form0 + '{:s}{:02d}:{:02d}'
12✔
1908
            return form.format(self.year, self.month, self.day, sep,
12✔
1909
                               self.hour, self.minute)
1910
        elif timespec == 'seconds':
12✔
1911
            form = form0 + '{:s}{:02d}:{:02d}:{:02d}'
12✔
1912
            return form.format(self.year, self.month, self.day, sep,
12✔
1913
                               self.hour, self.minute, self.second)
1914
        elif timespec in ['auto', 'microseconds', 'milliseconds']:
12✔
1915
            second = '{:02d}'.format(self.second)
12✔
1916
            if timespec == 'milliseconds':
12✔
1917
                millisecs = int(round(self.microsecond / 1000, 0))
12✔
1918
                second += '.{:03d}'.format(millisecs)
12✔
1919
            elif timespec == 'microseconds':
12✔
1920
                second += '.{:06d}'.format(self.microsecond)
12✔
1921
            else:
1922
                if self.microsecond > 0:
12✔
1923
                    second += '.{:06d}'.format(self.microsecond)
×
1924
            form = form0 + '{:s}{:02d}:{:02d}:{:s}'
12✔
1925
            return form.format(self.year, self.month, self.day, sep,
12✔
1926
                               self.hour, self.minute, second)
1927
        else:
1928
            raise ValueError('illegal timespec')
12✔
1929

1930
    def replace(self, **kwargs):
12✔
1931
        """
1932
        Return datetime with new specified fields
1933

1934
        Identical to cftime.datetime
1935

1936
        """
1937
        args = {"year": self.year,
12✔
1938
                "month": self.month,
1939
                "day": self.day,
1940
                "hour": self.hour,
1941
                "minute": self.minute,
1942
                "second": self.second,
1943
                "microsecond": self.microsecond,
1944
                "has_year_zero": self.has_year_zero,
1945
                "calendar": self.calendar}
1946

1947
        if 'dayofyr' in kwargs or 'dayofwk' in kwargs:
12✔
1948
            raise ValueError('Replacing the dayofyr or dayofwk of a datetime'
12✔
1949
                             ' is not supported.')
1950

1951
        if 'calendar' in kwargs:
12✔
1952
            raise ValueError('Replacing the calendar of a datetime is '
12✔
1953
                             'not supported.')
1954

1955
        # if attempting to set year to zero, also set has_year_zero=True
1956
        # (issue #248)
1957
        if 'year' in kwargs:
12✔
1958
            if (kwargs['year'] == 0) and ('has_year_zero' not in kwargs):
12✔
1959
                kwargs['has_year_zero'] = True
×
1960

1961
        for name, value in kwargs.items():
12✔
1962
            args[name] = value
12✔
1963

1964
        return self.__class__(**args)
12✔
1965

1966
    def round_microseconds(self):
12✔
1967
        """
1968
        Mathematically round microseconds to nearest second.
1969

1970
        """
1971
        iadd = round(self.microsecond / 1000000.)
12✔
1972
        if iadd:
12✔
1973
            other = timedelta(seconds=1)
12✔
1974
            odt = self.__add__(other)
12✔
1975
        else:
1976
            odt = self
12✔
1977
        odt.microsecond = 0
12✔
1978
        return odt
12✔
1979

1980
    def strftime(self, format=None):
12✔
1981
        """
1982
        Return a string representing the date, controlled by an explicit format
1983
        string
1984

1985
        For a complete list of formatting directives, see section 'strftime()
1986
        and strptime() Behavior' in the base Python documentation.
1987

1988
        Identical to cftime.datetime
1989

1990
        """
1991
        if format is None:
12✔
1992
            format = self.format()
12✔
1993
        return _strftime(self, format)
12✔
1994

1995
    def timetuple(self):
12✔
1996
        """
1997
        Return a time.struct_time such as returned by time.localtime()
1998

1999
        The DST flag is -1. d.timetuple() is equivalent to
2000
        time.struct_time((d.year, d.month, d.day, d.hour, d.minute,
2001
        d.second, d.weekday(), yday, dst)), where yday is the
2002
        day number within the current year starting with 1 for January 1st.
2003

2004
        Identical to cftime.datetime
2005

2006
        """
2007
        return ptime.struct_time((self.year, self.month, self.day, self.hour,
12✔
2008
                                  self.minute, self.second, self.dayofwk(),
2009
                                  self.dayofyr(), -1))
2010

2011
    def toordinal(self, fractional=False):
12✔
2012
        """
2013
        Julian day (integer) ordinal
2014

2015
        Day 0 starts at noon January 1 of the year -4713 for the
2016
        Excel calendars.
2017

2018
        Day 0 starts at noon on January 1 of the year zero for
2019
        the decimal calendars.
2020

2021
        If fractional=True, fractional part of day is included (default
2022
        False).
2023

2024
        """
2025
        ijd = _int_julian_day_from_date(
12✔
2026
            self.year, self.month, self.day, self.calendar,
2027
            has_year_zero=self.has_year_zero)
2028
        if fractional:
12✔
2029
            # At this point ijd is an integer representing noon UTC on the
2030
            # given year, month, day.
2031
            # Compute fractional day from hour, minute, second, microsecond
2032
            fracday = ( self.hour / np.array(24., np.longdouble) +
12✔
2033
                        self.minute / np.array(1440., np.longdouble) +
2034
                        (self.second +
2035
                         self.microsecond / (np.array(1.e6, np.longdouble))) /
2036
                        np.array(86400., np.longdouble) )
2037
            return ijd - 0.5 + fracday
12✔
2038
        else:
2039
            return ijd
12✔
2040

2041
    def to_tuple(self):
12✔
2042
        """
2043
        Turn a datetime instance into a tuple of integers. Elements go
2044
        in the order of decreasing significance, making it easy to compare
2045
        datetime instances. Parts of the state that don't affect ordering
2046
        are omitted.
2047
        to_tuple(dt) is identical to (dt.year, dt.month, dt.day,
2048
        dt.hour, dt.minute, dt.second, dt.microsecond).
2049
        Compare to timetuple().
2050

2051
        Identical to cftime.datetime
2052

2053
        """
2054
        return (self.year, self.month, self.day, self.hour, self.minute,
12✔
2055
                self.second, self.microsecond)
2056

2057
    def _add_timedelta(self, other):
12✔
2058
        return self.__add__(other)
12✔
2059

2060
    def _getstate(self):
12✔
2061
        """
2062
        return args and kwargs needed to create class instance
2063

2064
        Identical to cftime.datetime
2065

2066
        """
2067
        args = (self.year, self.month, self.day)
12✔
2068
        kwargs = {'hour': self.hour,
12✔
2069
                  'minute': self.minute,
2070
                  'second': self.second,
2071
                  'microsecond': self.microsecond,
2072
                  'dayofwk': self._dayofwk,
2073
                  'dayofyr': self._dayofyr,
2074
                  'calendar': self.calendar,
2075
                  'has_year_zero': self.has_year_zero}
2076
        return args, kwargs
12✔
2077

2078
    def _to_real_datetime(self):
12✔
2079
        """
2080
        Extended Python datetime class
2081

2082
        Extra attributes are  dayofwk, dayofyr, and daysinmonth.
2083

2084
        Identical to cftime.datetime
2085

2086
        """
2087
        return cf.real_datetime(self.year, self.month, self.day,
12✔
2088
                                self.hour, self.minute, self.second,
2089
                                self.microsecond)
2090

2091
    def __add__(self, other):
12✔
2092
        """
2093
        Add timedelta to datetime
2094

2095
        """
2096
        if isinstance(self, datetime) and isinstance(other, timedelta):
12✔
2097
            dt = self
12✔
2098
            calendar = self.calendar
12✔
2099
            # has_year_zero = self.has_year_zero
2100
            delta = other
12✔
2101
        elif isinstance(self, timedelta) and isinstance(other, datetime):
×
2102
            dt = other
×
2103
            calendar = other.calendar
×
2104
            # has_year_zero = other.has_year_zero
2105
            delta = self
×
2106
        else:
2107
            return NotImplemented
×
2108
        # dt = self
2109
        # calendar = self.calendar
2110
        # has_year_zero = self.has_year_zero
2111
        # delta = other
2112
        if calendar == 'decimal360':
12✔
2113
            with warnings.catch_warnings():
12✔
2114
                warnings.simplefilter("ignore")
12✔
2115
                cfdt = cf.datetime(*to_tuple(dt), calendar='360_day',
12✔
2116
                                   has_year_zero=dt.has_year_zero)
2117
            cfdt = cfdt + delta
12✔
2118
            year, month, day, hour, minute, second, microsecond = (
12✔
2119
                cfdt.year, cfdt.month, cfdt.day, cfdt.hour, cfdt.minute,
2120
                cfdt.second, cfdt.microsecond)
2121
        else:
2122
            year, month, day, hour, minute, second, microsecond = (
12✔
2123
                _add_timedelta(dt, delta))
2124
        return datetime(year, month, day,
12✔
2125
                        hour, minute, second, microsecond,
2126
                        calendar=dt.calendar, has_year_zero=dt.has_year_zero)
2127

2128
    def __eq__(self, other):
12✔
2129
        """
2130
        Compare two datetime instances
2131

2132
        """
2133
        dt = self
12✔
2134
        if isinstance(other, (datetime, cf.datetime)):
12✔
2135
            dt_other = other
12✔
2136
            # comparing two datetime instances
2137
            if ( (dt.calendar == dt_other.calendar) and
12✔
2138
                 (dt.has_year_zero == dt_other.has_year_zero) ):
2139
                return to_tuple(dt) == to_tuple(dt_other)
12✔
2140
            else:
2141
                ord1 = 0
×
2142
                if dt.calendar == 'decimal':
×
2143
                    ord1 = 1721425
×
2144
                ord2 = 0
×
2145
                if dt_other.calendar == 'decimal':  # pragma: no cover
2146
                    ord2 = 1721425
2147
                return (dt.toordinal(fractional=True) + ord1 ==
×
2148
                        dt_other.toordinal(fractional=True) + ord2)
2149
        else:
2150
            return NotImplemented
×
2151

2152
    def __format__(self, format):
12✔
2153
        """
2154
        Return a string representing the date, controlled by an explicit format
2155
        string
2156

2157
        For a complete list of formatting directives, see section 'strftime()
2158
        and strptime() Behavior' in the base Python documentation.
2159

2160
        Identical to cftime.datetime
2161

2162
        """
2163
        # the string format "{t_obj}".format(t_obj=t_obj)
2164
        # without an explicit format gives an empty string (format='')
2165
        # so set this to None to get the default strftime behaviour
2166
        if not format:
12✔
2167
            format = None
12✔
2168
        return self.strftime(format)
12✔
2169

2170
    def __hash__(self):
12✔
2171
        """
2172
        Identical to cftime.datetime
2173

2174
        """
2175
        try:
×
2176
            d = self._to_real_datetime()
×
2177
        except ValueError:
×
2178
            return hash(self.timetuple())
×
2179
        return hash(d)
×
2180

2181
    def __reduce__(self):
12✔
2182
        """
2183
        Special method that allows instance to be pickled
2184

2185
        Identical to cftime.datetime
2186

2187
        """
2188
        args, kwargs = self._getstate()
×
2189
        date_type = type(self)
×
2190
        return (_create_datetime, (date_type, args, kwargs))
×
2191

2192
    def __repr__(self):
2193
        """
2194
        String representation
2195

2196
        """
2197
        return ("{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8},"
2198
                " calendar={9}, has_year_zero={10})".format(
2199
                    'pyjams',
2200
                    self.__class__.__name__,
2201
                    self.year, self.month, self.day,
2202
                    self.hour, self.minute, self.second,
2203
                    self.microsecond, self.calendar, self.has_year_zero))
2204

2205
    def __str__(self):
2206
        """
2207
        ISO date representation
2208

2209
        Identical to cftime.datetime
2210

2211
        """
2212
        return self.isoformat(' ')
2213

2214
    def __sub__(self, other):
12✔
2215
        """
2216
        Substract timedelta or datetime from the datetime instance
2217

2218
        """
2219
        if isinstance(self, datetime):  # left arg is a datetime instance
12✔
2220
            dt = self
12✔
2221
            if isinstance(other, datetime):
12✔
2222
                # datetime - datetime
2223
                if dt.calendar != other.calendar:
12✔
2224
                    raise TypeError("Cannot compute the time difference"
×
2225
                                    " between dates with different calendars")
2226
                if dt.calendar == "":
12✔
2227
                    raise TypeError("Cannot compute the time difference"
×
2228
                                    " between dates that are not"
2229
                                    " calendar-aware")
2230
                if dt.has_year_zero != other.has_year_zero:
12✔
2231
                    raise TypeError("Cannot compute the time difference"
×
2232
                                    " between dates with different year zero"
2233
                                    " conventions")
2234
                ord1 = 0
12✔
2235
                if self.calendar == 'decimal':
12✔
2236
                    ord1 = 1721425
12✔
2237
                ord2 = 0
12✔
2238
                if other.calendar == 'decimal':
12✔
2239
                    ord2 = 1721425
12✔
2240
                ordinal_self = self.toordinal() + ord1  # julian day
12✔
2241
                ordinal_other = other.toordinal() + ord2
12✔
2242
                days = ordinal_self - ordinal_other
12✔
2243
                seconds_self = dt.second + 60 * dt.minute + 3600 * dt.hour
12✔
2244
                seconds_other = (other.second + 60 * other.minute +
12✔
2245
                                 3600 * other.hour)
2246
                seconds = seconds_self - seconds_other
12✔
2247
                microseconds = dt.microsecond - other.microsecond
12✔
2248
                return timedelta(days, seconds, microseconds)
12✔
2249
            elif (isinstance(other, datetime_python) or
12✔
2250
                  isinstance(other, cf.real_datetime)):
2251
                # datetime - real_datetime
2252
                if not dt.datetime_compatible:
×
2253
                    msg = ("Cannot compute the time difference between dates"
×
2254
                           " with different calendars. One of the datetime"
2255
                           " objects may have been converted to a native"
2256
                           " python datetime instance. Try using"
2257
                           " only_use_cftime_datetimes=True when creating the"
2258
                           " datetime object.")
2259
                    raise TypeError(msg)
×
2260
                return dt._to_real_datetime() - other
×
2261
            elif isinstance(other, timedelta):
12✔
2262
                # datetime - timedelta
2263
                if dt.calendar == 'decimal360':
12✔
2264
                    with warnings.catch_warnings():
12✔
2265
                        warnings.simplefilter("ignore")
12✔
2266
                        cfdt = cf.datetime(*to_tuple(dt), calendar='360_day',
12✔
2267
                                           has_year_zero=dt.has_year_zero)
2268
                    cfdt = cfdt - other
12✔
2269
                    year, month, day, hour, minute, second, microsecond = (
12✔
2270
                        cfdt.year, cfdt.month, cfdt.day, cfdt.hour,
2271
                        cfdt.minute, cfdt.second, cfdt.microsecond)
2272
                else:
2273
                    year, month, day, hour, minute, second, microsecond = (
12✔
2274
                        _add_timedelta(dt, -other))
2275
                return datetime(year, month, day,
12✔
2276
                                hour, minute, second, microsecond,
2277
                                calendar=dt.calendar,
2278
                                has_year_zero=dt.has_year_zero)
2279
            else:
2280
                return NotImplemented
×
2281
        else:
2282
            if ( isinstance(self, datetime_python) or
×
2283
                 isinstance(self, cf.real_datetime) ):
2284
                # real_datetime - datetime
2285
                if not other.datetime_compatible:
×
2286
                    msg = ("Cannot compute the time difference between dates"
×
2287
                           " with different calendars. One of the datetime"
2288
                           " objects may have been converted to a native"
2289
                           " python datetime instance. Try using"
2290
                           " only_use_cftime_datetimes=True when creating the"
2291
                           " datetime object.")
2292
                    raise TypeError(msg)
×
2293
                return self - other._to_real_datetime()
×
2294
            else:
2295
                return NotImplemented
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc