[Git][debian-gis-team/cftime][master] 4 commits: New upstream version 1.6.2
Bas Couwenberg (@sebastic)
gitlab at salsa.debian.org
Mon Sep 19 04:35:52 BST 2022
Bas Couwenberg pushed to branch master at Debian GIS Project / cftime
Commits:
ca6cc7bd by Bas Couwenberg at 2022-09-19T05:26:57+02:00
New upstream version 1.6.2
- - - - -
36d6e531 by Bas Couwenberg at 2022-09-19T05:26:59+02:00
Update upstream source from tag 'upstream/1.6.2'
Update to upstream version '1.6.2'
with Debian dir 4db8468d2b402fd4a791904c77c688979677cf60
- - - - -
04c6ea52 by Bas Couwenberg at 2022-09-19T05:28:05+02:00
New upstream release.
- - - - -
14ef9aec by Bas Couwenberg at 2022-09-19T05:28:57+02:00
Set distribution to unstable.
- - - - -
7 changed files:
- + .github/workflows/cibuildwheel.yml
- Changelog
- README.md
- debian/changelog
- src/cftime/_cftime.pyx
- + src/cftime/_strptime.py
- test/test_cftime.py
Changes:
=====================================
.github/workflows/cibuildwheel.yml
=====================================
@@ -0,0 +1,88 @@
+name: Wheels
+
+on:
+ pull_request:
+
+ push:
+ tags:
+ - "v*"
+
+jobs:
+ build_bdist:
+ name: "Build ${{ matrix.os }} (${{ matrix.arch }}) wheels"
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: ["ubuntu-latest", "windows-latest", "macos-latest"]
+ arch: ["x86_64", "arm64", "AMD64"]
+ exclude:
+ - os: ubuntu-latest
+ arch: arm64
+ - os: ubuntu-latest
+ arch: AMD64
+ - os: windows-latest
+ arch: arm64
+ - os: windows-latest
+ arch: x86_64
+ - os: macos-latest
+ arch: AMD64
+
+ steps:
+ - uses: actions/checkout at v3
+ with:
+ fetch-depth: 0
+
+ - name: "Building ${{ matrix.os }} (${{ matrix.arch }}) wheels"
+ uses: pypa/cibuildwheel at v2.9.0
+ env:
+ # Skips pypy and musllinux for now.
+ CIBW_SKIP: "pp* cp36-* cp37-* *-musllinux*"
+ CIBW_ARCHS: ${{ matrix.arch }}
+ CIBW_BUILD_FRONTEND: build
+ CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014
+ CIBW_TEST_SKIP: "*_arm64"
+ CIBW_TEST_REQUIRES: pytest
+ CIBW_TEST_COMMAND: >
+ python -c "import cftime; print(f'cftime v{cftime.__version__}')" &&
+ python -m pip install -r {package}/requirements-dev.txt &&
+ python -m pytest -vv {package}/test
+
+ - uses: actions/upload-artifact at v3
+ with:
+ name: pypi-artifacts
+ path: ${{ github.workspace }}/wheelhouse/*.whl
+
+
+ show-artifacts:
+ needs: [build_bdist]
+ name: "Show artifacts"
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/download-artifact at v3
+ with:
+ name: pypi-artifacts
+ path: ${{ github.workspace }}/dist
+
+ - shell: bash
+ run: |
+ ls -l ${{ github.workspace }}/dist
+
+
+ publish-artifacts-pypi:
+ needs: [build_bdist]
+ name: "Publish to PyPI"
+ runs-on: ubuntu-latest
+ # upload to PyPI for every tag starting with 'v'
+ if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
+ steps:
+ - uses: actions/download-artifact at v3
+ with:
+ name: pypi-artifacts
+ path: ${{ github.workspace }}/dist
+
+ - uses: pypa/gh-action-pypi-publish at release/v1
+ with:
+ user: __token__
+ password: ${{ secrets.PYPI_PASSWORD }}
+ print_hash: true
=====================================
Changelog
=====================================
@@ -1,3 +1,13 @@
+version 1.6.2 (release tag v1.6.2rel)
+=====================================
+ * num2date should not fail on an empty integer array (issue #287).
+ * longdouble keyword in date2num so that a roundtrip from a time to a date
+ and back again does not lose microsecond precision when the units require
+ the times be encoded as floating point values (PR #284)
+ * added strptime method (issue #277).
+ * cibuildwheel wheel-building workflow added to github actions by @ocefpaf (triggers binary
+ wheel builds and uploads to pypi automatically when GH release created). PR #290.
+
version 1.6.1 (release tag v1.6.1rel)
=====================================
* fix failing tests on windows with numpy 1.23.0 (issue #278)
@@ -13,18 +23,18 @@ version 1.6.0 (release tag v1.6.0rel)
version 1.5.2 (release tag v1.5.2rel)
=====================================
- * silently change calendar='gregorian' to 'standard' internally,
+ * silently change calendar='gregorian' to 'standard' internally,
since 'gregorian' deprecated in CF v1.9 (issue #256).
* add "is_leap_year" function (issue #259).
* wheels that work on Apple M1 (arm64) available on pypi.
-
+
version 1.5.1.1
===============
* no code changes, just new binary wheels for python 3.10.
version 1.5.1 (release tag v1.5.1.rel)
======================================
- * added support for "common_year" and "common_years" units for "noleap"
+ * added support for "common_year" and "common_years" units for "noleap"
and "365_day" calendars (issue #5, PR #246)
* check consistency of year arg and has_year_zero kwarg in cftime.datetime
(issue #248). Also assume if has_year_zero not specified it should be True
@@ -39,7 +49,6 @@ version 1.5.1 (release tag v1.5.1.rel)
for this calendar). Issue warning when trying to
to create a cftime.datetime instance that is not allowed in CF (PR #238).
-
version 1.5.0 (release tag v1.5.0.rel)
======================================
* clean-up deprecated calendar specific subclasses (PR #231).
@@ -47,13 +56,13 @@ version 1.5.0 (release tag v1.5.0.rel)
(via `cftime.datetime.__format__`) PR #232.
* add support for astronomical year numbering (including year zero) for
real-world calendars using 'has_year_zero' cftime.datetime kwarg (PR #234).
- Default is False for 'real-world' calendars ('julian', 'gregorian'/'standard',
+ Default is False for 'real-world' calendars ('julian', 'gregorian'/'standard',
'proleptic_gregorian'). Ignored for idealized calendars like '360_day
(they always have year zero).
- * add "change_calendar" cftime.datetime method to switch to another
+ * add "change_calendar" cftime.datetime method to switch to another
'real-world' calendar. Enable comparison of cftime.datetime instances
with different 'real-world' calendars (using the new change_calendar method)
- * remove legacy `utime` class, and legacy `JulianDayFromDate` and
+ * remove legacy `utime` class, and legacy `JulianDayFromDate` and
`DateFromJulianDay` functions (replaced by `cftime.datetime.toordinal`
and `cftime.datetime.fromordinal`). PR #235.
* Change ValueError to TypeError in __sub__ (issue #236, PR #236).
@@ -61,7 +70,7 @@ version 1.5.0 (release tag v1.5.0.rel)
version 1.4.1 (release tag v1.4.1.rel)
======================================
* Restore use of calendar-specific sub-classes in `cftime.num2date`,
- `cftime.datetime.__add__`, and `cftime.datetime.__sub__`. The use of them
+ `cftime.datetime.__add__`, and `cftime.datetime.__sub__`. The use of them
will be removed in a later release.
* add 'fromordinal' static method to create a cftime.datetime instance
from a julian day ordinal and calendar (inverse of 'toordinal').
@@ -72,9 +81,9 @@ version 1.4.0 (release tag v1.4.0.rel)
and times allow. Previously this would only be true if the units were
'microseconds' (PR #225). In other circumstances, as before, `cftime.date2num`
will return an array of floats.
- * Rewrite of julian day/calendar functions (_IntJulianDayToCalendar and
+ * Rewrite of julian day/calendar functions (_IntJulianDayToCalendar and
_IntJulianDayFromCalendar) to remove GPL'ed code. cftime license
- changed to MIT (to be consistent with netcdf4-python).
+ changed to MIT (to be consistent with netcdf4-python).
* Added datetime.toordinal() (returns julian day, kwarg 'fractional'
can be used to include fractional day).
* cftime.datetime no longer uses calendar-specific sub-classes.
@@ -100,7 +109,7 @@ version 1.3.0 (release tag v1.3.0rel)
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.
+ digits can be handled.
* Change default calendar in cftime.date2num from 'standard' to None
(calendar associated with first input datetime object is used).
* add `cftime.datetime.tzinfo=None` for compatibility with python datetime
@@ -131,7 +140,6 @@ version 1.2.0 (release tag v1.2.0rel)
* utime.date2num/utime.num2date now just call module level functions.
JulianDayFromDate/DateFromJulianDay no longer used internally (PR #180).
-
version 1.1.3 (release tag v1.1.3rel)
=====================================
* add isoformat method for compatibility with python datetime (issue #152).
@@ -146,33 +154,27 @@ version 1.1.2 (release tag v1.1.2rel)
version 1.1.1.2 (release tag v1.1.1.2rel)
=========================================
- * include pyproject.toml in MANIFEST.in so it gets
+ * include pyproject.toml in MANIFEST.in so it gets
included in source tarball (issue #154).
version 1.1.1.1 (release tag v1.1.1.1rel)
=========================================
- * Fix error installing with pip on python 3.8 by following
+ * Fix error installing with pip on python 3.8 by following
PEP 517 (issue #148, PR #149)
version 1.1.1 (release tag v1.1.1rel)
=====================================
-
* fix microsecond formatting issue, ensure identical results
computed for arrays and scales (issue #143, PR #146).
version 1.1.0 (release tag v1.1.0rel)
=====================================
-
* improved exceptions for time differences (issue #128, PR #131).
-
* fix intersphinx entries (issue #133, PR #133)
-
* make only_use_cftime_datetimes=True by default, so cftime datetime
instances are returned by default by num2date (instead of returning python
datetime instances where possible). Issue #136, PR #135.
-
* Add daysinmonth attribute (issue #137, PR #138).
-
* If only_use_python_datetimes=True and only_use_cftime_datetimes=False,
num2date only returns python datetime instances and raises an exception
if this is not possible. num2pydate convenience function added which just calls
@@ -180,12 +182,10 @@ version 1.1.0 (release tag v1.1.0rel)
only_use_cftime_datetimes=False.
Remove positive times check, raise ValueError if python datetime
tries to compute a date before MINYEAR (issue #134, PR #139)
-
* Fix for fractional seconds in reference date in units string (issue #140,
PR # 141).
version 1.0.4.2 release
=======================
-
- * fix for issue #126 (date2num error when converting a DatetimeProlepticGregorian
+ * fix for issue #126 (date2num error when converting a DatetimeProlepticGregorian
object). PR #127.
=====================================
README.md
=====================================
@@ -11,6 +11,9 @@ Time-handling functionality from netcdf4-python
## News
For details on the latest updates, see the [Changelog](https://github.com/Unidata/cftime/blob/master/Changelog).
+
+9/18/2022: Version 1.6.2 released. strptime method added, fix for num2date failure on
+empty integer array, date2num 'longdouble' keyword added. New wheel building workflow.
6/30/2022: Version 1.6.1 released. Fixes for numpy 1.23.0, updated CI/CD.
=====================================
debian/changelog
=====================================
@@ -1,3 +1,9 @@
+cftime (1.6.2-1) unstable; urgency=medium
+
+ * New upstream release.
+
+ -- Bas Couwenberg <sebastic at debian.org> Mon, 19 Sep 2022 05:28:47 +0200
+
cftime (1.6.1-1) unstable; urgency=medium
* New upstream release.
=====================================
src/cftime/_cftime.pyx
=====================================
@@ -8,12 +8,11 @@ from numpy cimport int64_t, int32_t
import cython
import numpy as np
import re
-import sys
import time
from datetime import datetime as datetime_python
from datetime import timedelta, MINYEAR, MAXYEAR
-import time # strftime
import warnings
+from ._strptime import _strptime
microsec_units = ['microseconds','microsecond', 'microsec', 'microsecs']
millisec_units = ['milliseconds', 'millisecond', 'millisec', 'millisecs', 'msec', 'msecs', 'ms']
@@ -38,7 +37,7 @@ cdef int[12] _dayspermonth_leap = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 3
cdef int[13] _cumdayspermonth = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365]
cdef int[13] _cumdayspermonth_leap = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366]
-__version__ = '1.6.1'
+__version__ = '1.6.2'
# 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.
@@ -101,7 +100,11 @@ def _dateparse(timestr,calendar,has_year_zero=None):
raise ValueError("'%s' units only allowed for '365_day' and 'noleap' calendars" % units)
else:
raise ValueError(
- "units must be one of 'seconds', 'minutes', 'hours' or 'days' (or singular version of these), got '%s'" % units)
+ "In general, units must be one of 'microseconds', 'milliseconds', "
+ "'seconds', 'minutes', 'hours', or 'days' (or select abbreviated "
+ "versions of these). For the '360_day' calendar, "
+ "'months' can also be used, or for the 'noleap' calendar 'common_years' "
+ "can also be used. Got '%s' instead, which are not recognized." % units)
# parse the date string.
year, month, day, hour, minute, second, microsecond, utc_offset =\
_parse_date( isostring.strip() )
@@ -122,12 +125,15 @@ def _dateparse(timestr,calendar,has_year_zero=None):
return basedate
def _can_use_python_datetime(date,calendar):
- gregorian = datetime(1582,10,15,calendar=calendar,has_year_zero=date.has_year_zero)
- return ((calendar == 'proleptic_gregorian' and date.year >= MINYEAR and date.year <= MAXYEAR) or \
- (calendar in ['gregorian','standard'] and date > gregorian and date.year <= MAXYEAR))
+ #gregorian = datetime(1582,10,15,calendar=calendar,has_year_zero=date.has_year_zero)
+ #return ((calendar == 'proleptic_gregorian' and date.year >= MINYEAR and date.year <= MAXYEAR) or \
+ # (calendar in ['gregorian','standard'] and date > gregorian and date.year <= MAXYEAR))
+ return (calendar == 'proleptic_gregorian' and date.year >= MINYEAR and date.year <= MAXYEAR) or \
+ ((calendar in ['gregorian','standard'] and date.year <= MAXYEAR) and (date.year > 1582 or \
+ (date.year == 1582 and date.month >= 10 and date.day > 15)))
@cython.embedsignature(True)
-def date2num(dates,units,calendar=None,has_year_zero=None):
+def date2num(dates, units, calendar=None, has_year_zero=None, longdouble=False):
"""
Return numeric time values given datetime objects. The units
of the numeric time values are described by the **units** argument
@@ -172,6 +178,12 @@ def date2num(dates,units,calendar=None,has_year_zero=None):
This kwarg is not needed to define calendar systems allowed by CF
(the calendar-specific defaults do this).
+ **longdouble**: If set True, output is in the long double float type
+ (numpy.float128) instead of float (numpy.float64), allowing microsecond
+ accuracy when converting a time value to a date and back again. Otherwise
+ this is only possible if the discretization of the time variable is an
+ integer multiple of the units.
+
returns a numeric time value, or an array of numeric time values
with approximately 1 microsecond accuracy.
"""
@@ -215,7 +227,7 @@ def date2num(dates,units,calendar=None,has_year_zero=None):
has_year_zero = _year_zero_defaults(calendar)
# if calendar is None or '', use calendar of first input cftime.datetime instances.
- # if inputs are 'real' python datetime instances, use propleptic gregorian.
+ # if inputs are 'real' python datetime instances, use proleptic gregorian.
if not calendar:
if all_python_datetimes:
calendar = 'proleptic_gregorian'
@@ -274,8 +286,25 @@ def date2num(dates,units,calendar=None,has_year_zero=None):
quotient = np.int64(td // unit_timedelta)
times.append(quotient)
else:
- times.append(td / unit_timedelta)
-
+ if longdouble:
+ # Division of timedelta's is in float64 precision,
+ # i.e. losing microsecond precision.
+ # Conversion to float128 helps but can still lead to imprecision
+ # of +-1 microsecond in division:
+ # quotient = (np.longdouble(td.total_seconds()) /
+ # np.longdouble(unit_timedelta.total_seconds()))
+ # -> Convert to (64-bit) integers of microseconds
+ mtd = (td.days * 86400000000 +
+ td.seconds * 1000000 +
+ td.microseconds)
+ munit = (unit_timedelta.days * 86400000000 +
+ unit_timedelta.seconds * 1000000 +
+ unit_timedelta.microseconds)
+ quotient = np.longdouble(mtd) / np.longdouble(munit)
+ else:
+ quotient = td / unit_timedelta
+ times.append(quotient)
+
if ismasked: # convert to masked array if input was masked array
times = np.array(times, dtype=float) # None -> nan
times = np.ma.masked_invalid(times)
@@ -417,6 +446,8 @@ def scale_times(num, factor):
if num.dtype.kind == "f":
return factor * num
else:
+ if num.size == 0: # empty array (issue #287)
+ return num
# Python integers have arbitrary precision, so convert min and max
# returned by NumPy functions through item, prior to multiplying by
# factor.
@@ -666,7 +697,7 @@ def date2index(dates, nctime, calendar=None, select='exact', has_year_zero=None)
has_year_zero = _year_zero_defaults(calendar)
# if calendar is None or '', use calendar of first input cftime.datetime instances.
- # if inputs are 'real' python datetime instances, use propleptic gregorian.
+ # if inputs are 'real' python datetime instances, use proleptic gregorian.
if not calendar:
d0 = dates_test.item(0)
if isinstance(d0,datetime_python):
@@ -1208,6 +1239,48 @@ The default format of the string produced by strftime is controlled by self.form
format = self.format
return _strftime(self, format)
+ @staticmethod
+ def strptime(datestring, format, calendar='standard', has_year_zero=None):
+ """
+ Return a datetime corresponding to date_string, parsed according to format,
+ with a specified calendar and year zero convention.
+ The format directives 'y','Y','m','B','b','d','H','M','S' and 'f'
+ are supported for all calendars and dates. If the date is valid
+ in the python 'proleptic_gregorian' calendar, then python's
+ datetime.strptime is used. For a complete list of formatting directives
+ supported in python's datetime.strptime, see section
+ 'strftime() and strptime() Behavior' in the base Python documentation.
+ """
+ # if possible use python's datetime.strptime to get a python datetime instance
+ # (works for dates in proleptic_gregorian calendar)
+ fd = [d[0] for d in format.split('%') if d] # extract format descriptors
+ # calendar specific format descriptors that won't work will all calendars
+ special_fd = ['a', 'A', 'w', 'j', 'U', 'W', 'G', 'u', 'V']
+ try:
+ pydatetime = datetime_python.strptime(datestring, format)
+ # remove time zone offset
+ if getattr(pydatetime, 'tzinfo',None) is not None:
+ pydatetime = pydatetime.replace(tzinfo=None) - pydatetime.utcoffset()
+ compatible_date =\
+ calendar == 'proleptic_gregorian' or \
+ (calendar in ['gregorian','standard'] and (pydatetime.year > 1582 or \
+ (pydatetime.year == 1582 and pydatetime.month > 10) or \
+ (pydatetime.year == 1582 and pydatetime.month == 10 and pydatetime.day > 15)))
+ if not compatible_date and any(x in special_fd for x in fd):
+ msg='one of the supplied format directives may not be consistent with the chosen calendar'
+ raise KeyError(msg)
+ # convert the cftime datetime instance
+ return datetime(pydatetime.year, pydatetime.month, pydatetime.day,
+ pydatetime.hour, pydatetime.minute, pydatetime.second,
+ pydatetime.microsecond, calendar=calendar, has_year_zero=has_year_zero)
+ # otherwise use a stripped-down version of C-python's _strptime.py
+ # (doesn't understand all possible formats, just
+ # 'y','Y','m','B','b','d','H','M','S' and 'f')
+ except ValueError:
+ year,month,day,hour,minute,second,microsecond = _strptime(datestring,format)
+ return datetime(year,month,day,hour,minute,second,microsecond,
+ calendar=calendar,has_year_zero=has_year_zero)
+
def __format__(self, format):
# the string format "{t_obj}".format(t_obj=t_obj)
# without an explicit format gives an empty string (format='')
@@ -1280,25 +1353,43 @@ The default format of the string produced by strftime is controlled by self.form
def __str__(self):
return self.isoformat(' ')
- def isoformat(self,sep='T',timespec='auto'):
- second = ":%02i" %self.second
- if (timespec == 'auto' and self.microsecond) or timespec == 'microseconds':
- second += ".%06i" % self.microsecond
- if timespec == 'milliseconds':
- millisecs = self.microsecond/1000
- second += ".%03i" % millisecs
- if timespec in ['auto', 'microseconds', 'milliseconds']:
- return "%04i-%02i-%02i%s%02i:%02i%s" %\
- (self.year, self.month, self.day, sep, self.hour, self.minute, second)
- elif timespec == 'seconds':
- return "%04i-%02i-%02i%s%02i:%02i:%02i" %\
- (self.year, self.month, self.day, sep, self.hour, self.minute, self.second)
- elif timespec == 'minutes':
- return "%04i-%02i-%02i%s%02i:%02i" %\
- (self.year, self.month, self.day, sep, self.hour, self.minute)
+ def isoformat(self, sep='T', timespec='auto'):
+ """
+ ISO date representation
+
+ """
+ if self.year < 0:
+ form0 = '{:05d}-{:02d}-{:02d}'
+ else:
+ form0 = '{:04d}-{:02d}-{:02d}'
+ if timespec == 'days':
+ form = form0
+ return form.format(self.year, self.month, self.day)
elif timespec == 'hours':
- return "%04i-%02i-%02i%s%02i" %\
- (self.year, self.month, self.day, sep, self.hour)
+ form = form0 + '{:s}{:02d}'
+ return form.format(self.year, self.month, self.day, sep,
+ self.hour)
+ elif timespec == 'minutes':
+ form = form0 + '{:s}{:02d}:{:02d}'
+ return form.format(self.year, self.month, self.day, sep,
+ self.hour, self.minute)
+ elif timespec == 'seconds':
+ form = form0 + '{:s}{:02d}:{:02d}:{:02d}'
+ return form.format(self.year, self.month, self.day, sep,
+ self.hour, self.minute, self.second)
+ elif timespec in ['auto', 'microseconds', 'milliseconds']:
+ second = '{:02d}'.format(self.second)
+ if timespec == 'milliseconds':
+ millisecs = int(round(self.microsecond / 1000, 0))
+ second += '.{:03d}'.format(millisecs)
+ elif timespec == 'microseconds':
+ second += '.{:06d}'.format(self.microsecond)
+ else:
+ if self.microsecond > 0:
+ second += '.{:06d}'.format(self.microsecond)
+ form = form0 + '{:s}{:02d}:{:02d}:{:s}'
+ return form.format(self.year, self.month, self.day, sep,
+ self.hour, self.minute, second)
else:
raise ValueError('illegal timespec')
@@ -1565,14 +1656,24 @@ cdef _findall(text, substr):
# Every 28 years the calendar repeats, except through century leap
# years where it's 6 years. But only if you're using the Gregorian
# calendar. ;)
-
-
+# Make also 4-digit negative years
+# Allow .%f for microseconds
cdef _strftime(datetime dt, fmt):
if _illegal_s.search(fmt):
raise TypeError("This strftime implementation does not handle %s")
# don't use strftime method at all.
# if dt.year > 1900:
# return dt.strftime(fmt)
+ if '%f' in fmt:
+ if not fmt.endswith('.%f'):
+ raise TypeError('If %f is used for microseconds it must be the'
+ ' at the end as .%f')
+ else:
+ ihavems = True
+ fmt1 = fmt[:-3]
+ else:
+ ihavems = False
+ fmt1 = fmt
year = dt.year
# For every non-leap year century, advance by
@@ -1584,10 +1685,10 @@ cdef _strftime(datetime dt, fmt):
# Move to around the year 2000
year = year + ((2000 - year) // 28) * 28
timetuple = dt.timetuple()
- s1 = time.strftime(fmt, (year,) + timetuple[1:])
+ s1 = time.strftime(fmt1, (year,) + timetuple[1:])
sites1 = _findall(s1, str(year))
- s2 = time.strftime(fmt, (year + 28,) + timetuple[1:])
+ s2 = time.strftime(fmt1, (year + 28,) + timetuple[1:])
sites2 = _findall(s2, str(year + 28))
sites = []
@@ -1596,9 +1697,14 @@ cdef _strftime(datetime dt, fmt):
sites.append(site)
s = s1
- syear = "%04d" % (dt.year,)
+ if dt.year < 0:
+ syear = "%05d" % (dt.year,)
+ else:
+ syear = "%04d" % (dt.year,)
for site in sites:
s = s[:site] + syear + s[site + 4:]
+ if ihavems:
+ s = s + '.{:06d}'.format(dt.microsecond)
return s
cdef bint is_leap_julian(int year, bint has_year_zero):
=====================================
src/cftime/_strptime.py
=====================================
@@ -0,0 +1,165 @@
+"""stripped-down version of _strptime.py from C python"""
+from re import compile as re_compile
+from re import IGNORECASE
+from re import escape as re_escape
+from _thread import allocate_lock as _thread_allocate_lock
+from calendar import month_name, month_abbr
+
+__all__ = []
+month_name = list(month_name)
+month_name = [m.lower() for m in month_name]
+month_abbr = list(month_abbr)
+month_abbr = [m.lower() for m in month_abbr]
+
+class TimeRE(dict):
+ """Handle conversion from format directives to regexes."""
+
+ def __init__(self):
+ """Create keys/values.
+
+ Order of execution is important for dependency reasons.
+
+ """
+ base = super()
+ base.__init__({
+ # The " [1-9]" part of the regex is to make %c from ANSI C work
+ 'd': r"(?P<d>3[0-1]|[1-2]\d|0[1-9]|[1-9]| [1-9])",
+ 'f': r"(?P<f>[0-9]{1,6})",
+ 'H': r"(?P<H>2[0-3]|[0-1]\d|\d)",
+ 'm': r"(?P<m>1[0-2]|0[1-9]|[1-9])",
+ 'M': r"(?P<M>[0-5]\d|\d)",
+ 'S': r"(?P<S>6[0-1]|[0-5]\d|\d)",
+ 'y': r"(?P<y>\d\d)",
+# 'Y': r"(?P<Y>\d\d\d\d)",
+ 'Y': r"(?P<Y>[+-]?[0-9]+)", # handle neg and > 4 digits
+ 'B': self.__seqToRE(month_name[1:], 'B'),
+ 'b': self.__seqToRE(month_abbr[1:], 'b'),
+ '%': '%'})
+
+ def __seqToRE(self, to_convert, directive):
+ """Convert a list to a regex string for matching a directive.
+
+ Want possible matching values to be from longest to shortest. This
+ prevents the possibility of a match occurring for a value that also
+ a substring of a larger value that should have matched (e.g., 'abc'
+ matching when 'abcdef' should have been the match).
+
+ """
+ to_convert = sorted(to_convert, key=len, reverse=True)
+ for value in to_convert:
+ if value != '':
+ break
+ else:
+ return ''
+ regex = '|'.join(re_escape(stuff) for stuff in to_convert)
+ regex = '(?P<%s>%s' % (directive, regex)
+ return '%s)' % regex
+
+ def pattern(self, format):
+ """Return regex pattern for the format string.
+ Need to make sure that any characters that might be interpreted as
+ regex syntax are escaped.
+ """
+ processed_format = ''
+ # The sub() call escapes all characters that might be misconstrued
+ # as regex syntax. Cannot use re.escape since we have to deal with
+ # format directives (%m, etc.).
+ regex_chars = re_compile(r"([\\.^$*+?\(\){}\[\]|])")
+ format = regex_chars.sub(r"\\\1", format)
+ whitespace_replacement = re_compile(r'\s+')
+ format = whitespace_replacement.sub(r'\\s+', format)
+ while '%' in format:
+ directive_index = format.index('%')+1
+ processed_format = "%s%s%s" % (processed_format,
+ format[:directive_index-1],
+ self[format[directive_index]])
+ format = format[directive_index+1:]
+ return "%s%s" % (processed_format, format)
+
+ def compile(self, format):
+ """Return a compiled re object for the format string."""
+ return re_compile(self.pattern(format), IGNORECASE)
+
+_cache_lock = _thread_allocate_lock()
+# DO NOT modify _TimeRE_cache or _regex_cache without acquiring the cache lock
+# first!
+_TimeRE_cache = TimeRE()
+_CACHE_MAX_SIZE = 5 # Max number of regexes stored in _regex_cache
+_regex_cache = {}
+
+def _strptime(data_string, format):
+ """Return a 7-tuple consisting of the data required to construct a
+ datetime based on the input string and the format string."""
+
+ for index, arg in enumerate([data_string, format]):
+ if not isinstance(arg, str):
+ msg = "strptime() argument {} must be str, not {}"
+ raise TypeError(msg.format(index, type(arg)))
+
+ global _TimeRE_cache, _regex_cache
+ with _cache_lock:
+ if len(_regex_cache) > _CACHE_MAX_SIZE:
+ _regex_cache.clear()
+ format_regex = _regex_cache.get(format)
+ if not format_regex:
+ try:
+ format_regex = _TimeRE_cache.compile(format)
+ # KeyError raised when a bad format is found; can be specified as
+ # \\, in which case it was a stray % but with a space after it
+ except KeyError as err:
+ bad_directive = err.args[0]
+ if bad_directive == "\\":
+ bad_directive = "%"
+ del err
+ if bad_directive in ['I','a','A','w','j','u','U','V','W','G']:
+ msg="'%s' directive not supported for dates not valid in python %s calendar"
+ raise ValueError(msg %
+ (bad_directive,'proleptic_gregorian'))
+ else:
+ raise ValueError("'%s' is a bad directive in format '%s'" %
+ (bad_directive, format)) from None
+ # IndexError only occurs when the format string is "%"
+ except IndexError:
+ raise ValueError("stray %% in format '%s'" % format) from None
+ _regex_cache[format] = format_regex
+ found = format_regex.match(data_string)
+ if not found:
+ raise ValueError("time data %r does not match format %r" %
+ (data_string, format))
+ if len(data_string) != found.end():
+ raise ValueError("unconverted data remains: %s" %
+ data_string[found.end():])
+
+ month = day = 1
+ hour = minute = second = fraction = 0
+ found_dict = found.groupdict()
+ for group_key in found_dict.keys():
+ if group_key == 'y':
+ year = int(found_dict['y'])
+ if year <= 68:
+ year += 2000
+ else:
+ year += 1900
+ elif group_key == 'Y':
+ year = int(found_dict['Y'])
+ elif group_key == 'm':
+ month = int(found_dict['m'])
+ elif group_key == 'B':
+ month = month_name.index(found_dict['B'].lower())
+ elif group_key == 'b':
+ month = month_abbr.index(found_dict['b'].lower())
+ elif group_key == 'd':
+ day = int(found_dict['d'])
+ elif group_key == 'H':
+ hour = int(found_dict['H'])
+ elif group_key == 'M':
+ minute = int(found_dict['M'])
+ elif group_key == 'S':
+ second = int(found_dict['S'])
+ elif group_key == 'f':
+ s = found_dict['f']
+ # Pad to always return microseconds.
+ s += "0" * (6 - len(s))
+ fraction = int(s)
+
+ return year,month,day,hour,minute,second,fraction
=====================================
test/test_cftime.py
=====================================
@@ -732,7 +732,7 @@ class cftimeTestCase(unittest.TestCase):
# issue #140 (fractional seconds in reference date)
d = datetime.strptime('2018-01-23 09:27:10.950000',"%Y-%m-%d %H:%M:%S.%f")
units = 'seconds since 2018-01-23 09:31:42.94'
- assert(cftime.date2num(d, units) == -271.99)
+ assert(float(cftime.date2num(d, units)) == -271.99)
# issue 143 - same answer for arrays vs scalars.
units = 'seconds since 1970-01-01 00:00:00'
times_in = [1261440000.0, 1261440001.0, 1261440002.0, 1261440003.0,
@@ -751,7 +751,7 @@ class cftimeTestCase(unittest.TestCase):
# issue #152 add isoformat()
assert(d.isoformat()[0:24] == '2009-12-22T00:00:00.0156')
assert(d.isoformat(sep=' ')[0:24] == '2009-12-22 00:00:00.0156')
- assert(d.isoformat(sep=' ',timespec='milliseconds') == '2009-12-22 00:00:00.015')
+ assert(d.isoformat(sep=' ',timespec='milliseconds') == '2009-12-22 00:00:00.016')
assert(d.isoformat(sep=' ',timespec='seconds') == '2009-12-22 00:00:00')
assert(d.isoformat(sep=' ',timespec='minutes') == '2009-12-22 00:00')
assert(d.isoformat(sep=' ',timespec='hours') == '2009-12-22 00')
@@ -921,6 +921,9 @@ class cftimeTestCase(unittest.TestCase):
assert(not cftime.is_leap_year(1,calendar='standard',has_year_zero=True))
assert(not cftime.is_leap_year(1,calendar='365_day'))
assert(cftime.is_leap_year(1,calendar='366_day'))
+ # num2date should not fail on an empty int array (issue #287)
+ d = cftime.num2date(np.array([], dtype="int64"), "days since 1970-01-01",\
+ calendar="proleptic_gregorian", only_use_cftime_datetimes=True)
class TestDate2index(unittest.TestCase):
@@ -1686,6 +1689,59 @@ def test_string_format():
assert dt.strftime('%H%m%d') == '{0:%H%m%d}'.format(dt)
assert 'the year is 2000' == 'the year is {dt:%Y}'.format(dt=dt)
+
+def test_string_format2():
+ dt = cftime.datetime(-4713, 1, 1, 12, 0, 0, 10)
+ # check a given format string acts like strftime
+ assert dt.strftime('%H%m%d') == '{0:%H%m%d}'.format(dt)
+ assert dt.strftime() == '-4713-01-01 12:00:00'
+ assert dt.strftime('%Y-%m-%d %H:%M:%S') == '-4713-01-01 12:00:00'
+ assert dt.strftime('%Y-%m-%d %H:%M:%S.%f') == '-4713-01-01 12:00:00.000010'
+ assert dt.strftime('%d.%m.%Y %H:%M:%S.%f') == '01.01.-4713 12:00:00.000010'
+ dt = cftime.datetime(-713, 1, 1, 12, 0, 0, 10)
+ assert dt.strftime('%H%m%d') == '{0:%H%m%d}'.format(dt)
+ assert dt.strftime() == '-0713-01-01 12:00:00'
+ assert dt.strftime('%Y-%m-%d %H:%M:%S') == '-0713-01-01 12:00:00'
+ assert dt.strftime('%Y-%m-%d %H:%M:%S.%f') == '-0713-01-01 12:00:00.000010'
+ assert dt.strftime('%d.%m.%Y %H:%M:%S.%f') == '01.01.-0713 12:00:00.000010'
+
+def test_strptime():
+ d = cftime.datetime.strptime('24/Aug/2004:17:57:26 +0200', '%d/%b/%Y:%H:%M:%S %z',calendar='julian',has_year_zero=True)
+ assert(repr(d) == "cftime.datetime(2004, 8, 24, 15, 57, 26, 0, calendar='julian', has_year_zero=True)")
+ d = cftime.datetime.strptime("0000-02-30",\
+ "%Y-%m-%d",calendar='360_day',has_year_zero=True)
+ assert(repr(d) == "cftime.datetime(0, 2, 30, 0, 0, 0, 0, calendar='360_day', has_year_zero=True)")
+ d = cftime.datetime.strptime('-99999-02-29 10:18:32.926',\
+ '%Y-%m-%d %H:%M:%S.%f',calendar='366_day')
+ assert(repr(d) == "cftime.datetime(-99999, 2, 29, 10, 18, 32, 926000, calendar='all_leap', has_year_zero=True)")
+ d = cftime.datetime.strptime('24/Aug/-4712:17:57:26', '%d/%b/%Y:%H:%M:%S',calendar='julian')
+ assert(repr(d) == "cftime.datetime(-4712, 8, 24, 17, 57, 26, 0, calendar='julian', has_year_zero=False)")
+ d = cftime.datetime.strptime('24/August/-4712:17:57:26', '%d/%B/%Y:%H:%M:%S',calendar='julian')
+ assert(repr(d) == "cftime.datetime(-4712, 8, 24, 17, 57, 26, 0, calendar='julian', has_year_zero=False)")
+ d = cftime.datetime.strptime("-4712", "%Y", calendar="julian")
+ assert(repr(d) == "cftime.datetime(-4712, 1, 1, 0, 0, 0, 0, calendar='julian', has_year_zero=False)")
+ # should fail with KeyError
+ try:
+ d=cftime.datetime.strptime("2000-45-3", "%G-%V-%u", calendar="noleap")
+ except KeyError:
+ pass
+ else:
+ raise AssertionError
+
+
+def test_string_isoformat():
+ dt = cftime.datetime(-4713, 1, 1, 12, 0, 0, 10)
+ assert dt.isoformat() == '-4713-01-01T12:00:00.000010'
+ assert dt.isoformat(' ', 'days') == '-4713-01-01'
+ assert dt.isoformat(' ', 'seconds') == '-4713-01-01 12:00:00'
+ assert dt.isoformat(' ', 'microseconds') == '-4713-01-01 12:00:00.000010'
+ dt = cftime.datetime(-713, 1, 1, 12, 0, 0, 10)
+ assert dt.isoformat() == '-0713-01-01T12:00:00.000010'
+ assert dt.isoformat(' ', 'days') == '-0713-01-01'
+ assert dt.isoformat(' ', 'seconds') == '-0713-01-01 12:00:00'
+ assert dt.isoformat(' ', 'microseconds') == '-0713-01-01 12:00:00.000010'
+
+
def test_dayofyr_after_replace(date_type):
date = date_type(1, 1, 1)
assert date.dayofyr == 1
@@ -2048,11 +2104,16 @@ def test_date2num_num2date_roundtrip(encoding_units, freq, calendar):
assert encoded.dtype == np.int64
np.testing.assert_equal(decoded, times)
else:
+ # if sys.platform.startswith("win"):
+ # assert encoded.dtype == np.float64
+ # else:
+ # assert encoded.dtype == np.float128
assert encoded.dtype == np.float64
tolerance = timedelta(microseconds=2000)
meets_tolerance = np.abs(decoded - times) <= tolerance
assert np.all(meets_tolerance)
+
def test_date2num_missing_data():
# Masked array
a = [
@@ -2064,7 +2125,7 @@ def test_date2num_missing_data():
mask = [True, False, True, False]
array = np.ma.array(a, mask=mask)
out = date2num(array, units="days since 2000-12-01", calendar="standard")
- assert ((out == np.ma.array([-99, 1, -99, 3] , mask=mask)).all())
+ assert ((out == np.ma.array([-99, 1, -99, 3], mask=mask)).all())
assert ((out.mask == mask).all())
# Scalar masked array
@@ -2101,5 +2162,58 @@ def test_num2date_empty_array():
np.testing.assert_equal(result, expected)
+DATEPARSE_ERROR_TESTS = [
+ ("foo", "In general, units must be"),
+ ("months", "'months since' units only allowed"),
+ ("common_years", "'common_years' units only allowed")
+]
+
+
+ at pytest.mark.parametrize(("units", "match"), DATEPARSE_ERROR_TESTS)
+def test_num2date_unrecognized_units(units, match):
+ with pytest.raises(ValueError, match=match):
+ num2date(0.0, units=f"{units} since 2000-01-01", calendar="standard")
+
+
+ at pytest.mark.parametrize(("units", "match"), DATEPARSE_ERROR_TESTS)
+def test_date2num_unrecognized_units(units, match):
+ date = cftime.datetime(2000, 1, 1, calendar="standard")
+ with pytest.raises(ValueError, match=match):
+ date2num(date, units=f"{units} since 2000-01-01", calendar="standard")
+
+
+def test_num2date_precision():
+ if sys.platform.startswith("win"):
+ pytest.skip("skipping tests that require float128 on windows")
+ testdates = [(1271, 3, 18, 19, 41, 33),
+ (1271, 3, 18, 19, 41, 32, 999998)]
+ unitinc = ['microseconds', 'seconds', 'minutes', 'hours', 'days']
+ for cc in ['standard', 'gregorian', 'julian', 'proleptic_gregorian',
+ 'noleap', 'all_leap', '365_day', '366_day', '360_day']:
+ for uinc in unitinc:
+ if cc in ['standard', 'gregorian', 'julian']:
+ units = uinc + ' since -4713-01-01 12:00:00'
+ elif cc in ['proleptic_gregorian']:
+ units = uinc + ' since -4714-01-01 12:00:00'
+ elif cc in ['noleap', 'all_leap', '365_day', '366_day', '360_day']:
+ units = uinc + ' since 0000-01-01 12:00:00'
+ # scalar
+ date = datetimex(*testdates[0], calendar=cc)
+ num = date2num(date, units, calendar=cc, longdouble=True)
+ date2 = num2date(num, units, calendar=cc)
+ assert date == date2
+ # array
+ date = [ datetimex(*dd, calendar=cc) for dd in testdates ]
+ num = date2num(date, units, calendar=cc, longdouble=True)
+ date2 = num2date(num, units, calendar=cc)
+ for i in range(len(date)):
+ assert date[i] == date2[i]
+ # masked array
+ num = np.ma.array(num, mask=(True, False))
+ date2 = num2date(num, units, calendar=cc)
+ assert np.ma.is_masked(date2[0])
+ assert date[1] == date2[1]
+
+
if __name__ == '__main__':
unittest.main()
View it on GitLab: https://salsa.debian.org/debian-gis-team/cftime/-/compare/ec1e403dc339e0b348e1a509c1da27eaada1f0ab...14ef9aec4a62c80ff0ffde413095fe3f569eb0b4
--
View it on GitLab: https://salsa.debian.org/debian-gis-team/cftime/-/compare/ec1e403dc339e0b348e1a509c1da27eaada1f0ab...14ef9aec4a62c80ff0ffde413095fe3f569eb0b4
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20220919/f6afee18/attachment-0001.htm>
More information about the Pkg-grass-devel
mailing list