Skip to content
Commits on Source (5)
......@@ -2,11 +2,11 @@ environment:
CONDA_INSTALL_LOCN: C:\\Miniconda36-x64
matrix:
- TARGET_ARCH: x64
NPY: 1.15
NPY: 1.16
PY: 3.6
- TARGET_ARCH: x64
NPY: 1.15
NPY: 1.16
PY: 3.7
platform:
......@@ -25,23 +25,21 @@ install:
# Add path, activate `conda` and update conda.
- cmd: call %CONDA_INSTALL_LOCN%\Scripts\activate.bat
- cmd: conda.exe config --set always_yes yes --set changeps1 no --set show_channel_urls true
- cmd: conda.exe update conda
- cmd: conda.exe config --remove channels defaults --force
- cmd: conda.exe config --add channels conda-forge --force
- cmd: conda config --set always_yes yes --set changeps1 no --set show_channel_urls true
- cmd: conda update conda
- cmd: conda config --add channels conda-forge --force
- cmd: conda config --set channel_priority strict
- cmd: set PYTHONUNBUFFERED=1
- cmd: conda.exe install conda-build vs2008_express_vc_python_patch
- cmd: conda install conda-build vs2008_express_vc_python_patch
- cmd: call setup_x64
- cmd: conda.exe info --all
- cmd: conda.exe list
- cmd: conda.exe create --name TEST python=%PY% numpy=%NPY% cython pip pytest pytest-cov
- cmd: conda info --all
- cmd: conda activate TEST
# Skip .NET project specific build phase.
build: off
test_script:
- conda.exe create --name TEST python=%PY% numpy=%NPY% cython pip pytest pytest-cov
- conda activate TEST
- python -m pip install . --no-deps --ignore-installed --no-cache-dir -vvv
- py.test -vv test
......@@ -10,6 +10,8 @@ Time-handling functionality from netcdf4-python
[![Commits Status](https://img.shields.io/github/commits-since/UniData/cftime/latest.svg)](https://github.com/UniData/cftime/commits/master)
## News
10/21/2019: version 1.0.4 released.
12/05/2018: version 1.0.3.4 released (just to fix a problem with the source
tarball on pypi).
......
......@@ -17,8 +17,9 @@ try:
except ImportError: # python 3.x
pass
microsec_units = ['microseconds','microsecond', 'microsec', 'microsecs']
millisec_units = ['milliseconds', 'millisecond', 'millisec', 'millisecs']
millisec_units = ['milliseconds', 'millisecond', 'millisec', 'millisecs', 'msec', 'msecs', 'ms']
sec_units = ['second', 'seconds', 'sec', 'secs', 's']
min_units = ['minute', 'minutes', 'min', 'mins']
hr_units = ['hour', 'hours', 'hr', 'hrs', 'h']
......@@ -42,7 +43,7 @@ cdef int[13] _spm_366day = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 33
_rop_lookup = {Py_LT: '__gt__', Py_LE: '__ge__', Py_EQ: '__eq__',
Py_GT: '__lt__', Py_GE: '__le__', Py_NE: '__ne__'}
__version__ = '1.0.3.4'
__version__ = '1.0.4'
# 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.
......@@ -93,7 +94,7 @@ def _dateparse(timestr):
(units, isostring) = _datesplit(timestr)
# parse the date string.
year, month, day, hour, minute, second, utc_offset =\
year, month, day, hour, minute, second, microsecond, utc_offset =\
_parse_date( isostring.strip() )
if year >= MINYEAR:
basedate = real_datetime(year, month, day, hour, minute, second)
......@@ -121,7 +122,7 @@ cdef _parse_date_and_units(timestr,calendar='standard'):
raise ValueError(
"units must be one of 'seconds', 'minutes', 'hours' or 'days' (or singular version of these), got '%s'" % units)
# parse the date string.
year, month, day, hour, minute, second, utc_offset = _parse_date(
year, month, day, hour, minute, second, microsecond, utc_offset = _parse_date(
isostring.strip())
return units, utc_offset, datetime(year, month, day, hour, minute, second)
......@@ -182,6 +183,9 @@ def date2num(dates,units,calendar='standard'):
ismasked = True
times = []
for date in dates.flat:
if getattr(date, 'tzinfo') is not None:
date = date.replace(tzinfo=None) - date.utcoffset()
if ismasked and not date:
times.append(None)
else:
......@@ -408,6 +412,9 @@ def JulianDayFromDate(date, calendar='standard'):
cdef Py_ssize_t i
for i in range(i_max):
d = date[i]
if getattr(d, 'tzinfo', None) is not None:
d = d.replace(tzinfo=None) - d.utcoffset()
year[i] = d.year
month[i] = d.month
day[i] = d.day
......@@ -909,7 +916,12 @@ cpdef _parse_date(datestring):
The timezone is parsed from the date string, assuming UTC
by default.
Note that a seconds element with a fractional component
(e.g. 12.5) is converted into integer seconds and integer
microseconds.
Adapted from pyiso8601 (http://code.google.com/p/pyiso8601/)
"""
if not isinstance(datestring, str) and not isinstance(datestring, unicode):
raise ValueError("Expecting a string %r" % datestring)
......@@ -924,13 +936,14 @@ cpdef _parse_date(datestring):
groups["minute"] = 0
if groups["second"] is None:
groups["second"] = 0
# if groups["fraction"] is None:
# groups["fraction"] = 0
# else:
# groups["fraction"] = int(float("0.%s" % groups["fraction"]) * 1e6)
if groups["fraction"] is None:
groups["fraction"] = 0
else:
groups["fraction"] = int(float("0.%s" % groups["fraction"]) * 1e6)
iyear = int(groups["year"])
return iyear, int(groups["month"]), int(groups["day"]),\
int(groups["hour"]), int(groups["minute"]), int(groups["second"]),\
int(groups["fraction"]),\
tzoffset_mins
cdef _check_index(indices, times, nctime, calendar, select):
......@@ -1220,9 +1233,11 @@ Gregorial calendar.
"hour": self.hour,
"minute": self.minute,
"second": self.second,
"microsecond": self.microsecond,
"dayofwk": self.dayofwk,
"dayofyr": self.dayofyr}
"microsecond": self.microsecond}
if 'dayofyr' in kwargs or 'dayofwk' in kwargs:
raise ValueError('Replacing the dayofyr or dayofwk of a datetime is '
'not supported.')
for name, value in kwargs.items():
args[name] = value
......@@ -1246,13 +1261,17 @@ Gregorial calendar.
self.microsecond)
def __repr__(self):
return "{0}.{1}{2}".format('cftime',
return "{0}.{1}({2})".format('cftime',
self.__class__.__name__,
self._getstate())
str(self))
def __str__(self):
return "{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}".format(
self.year, self.month, self.day, self.hour, self.minute, self.second)
second = '{:02d}'.format(self.second)
if self.microsecond:
second += '.{}'.format(self.microsecond)
return "{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{}".format(
self.year, self.month, self.day, self.hour, self.minute, second)
def __hash__(self):
try:
......
cftime (1.0.3.4-4) UNRELEASED; urgency=medium
cftime (1.0.4-1) unstable; urgency=medium
* New upstream release.
* Bump Standards-Version to 4.4.1, no changes.
* Drop unused override for file-references-package-build-path.
-- Bas Couwenberg <sebastic@debian.org> Mon, 30 Sep 2019 18:50:41 +0200
-- Bas Couwenberg <sebastic@debian.org> Tue, 22 Oct 2019 06:14:33 +0200
cftime (1.0.3.4-3) unstable; urgency=medium
......
# Cannot easily be fixed
file-references-package-build-path *
......@@ -19,6 +19,33 @@ from cftime import (DateFromJulianDay, Datetime360Day, DatetimeAllLeap,
DatetimeProlepticGregorian, JulianDayFromDate, _parse_date,
date2index, date2num, num2date, utime)
try:
from datetime import timezone
except ImportError: # python2.7
from datetime import tzinfo
class timezone(tzinfo):
"""
Fixed offset in minutes east from UTC. adapted from
python 2.7 docs FixedOffset
"""
def __init__(self, offset, name):
self.__offset = offset
self.__name = name
def utcoffset(self, dt):
return self.__offset
def tzname(self, dt):
return self.__name
def dst(self, dt):
return timedelta(hours=0)
utc = timezone(timedelta(hours=0), 'UTC')
est = timezone(timedelta(hours=-5), 'UTC')
# test cftime module for netCDF time <--> python datetime conversions.
dtime = namedtuple('dtime', ('values', 'units', 'calendar'))
......@@ -81,7 +108,20 @@ class cftimeTestCase(unittest.TestCase):
self.cdftime_noleap_capcal = utime(
'days since 1600-02-28 00:00:00', calendar='NOLEAP')
def runTest(self):
def test_tz_aware(self):
"""testing with timezone"""
self.assertTrue(self.cdftime_mixed.units == 'hours')
d1 = datetime(1582, 10, 4, 23, tzinfo=utc)
t1 = self.cdftime_mixed.date2num(d1)
d2 = datetime(1582, 10, 4, 18, tzinfo=est)
t2 = self.cdftime_mixed.date2num(d2)
d3 = d2.replace(tzinfo=None)
t3 = self.cdftime_mixed.date2num(d3)
assert_almost_equal(t1, 13865687.0)
assert_almost_equal(t2, 13865687.0)
assert_almost_equal(t3, 13865682.0)
def test_tz_naive(self):
"""testing cftime"""
# test mixed julian/gregorian calendar
# check attributes.
......@@ -734,6 +774,15 @@ class TestDate2index(unittest.TestCase):
values=date2num(dates, units),
units=units)
def test_tz_aware(self):
"""implicit test of date2num"""
dutc = datetime(1950, 2, 1, 0, tzinfo=utc)
t1 = date2index(dutc, self.standardtime)
assert_equal(t1, 31)
dest = datetime(1950, 1, 31, 19, tzinfo=est)
t2 = date2index(dest, self.standardtime)
assert_equal(t2, 31)
def test_simple(self):
t = date2index(datetime(1950, 2, 1), self.standardtime)
assert_equal(t, 31)
......@@ -1043,8 +1092,6 @@ class DateTime(unittest.TestCase):
self.assertEqual(self.date1_365_day.replace(minute=3).minute, 3)
self.assertEqual(self.date1_365_day.replace(second=3).second, 3)
self.assertEqual(self.date1_365_day.replace(microsecond=3).microsecond, 3)
self.assertEqual(self.date1_365_day.replace(dayofwk=3).dayofwk, 3)
self.assertEqual(self.date1_365_day.replace(dayofyr=3).dayofyr, 3)
def test_pickling(self):
"Test reversibility of pickling."
......@@ -1186,25 +1233,25 @@ class issue17TestCase(unittest.TestCase):
"Test timezone parsing in _parse_date"
# these should succeed and are ISO8601 compliant
expected_parsed_date = (2017, 5, 1, 0, 0, 0, 60.0)
expected_parsed_date = (2017, 5, 1, 0, 0, 0, 0, 60.0)
for datestr in ("2017-05-01 00:00+01:00", "2017-05-01 00:00+0100", "2017-05-01 00:00+01"):
d = _parse_date(datestr)
assert_equal(d, expected_parsed_date)
# some more tests with non-zero minutes, should all be ISO compliant and work
expected_parsed_date = (2017, 5, 1, 0, 0, 0, 85.0)
expected_parsed_date = (2017, 5, 1, 0, 0, 0, 0, 85.0)
for datestr in ("2017-05-01 00:00+01:25", "2017-05-01 00:00+0125"):
d = _parse_date(datestr)
assert_equal(d, expected_parsed_date)
# these are NOT ISO8601 compliant and should not even be parseable but will be parsed with timezone anyway
# because, due to support of other legacy time formats, they are difficult to reject
# ATTENTION: only the hours part of this will be parsed, single-digit minutes will be ignored!
expected_parsed_date = (2017, 5, 1, 0, 0, 0, 60.0)
expected_parsed_date = (2017, 5, 1, 0, 0, 0, 0, 60.0)
for datestr in ("2017-05-01 00:00+01:0", "2017-05-01 00:00+01:", "2017-05-01 00:00+01:5"):
d = _parse_date(datestr)
assert_equal(d, expected_parsed_date)
# these should not even be parseable as datestrings but are parseable anyway with ignored timezone
# this is because the module also supports some legacy, non-standard time strings
expected_parsed_date = (2017, 5, 1, 0, 0, 0, 0.0)
expected_parsed_date = (2017, 5, 1, 0, 0, 0, 0, 0.0)
for datestr in ("2017-05-01 00:00+1",):
d = _parse_date(datestr)
assert_equal(d, expected_parsed_date)
......@@ -1368,8 +1415,9 @@ def test_valid_julian_gregorian_mixed_dates(date_type, date_args):
@pytest.mark.parametrize(
'date_args',
[(1, 2, 3, 4, 5, 6), (10, 2, 3, 4, 5, 6), (100, 2, 3, 4, 5, 6),
(1000, 2, 3, 4, 5, 6)],
ids=['1', '10', '100', '1000'])
(1000, 2, 3, 4, 5, 6),
(2000, 1, 1, 12, 34, 56, 123456)],
ids=['1', '10', '100', '1000', '2000'])
def test_str_matches_datetime_str(date_type, date_args):
assert str(date_type(*date_args)) == str(datetime(*date_args))
......@@ -1419,13 +1467,28 @@ def test_num2date_only_use_cftime_datetimes_post_gregorian(
def test_repr():
expected = 'cftime.datetime(2000, 1, 1, 0, 0, 0, 0, -1, 1)'
# dayofwk, dayofyr not set
expected = 'cftime.datetime(2000-01-01 00:00:00)'
assert repr(datetimex(2000, 1, 1)) == expected
expected = 'cftime.DatetimeGregorian(2000, 1, 1, 0, 0, 0, 0, 5, 1)'
# dayofwk, dayofyr are set
assert repr(DatetimeGregorian(2000, 1, 1)) == expected
def test_dayofyr_after_replace(date_type):
date = date_type(1, 1, 1)
assert date.dayofyr == 1
assert date.replace(day=2).dayofyr == 2
def test_dayofwk_after_replace(date_type):
date = date_type(1, 1, 1)
original_dayofwk = date.dayofwk
expected = (original_dayofwk + 1) % 7
result = date.replace(day=2).dayofwk
assert result == expected
@pytest.mark.parametrize('argument', ['dayofyr', 'dayofwk'])
def test_replace_dayofyr_or_dayofwk_error(date_type, argument):
with pytest.raises(ValueError):
date_type(1, 1, 1).replace(**{argument: 3})
if __name__ == '__main__':
......