Skip to content

Commit

Permalink
Merge cf6b425 into ff4ac98
Browse files Browse the repository at this point in the history
  • Loading branch information
jswhit committed Oct 28, 2020
2 parents ff4ac98 + cf6b425 commit d4fbbaf
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 19 deletions.
4 changes: 4 additions & 0 deletions Changelog
Expand Up @@ -6,6 +6,10 @@ version 1.3.0 (release tag v1.3.0rel)
methods, like dayofwk, dayofyr, __add__ and __sub__, will not work). Fixes issue #198.
The calendar specific sub-classes are now deprecated, but remain for now
as stubs that just instantiate the base class and override __repr__.
* update regex in _cpdef _parse_date so reference years with more than four
digits can be handled. Allow 'calendar=None' inr cftime.date2num (calendar associated with first
input datetime object is used - we may want to change the default from 'standard' to None
in a future release).

version 1.2.1 (release tag v1.2.1rel)
=====================================
Expand Down
62 changes: 43 additions & 19 deletions cftime/_cftime.pyx
Expand Up @@ -58,7 +58,7 @@ __version__ = '1.3.0'
# Adapted from http://delete.me.uk/2005/03/iso8601.html
# Note: This regex ensures that all ISO8601 timezone formats are accepted - but, due to legacy support for other timestrings, not all incorrect formats can be rejected.
# For example, the TZ spec "+01:0" will still work even though the minutes value is only one character long.
ISO8601_REGEX = re.compile(r"(?P<year>[+-]?[0-9]{1,4})(-(?P<month>[0-9]{1,2})(-(?P<day>[0-9]{1,2})"
ISO8601_REGEX = re.compile(r"(?P<year>[+-]?[0-9]+)(-(?P<month>[0-9]{1,2})(-(?P<day>[0-9]{1,2})"
r"(((?P<separator1>.)(?P<hour>[0-9]{1,2}):(?P<minute>[0-9]{1,2})(:(?P<second>[0-9]{1,2})(\.(?P<fraction>[0-9]+))?)?)?"
r"((?P<separator2>.?)(?P<timezone>Z|(([-+])([0-9]{2})((:([0-9]{2}))|([0-9]{2}))?)))?)?)?)?"
)
Expand Down Expand Up @@ -184,38 +184,62 @@ def date2num(dates,units,calendar='standard'):
returns a numeric time value, or an array of numeric time values
with approximately 1 microsecond accuracy.
"""
calendar = calendar.lower()
basedate = _dateparse(units,calendar=calendar)
(unit, isostring) = _datesplit(units)
# real-world calendars limited to positive reference years.
if calendar in ['julian', 'standard', 'gregorian', 'proleptic_gregorian']:
if basedate.year == 0:
msg='zero not allowed as a reference year, does not exist in Julian or Gregorian calendars'
raise ValueError(msg)
if unit not in UNIT_CONVERSION_FACTORS:
raise ValueError("Unsupported time units provided, {!r}.".format(unit))
if unit in ["months", "month"] and calendar != "360_day":
raise ValueError("Units of months only valid for 360_day calendar.")
factor = UNIT_CONVERSION_FACTORS[unit]
can_use_python_basedatetime = _can_use_python_datetime(basedate,calendar)

# input a scale or array-like?
isscalar = False
try:
dates[0]
except:
isscalar = True

# masked array input?
ismasked = False
if np.ma.isMA(dates) and np.ma.is_masked(dates):
mask = dates.mask
ismasked = True
dates = np.asanyarray(dates)
shape = dates.shape
# are all dates python datetime instances?

# are all input dates 'real' python datetime objects?
dates = np.asanyarray(dates) # convert to numpy array
shape = dates.shape # save shape of input
all_python_datetimes = True
for date in dates.flat:
if not isinstance(date,datetime_python):
all_python_datetimes = False
break

# if calendar is None or '', use calendar of first input cftime.datetime instances.
# if inputs are 'real' python datetime instances, use propleptic gregorian.
if not calendar:
if all_python_datetimes:
calendar = 'proleptic_gregorian'
else:
if isscalar:
d0 = dates.item()
else:
d0 = dates.flat[0]
if isinstance(d0,datetime_python):
calendar = 'proleptic_gregorian'
else:
try:
calendar = d0.calendar
except AttributeError:
raise ValueError('no calendar specified',type(d0))

calendar = calendar.lower()
basedate = _dateparse(units,calendar=calendar)
(unit, isostring) = _datesplit(units)
# real-world calendars cannot have zero as a reference year.
if calendar in ['julian', 'standard', 'gregorian', 'proleptic_gregorian']:
if basedate.year == 0:
msg='zero not allowed as a reference year, does not exist in Julian or Gregorian calendars'
raise ValueError(msg)
if unit not in UNIT_CONVERSION_FACTORS:
raise ValueError("Unsupported time units provided, {!r}.".format(unit))
if unit in ["months", "month"] and calendar != "360_day":
raise ValueError("Units of months only valid for 360_day calendar.")
factor = UNIT_CONVERSION_FACTORS[unit]
can_use_python_basedatetime = _can_use_python_datetime(basedate,calendar)

if can_use_python_basedatetime and all_python_datetimes:
use_python_datetime = True
if not isinstance(basedate, datetime_python):
Expand Down Expand Up @@ -1555,7 +1579,7 @@ cdef tuple add_timedelta(datetime dt, delta, bint (*is_leap)(int), bint julian_g
if month > 12:
month = 1
year += 1
if year == 0:
if year == 0 and not has_year_zero:
year = 1
month_length = month_lengths(is_leap, year)
day = 1
Expand Down
23 changes: 23 additions & 0 deletions test/test_cftime.py
Expand Up @@ -772,6 +772,9 @@ def roundtrip(delta,eps,units):
dt1 = datetime(1810, 4, 24, 16, 15, 10)
units = 'days since -4713-01-01 12:00'
dt2 = num2date(date2num(dt1, units), units)
# switch to these if default calendar for date2num changed to None
#dt2 = num2date(date2num(dt1, units), units, calendar='proleptic_gregorian')
#dt2 = num2date(date2num(dt1, units, calendar='standard'), units)
assert(dt1 == dt2)
# issue #189 - leap years calculated incorrectly for negative years in proleptic_gregorian calendar
dt1 = datetime(2020, 4, 24, 16, 15, 10)
Expand All @@ -785,6 +788,26 @@ def roundtrip(delta,eps,units):
dt = cftime.datetime(2020, 1, 1, calendar=cal)
dt += timedelta(hours=1)
assert(str(dt) == '2020-01-01 01:00:00')
# issue #193 - years with more than four digits in reference date
assert(cftime.date2num(cftime.datetime(18000, 12, 1, 0, 0), 'days since 18000-1-1', '360_day') == 330.0)
# julian day not including year zero
d = cftime.datetime(2020, 12, 1, 12, calendar='julian')
units = 'days since -4713-1-1 12:00'
jd = cftime.date2num(d,units,calendar='julian')
assert(jd == 2459198.0)
# if calendar=None, use input date to determine calendar
jd = cftime.date2num(d,units,calendar=None)
assert(jd == 2459198.0)
# if no calendar specified, default assumed 'standard'
jd = cftime.date2num(d,units)
assert(jd == 2459185.0)
# switch to these if default calendar for date2num switched to None
# if no calendar specified, use input date to determine calendar
#jd = cftime.date2num(d,units)
#assert(jd == 2459198.0)
## use 'standard' calendar
#jd = cftime.date2num(d,units,calendar='standard')
#assert(jd == 2459185.0)


class TestDate2index(unittest.TestCase):
Expand Down

0 comments on commit d4fbbaf

Please sign in to comment.