[Git][debian-gis-team/cftime][master] 9 commits: Update branch in gbp.conf & Vcs-Git URL.

Bas Couwenberg (@sebastic) gitlab at salsa.debian.org
Sun Aug 15 09:02:13 BST 2021



Bas Couwenberg pushed to branch master at Debian GIS Project / cftime


Commits:
68d105c3 by Bas Couwenberg at 2021-05-21T06:16:34+02:00
Update branch in gbp.conf & Vcs-Git URL.

- - - - -
ee2bdb35 by Bas Couwenberg at 2021-05-21T06:17:02+02:00
New upstream version 1.5.0+ds
- - - - -
69641510 by Bas Couwenberg at 2021-05-21T06:17:03+02:00
Update upstream source from tag 'upstream/1.5.0+ds'

Update to upstream version '1.5.0+ds'
with Debian dir 357d55dabe973d00ff30d238e980df82de884062
- - - - -
1fbfc730 by Bas Couwenberg at 2021-05-21T06:19:32+02:00
New upstream release.

- - - - -
7aefb924 by Bas Couwenberg at 2021-05-21T06:46:26+02:00
Add patch to fix spelling errors.

- - - - -
65fb8a04 by Bas Couwenberg at 2021-05-21T06:46:26+02:00
Set distribution to experimental.

- - - - -
7fd725df by Bas Couwenberg at 2021-05-21T07:06:50+02:00
Fix changelog.

- - - - -
9c1b41b6 by Bas Couwenberg at 2021-08-15T09:52:52+02:00
Revert "Update branch in gbp.conf & Vcs-Git URL."

This reverts commit 68d105c3848ad7b9f0d449ae4e6c102f94155542.

- - - - -
7dcad5be by Bas Couwenberg at 2021-08-15T09:53:24+02:00
Move from experimental to unstable.

- - - - -


9 changed files:

- Changelog
- README.md
- debian/changelog
- + debian/patches/series
- + debian/patches/spelling-errors.patch
- src/cftime/__init__.py
- src/cftime/_cftime.pyx
- − src/cftime/_cftime_legacy.pyx
- test/test_cftime.py


Changes:

=====================================
Changelog
=====================================
@@ -1,3 +1,20 @@
+version 1.5.0 (release tag v1.5.0.rel)
+======================================
+ * clean-up deprecated calendar specific subclasses (PR #231).
+ * added string formatting support to `cftime.datetime` objects
+   (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', 
+   'proleptic_gregorian'). Ignored for idealized calendars like '360_day
+   (they always have year zero).
+ * 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 
+   `DateFromJulianDay` functions (replaced by `cftime.datetime.toordinal`
+   and `cftime.datetime.fromordinal`).  PR #235.
+ * Change ValueError to TypeError in __sub__ (issue #236, PR #236).
 
 version 1.4.1 (release tag v1.4.1.rel)
 ======================================


=====================================
README.md
=====================================
@@ -7,10 +7,21 @@ Time-handling functionality from netcdf4-python
 [![Tag Status](https://img.shields.io/github/tag/UniData/cftime.svg)](https://github.com/Unidata/cftime/tags)
 [![Release Status](https://img.shields.io/github/release/UniData/cftime.svg)](https://github.com/Unidata/cftime/releases)
 [![Commits Status](https://img.shields.io/github/commits-since/UniData/cftime/latest.svg)](https://github.com/UniData/cftime/commits/master)
+[![DOI](https://zenodo.org/badge/73107250.svg)](https://zenodo.org/badge/latestdoi/73107250)
 
 ## News
 For details on the latest updates, see the [Changelog](https://github.com/Unidata/cftime/blob/master/Changelog).
 
+5/20/2021:  Version 1.5.0 released.  Includes support for astronomical year numbering
+(including the year zero) for real-world calendars ('julian', 'gregorian'/'standard',
+and 'proleptic_gregorian') using 'has_year_zero' `cftime.datetime` kwarg..
+Added a 'change_calendar' `cftime.datetime` method to switch to another 
+'real-world' calendar to enable comparison of instances with different calendars.
+Some legacy classes and functions removed (`utime`, `JulianDayFromDate` and
+`DateFromJulianDay`). The functionality of `JulianDayFromDate` and 
+`DateFromJulianDay` is now available from the methods `cftime.datetime.toordinal`
+and `cftime.datetime.fromordinal`.
+
 2/2/2021:  Version 1.4.1 released. Restore use of calendar-specific subclasses
 in `cftime.num2date`, `cftime.datetime.__add__`, and `cftime.datetime.__sub__`.
 The use of this will be removed in a later release.


=====================================
debian/changelog
=====================================
@@ -1,13 +1,21 @@
-cftime (1.4.1+ds-2) UNRELEASED; urgency=medium
+cftime (1.5.0+ds-1) unstable; urgency=medium
 
-  [Helmut Grohne]
-  * Annotate test dependencies <!nocheck>.
-    (closes: #982583)
+  * Move from experimental to unstable.
+
+ -- Bas Couwenberg <sebastic at debian.org>  Sun, 15 Aug 2021 09:53:22 +0200
+
+cftime (1.5.0+ds-1~exp1) experimental; urgency=medium
 
   [ Bas Couwenberg ]
+  * New upstream release.
   * Update watch file for GitHub URL changes.
+  * Add patch to fix spelling errors.
+
+  [Helmut Grohne]
+  * Annotate test dependencies <!nocheck>.
+    (closes: #982583)
 
- -- Bas Couwenberg <sebastic at debian.org>  Fri, 12 Feb 2021 08:54:11 +0100
+ -- Bas Couwenberg <sebastic at debian.org>  Fri, 21 May 2021 06:27:21 +0200
 
 cftime (1.4.1+ds-1) unstable; urgency=medium
 


=====================================
debian/patches/series
=====================================
@@ -0,0 +1 @@
+spelling-errors.patch


=====================================
debian/patches/spelling-errors.patch
=====================================
@@ -0,0 +1,16 @@
+Description: Fix spelling errors.
+ * allowd -> allowed
+Author: Bas Couwenberg <sebastic at debian.org>
+Forwarded: https://github.com/Unidata/cftime/pull/240
+
+--- a/src/cftime/_cftime.pyx
++++ b/src/cftime/_cftime.pyx
+@@ -1287,7 +1287,7 @@ The default format of the string produce
+ 
+         Day 0 starts at noon January 1 of the year -4713 for the
+         julian, gregorian and standard calendars (year -4712 if year
+-        zero allowd).
++        zero allowed).
+ 
+         Day 0 starts at noon on November 24 of the year -4714 for the
+         proleptic gregorian calendar (year -4713 if year zero allowed).


=====================================
src/cftime/__init__.py
=====================================
@@ -1,10 +1,11 @@
-from ._cftime import datetime, real_datetime, _parse_date
+from ._cftime import (datetime, real_datetime,
+                      _parse_date, _dateparse, _datesplit)
 from ._cftime import num2date, date2num, date2index, time2index, num2pydate
-from ._cftime import microsec_units, millisec_units, \
-                     sec_units, hr_units, day_units, min_units,\
-                     UNIT_CONVERSION_FACTORS
-from ._cftime import __version__
-# legacy functions in _cftime_legacy.pyx
-from ._cftime import DatetimeNoLeap, DatetimeAllLeap, Datetime360Day, DatetimeJulian, \
-                     DatetimeGregorian, DatetimeProlepticGregorian
-from ._cftime import utime, JulianDayFromDate, DateFromJulianDay
+from ._cftime import (microsec_units, millisec_units,
+                     sec_units, hr_units, day_units, min_units,
+                     UNIT_CONVERSION_FACTORS)
+from ._cftime import __version__, CFWarning
+# these will be removed in a future release
+from ._cftime import (DatetimeNoLeap, DatetimeAllLeap, Datetime360Day,
+                     Datetime360Day, DatetimeJulian, 
+                     DatetimeGregorian, DatetimeProlepticGregorian)


=====================================
src/cftime/_cftime.pyx
=====================================
@@ -15,7 +15,6 @@ from datetime import timedelta, MINYEAR, MAXYEAR
 import time                     # strftime
 import warnings
 
-
 microsec_units = ['microseconds','microsecond', 'microsec', 'microsecs']
 millisec_units = ['milliseconds', 'millisecond', 'millisec', 'millisecs', 'msec', 'msecs', 'ms']
 sec_units =      ['second', 'seconds', 'sec', 'secs', 's']
@@ -30,6 +29,7 @@ _units = microsec_units+millisec_units+sec_units+min_units+hr_units+day_units
 # for definitions.
 _calendars = ['standard', 'gregorian', 'proleptic_gregorian',
               'noleap', 'julian', 'all_leap', '365_day', '366_day', '360_day']
+_idealized_calendars= ['all_leap','noleap','366_day','365_day']
 # Following are number of days per month
 cdef int[12] _dayspermonth      = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
 cdef int[12] _dayspermonth_leap = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
@@ -37,11 +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]
 
-# Reverse operator lookup for datetime.__richcmp__
-_rop_lookup = {Py_LT: '__gt__', Py_LE: '__ge__', Py_EQ: '__eq__',
-               Py_GT: '__lt__', Py_GE: '__le__', Py_NE: '__ne__'}
-
-__version__ = '1.4.1'
+__version__ = '1.5.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.
@@ -86,12 +82,16 @@ def _datesplit(timestr):
 
     return units.lower(), remainder
 
-def _dateparse(timestr,calendar):
+def _dateparse(timestr,calendar,has_year_zero=None):
     """parse a string of the form time-units since yyyy-mm-dd hh:mm:ss,
     return a datetime instance"""
     # same as version in cftime, but returns a timezone naive
     # python datetime instance with the utc_offset included.
 
+    calendar = calendar.lower()
+    # set calendar-specific defaults for has_year_zero
+    if has_year_zero is None:
+        has_year_zero = _year_zero_defaults(calendar)
     (units, isostring) = _datesplit(timestr)
     if not ((units in month_units and calendar=='360_day') or units in _units):
         if units in month_units and calendar != '360_day':
@@ -113,19 +113,19 @@ def _dateparse(timestr,calendar):
         raise ValueError(
             'there are only 30 days in every month with the 360_day calendar')
     basedate = datetime(year, month, day, hour, minute, second,
-                        microsecond,calendar=calendar)
+                        microsecond,calendar=calendar,has_year_zero=has_year_zero)
     # subtract utc_offset from basedate time instance (which is timezone naive)
     if utc_offset:
         basedate -= timedelta(days=utc_offset/1440.)
     return basedate
 
 def _can_use_python_datetime(date,calendar):
-    gregorian = datetime(1582,10,15,calendar=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))
 
 @cython.embedsignature(True)
-def date2num(dates,units,calendar=None):
+def date2num(dates,units,calendar=None,has_year_zero=None):
     """
     Return numeric time values given datetime objects. The units
     of the numeric time values are described by the **units** argument
@@ -146,12 +146,24 @@ def date2num(dates,units,calendar=None):
 
     **calendar**: describes the calendar to be used in the time calculations.
     All the values currently defined in the
-    [CF metadata convention](http://cfconventions.org)
+    `CF metadata convention <http://cfconventions.org>`__ are supported.
     Valid calendars **'standard', 'gregorian', 'proleptic_gregorian'
     'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'**.
-    Default is `None` which means the calendar associated with the rist
+    Default is `None` which means the calendar associated with the first
     input datetime instance will be used.
 
+    **has_year_zero**: If set to True, astronomical year numbering
+    is used and the year zero exists.  If set to False for real-world
+    calendars, then historical year numbering is used and the year 1 is
+    preceded by year -1 and no year zero exists.
+    The defaults are False for real-world calendars
+    and True for idealized calendars.
+    The defaults can only be over-ridden for the real-world calendars,
+    for the the idealized calendars the year zero 
+    always exists and the has_year_zero kwarg is ignored.
+    This kwarg is not needed to define calendar systems allowed by CF
+    (the calendar-specific defaults do this).
+
     returns a numeric time value, or an array of numeric time values
     with approximately 1 microsecond accuracy.
     """
@@ -178,6 +190,22 @@ def date2num(dates,units,calendar=None):
            all_python_datetimes = False
            break
 
+    # if has_year_zero is None and calendar is None, use first input cftime.datetime instance
+    # to determine if year zero is to be included.
+    # If calendar is specified, use calendar specific defaults
+    if has_year_zero is None:
+        if calendar is None:
+            d0 = dates.item(0)
+            if isinstance(d0,datetime_python):
+                has_year_zero = False
+            else:
+                try:
+                    has_year_zero = d0.has_year_zero
+                except AttributeError:
+                    raise ValueError('has_year_zero not specified',type(d0))
+        else:
+            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 not calendar:
@@ -194,10 +222,10 @@ def date2num(dates,units,calendar=None):
                     raise ValueError('no calendar specified',type(d0))
 
     calendar = calendar.lower()
-    basedate = _dateparse(units,calendar=calendar)
+    basedate = _dateparse(units,calendar=calendar,has_year_zero=has_year_zero)
     (unit, isostring) = _datesplit(units)
     # real-world calendars cannot have zero as a reference year.
-    if calendar in ['julian', 'standard', 'gregorian', 'proleptic_gregorian']:
+    if calendar in ['julian', 'standard', 'gregorian', 'proleptic_gregorian'] and not has_year_zero:
         if basedate.year == 0:
             msg='zero not allowed as a reference year, does not exist in Julian or Gregorian calendars'
             raise ValueError(msg)
@@ -217,7 +245,7 @@ def date2num(dates,units,calendar=None):
     else:
         use_python_datetime = False
         # convert basedate to specified calendar
-        basedate =  to_calendar_specific_datetime(basedate, calendar, False)
+        basedate =  to_calendar_specific_datetime(basedate, calendar, False, has_year_zero=has_year_zero)
     times = []; n = 0
     for date in dates.flat:
         # use python datetime if possible.
@@ -226,7 +254,7 @@ def date2num(dates,units,calendar=None):
             if getattr(date, 'tzinfo',None) is not None:
                 date = date.replace(tzinfo=None) - date.utcoffset()
         else: # convert date to same calendar specific cftime.datetime instance
-            date = to_calendar_specific_datetime(date, calendar, False)
+            date = to_calendar_specific_datetime(date, calendar, False, has_year_zero=has_year_zero)
         if ismasked and mask.flat[n]:
             times.append(None)
         else:
@@ -307,7 +335,7 @@ DATE_TYPES = {
      "gregorian": DatetimeGregorian
  }
 
-def to_calendar_specific_datetime(dt, calendar, use_python_datetime):
+def to_calendar_specific_datetime(dt, calendar, use_python_datetime,has_year_zero=None):
     if use_python_datetime:
         return real_datetime(
                dt.year,
@@ -327,7 +355,7 @@ def to_calendar_specific_datetime(dt, calendar, use_python_datetime):
                dt.minute,
                dt.second,
                dt.microsecond,
-               calendar=calendar)
+               calendar=calendar,has_year_zero=has_year_zero)
 
 _MAX_INT64 = np.iinfo("int64").max
 _MIN_INT64 = np.iinfo("int64").min
@@ -390,7 +418,8 @@ def num2date(
     units,
     calendar='standard',
     only_use_cftime_datetimes=True,
-    only_use_python_datetimes=False
+    only_use_python_datetimes=False,
+    has_year_zero=None
 ):
     """
     Return datetime objects given numeric time values. The units
@@ -408,7 +437,7 @@ def num2date(
 
     **calendar**: describes the calendar used in the time calculations.
     All the values currently defined in the
-    [CF metadata convention](http://cfconventions.org)
+    `CF metadata convention <http://cfconventions.org>`__ are supported.
     Valid calendars **'standard', 'gregorian', 'proleptic_gregorian'
     'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'**.
     Default is **'standard'**, which is a mixed Julian/Gregorian calendar.
@@ -421,6 +450,18 @@ def num2date(
     objects and raise an error if this is not possible. Ignored unless
     **only_use_cftime_datetimes=False**. Default **False**.
 
+    **has_year_zero**: if set to True, astronomical year numbering
+    is used and the year zero exists.  If set to False for real-world
+    calendars, then historical year numbering is used and the year 1 is
+    preceded by year -1 and no year zero exists.
+    The defaults are False for real-world calendars
+    and True for idealized calendars.
+    The defaults can only be over-ridden for the real-world calendars,
+    for the the idealized calendars the year zero 
+    always exists and the has_year_zero kwarg is ignored.
+    This kwarg is not needed to define calendar systems allowed by CF
+    (the calendar-specific defaults do this).
+
     returns a datetime instance, or an array of datetime instances with
     microsecond accuracy, if possible.
 
@@ -437,7 +478,10 @@ def num2date(
     contains one.
     """
     calendar = calendar.lower()
-    basedate = _dateparse(units,calendar=calendar)
+    # set calendar-specific defaults for has_year_zero
+    if has_year_zero is None:
+        has_year_zero = _year_zero_defaults(calendar)
+    basedate = _dateparse(units,calendar=calendar,has_year_zero=has_year_zero)
 
     can_use_python_datetime=_can_use_python_datetime(basedate,calendar)
     if not only_use_cftime_datetimes and only_use_python_datetimes:
@@ -448,9 +492,9 @@ def num2date(
     unit, ignore = _datesplit(units)
 
     # real-world calendars limited to positive reference years.
-    if calendar in ['julian', 'standard', 'gregorian', 'proleptic_gregorian']:
+    if calendar in ['julian', 'standard', 'gregorian', 'proleptic_gregorian'] and not has_year_zero:
         if basedate.year == 0:
-            msg='zero not allowed as a reference year, does not exist in Julian or Gregorian calendars'
+            msg='zero not allowed as a reference year unless has_year_zero=True'
             raise ValueError(msg)
 
     use_python_datetime = False
@@ -462,7 +506,7 @@ def num2date(
         # return python datetime if possible.
         use_python_datetime = True
 
-    basedate = to_calendar_specific_datetime(basedate, calendar, use_python_datetime)
+    basedate = to_calendar_specific_datetime(basedate, calendar, use_python_datetime, has_year_zero=has_year_zero)
 
     if unit not in UNIT_CONVERSION_FACTORS:
         raise ValueError("Unsupported time units provided, {!r}.".format(unit))
@@ -487,7 +531,7 @@ def num2date(
 
 
 @cython.embedsignature(True)
-def date2index(dates, nctime, calendar=None, select='exact'):
+def date2index(dates, nctime, calendar=None, select='exact', has_year_zero=None):
     """
     Return indices of a netCDF time variable corresponding to the given dates.
 
@@ -497,14 +541,13 @@ def date2index(dates, nctime, calendar=None, select='exact'):
     **nctime**: A netCDF time variable object. The nctime object must have a
     **units** attribute.
 
-    **calendar**: describes the calendar used in the time calculations.
+    **calendar**: describes the calendar to be used in the time calculations.
     All the values currently defined in the
-    [CF metadata convention](http://cfconventions.org)
+    `CF metadata convention <http://cfconventions.org>`__ are supported.
     Valid calendars **'standard', 'gregorian', 'proleptic_gregorian'
     'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'**.
-    Default is **'standard'**, which is a mixed Julian/Gregorian calendar.
-    If **calendar** is None, its value is given by **nctime.calendar** or
-    **standard** if no such attribute exists.
+    Default is `None` which means the calendar associated with the first
+    input datetime instance will be used.
 
     **select**: **'exact', 'before', 'after', 'nearest'**
     The index selection method. **exact** will return the indices perfectly
@@ -513,6 +556,18 @@ def date2index(dates, nctime, calendar=None, select='exact'):
     an exact match cannot be found. **nearest** will return the indices that
     correspond to the closest dates.
 
+    **has_year_zero**: if set to True, astronomical year numbering
+    is used and the year zero exists.  If set to False for real-world
+    calendars, then historical year numbering is used and the year 1 is
+    preceded by year -1 and no year zero exists.
+    The defaults are False for real-world calendars
+    and True for idealized calendars.
+    The defaults can only be over-ridden for the real-world calendars,
+    for the the idealized calendars the year zero 
+    always exists and the has_year_zero kwarg is ignored.
+    This kwarg is not needed to define calendar systems allowed by CF
+    (the calendar-specific defaults do this).
+
     returns an index (indices) of the netCDF time variable corresponding
     to the given datetime object(s).
     """
@@ -520,22 +575,51 @@ def date2index(dates, nctime, calendar=None, select='exact'):
         nctime.units
     except AttributeError:
         raise AttributeError("netcdf time variable is missing a 'units' attribute")
-    if calendar == None:
-        calendar = getattr(nctime, 'calendar', 'standard')
+
+    dates_test = np.asanyarray(dates) # convert to numpy array
+
+    # if has_year_zero is None and calendar is None, use first input cftime.datetime instance
+    # to determine if year zero is to be included.
+    # If calendar is specified, use calendar specific defaults
+    if has_year_zero is None:
+        if calendar is None:
+            d0 = dates_test.item(0)
+            if isinstance(d0,datetime_python):
+                has_year_zero = False
+            else:
+                try:
+                    has_year_zero = d0.has_year_zero
+                except AttributeError:
+                    raise ValueError('has_year_zero not specified',type(d0))
+        else:
+            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 not calendar:
+        d0 = dates_test.item(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(nctime.units,calendar=calendar)
+    basedate = _dateparse(nctime.units,calendar=calendar,has_year_zero=has_year_zero)
     # real-world calendars limited to positive reference years.
-    if calendar in ['julian', 'standard', 'gregorian', 'proleptic_gregorian']:
+    if calendar in ['julian', 'standard', 'gregorian', 'proleptic_gregorian'] and not has_year_zero:
         if basedate.year == 0:
             msg='zero not allowed as a reference year, does not exist in Julian or Gregorian calendars'
             raise ValueError(msg)
 
     if _can_use_python_datetime(basedate,calendar):
         # use python datetime
-        times = date2num(dates,nctime.units,calendar=calendar)
+        times = date2num(dates,nctime.units,calendar=calendar,has_year_zero=has_year_zero)
         return time2index(times, nctime, calendar, select)
     else: # use cftime module for other cases
-        return _date2index(dates, nctime, calendar, select)
+        return _date2index(dates, nctime, calendar, select, has_year_zero=has_year_zero)
 
 
 cdef _parse_timezone(tzstring):
@@ -597,7 +681,7 @@ cpdef _parse_date(datestring):
         int(groups["fraction"]),\
         tzoffset_mins
 
-cdef _check_index(indices, times, nctime, calendar, select):
+cdef _check_index(indices, times, nctime, select):
     """Return True if the time indices given correspond to the given times,
     False otherwise.
 
@@ -612,9 +696,6 @@ cdef _check_index(indices, times, nctime, calendar, select):
     nctime : netCDF Variable object
     NetCDF time object.
 
-    calendar : string
-    Calendar of nctime.
-
     select : string
     Index selection method.
     """
@@ -661,10 +742,8 @@ cdef _check_index(indices, times, nctime, calendar, select):
         return np.all(delta_check <= delta_after) and np.all(delta_check <= delta_before)
 
 
-def _date2index(dates, nctime, calendar=None, select='exact'):
+def _date2index(dates, nctime, calendar=None, select='exact', has_year_zero=None):
     """
-    _date2index(dates, nctime, calendar=None, select='exact')
-
     Return indices of a netCDF time variable corresponding to the given dates.
 
     **dates**: A datetime object or a sequence of datetime objects.
@@ -687,15 +766,24 @@ def _date2index(dates, nctime, calendar=None, select='exact'):
     corresponding to the dates just before or just after the given dates if
     an exact match cannot be found. `nearest` will return the indices that
     correspond to the closest dates.
+
+    **has_year_zero**: if set to True, astronomical year numbering
+    is used and the year zero exists.  If set to False for real-world
+    calendars, then historical year numbering is used and the year 1 is
+    preceded by year -1 and no year zero exists.
+    The defaults are False for real-world calendars
+    and True for idealized calendars.
+    The defaults can only be over-ridden for the real-world calendars,
+    for the the idealized calendars the year zero 
+    always exists and the has_year_zero kwarg is ignored.
+    This kwarg is not needed to define calendar systems allowed by CF
+    (the calendar-specific defaults do this).
     """
     try:
         nctime.units
     except AttributeError:
         raise AttributeError("netcdf time variable is missing a 'units' attribute")
-    # Setting the calendar.
-    if calendar == None:
-        calendar = getattr(nctime, 'calendar', 'standard')
-    times = date2num(dates,nctime.units,calendar=calendar)
+    times = date2num(dates,nctime.units,calendar=calendar, has_year_zero=has_year_zero)
     return time2index(times, nctime, calendar=calendar, select=select)
 
 
@@ -753,7 +841,7 @@ def time2index(times, nctime, calendar=None, select='exact'):
     # Checking that the index really corresponds to the given time.
     # If the times do not correspond, then it means that the times
     # are not increasing uniformly and we try the bisection method.
-    if not _check_index(index, times, nctime, calendar, select):
+    if not _check_index(index, times, nctime, select):
 
         # Use the bisection method. Assumes nctime is ordered.
         import bisect
@@ -820,6 +908,24 @@ cdef to_tuple(dt):
     return (dt.year, dt.month, dt.day, dt.hour, dt.minute,
             dt.second, dt.microsecond)
 
+cdef _year_zero_defaults(calendar):
+    if calendar: calendar = calendar.lower()
+    if calendar in ['standard','gregorian','julian']:
+       return False
+    elif calendar in ['proleptic_gregorian']:
+       #return True # ISO 8601 year zero=1 BC
+       return False
+    elif calendar in _idealized_calendars:
+       return True
+    else:
+       return False
+
+# factory function without optional kwargs that can be used in datetime.__reduce__
+def _create_datetime(args, kwargs): return datetime(*args, **kwargs)
+# custorm warning for invalid CF dates.
+class CFWarning(UserWarning):
+    pass
+
 @cython.embedsignature(True)
 cdef class datetime(object):
     """
@@ -834,7 +940,7 @@ for cftime.datetime instances using
 'gregorian' and 'proleptic_gregorian' calendars.
 
 All the calendars currently defined in the
-[CF metadata convention](http://cfconventions.org) are supported.
+`CF metadata convention <http://cfconventions.org>`__ are supported.
 Valid calendars are 'standard', 'gregorian', 'proleptic_gregorian'
 'noleap', '365_day', '360_day', 'julian', 'all_leap', '366_day'.
 Default is 'standard', which is a mixed Julian/Gregorian calendar.
@@ -844,8 +950,20 @@ and 'noleap'/'365_day'.
 If the calendar kwarg is set to a blank string ('') or None (the default is 'standard') the 
 instance will not be calendar-aware and some methods will not work.
 
+If the has_year_zero kwarg is set to True, astronomical year numbering
+is used and the year zero exists.  If set to False for real-world
+calendars, then historical year numbering is used and the year 1 is
+preceded by year -1 and no year zero exists.
+The defaults are False for real-world calendars
+and True for idealized calendars.
+The defaults can only be over-ridden for the real-world calendars,
+for the the idealized calendars the year zero 
+always exists and the has_year_zero kwarg is ignored.
+This kwarg is not needed to define calendar systems allowed by CF
+(the calendar-specific defaults do this).
+
 Has isoformat, strftime, timetuple, replace, dayofwk, dayofyr, daysinmonth,
-__repr__, __add__, __sub__, __str__ and comparison methods. 
+__repr__,__format__, __add__, __sub__, __str__ and comparison methods. 
 
 dayofwk, dayofyr, daysinmonth, __add__ and __sub__ only work for calendar-aware
 instances.
@@ -868,7 +986,7 @@ The default format of the string produced by strftime is controlled by self.form
 
     def __init__(self, int year, int month, int day, int hour=0, int minute=0,
                        int second=0, int microsecond=0, int dayofwk=-1, 
-                       int dayofyr = -1, calendar='standard'):
+                       int dayofyr = -1, calendar='standard', has_year_zero=None):
 
         self.year = year
         self.month = month
@@ -882,6 +1000,16 @@ The default format of the string produced by strftime is controlled by self.form
         self.tzinfo = None
         if calendar:
             calendar = calendar.lower()
+        # set calendar-specific defaults for has_year_zero
+        if has_year_zero is None:
+            has_year_zero = _year_zero_defaults(calendar)
+        if not has_year_zero and calendar in _idealized_calendars:
+            warnings.warn('has_year_zero kwarg ignored for idealized calendars (always True)')
+        #if (calendar in ['julian','gregorian','standard'] and year <= 0) or\
+        #   (calendar == 'proleptic_gregorian' and not has_year_zero and year < 1):
+        #    msg="this date/calendar/year zero convention is not supported by CF"
+        #    warnings.warn(msg,category=CFWarning)
+        self.has_year_zero = has_year_zero
         if calendar == 'gregorian' or calendar == 'standard':
             # dates after 1582-10-15 can be converted to and compared to
             # proleptic Gregorian dates
@@ -890,38 +1018,31 @@ The default format of the string produced by strftime is controlled by self.form
                 self.datetime_compatible = True
             else:
                 self.datetime_compatible = False
-            assert_valid_date(self, is_leap_gregorian, True)
-            self.has_year_zero = False
+            assert_valid_date(self, is_leap_gregorian, True, has_year_zero=has_year_zero)
         elif calendar == 'noleap' or calendar == '365_day':
             self.calendar = 'noleap'
             self.datetime_compatible = False
             assert_valid_date(self, no_leap, False, has_year_zero=True)
-            self.has_year_zero = True
         elif calendar == 'all_leap' or calendar == '366_day':
             self.calendar = 'all_leap'
             self.datetime_compatible = False
             assert_valid_date(self, all_leap, False, has_year_zero=True)
-            self.has_year_zero = True
         elif calendar == '360_day':
             self.calendar = calendar
             self.datetime_compatible = False
             assert_valid_date(self, no_leap, False, has_year_zero=True, is_360_day=True)
-            self.has_year_zero = True
         elif calendar == 'julian':
             self.calendar = calendar
             self.datetime_compatible = False
-            assert_valid_date(self, is_leap_julian, False)
-            self.has_year_zero = False
+            assert_valid_date(self, is_leap_julian, False, has_year_zero=has_year_zero)
         elif calendar == 'proleptic_gregorian':
             self.calendar = calendar
             self.datetime_compatible = True
-            assert_valid_date(self, is_leap_proleptic_gregorian, False)
-            self.has_year_zero = False
+            assert_valid_date(self, is_leap_proleptic_gregorian, False, has_year_zero=has_year_zero)
         elif calendar == '' or calendar is None:
             # instance not calendar-aware, some method will not work
             self.calendar = ''
             self.datetime_compatible = False
-            self.has_year_zero = False
         else:
             raise ValueError(
                 "calendar must be one of %s, got '%s'" % (str(_calendars), calendar))
@@ -981,6 +1102,14 @@ The default format of the string produced by strftime is controlled by self.form
             format = self.format
         return _strftime(self, format)
 
+    def __format__(self, format):
+        # the string format "{t_obj}".format(t_obj=t_obj)
+        # without an explicit format gives an empty string (format='')
+        # so set this to None to get the default strftime behaviour
+        if format == '':
+            format = None
+        return self.strftime(format)
+
     def replace(self, **kwargs):
         """Return datetime with new specified fields."""
         args = {"year": self.year,
@@ -1022,14 +1151,18 @@ The default format of the string produced by strftime is controlled by self.form
                              self.microsecond)
 
     def __repr__(self):
+        if self.__class__.__name__ != 'datetime': # a calendar-specific sub-class
+            return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8}, has_year_zero={9})".format('cftime',
+            self.__class__.__name__,
+            self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond,self.has_year_zero)
         if self.calendar == None:
-            return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8}, calendar={9})".format('cftime',
+            return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8}, calendar={9}, has_year_zero={10})".format('cftime',
             self.__class__.__name__,
-            self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond,self.calendar)
+            self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond,self.calendar,self.has_year_zero)
         else:
-            return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8}, calendar='{9}')".format('cftime',
+            return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8}, calendar='{9}', has_year_zero={10})".format('cftime',
             self.__class__.__name__,
-            self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond,self.calendar)
+            self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond,self.calendar,self.has_year_zero)
 
     def __str__(self):
         return self.isoformat(' ')
@@ -1068,19 +1201,27 @@ The default format of the string produced by strftime is controlled by self.form
                 self.second, self.microsecond)
 
     def __richcmp__(self, other, int op):
-        cdef datetime dt, dt_other
+        cdef datetime dt, dt_other, d1, d2
         dt = self
         if isinstance(other, datetime):
             dt_other = other
             # comparing two datetime instances
-            if dt.calendar == dt_other.calendar:
+            if dt.calendar == dt_other.calendar and dt.has_year_zero == dt_other.has_year_zero:
                 return PyObject_RichCompare(dt.to_tuple(), dt_other.to_tuple(), op)
             else:
-                # Note: it *is* possible to compare datetime
-                # instances that use difference calendars by using
-                # date2num, but this implementation does
-                # not attempt it.
-                raise TypeError("cannot compare {0!r} and {1!r} (different calendars)".format(dt, dt_other))
+                # convert both to common calendar (ISO 8601), then compare
+                try:
+                    if self.calendar == 'proleptic_gregorian' and self.has_year_zero:
+                        d1 = self
+                    else:
+                        d1 = self.change_calendar('proleptic_gregorian',has_year_zero=True)
+                    if other.calendar == 'proleptic_gregorian' and other.has_year_zero:
+                        d2 = other
+                    else:
+                        d2 = other.change_calendar('proleptic_gregorian',has_year_zero=True)
+                except ValueError: # change_calendar won't work for idealized calendars (ValueError)
+                    raise TypeError("cannot compare {0!r} and {1!r}".format(dt, dt_other))
+                return PyObject_RichCompare(d1.to_tuple(), d2.to_tuple(), op)
         elif isinstance(other, datetime_python):
             # comparing datetime and real_datetime
             if not dt.datetime_compatible:
@@ -1091,37 +1232,65 @@ The default format of the string produced by strftime is controlled by self.form
             return NotImplemented
 
     cdef _getstate(self):
-        return (self.year, self.month, self.day, self.hour,
-                self.minute, self.second, self.microsecond,
-                self._dayofwk, self._dayofyr, self.calendar)
+        """return args and kwargs needed to create class instance"""
+        args = (self.year, self.month, self.day)
+        kwargs = {'hour': self.hour,
+                  'minute': self.minute,
+                  'second': self.second,
+                  'microsecond': self.microsecond,
+                  'dayofwk': self._dayofwk,
+                  'dayofyr': self._dayofyr,
+                  'calendar': self.calendar,
+                  'has_year_zero': self.has_year_zero}
+        return args, kwargs
 
     def __reduce__(self):
         """special method that allows instance to be pickled"""
-        return (self.__class__, self._getstate())
+        args, kwargs = self._getstate()
+        return (_create_datetime, (args, kwargs))
 
     cdef _add_timedelta(self, other):
         return NotImplemented
 
     @staticmethod
-    def fromordinal(jday,calendar='standard'):
-        """Create a datetime instance from a julian day ordinal and calendar
-        (inverseeof toordinal)."""
+    def fromordinal(jday,calendar='standard',has_year_zero=None):
+        """Create a datetime instance from a julian day ordinal, calendar
+        and (optionally) year zero convention (inverse of toordinal). The
+        Julian day number is the number of days since noon UTC January 1, 4713
+        in the proleptic julian calendar with no year zero  (November 24, 4713 
+        in the proleptic gregorian calendar that includes the year zero). For
+        idealized calendars, the origin is noon UTC of the year zero."""
+        calendar = calendar.lower()
+        # set calendar-specific defaults for has_year_zero
+        if has_year_zero is None:
+            has_year_zero = _year_zero_defaults(calendar)
         if calendar in ['standard','julian','gregorian']:
-            units = 'days since -4713-1-1-12'
+            if has_year_zero:
+               units = 'days since -4712-1-1-12'
+            else:
+               units = 'days since -4713-1-1-12'
         elif calendar == 'proleptic_gregorian':
-            units = 'days since -4714-11-24-12'
+            if has_year_zero:
+                units = 'days since -4713-11-24-12'
+            else:
+                units = 'days since -4714-11-24-12'
         else:
             units = 'days since 0-1-1-12'
-        return num2date(jday,units=units,calendar=calendar)
+        # suppress warning about invalid CF date (year <= 0)
+        #with warnings.catch_warnings():
+        #    warnings.simplefilter("ignore",category=CFWarning)
+        jd = num2date(jday,units=units,calendar=calendar,has_year_zero=has_year_zero)
+        return jd
 
     def toordinal(self,fractional=False):
         """Return (integer) julian day ordinal.
 
         Day 0 starts at noon January 1 of the year -4713 for the
-        julian, gregorian and standard calendars.
+        julian, gregorian and standard calendars (year -4712 if year
+        zero allowd).
 
         Day 0 starts at noon on November 24 of the year -4714 for the
-        proleptic gregorian calendar.
+        proleptic gregorian calendar (year -4713 if year zero allowed).
 
         Day 0 starts at noon on January 1 of the year zero is for the
         360_day, 365_day, 366_day, all_leap and noleap calendars.
@@ -1142,15 +1311,27 @@ The default format of the string produced by strftime is controlled by self.form
         else:
             return ijd
 
+    def change_calendar(self,calendar,has_year_zero=None):
+        cdef datetime dt
+        """return a new cftime.datetime instance with a different 'real-world' calendar."""
+        if calendar in _idealized_calendars or self.calendar in _idealized_calendars:
+            raise ValueError('change_calendar only works for real-world calendars')
+        # fixed frame of reference is days since -4713-1-1 in the Julian calendar with no year zero
+        return self.fromordinal(self.toordinal(fractional=True),
+                                calendar=calendar,has_year_zero=has_year_zero)
+
     def __add__(self, other):
         cdef datetime dt
+        cdef bint has_year_zero
         if isinstance(self, datetime) and isinstance(other, timedelta):
             dt = self
             calendar = self.calendar
+            has_year_zero = self.has_year_zero
             delta = other
         elif isinstance(self, timedelta) and isinstance(other, datetime):
             dt = other
             calendar = other.calendar
+            has_year_zero = other.has_year_zero
             delta = self
         else:
             return NotImplemented
@@ -1166,27 +1347,30 @@ The default format of the string produced by strftime is controlled by self.form
             #return dt.__class__(*add_timedelta(dt, delta, all_leap, False, True),calendar=calendar)
             return DatetimeAllLeap(*add_timedelta(dt, delta, all_leap, False, True))
         elif calendar == 'julian':
-            #return dt.__class__(*add_timedelta(dt, delta, is_leap_julian, False, False),calendar=calendar)
-            return DatetimeJulian(*add_timedelta(dt, delta, is_leap_julian, False, False))
+            #return dt.__class__(*add_timedelta(dt, delta, is_leap_julian, False, has_year_zero),calendar=calendar,has_year_zero=has_year_zero)
+            return DatetimeJulian(*add_timedelta(dt, delta, is_leap_julian, False, has_year_zero),has_year_zero=has_year_zero)
         elif calendar == 'gregorian':
-            #return dt.__class__(*add_timedelta(dt, delta, is_leap_gregorian, True, False),calendar=calendar)
-            return DatetimeGregorian(*add_timedelta(dt, delta, is_leap_gregorian, True, False))
+            #return dt.__class__(*add_timedelta(dt, delta, is_leap_gregorian, True, has_year_zero),calendar=calendar,has_year_zero=has_year_zero)
+            return DatetimeGregorian(*add_timedelta(dt, delta, is_leap_gregorian, True, has_year_zero),has_year_zero=has_year_zero)
         elif calendar == 'proleptic_gregorian':
-            #return dt.__class__(*add_timedelta(dt, delta, is_leap_proleptic_gregorian, False, False),calendar=calendar)
-            return DatetimeProlepticGregorian(*add_timedelta(dt, delta, is_leap_proleptic_gregorian, False, False))
+            #return dt.__class__(*add_timedelta(dt, delta, is_leap_proleptic_gregorian, False, has_year_zero),calendar=calendar,has_year_zero=has_year_zero)
+            return DatetimeProlepticGregorian(*add_timedelta(dt, delta, is_leap_proleptic_gregorian, False, has_year_zero),has_year_zero=has_year_zero)
         else:
             return NotImplemented
 
     def __sub__(self, other):
         cdef datetime dt
+        cdef bint has_year_zero
         if isinstance(self, datetime): # left arg is a datetime instance
             dt = self
             if isinstance(other, datetime):
                 # datetime - datetime
                 if dt.calendar != other.calendar:
-                    raise ValueError("cannot compute the time difference between dates with different calendars")
+                    raise TypeError("cannot compute the time difference between dates with different calendars")
                 if dt.calendar == "":
-                    raise ValueError("cannot compute the time difference between dates that are not calendar-aware")
+                    raise TypeError("cannot compute the time difference between dates that are not calendar-aware")
+                if dt.has_year_zero != other.has_year_zero:
+                    raise TypeError("cannot compute the time difference between dates with year zero conventions")
                 ordinal_self = self.toordinal() # julian day
                 ordinal_other = other.toordinal()
                 days = ordinal_self - ordinal_other
@@ -1203,12 +1387,13 @@ Cannot compute the time difference between dates with different calendars.
 One of the datetime objects may have been converted to a native python
 datetime instance.  Try using only_use_cftime_datetimes=True when creating the
 datetime object."""
-                    raise ValueError(msg)
+                    raise TypeError(msg)
                 return dt._to_real_datetime() - other
             elif isinstance(other, timedelta):
                 # datetime - timedelta
                 # return calendar-specific subclasses for backward compatbility,
                 # even though after 1.3.0 this is no longer necessary.
+                has_year_zero=self.has_year_zero
                 if self.calendar == '360_day':
                     #return self.__class__(*add_timedelta_360_day(self, -other),calendar=self.calendar)
                     return Datetime360Day(*add_timedelta_360_day(self, -other))
@@ -1219,15 +1404,17 @@ datetime object."""
                     #return self.__class__(*add_timedelta(self, -other, all_leap, False, True),calendar=self.calendar)
                     return DatetimeAllLeap(*add_timedelta(self, -other, all_leap, False, True))
                 elif self.calendar == 'julian':
-                    #return self.__class__(*add_timedelta(self, -other, is_leap_julian, False, False),calendar=self.calendar)
-                    return DatetimeJulian(*add_timedelta(self, -other, is_leap_julian, False, False))
+                    #return self.__class__(*add_timedelta(self, -other, 
+                    #     is_leap_julian, False, has_year_zero),calendar=self.calendar,has_year_zero=self.has_year_zero)
+                    return DatetimeJulian(*add_timedelta(self, -other, is_leap_julian, False, has_year_zero),has_year_zero=self.has_year_zero)
                 elif self.calendar == 'gregorian':
-                    #return self.__class__(*add_timedelta(self, -other, is_leap_gregorian, True, False),calendar=self.calendar)
-                    return DatetimeGregorian(*add_timedelta(self, -other, is_leap_gregorian, True, False))
+                    #return self.__class__(*add_timedelta(self, -other, 
+                    #     is_leap_gregorian, True, has_year_zero),calendar=self.calendar,has_year_zero=self.has_year_zero)
+                    return DatetimeGregorian(*add_timedelta(self, -other, is_leap_gregorian, True, has_year_zero),has_year_zero=self.has_year_zero)
                 elif self.calendar == 'proleptic_gregorian':
                     #return self.__class__(*add_timedelta(self, -other,
-                    #    is_leap_proleptic_gregorian, False, False),calendar=self.calendar)
-                    return DatetimeProlepticGregorian(*add_timedelta(self, -other, is_leap_proleptic_gregorian, False, False))
+                    #    is_leap_proleptic_gregorian, False, has_year_zero),calendar=self.calendar,has_year_zero=self.has_year_zero)
+                    return DatetimeProlepticGregorian(*add_timedelta(self, -other, is_leap_proleptic_gregorian, False, has_year_zero),has_year_zero=self.has_year_zero)
                 else:
                     return NotImplemented
             else:
@@ -1241,7 +1428,7 @@ Cannot compute the time difference between dates with different calendars.
 One of the datetime objects may have been converted to a native python
 datetime instance.  Try using only_use_cftime_datetimes=True when creating the
 datetime object."""
-                    raise ValueError(msg)
+                    raise TypeError(msg)
                 return self - other._to_real_datetime()
             else:
                 return NotImplemented
@@ -1300,33 +1487,33 @@ cdef _strftime(datetime dt, fmt):
         s = s[:site] + syear + s[site + 4:]
     return s
 
-cdef bint is_leap_julian(int year):
+cdef bint is_leap_julian(int year, bint has_year_zero):
     "Return 1 if year is a leap year in the Julian calendar, 0 otherwise."
-    return _is_leap(year, calendar='julian')
+    return _is_leap(year, calendar='julian',has_year_zero=has_year_zero)
 
-cdef bint is_leap_proleptic_gregorian(int year):
+cdef bint is_leap_proleptic_gregorian(int year, bint has_year_zero):
     "Return 1 if year is a leap year in the Proleptic Gregorian calendar, 0 otherwise."
-    return _is_leap(year, calendar='proleptic_gregorian')
+    return _is_leap(year, calendar='proleptic_gregorian',has_year_zero=has_year_zero)
 
-cdef bint is_leap_gregorian(int year):
+cdef bint is_leap_gregorian(int year, bint has_year_zero):
     "Return 1 if year is a leap year in the Gregorian calendar, 0 otherwise."
-    return _is_leap(year, calendar='standard')
+    return _is_leap(year, calendar='standard',has_year_zero=has_year_zero)
 
-cdef bint all_leap(int year):
+cdef bint all_leap(int year, bint has_year_zero):
     "Return True for all years."
     return True
 
-cdef bint no_leap(int year):
+cdef bint no_leap(int year, bint has_year_zero):
     "Return False for all years."
     return False
 
-cdef int * month_lengths(bint (*is_leap)(int), int year):
-    if is_leap(year):
+cdef int * month_lengths(bint (*is_leap)(int,bint), int year, bint has_year_zero):
+    if is_leap(year,has_year_zero):
         return _dayspermonth_leap
     else:
         return _dayspermonth
 
-cdef void assert_valid_date(datetime dt, bint (*is_leap)(int),
+cdef void assert_valid_date(datetime dt, bint (*is_leap)(int, bint),
                             bint julian_gregorian_mixed,
                             bint has_year_zero=False,
                             bint is_360_day=False) except *:
@@ -1338,7 +1525,8 @@ cdef void assert_valid_date(datetime dt, bint (*is_leap)(int),
     if is_360_day:
         month_length = 12*[30]
     else:
-        month_length = month_lengths(is_leap, dt.year)
+        month_length = month_lengths(is_leap, dt.year, has_year_zero)
+
 
     if dt.month < 1 or dt.month > 12:
         raise ValueError("invalid month provided in {0!r}".format(dt))
@@ -1375,7 +1563,7 @@ cdef void assert_valid_date(datetime dt, bint (*is_leap)(int),
 # The date of the transition from the Julian to Gregorian calendar and
 # the number of invalid dates are hard-wired (1582-10-4 is the last day
 # of the Julian calendar, after which follows 1582-10-15).
-cdef tuple add_timedelta(datetime dt, delta, bint (*is_leap)(int), bint julian_gregorian_mixed, bint has_year_zero):
+cdef tuple add_timedelta(datetime dt, delta, bint (*is_leap)(int,bint), bint julian_gregorian_mixed, bint has_year_zero):
     cdef int microsecond, second, minute, hour, day, month, year
     cdef int delta_microseconds, delta_seconds, delta_days
     cdef int* month_length
@@ -1395,7 +1583,7 @@ cdef tuple add_timedelta(datetime dt, delta, bint (*is_leap)(int), bint julian_g
     month = dt.month
     year = dt.year
 
-    month_length = month_lengths(is_leap, year)
+    month_length = month_lengths(is_leap, year, has_year_zero)
 
     n_invalid_dates = 10 if julian_gregorian_mixed else 0
 
@@ -1423,7 +1611,7 @@ cdef tuple add_timedelta(datetime dt, delta, bint (*is_leap)(int), bint julian_g
                 year -= 1
                 if year == 0 and not has_year_zero:
                     year = -1
-                month_length = month_lengths(is_leap, year)
+                month_length = month_lengths(is_leap, year, has_year_zero)
             day = month_length[month-1]
         else:
             day += delta_days
@@ -1441,7 +1629,7 @@ cdef tuple add_timedelta(datetime dt, delta, bint (*is_leap)(int), bint julian_g
                 year += 1
                 if year == 0 and not has_year_zero:
                     year = 1
-                month_length = month_lengths(is_leap, year)
+                month_length = month_lengths(is_leap, year, has_year_zero)
             day = 1
         else:
             day += delta_days
@@ -1491,15 +1679,18 @@ cdef tuple add_timedelta_360_day(datetime dt, delta):
 
     return (year, month, day, hour, minute, second, microsecond, -1, -1)
 
-cdef _is_leap(int year, calendar, has_year_zero=False):
+cdef _is_leap(int year, calendar, has_year_zero=None):
     cdef int tyear
     cdef bint leap
     calendar = _check_calendar(calendar)
+    # set calendar-specific defaults for has_year_zero
+    if has_year_zero is None:
+        has_year_zero = _year_zero_defaults(calendar)
     if year == 0 and not has_year_zero:
         raise ValueError('year zero does not exist in the %s calendar' %\
                 calendar)
     # Because there is no year 0 in the Julian calendar, years -1, -5, -9, etc
-    # are leap years.
+    # are leap years. year zero is a leap year if it exists.
     if year < 0 and not has_year_zero:
         tyear = year + 1
     else:
@@ -1523,6 +1714,7 @@ cdef _is_leap(int year, calendar, has_year_zero=False):
 
 cdef _check_calendar(calendar):
     """validate calendars, convert to subset of names to get rid of synonyms"""
+    calendar = calendar.lower()
     if calendar not in _calendars:
         raise ValueError('unsupported calendar')
     calout = calendar
@@ -1542,7 +1734,7 @@ cdef _check_calendar(calendar):
 # with modifications to handle non-real-world calendars and negative years.
 
 
-cdef _IntJulianDayFromDate(int year,int month,int day,calendar,skip_transition=False,has_year_zero=False):
+cdef _IntJulianDayFromDate(int year,int month,int day,calendar,skip_transition=False,has_year_zero=None):
     """Compute integer Julian Day from year,month,day and calendar.
 
     Allowed calendars are 'standard', 'gregorian', 'julian',
@@ -1553,18 +1745,22 @@ cdef _IntJulianDayFromDate(int year,int month,int day,calendar,skip_transition=F
     'all_leap' is a synonym for '366_day'
     'gregorian' is a synonym for 'standard'
 
-    Negative years allowed back to -4714
-    (proleptic_gregorian) or -4713 (standard or gregorian calendar).
-
-    Negative year values are allowed in 360_day,365_day,366_day calendars.
-
     Integer julian day is number of days since noon UTC -4713-1-1
     in the julian or mixed julian/gregorian calendar, or noon UTC
-    -4714-11-24 in the proleptic_gregorian calendar. Reference
-    date is noon UTC 0-1-1 for other calendars.
-
-    There is no year zero in standard (mixed), julian, or proleptic_gregorian
-    calendars by default. If has_year_zero=True, then year zero is included.
+    -4714-11-24 in the proleptic_gregorian calendar (without year zero).
+    Reference date is noon UTC 0-1-1 for other calendars.
+
+    If the has_year_zero kwarg is set to True, astronomical year numbering
+    is used and the year zero exists.  If set to False for real-world
+    calendars, then historical year numbering is used and the year 1 is
+    preceded by year -1 and no year zero exists.
+    The defaults are False for real-world calendars
+    and True for idealized calendars.
+    The defaults can only be over-ridden for the real-world calendars,
+    for the the idealized calendars the year zero 
+    always exists and the has_year_zero kwarg is ignored.
+    This kwarg is not needed to define calendar systems allowed by CF
+    (the calendar-specific defaults do this).
 
     Subtract 0.5 to get 00 UTC on that day.
 
@@ -1581,7 +1777,9 @@ cdef _IntJulianDayFromDate(int year,int month,int day,calendar,skip_transition=F
         msg = "date %04d-%02d-%02d does not exist in the %s calendar" %\
         (year,month,day,calendar)
         raise ValueError(msg)
-
+    # set calendar-specific defaults for has_year_zero
+    if has_year_zero is None:
+        has_year_zero = _year_zero_defaults(calendar)
     if calendar == '360_day':
         return year*360 + (month-1)*30 + day - 1
     elif calendar == '365_day':
@@ -1595,9 +1793,6 @@ cdef _IntJulianDayFromDate(int year,int month,int day,calendar,skip_transition=F
     if year == 0 and not has_year_zero:
         raise ValueError('year zero does not exist in the %s calendar' %\
                 calendar)
-    if (calendar == 'proleptic_gregorian'  and year < -4714) or\
-       (calendar in ['julian','standard']  and year < -4713):
-        raise ValueError('year out of range for %s calendar' % calendar)
     leap = _is_leap(year,calendar,has_year_zero=has_year_zero)
     if not leap and month == 2 and day == 29:
         raise ValueError('%s is not a leap year' % year)
@@ -1634,6 +1829,8 @@ cdef _IntJulianDayFromDate(int year,int month,int day,calendar,skip_transition=F
             else:
                 return jday_greg
 
+# legacy calendar specific sub-classes (will be removed in a future release).
+
 @cython.embedsignature(True)
 cdef class DatetimeNoLeap(datetime):
     """
@@ -1643,14 +1840,6 @@ but uses the "noleap" ("365_day") calendar.
     def __init__(self, *args, **kwargs):
         kwargs['calendar']='noleap'
         super().__init__(*args, **kwargs)
-    def __repr__(self):
-        return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime',
-        self.__class__.__name__,
-        self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond)
-    cdef _getstate(self):
-        return (self.year, self.month, self.day, self.hour,
-                self.minute, self.second, self.microsecond,
-                self._dayofwk, self._dayofyr)
 
 @cython.embedsignature(True)
 cdef class DatetimeAllLeap(datetime):
@@ -1661,14 +1850,6 @@ but uses the "all_leap" ("366_day") calendar.
     def __init__(self, *args, **kwargs):
         kwargs['calendar']='all_leap'
         super().__init__(*args, **kwargs)
-    def __repr__(self):
-        return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime',
-        self.__class__.__name__,
-        self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond)
-    cdef _getstate(self):
-        return (self.year, self.month, self.day, self.hour,
-                self.minute, self.second, self.microsecond,
-                self._dayofwk, self._dayofyr)
 
 @cython.embedsignature(True)
 cdef class Datetime360Day(datetime):
@@ -1679,14 +1860,6 @@ but uses the "360_day" calendar.
     def __init__(self, *args, **kwargs):
         kwargs['calendar']='360_day'
         super().__init__(*args, **kwargs)
-    def __repr__(self):
-        return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime',
-        self.__class__.__name__,
-        self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond)
-    cdef _getstate(self):
-        return (self.year, self.month, self.day, self.hour,
-                self.minute, self.second, self.microsecond,
-                self._dayofwk, self._dayofyr)
 
 @cython.embedsignature(True)
 cdef class DatetimeJulian(datetime):
@@ -1697,71 +1870,23 @@ but uses the "julian" calendar.
     def __init__(self, *args, **kwargs):
         kwargs['calendar']='julian'
         super().__init__(*args, **kwargs)
-    def __repr__(self):
-        return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime',
-        self.__class__.__name__,
-        self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond)
-    cdef _getstate(self):
-        return (self.year, self.month, self.day, self.hour,
-                self.minute, self.second, self.microsecond,
-                self._dayofwk, self._dayofyr)
-
 
 @cython.embedsignature(True)
 cdef class DatetimeGregorian(datetime):
     """
 Phony datetime object which mimics the python datetime object,
 but uses the mixed Julian-Gregorian ("standard", "gregorian") calendar.
-
-The last date of the Julian calendar is 1582-10-4, which is followed
-by 1582-10-15, using the Gregorian calendar.
-
-Instances using the date after 1582-10-15 can be compared to
-datetime.datetime instances and used to compute time differences
-(datetime.timedelta) by subtracting a DatetimeGregorian instance from
-a datetime.datetime instance or vice versa.
     """
     def __init__(self, *args, **kwargs):
         kwargs['calendar']='gregorian'
         super().__init__(*args, **kwargs)
-    def __repr__(self):
-        return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime',
-                                     self.__class__.__name__,
-                                     self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond)
-    cdef _getstate(self):
-        return (self.year, self.month, self.day, self.hour,
-                self.minute, self.second, self.microsecond,
-                self._dayofwk, self._dayofyr)
 
 @cython.embedsignature(True)
 cdef class DatetimeProlepticGregorian(datetime):
     """
 Phony datetime object which mimics the python datetime object,
 but allows for dates that don't exist in the proleptic gregorian calendar.
-
-Supports timedelta operations by overloading + and -.
-
-Has strftime, timetuple, replace, __repr__, and __str__ methods. The
-format of the string produced by __str__ is controlled by self.format
-(default %Y-%m-%d %H:%M:%S). Supports comparisons with other
-datetime instances using the same calendar; comparison with
-native python datetime instances is possible for cftime.datetime
-instances using 'gregorian' and 'proleptic_gregorian' calendars.
-
-Instance variables are year,month,day,hour,minute,second,microsecond,dayofwk,dayofyr,
-format, and calendar.
     """
     def __init__(self, *args, **kwargs):
         kwargs['calendar']='proleptic_gregorian'
         super().__init__( *args, **kwargs)
-    def __repr__(self):
-        return "{0}.{1}({2}, {3}, {4}, {5}, {6}, {7}, {8})".format('cftime',
-                                     self.__class__.__name__,
-                                     self.year,self.month,self.day,self.hour,self.minute,self.second,self.microsecond)
-    cdef _getstate(self):
-        return (self.year, self.month, self.day, self.hour,
-                self.minute, self.second, self.microsecond,
-                self._dayofwk, self._dayofyr)
-
-# include legacy stuff no longer used by cftime.datetime
-include "_cftime_legacy.pyx"


=====================================
src/cftime/_cftime_legacy.pyx deleted
=====================================
@@ -1,504 +0,0 @@
-# stuff below no longer used by cftime.datetime, kept here for backwards compatibility.
-
-# The following function (_IntJulianDayToDate) is based on
-# algorithms described in the book
-# "Calendrical Calculations" by Dershowitz and Rheingold, 3rd edition, Cambridge University Press, 2007
-# and the C implementation provided at https://reingold.co/calendar.C
-# with modifications to handle non-real-world calendars and negative years.
-
-cdef _IntJulianDayToDate(int jday,calendar,skip_transition=False,has_year_zero=False):
-    """Compute the year,month,day,dow,doy given the integer Julian day.
-    and calendar. (dow = day of week with 0=Mon,6=Sun and doy is day of year).
-
-    Allowed calendars are 'standard', 'gregorian', 'julian',
-    'proleptic_gregorian','360_day', '365_day', '366_day', 'noleap',
-    'all_leap'.
-
-    'noleap' is a synonym for '365_day'
-    'all_leap' is a synonym for '366_day'
-    'gregorian' is a synonym for 'standard'
-
-    optional kwarg 'skip_transition':  When True, assume a 10-day
-    gap in Julian day numbers between Oct 4 and Oct 15 1582 (the transition
-    from Julian to Gregorian calendars).  Default False, ignored
-    unless calendar = 'standard'."""
-    cdef int year,month,day,dow,doy,yp1,jday_count,nextra
-    cdef int[12] dayspermonth
-    cdef int[13] cumdayspermonth
-
-    # validate inputs.
-    calendar = _check_calendar(calendar)
-
-    # compute day of week.
-    dow = (jday + 1) % 7
-    # convert to ISO 8601 (0 = Monday, 6 = Sunday), like python datetime
-    dow -= 1
-    if dow == -1: dow = 6
-
-    # handle all calendars except standard, julian, proleptic_gregorian.
-    if calendar == '360_day':
-        year   = jday//360
-        nextra = jday - year*360
-        doy    = nextra + 1 # Julday numbering starts at 0, doy starts at 1
-        month  = nextra//30 + 1
-        day    = doy - (month-1)*30
-        return year,month,day,dow,doy
-    elif calendar == '365_day':
-        year   = jday//365
-        nextra = jday - year*365
-        doy    = nextra + 1 # Julday numbering starts at 0, doy starts at 1
-        month  = 1
-        while doy > _cumdayspermonth[month]:
-            month += 1
-        day = doy - _cumdayspermonth[month-1]
-        return year,month,day,dow,doy
-    elif calendar == '366_day':
-        year   = jday//366
-        nextra = jday - year*366
-        doy    = nextra + 1 # Julday numbering starts at 0, doy starts at 1
-        month  = 1
-        while doy > _cumdayspermonth_leap[month]:
-            month += 1
-        day = doy - _cumdayspermonth_leap[month-1]
-        return year,month,day,dow,doy
-
-    # handle standard, julian, proleptic_gregorian calendars.
-    if jday < 0:
-        raise ValueError('julian day must be a positive integer')
-
-    # start with initial guess of year that is before jday=1 in both
-    # Julian and Gregorian calendars.
-    year = jday//366 - 4714
-
-    # account for 10 days in Julian/Gregorian transition.
-    if not skip_transition and calendar == 'standard' and jday > 2299160:
-        jday += 10
-
-    yp1 = year + 1
-    if yp1 == 0 and not has_year_zero:
-       yp1 = 1 # no year 0
-    # initialize jday_count to Jan 1 of next year
-    jday_count = _IntJulianDayFromDate(yp1,1,1,calendar,skip_transition=True,has_year_zero=has_year_zero)
-    # Advance years until we find the right one
-    # (stop iteration when jday_count jday >= specified jday)
-    while jday >= jday_count:
-        year += 1
-        if year == 0 and not has_year_zero:
-            year = 1
-        yp1 = year + 1
-        if yp1 == 0 and not has_year_zero:
-            yp1 = 1
-        jday_count = _IntJulianDayFromDate(yp1,1,1,calendar,skip_transition=True,has_year_zero=has_year_zero)
-    # now we know year.
-    # set days in specified month, cumulative days in computed year.
-    if _is_leap(year, calendar,has_year_zero=has_year_zero):
-        dayspermonth = _dayspermonth_leap
-        cumdayspermonth = _cumdayspermonth_leap
-    else:
-        dayspermonth = _dayspermonth
-        cumdayspermonth = _cumdayspermonth
-    # initialized month to Jan, initialize jday_count to end of Jan of
-    # calculated year.
-    month = 1
-    jday_count =\
-    _IntJulianDayFromDate(year,month,dayspermonth[month-1],calendar,skip_transition=True,has_year_zero=has_year_zero)
-    # now iterate by month until jday_count >= specified jday
-    while jday > jday_count:
-        month += 1
-        jday_count =\
-        _IntJulianDayFromDate(year,month,dayspermonth[month-1],calendar,skip_transition=True,has_year_zero=has_year_zero)
-    # back up jday_count to 1st day of computed month
-    jday_count = _IntJulianDayFromDate(year,month,1,calendar,skip_transition=True,has_year_zero=has_year_zero)
-    # now jday_count represents day 1 of computed month in computed year
-    # so computed day is just difference between jday_count and specified jday.
-    day = jday - jday_count + 1
-    # compute day in specified year.
-    doy = cumdayspermonth[month-1]+day
-    return year,month,day,dow,doy
-
-def _round_half_up(x):
-    # 'round half up' so 0.5 rounded to 1 (instead of 0 as in numpy.round)
-    return np.ceil(np.floor(2.*x)/2.)
-
- at cython.embedsignature(True)
-def JulianDayFromDate(date, calendar='standard'):
-    """JulianDayFromDate(date, calendar='standard')
-
-    creates a Julian Day from a 'datetime-like' object.  Returns the fractional
-    Julian Day (approximately 100 microsecond accuracy).
-
-    if calendar='standard' or 'gregorian' (default), Julian day follows Julian
-    Calendar on and before 1582-10-5, Gregorian calendar after 1582-10-15.
-
-    if calendar='proleptic_gregorian', Julian Day follows gregorian calendar.
-
-    if calendar='julian', Julian Day follows julian calendar.
-    """
-
-    # check if input was scalar and change return accordingly
-    isscalar = False
-    try:
-        date[0]
-    except:
-        isscalar = True
-
-    date = np.atleast_1d(np.array(date))
-    year = np.empty(len(date), dtype=np.int32)
-    month = year.copy()
-    day = year.copy()
-    hour = year.copy()
-    minute = year.copy()
-    second = year.copy()
-    microsecond = year.copy()
-    jd = np.empty(year.shape, np.longdouble)
-    cdef long double[:] jd_view = jd
-    cdef Py_ssize_t i_max = len(date)
-    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
-        hour[i] = d.hour
-        minute[i] = d.minute
-        second[i] = d.second
-        microsecond[i] = d.microsecond
-        jd_view[i] = <double>_IntJulianDayFromDate(<int>year[i],<int>month[i],<int>day[i],calendar)
-
-    # at this point jd is an integer representing noon UTC on the given
-    # year,month,day.
-    # compute fractional day from hour,minute,second,microsecond
-    fracday = hour / 24.0 + minute / 1440.0 + (second + microsecond/1.e6) / 86400.0
-    jd = jd - 0.5 + fracday
-
-    if isscalar:
-        return jd[0]
-    else:
-        return jd
-
- at cython.embedsignature(True)
-def DateFromJulianDay(JD, calendar='standard', only_use_cftime_datetimes=True,
-                      return_tuple=False):
-    """
-
-    returns a 'datetime-like' object given Julian Day. Julian Day is a
-    fractional day with approximately 100 microsecond accuracy.
-
-    if calendar='standard' or 'gregorian' (default), Julian day follows Julian
-    Calendar on and before 1582-10-5, Gregorian calendar after  1582-10-15.
-
-    if calendar='proleptic_gregorian', Julian Day follows gregorian calendar.
-
-    if calendar='julian', Julian Day follows julian calendar.
-
-    If only_use_cftime_datetimes is set to True, then cftime.datetime
-    objects are returned for all calendars.  Otherwise the datetime object is a
-    native python datetime object if the date falls in the Gregorian calendar
-    (i.e. calendar='proleptic_gregorian', or  calendar = 'standard'/'gregorian'
-    and the date is after 1582-10-15).
-    """
-
-    julian = np.atleast_1d(np.array(JD, dtype=np.longdouble))
-
-    def getdateinfo(julian):
-        # get the day (Z) and the fraction of the day (F)
-        # use 'round half up' rounding instead of numpy's even rounding
-        # so that 0.5 is rounded to 1.0, not 0 (cftime issue #49)
-        Z = np.atleast_1d(np.int32(_round_half_up(julian)))
-        F = (julian + 0.5 - Z).astype(np.longdouble)
-
-        cdef Py_ssize_t i_max = len(Z)
-        year = np.empty(i_max, dtype=np.int32)
-        month = np.empty(i_max, dtype=np.int32)
-        day = np.empty(i_max, dtype=np.int32)
-        dayofyr = np.zeros(i_max,dtype=np.int32)
-        dayofwk = np.zeros(i_max,dtype=np.int32)
-        cdef int ijd
-        cdef Py_ssize_t i
-        for i in range(i_max):
-            ijd = Z[i]
-            year[i],month[i],day[i],dayofwk[i],dayofyr[i] = _IntJulianDayToDate(ijd,calendar)
-
-        if calendar in ['standard', 'gregorian']:
-            ind_before = np.where(julian < 2299160.5)
-            ind_before = np.asarray(ind_before).any()
-        else:
-            ind_before = False
-
-        # compute hour, minute, second, microsecond, convert to int32
-        hour = np.clip((F * 24.).astype(np.int64), 0, 23)
-        F   -= hour / 24.
-        minute = np.clip((F * 1440.).astype(np.int64), 0, 59)
-        second = np.clip((F - minute / 1440.) * 86400., 0, None)
-        microsecond = (second % 1)*1.e6
-        hour = hour.astype(np.int32)
-        minute = minute.astype(np.int32)
-        second = second.astype(np.int32)
-        microsecond = microsecond.astype(np.int32)
-
-        return year,month,day,hour,minute,second,microsecond,dayofyr,dayofwk,ind_before
-
-    year,month,day,hour,minute,second,microsecond,dayofyr,dayofwk,ind_before =\
-    getdateinfo(julian)
-    # round to nearest second if within ms_eps microseconds
-    # (to avoid ugly errors in datetime formatting - alternative
-    # to adding small offset all the time as was done previously)
-    # see netcdf4-python issue #433 and cftime issue #78
-    # this is done by rounding microsends up or down, then
-    # recomputing year,month,day etc
-    # ms_eps is proportional to julian day,
-    # about 47 microseconds in 2000 for Julian base date in -4713
-    ms_eps = np.atleast_1d(np.array(np.finfo(np.float64).eps,np.longdouble))
-    ms_eps = 86400000000.*np.maximum(ms_eps*julian, ms_eps)
-    microsecond = np.where(microsecond < ms_eps, 0, microsecond)
-    indxms = microsecond > 1000000-ms_eps
-    if indxms.any():
-        julian[indxms] = julian[indxms] + 2*ms_eps[indxms]/86400000000..
-        year[indxms],month[indxms],day[indxms],hour[indxms],minute[indxms],second[indxms],microsecond2,dayofyr[indxms],dayofwk[indxms],ind_before2 =\
-        getdateinfo(julian[indxms])
-        microsecond[indxms] = 0
-
-    # check if input was scalar and change return accordingly
-    isscalar = False
-    try:
-        JD[0]
-    except:
-        isscalar = True
-
-    if calendar == 'proleptic_gregorian':
-        # datetime.datetime does not support years < 1
-        #if year < 0:
-        if only_use_cftime_datetimes:
-            datetime_type = DatetimeProlepticGregorian
-        else:
-            if (year < 0).any(): # netcdftime issue #28
-               datetime_type = DatetimeProlepticGregorian
-            else:
-               datetime_type = real_datetime
-    elif calendar in ('standard', 'gregorian'):
-        # return a 'real' datetime instance if calendar is proleptic
-        # Gregorian or Gregorian and all dates are after the
-        # Julian/Gregorian transition
-        if ind_before and not only_use_cftime_datetimes:
-            datetime_type = real_datetime
-        else:
-            datetime_type = DatetimeGregorian
-    elif calendar == "julian":
-        datetime_type = DatetimeJulian
-    elif calendar in ["noleap","365_day"]:
-        datetime_type = DatetimeNoLeap
-    elif calendar in ["all_leap","366_day"]:
-        datetime_type = DatetimeAllLeap
-    elif calendar == "360_day":
-        datetime_type = Datetime360Day
-    else:
-        raise ValueError("unsupported calendar: {0}".format(calendar))
-
-    if not isscalar:
-        if return_tuple:
-            return np.array([args for args in
-                            zip(year, month, day, hour, minute, second,
-                                microsecond,dayofwk,dayofyr)])
-        else:
-            return np.array([datetime_type(*args)
-                             for args in
-                             zip(year, month, day, hour, minute, second,
-                                 microsecond)])
-
-    else:
-        if return_tuple:
-            return (year[0], month[0], day[0], hour[0],
-                    minute[0], second[0], microsecond[0],
-                    dayofwk[0], dayofyr[0])
-        else:
-            return datetime_type(year[0], month[0], day[0], hour[0],
-                                 minute[0], second[0], microsecond[0])
-
-class utime:
-
-    """
-Performs conversions of netCDF time coordinate
-data to/from datetime objects.
-
-To initialize: `t = utime(unit_string,calendar='standard'`
-
-where
-
-`unit_string` is a string of the form
-`time-units since <time-origin>` defining the time units.
-
-Valid time-units are days, hours, minutes and seconds (the singular forms
-are also accepted). An example unit_string would be `hours
-since 0001-01-01 00:00:00`. months is allowed as a time unit
-*only* for the 360_day calendar.
-
-The calendar keyword describes the calendar used in the time calculations.
-All the values currently defined in the U{CF metadata convention
-<http://cf-pcmdi.llnl.gov/documents/cf-conventions/1.1/cf-conventions.html#time-coordinate>}
-are accepted. The default is 'standard', which corresponds to the mixed
-Gregorian/Julian calendar used by the udunits library. Valid calendars
-are:
-
-'gregorian' or 'standard' (default):
-
-Mixed Gregorian/Julian calendar as defined by udunits.
-
-'proleptic_gregorian':
-
-A Gregorian calendar extended to dates before 1582-10-15. That is, a year
-is a leap year if either (i) it is divisible by 4 but not by 100 or (ii)
-it is divisible by 400.
-
-'noleap' or '365_day':
-
-Gregorian calendar without leap years, i.e., all years are 365 days long..
-all_leap or 366_day Gregorian calendar with every year being a leap year,
-i.e., all years are 366 days long.
-
-'360_day':
-
-All years are 360 days divided into 30 day months.
-
-'julian':
-
-Proleptic Julian calendar, extended to dates after 1582-10-5. A year is a
-leap year if it is divisible by 4.
-
-The num2date and date2num class methods can used to convert datetime
-instances to/from the specified time units using the specified calendar.
-
-Example usage:
-
->>> from cftime import utime
->>> from datetime import  datetime
->>> cdftime = utime('hours since 0001-01-01 00:00:00')
->>> date = datetime.now()
->>> print date
-2016-10-05 08:46:27.245015
->>>
->>> t = cdftime.date2num(date)
->>> print t
-17669840.7742
->>>
->>> date = cdftime.num2date(t)
->>> print date
-2016-10-05 08:46:27.244996
->>>
-
-The resolution of the transformation operation is approximately a microsecond.
-
-Warning:  Dates between 1582-10-5 and 1582-10-15 do not exist in the
-'standard' or 'gregorian' calendars.  An exception will be raised if you pass
-a 'datetime-like' object in that range to the date2num class method.
-
-Words of Wisdom from the British MetOffice concerning reference dates:
-
-"udunits implements the mixed Gregorian/Julian calendar system, as
-followed in England, in which dates prior to 1582-10-15 are assumed to use
-the Julian calendar. Other software cannot be relied upon to handle the
-change of calendar in the same way, so for robustness it is recommended
-that the reference date be later than 1582. If earlier dates must be used,
-it should be noted that udunits treats 0 AD as identical to 1 AD."
-
- at ivar origin: datetime instance defining the origin of the netCDF time variable.
- at ivar calendar:  the calendar used (as specified by the `calendar` keyword).
- at ivar unit_string:  a string defining the the netCDF time variable.
- at ivar units:  the units part of `unit_string` (i.e. 'days', 'hours', 'seconds').
-    """
-
-    def __init__(self, unit_string, calendar='standard',
-                 only_use_cftime_datetimes=True,only_use_python_datetimes=False):
-        """
- at param unit_string: a string of the form
-`time-units since <time-origin>` defining the time units.
-
-Valid time-units are days, hours, minutes and seconds (the singular forms
-are also accepted). An example unit_string would be `hours
-since 0001-01-01 00:00:00`. months is allowed as a time unit
-*only* for the 360_day calendar.
-
- at keyword calendar: describes the calendar used in the time calculations.
-All the values currently defined in the U{CF metadata convention
-<http://cf-pcmdi.llnl.gov/documents/cf-conventions/1.1/cf-conventions.html#time-coordinate>}
-are accepted. The default is `standard`, which corresponds to the mixed
-Gregorian/Julian calendar used by the udunits library. Valid calendars
-are:
- - `gregorian` or `standard` (default):
- Mixed Gregorian/Julian calendar as defined by udunits.
- - `proleptic_gregorian`:
- A Gregorian calendar extended to dates before 1582-10-15. That is, a year
- is a leap year if either (i) it is divisible by 4 but not by 100 or (ii)
- it is divisible by 400.
- - `noleap` or `365_day`:
- Gregorian calendar without leap years, i.e., all years are 365 days long.
- - `all_leap` or `366_day`:
- Gregorian calendar with every year being a leap year, i.e.,
- all years are 366 days long.
- -`360_day`:
- All years are 360 days divided into 30 day months.
- -`julian`:
- Proleptic Julian calendar, extended to dates after 1582-10-5. A year is a
- leap year if it is divisible by 4.
-
- at keyword only_use_cftime_datetimes: if False, datetime.datetime
-objects are returned from num2date where possible; if True dates which subclass
-cftime.datetime are returned for all calendars. Default True.
-
- at keyword only_use_python_datetimes: always return python datetime.datetime
-objects and raise an error if this is not possible. Ignored unless
-**only_use_cftime_datetimes=False**. Default **False**.
-
- at returns: A class instance which may be used for converting times from netCDF
-units to datetime objects.
-        """
-        calendar = calendar.lower()
-        if calendar in _calendars:
-            self.calendar = calendar
-        else:
-            raise ValueError(
-                "calendar must be one of %s, got '%s'" % (str(_calendars), calendar))
-        self.origin = _dateparse(unit_string,calendar=calendar)
-        units, isostring = _datesplit(unit_string)
-        self.units = units
-        self.unit_string = unit_string
-        self.only_use_cftime_datetimes = only_use_cftime_datetimes
-        self.only_use_python_datetimes = only_use_python_datetimes
-
-    def date2num(self, date):
-        """
-        Returns `time_value` in units described by `unit_string`, using
-        the specified `calendar`, given a 'datetime-like' object.
-
-        The datetime object must represent UTC with no time-zone offset.
-        If there is a time-zone offset implied by L{unit_string}, it will
-        be applied to the returned numeric values.
-
-        Resolution is approximately a microsecond.
-
-        If calendar = 'standard' or 'gregorian' (indicating
-        that the mixed Julian/Gregorian calendar is to be used), an
-        exception will be raised if the 'datetime-like' object describes
-        a date between 1582-10-5 and 1582-10-15.
-
-        Works for scalars, sequences and numpy arrays.
-        Returns a scalar if input is a scalar, else returns a numpy array.
-        """
-        return date2num(date,self.unit_string,calendar=self.calendar)
-
-    def num2date(self, time_value):
-        """
-        Return a 'datetime-like' object given a `time_value` in units
-        described by `unit_string`, using `calendar`.
-
-        dates are in UTC with no offset, even if L{unit_string} contains
-        a time zone offset from UTC.
-
-        Resolution is approximately a microsecond.
-
-        Works for scalars, sequences and numpy arrays.
-        Returns a scalar if input is a scalar, else returns a numpy array.
-        """
-        return num2date(time_value,self.unit_string,calendar=self.calendar,only_use_cftime_datetimes=self.only_use_cftime_datetimes,only_use_python_datetimes=self.only_use_python_datetimes)


=====================================
test/test_cftime.py
=====================================
@@ -4,6 +4,7 @@ import copy
 import operator
 import sys
 import unittest
+import warnings
 from collections import namedtuple
 from datetime import datetime, timedelta, MINYEAR
 
@@ -14,10 +15,10 @@ from numpy.testing import assert_almost_equal, assert_equal
 import cftime
 from cftime import datetime as datetimex
 from cftime import real_datetime
-from cftime import (DateFromJulianDay, Datetime360Day, DatetimeAllLeap,
+from cftime import (Datetime360Day, DatetimeAllLeap,
                     DatetimeGregorian, DatetimeJulian, DatetimeNoLeap,
-                    DatetimeProlepticGregorian, JulianDayFromDate, _parse_date,
-                    date2index, date2num, num2date, utime, UNIT_CONVERSION_FACTORS)
+                    DatetimeProlepticGregorian, _parse_date,
+                    date2index, date2num, num2date,  UNIT_CONVERSION_FACTORS)
 
 try:
     from datetime import timezone
@@ -43,6 +44,24 @@ except ImportError: # python2.7
             return timedelta(hours=0)
 
 
+# legacy class included here since tests use it.
+class utime:
+    def __init__(self, unit_string, calendar='standard',
+                 only_use_cftime_datetimes=True,
+                 only_use_python_datetimes=False):
+        calendar = calendar.lower()
+        units, isostring = cftime._datesplit(unit_string)
+        self.origin = cftime._dateparse(unit_string,calendar=calendar)
+        self.units = units
+        self.calendar = calendar
+        self.unit_string = unit_string
+        self.only_use_cftime_datetimes = only_use_cftime_datetimes
+        self.only_use_python_datetimes = only_use_python_datetimes
+    def date2num(self, date):
+        return date2num(date,self.unit_string,calendar=self.calendar)
+    def num2date(self, time_value):
+        return num2date(time_value,self.unit_string,calendar=self.calendar,only_use_cftime_datetimes=self.only_use_cftime_datetimes,only_use_python_datetimes=self.only_use_python_datetimes)
+
 utc = timezone(timedelta(hours=0), 'UTC')
 est = timezone(timedelta(hours=-5), 'UTC')
 
@@ -291,16 +310,16 @@ class cftimeTestCase(unittest.TestCase):
         self.assertTrue(str(d) == str(date))
         # test julian day from date, date from julian day
         d = cftime.datetime(1858, 11, 17, calendar='standard')
-        # toordinal should produce same result as JulianDayFromDate
+        # astronomical year numbering (with year zero)
+        dz = cftime.datetime(1858, 11, 17, calendar='standard',has_year_zero=True)
+        # toordinal (with fractional = True) is same as old JulianDayFromDate
         mjd1 = d.toordinal(fractional=True)
-        mjd2 = JulianDayFromDate(d)
+        mjd1z = dz.toordinal(fractional=True)
         assert_almost_equal(mjd1, 2400000.5)
-        assert_almost_equal(mjd1,mjd2)
-        # fromordinal should produce the same result as DateFromJulianDay
-        date1 = DateFromJulianDay(mjd1)
-        date2 = cftime.datetime.fromordinal(mjd1)
+        assert_almost_equal(mjd1z, 2400000.5)
+        # fromordinal (same as old DateFromJulianDay)
+        date1 = cftime.datetime.fromordinal(mjd1)
         self.assertTrue(str(date1) == str(d))
-        self.assertTrue(str(date1) == str(date2))
         # test iso 8601 units string
         d = datetime(1970, 1, 1, 1)
         t = self.cdftime_iso.date2num(d)
@@ -559,18 +578,27 @@ class cftimeTestCase(unittest.TestCase):
         # caused incorrect rountrip num2date(date2num(date)) roundtrip for dates with year
         # < 0.
         u = utime("seconds since 1-1-1",calendar='julian')
-        date1 = datetimex(-1, 1, 1)
-        date2 = u.num2date(u.date2num(date1))
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore",category=cftime.CFWarning)
+            date1 = datetimex(-1, 1, 1)
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore",category=cftime.CFWarning)
+            date2 = u.num2date(u.date2num(date1))
         assert (date2.year == date1.year)
         assert (date2.month == date1.month)
         assert (date2.day == date1.day)
         assert (date2.hour == date1.hour)
         assert (date2.minute == date1.minute)
         assert (date2.second == date1.second)
-        assert_almost_equal(JulianDayFromDate(date1), 1721057.5)
+        assert_almost_equal(cftime.datetime.toordinal(date1,fractional=True), 1721057.5)
         # issue 596 - negative years fail in utime.num2date
-        u = utime("seconds since 1-1-1", "proleptic_gregorian")
-        d = u.num2date(u.date2num(datetimex(-1, 1, 1)))
+        units="seconds since 1-1-1"
+        calendar="proleptic_gregorian"
+        yrzero=False
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore",category=cftime.CFWarning)
+            din=datetimex(-1,1,1,calendar=calendar,has_year_zero=yrzero)
+            d=num2date(date2num(din,units),units,calendar=calendar,has_year_zero=yrzero)
         assert (d.year == -1)
         assert (d.month == 1)
         assert (d.day == 1)
@@ -611,7 +639,7 @@ class cftimeTestCase(unittest.TestCase):
         # n should always be 0 as all units refer to the same point in time
         assert_almost_equal(n, 0)
         # cftime issue #49
-        d = DateFromJulianDay(2450022.5, "standard")
+        d = cftime.datetime.fromordinal(2450022.5, calendar="standard")
         assert (d.year == 1995)
         assert (d.month == 11)
         assert (d.day == 1)
@@ -619,20 +647,22 @@ class cftimeTestCase(unittest.TestCase):
         assert (d.minute == 0)
         assert (d.second == 0)
         # cftime issue #52
-        d = DateFromJulianDay(1684958.5,calendar='gregorian')
-        assert (d.year == -100)
-        assert (d.month == 3)
-        assert (d.day == 2)
-        assert (d.hour == 0)
-        assert (d.minute == 0)
-        assert (d.second == 0)
-        d = DateFromJulianDay(1684958.5,calendar='standard')
-        assert (d.year == -100)
-        assert (d.month == 3)
-        assert (d.day == 2)
-        assert (d.hour == 0)
-        assert (d.minute == 0)
-        assert (d.second == 0)
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore",category=cftime.CFWarning)
+            d = cftime.datetime.fromordinal(1684958.5,calendar='gregorian')
+            assert (d.year == -100)
+            assert (d.month == 3)
+            assert (d.day == 2)
+            assert (d.hour == 0)
+            assert (d.minute == 0)
+            assert (d.second == 0)
+            d = cftime.datetime.fromordinal(1684958.5,calendar='standard')
+            assert (d.year == -100)
+            assert (d.month == 3)
+            assert (d.day == 2)
+            assert (d.hour == 0)
+            assert (d.minute == 0)
+            assert (d.second == 0)
         # test dayofwk, dayofyr attribute setting (cftime issue #13)
         d1 = DatetimeGregorian(2020,2,29)
         d2 = real_datetime(2020,2,29)
@@ -679,9 +709,11 @@ class cftimeTestCase(unittest.TestCase):
         # gives 2446433 (365 days more - is it counting year 0?)
         # however http://aa.usno.navy.mil/data/docs/JulianDate.php gives
         # 2446068, which agrees with us
-        units = "days since -4713-01-01T00:00:00Z"
-        t = date2num(datetime(1985,1,2), units, calendar="standard")
-        assert_almost_equal(t, 2446068)
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore",category=cftime.CFWarning)
+            units = "days since -4713-01-01T00:00:00Z"
+            t = date2num(datetime(1985,1,2), units, calendar="standard")
+            assert_almost_equal(t, 2446068)
 
         # issue #68: allow months since for 360_day calendar
         d = num2date(1, 'months since 0000-01-01 00:00:00', calendar='360_day')
@@ -743,7 +775,7 @@ class cftimeTestCase(unittest.TestCase):
         test = dates == np.ma.masked_array([datetime(1848, 1, 17, 6, 0, 0, 40), None],mask=[0,1])
         assert(test.all())
         dates = num2date(times, units=units, calendar='standard')
-        assert(str(dates)=="[cftime.DatetimeGregorian(1848, 1, 17, 6, 0, 0, 40) --]")
+        assert(str(dates)=="[cftime.DatetimeGregorian(1848, 1, 17, 6, 0, 0, 40, has_year_zero=False)\n --]")
 #  check that time range of 200,000 + years can be represented accurately
         calendar='standard'
         _MAX_INT64 = np.iinfo("int64").max
@@ -776,15 +808,20 @@ class cftimeTestCase(unittest.TestCase):
         assert (date2num(dates, units="hours since 2010-02-01 00:00:00") == 24.)
 # issue #187 - roundtrip near second boundary
         dt1 = datetime(1810, 4, 24, 16, 15, 10)
-        units = 'days since -4713-01-01 12:00'
-        dt2 = num2date(date2num(dt1, units), units, calendar='proleptic_gregorian')
-        dt2 = num2date(date2num(dt1, units, calendar='standard'), units)
-        assert(dt1 == dt2)
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore",category=cftime.CFWarning)
+            units = 'days since -4713-01-01 12:00'
+            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)
+        dt1 = datetime(2020, 4, 24, 16, 15, 10) # python datetime
         units = 'days since -4713-01-01 12:00'
         cal = 'proleptic_gregorian'
-        dt2 = num2date(date2num(dt1, units, cal), units, cal)
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore",category=cftime.CFWarning)
+            dt2 = num2date(date2num(dt1, units, cal, has_year_zero=False), units,
+                                    cal, has_year_zero=False)
         assert(dt1 == dt2)
 # issue #198 - cftime.datetime creates calendar specific datetimes that
 # support addition/subtraction of timedeltas.
@@ -796,19 +833,21 @@ class cftimeTestCase(unittest.TestCase):
         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, use calendar associated with datetime
-        # instance.
-        jd = cftime.date2num(d,units)
-        assert(jd == 2459198.0)
-        # use 'standard' calendar
-        jd = cftime.date2num(d,units,calendar='standard')
-        assert(jd == 2459185.0)
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore",category=cftime.CFWarning)
+            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, use calendar associated with datetime
+            # instance.
+            jd = cftime.date2num(d,units)
+            assert(jd == 2459198.0)
+            # use 'standard' calendar
+            jd = cftime.date2num(d,units,calendar='standard')
+            assert(jd == 2459185.0)
 
         # issue #211
         # (masked array handling in date2num - AttributeError:
@@ -820,6 +859,47 @@ class cftimeTestCase(unittest.TestCase):
              cftime.date2num(m, units="seconds since 2000-1-1")==[4.602096e+08]
               )
 
+        # test astronomical year numbering
+        jdref=2400000
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore",category=cftime.CFWarning)
+            for calendar in ['julian','gregorian','proleptic_gregorian']:
+                has_year_zero=False
+                try:
+                    # this should raise ValueError
+                    d = cftime.datetime(0,1,1,0,has_year_zero=has_year_zero,calendar=calendar)
+                except ValueError:
+                    d = cftime.datetime(-1,1,1,0,has_year_zero=has_year_zero,calendar=calendar)
+                    pass
+                else:
+                    raise AssertionError # fail if ValueError not raised
+                d2 = cftime.datetime(1,1,1,0,has_year_zero=has_year_zero,calendar=calendar)
+                assert((d2-d).days==366) # 1-1-1 is 366 days after -1-1-1 if no year zero.
+                has_year_zero=True
+                d = cftime.datetime(0,1,1,0,has_year_zero=has_year_zero,calendar=calendar)
+                d2 = cftime.datetime(1,1,1,0,has_year_zero=has_year_zero,calendar=calendar)
+                assert((d2-d).days==366) # 1-1-1 is 366 days after 0-1-1 if year zero allowed.
+                for has_year_zero in [True,False]:
+                    if calendar == 'julian':
+                        d = cftime.datetime(1858,11, 4,12,has_year_zero=has_year_zero,calendar=calendar)
+                    else:
+                        d = cftime.datetime(1858,11,16,12,has_year_zero=has_year_zero,calendar=calendar)
+                    if has_year_zero:
+                        if calendar == 'proleptic_gregorian':
+                            d0 = cftime.datetime(-4713,11,24,12,has_year_zero=has_year_zero,calendar=calendar)
+                        else:
+                            d0 = cftime.datetime(-4712, 1, 1,12,has_year_zero=has_year_zero,calendar=calendar)
+                    else:
+                        if calendar == 'proleptic_gregorian':
+                            d0 = cftime.datetime(-4714,11,24,12,has_year_zero=has_year_zero,calendar=calendar)
+                        else:
+                            d0 = cftime.datetime(-4713, 1, 1,12,has_year_zero=has_year_zero,calendar=calendar)
+                    jd = d.toordinal()
+                    assert((d-d0).days == jdref)
+                    assert(jd == jdref)
+                    assert(d.toordinal() == jdref)
+                    d2 = cftime.datetime.fromordinal(jd,calendar=calendar,has_year_zero=has_year_zero)
+                    assert(d2 == d)
 
 
 class TestDate2index(unittest.TestCase):
@@ -933,11 +1013,12 @@ class TestDate2index(unittest.TestCase):
             raise ValueError('This test should have failed.')
 
     def test_select_dummy(self):
+        calendar='standard'
         nutime = self.TestTime(datetime(1950, 1, 1), 366, 24,
-                               'hours since 1400-01-01', 'standard')
+                               'hours since 1400-01-01', calendar)
 
-        dates = [datetime(1950, 1, 2, 6), datetime(
-            1950, 1, 3), datetime(1950, 1, 3, 18)]
+        dates = [datetimex(1950, 1, 2, 6, calendar=calendar), datetimex(
+            1950, 1, 3, calendar=calendar), datetimex(1950, 1, 3, 18, calendar=calendar)]
 
         t = date2index(dates, nutime, select='before')
         assert_equal(t, [1, 2, 2])
@@ -951,6 +1032,7 @@ class TestDate2index(unittest.TestCase):
     def test_select_nc(self):
         nutime = self.time_vars['time']
 
+        # these are python datetimes ('proleptic_gregorian' calendar).
         dates = [datetime(1950, 1, 2, 6), datetime(
             1950, 1, 3), datetime(1950, 1, 3, 18)]
 
@@ -1040,7 +1122,9 @@ class issue584TestCase(unittest.TestCase):
 
         # Pick the date corresponding to the Julian day of 1.0 to test
         # the transition from positive to negative Julian days.
-        julian_day = converter.date2num(datetimex(-4712, 1, 2, 12))
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore",category=cftime.CFWarning)
+            julian_day = converter.date2num(datetimex(-4712, 1, 2, 12))
         # should be a Tuesday
 
         old_date = converter.num2date(julian_day)
@@ -1062,6 +1146,11 @@ class DateTime(unittest.TestCase):
         self.date1_365_day = DatetimeNoLeap(-5000, 1, 2, 12)
         self.date2_365_day = DatetimeNoLeap(-5000, 1, 3, 12)
         self.date3_gregorian = DatetimeGregorian(1969,  7, 20, 12)
+        self.date3_gregorian_yearzero = DatetimeGregorian(1969,  7, 20, 12, has_year_zero=True)
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore",category=cftime.CFWarning)
+            self.date4_proleptic_gregorian = cftime.datetime(-1,5,5,2,30,59,999999,calendar='proleptic_gregorian',has_year_zero=False)
+        self.date4_julian = self.date4_proleptic_gregorian.change_calendar('julian',True)
 
         # last day of the Julian calendar in the mixed Julian/Gregorian calendar
         self.date4_gregorian = DatetimeGregorian(1582, 10, 4)
@@ -1110,8 +1199,10 @@ class DateTime(unittest.TestCase):
                          DatetimeAllLeap(11, 2, 1))
 
         # The Gregorian calendar has no year zero.
-        self.assertEqual(DatetimeGregorian(-1, 12, 31) + self.delta,
-                         DatetimeGregorian(1, 1, 1, 1))
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore",category=cftime.CFWarning)
+            self.assertEqual(DatetimeGregorian(-1, 12, 31) + self.delta,
+                             DatetimeGregorian(1, 1, 1, 1))
 
         def invalid_add_1():
             self.date1_365_day + 1
@@ -1156,9 +1247,11 @@ class DateTime(unittest.TestCase):
         self.assertEqual(self.date6_proleptic_gregorian - self.delta,
                          DatetimeProlepticGregorian(1582, 10, 13, 23))
 
-        # The Gregorian calendar has no year zero.
-        self.assertEqual(DatetimeGregorian(1, 1, 1) - self.delta,
-                         DatetimeGregorian(-1, 12, 30, 23))
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore",category=cftime.CFWarning)
+            # The Gregorian calendar has no year zero.
+            self.assertEqual(DatetimeGregorian(1, 1, 1) - self.delta,
+                             DatetimeGregorian(-1, 12, 30, 23))
 
         # The 360_day calendar has year zero.
 
@@ -1188,11 +1281,11 @@ class DateTime(unittest.TestCase):
         def invalid_sub_5():
             self.date3_gregorian - self.date1_365_day
 
-        for func in [invalid_sub_1, invalid_sub_2]:
-            self.assertRaises(TypeError, func)
+        def invalid_sub_6():
+            self.date3_gregorian - self.date3_gregorian_yearzero
 
-        for func in [invalid_sub_3, invalid_sub_4, invalid_sub_5]:
-            self.assertRaises(ValueError, func)
+        for func in [invalid_sub_1, invalid_sub_2, invalid_sub_3, invalid_sub_4, invalid_sub_5, invalid_sub_6]:
+            self.assertRaises(TypeError, func)
 
     def test_replace(self):
         self.assertEqual(self.date1_365_day.replace(year=4000).year, 4000)
@@ -1224,7 +1317,9 @@ class DateTime(unittest.TestCase):
                          "1969-07-20 12:00:00")
 
         def invalid_year():
-            DatetimeGregorian(0, 1, 1) + self.delta
+            with warnings.catch_warnings():
+                warnings.simplefilter("ignore",category=cftime.CFWarning)
+                DatetimeGregorian(0, 1, 1) + self.delta
 
         def invalid_month():
             DatetimeGregorian(1, 13, 1) + self.delta
@@ -1250,6 +1345,9 @@ class DateTime(unittest.TestCase):
         self.assertTrue(self.datetime_date1 > self.date3_gregorian)
         # compare datetime and real_datetime
         self.assertFalse(self.date3_gregorian > self.datetime_date1)
+        # compare different calendars (uses change_calendar method)
+        self.assertTrue(self.date3_gregorian_yearzero == self.date3_gregorian)
+        self.assertTrue(self.date4_proleptic_gregorian == self.date4_julian)
 
         def not_comparable_1():
             "compare two datetime instances with different calendars"
@@ -1269,9 +1367,9 @@ class DateTime(unittest.TestCase):
 
         def not_comparable_5():
             "compare non-datetime to a datetime instance"
-            0 < self.date_1_365_day
+            0 < self.date1_365_day
 
-        for func in [not_comparable_1, not_comparable_2, not_comparable_3, not_comparable_4]:
+        for func in [not_comparable_1, not_comparable_2, not_comparable_3, not_comparable_4, not_comparable_5]:
             self.assertRaises(TypeError, func)
 
     @pytest.mark.skipif(sys.version_info.major != 2,
@@ -1427,12 +1525,17 @@ def days_per_month_leap_year(date_type, month):
 
 
 def test_zero_year(date_type):
-    # Year 0 is valid in the 360,365 and 366 day calendars
-    if date_type in [DatetimeNoLeap, DatetimeAllLeap, Datetime360Day]:
-        date_type(0, 1, 1)
-    else:
-        with pytest.raises(ValueError):
+    # Year 0 is valid in the 360,365 and 366 day and 
+    # Proleptic Gregorian calendars by default.
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore",category=cftime.CFWarning)
+        #if date_type in [DatetimeNoLeap, DatetimeAllLeap, Datetime360Day,
+        #        DatetimeProlepticGregorian]:
+        if date_type in [DatetimeNoLeap, DatetimeAllLeap, Datetime360Day]: 
             date_type(0, 1, 1)
+        else:
+            with pytest.raises(ValueError):
+                date_type(0, 1, 1)
 
 
 def test_invalid_month(date_type):
@@ -1553,8 +1656,10 @@ _EXPECTED_DATE_TYPES = {'noleap': DatetimeNoLeap,
 )
 def test_num2date_only_use_cftime_datetimes_negative_years(
         calendar, expected_date_type):
-    result = num2date(-1000., units='days since 0001-01-01', calendar=calendar,
-                      only_use_cftime_datetimes=True)
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore",category=cftime.CFWarning)
+        result = num2date(-1000., units='days since 0001-01-01', calendar=calendar,
+                          only_use_cftime_datetimes=True)
     assert isinstance(result, datetimex)
     assert (result.calendar == adjust_calendar(calendar))
 
@@ -1584,11 +1689,18 @@ def test_num2date_only_use_cftime_datetimes_post_gregorian(
 
 
 def test_repr():
-    expected = "cftime.datetime(2000, 1, 1, 0, 0, 0, 0, calendar='gregorian')"
+    expected = "cftime.datetime(2000, 1, 1, 0, 0, 0, 0, calendar='gregorian', has_year_zero=False)"
     assert repr(datetimex(2000, 1, 1, calendar='standard')) == expected
-    expected = "cftime.datetime(2000, 1, 1, 0, 0, 0, 0, calendar='')"
+    expected = "cftime.datetime(2000, 1, 1, 0, 0, 0, 0, calendar='', has_year_zero=False)"
     assert repr(datetimex(2000, 1, 1, calendar=None)) == expected
 
+def test_string_format():
+    dt = cftime.datetime(2000, 1, 1)
+    # check the default formatting is the same as strftime
+    assert dt.strftime() == '{0}'.format(dt)
+    # check a given format string acts like strftime
+    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_dayofyr_after_replace(date_type):
     date = date_type(1, 1, 1)



View it on GitLab: https://salsa.debian.org/debian-gis-team/cftime/-/compare/1926ac706f01c39cb8f4b18e9c5050951d14a7a2...7dcad5be727508156653293c730930e5971cf414

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/cftime/-/compare/1926ac706f01c39cb8f4b18e9c5050951d14a7a2...7dcad5be727508156653293c730930e5971cf414
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/20210815/1450fdda/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list