from __future__ import print_function

import copy
import operator
import sys
import unittest
import warnings
from collections import namedtuple
from datetime import datetime, timedelta, MINYEAR

import numpy as np
import pytest
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 (Datetime360Day, DatetimeAllLeap,
                    DatetimeGregorian, DatetimeJulian, DatetimeNoLeap,
                    DatetimeProlepticGregorian, _parse_date,
                    date2index, date2num, num2date,  UNIT_CONVERSION_FACTORS)

try:
    from datetime import timezone
except ImportError: # python2.7
    from datetime import tzinfo
    class timezone(tzinfo):
        """
        Fixed offset in minutes east from UTC. adapted from
        python 2.7 docs FixedOffset
        """

        def __init__(self, offset, name):
            self.__offset = offset
            self.__name = name

        def utcoffset(self, dt):
            return self.__offset

        def tzname(self, dt):
            return self.__name

        def dst(self, dt):
            return timedelta(hours=0)


# 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')

# test cftime module for netCDF time <--> python datetime conversions.

dtime = namedtuple('dtime', ('values', 'units', 'calendar'))
dateformat =  '%Y-%m-%d %H:%M:%S'

calendars=['standard', 'gregorian', 'proleptic_gregorian', 'noleap', 'julian',\
           'all_leap', '365_day', '366_day', '360_day']
def adjust_calendar(calendar):
    # check for and remove calendar synonyms.
    calendar = calendar.lower()
    if calendar == 'gregorian' or calendar == 'standard':
        return 'gregorian'
    elif calendar == 'noleap' or calendar == '365_day':
        return 'noleap'
    elif calendar == 'all_leap' or calendar == '366_day':
        return 'all_leap'
    else:
        return calendar

class CFTimeVariable(object):
    '''dummy "netCDF" variable to hold time info'''
    def __init__(self, values, calendar='standard', units=None):

        self.values = np.asarray(values)
        self.calendar = calendar
        self.units = units

    def __array__(self):
        # np special method that returns a np array.
        return self.values[...]

    def __getitem__(self, indexers):
        return self.values[indexers]

    def __repr__(self):
        print('units: ', self.units)
        print('calendar: ', self.calendar)
        print('')
        return repr(self.values)

    def __str__(self):
        return self.__repr__()

    def __len__(self):
        return len(self.values)

    @property
    def shape(self):
        return self.values.shape


class cftimeTestCase(unittest.TestCase):

    def setUp(self):
        self.cdftime_mixed = utime('hours since 0001-01-01 00:00:00')
        self.cdftime_julian = utime(
            'hours since 0001-01-01 00:00:00', calendar='julian')
        self.cdftime_mixed_tz = utime('hours since 0001-01-01 00:00:00 -06:00')
        self.cdftime_pg = utime('seconds since 0001-01-01 00:00:00',
                                calendar='proleptic_gregorian')
        self.cdftime_noleap = utime(
            'days since 1600-02-28 00:00:00', calendar='noleap')
        self.cdftime_leap = utime(
            'days since 1600-02-29 00:00:00', calendar='all_leap')
        self.cdftime_360day = utime(
            'days since 1600-02-30 00:00:00', calendar='360_day')
        self.cdftime_jul = utime(
            'hours since 1000-01-01 00:00:00', calendar='julian')
        self.cdftime_iso = utime("seconds since 1970-01-01T00:00:00Z")
        self.cdftime_leading_space = utime("days since  850-01-01 00:00:00")
        self.cdftime_mixed_capcal = utime('hours since 0001-01-01 00:00:00',
                                          calendar='Standard')
        self.cdftime_noleap_capcal = utime(
            'days since 1600-02-28 00:00:00', calendar='NOLEAP')

    def test_tz_aware(self):
        """testing with timezone"""
        self.assertTrue(self.cdftime_mixed.units == 'hours')
        d1 = datetime(1582, 10, 4, 23, tzinfo=utc)
        t1 = self.cdftime_mixed.date2num(d1)
        d2 = datetime(1582, 10, 4, 18, tzinfo=est)
        t2 = self.cdftime_mixed.date2num(d2)
        assert_almost_equal(t1, 13865687.0)
        assert_almost_equal(t2, 13865682.0) # est is 5 hours behind utc

    def test_tz_naive(self):
        """testing cftime"""
        # test mixed julian/gregorian calendar
        # check attributes.
        self.assertTrue(self.cdftime_mixed.units == 'hours')
        self.assertTrue(
            str(self.cdftime_mixed.origin) == '0001-01-01 00:00:00')
        self.assertTrue(
            self.cdftime_mixed.unit_string == 'hours since 0001-01-01 00:00:00')
        self.assertTrue(self.cdftime_mixed.calendar == 'standard')
        # check date2num method. (date before switch)
        d = datetime(1582, 10, 4, 23)
        t1 = self.cdftime_mixed.date2num(d)
        assert_almost_equal(t1, 13865687.0)
        # check num2date method.
        d2 = self.cdftime_mixed.num2date(t1)
        self.assertTrue(str(d) == str(d2))
        # this is a non-existant date, should raise ValueError.
        d = datetime(1582, 10, 5, 0)
        self.assertRaises(ValueError, self.cdftime_mixed.date2num, d)
        # check date2num/num2date with date after switch.
        d = datetime(1582, 10, 15, 0)
        t2 = self.cdftime_mixed.date2num(d)
        assert_almost_equal(t2, 13865688.0)
        d2 = self.cdftime_mixed.num2date(t2)
        self.assertTrue(str(d) == str(d2))
        # check day of year.
        ndayr = d.timetuple()[7]
        self.assertTrue(ndayr == 288)
        # test using np arrays.
        t = np.arange(t2, t2 + 240.0, 12.)
        t = np.reshape(t, (4, 5))
        d = self.cdftime_mixed.num2date(t)
        self.assertTrue(d.shape == t.shape)
        d_check = "1582-10-15 00:00:001582-10-15 12:00:001582-10-16 00:00:001582-10-16 12:00:001582-10-17 00:00:001582-10-17 12:00:001582-10-18 00:00:001582-10-18 12:00:001582-10-19 00:00:001582-10-19 12:00:001582-10-20 00:00:001582-10-20 12:00:001582-10-21 00:00:001582-10-21 12:00:001582-10-22 00:00:001582-10-22 12:00:001582-10-23 00:00:001582-10-23 12:00:001582-10-24 00:00:001582-10-24 12:00:00"
        d2 = [str(dd) for dd in d.flat]
        self.assertTrue(d_check == ''.join(d2))
        # test julian calendar with np arrays
        d = self.cdftime_julian.num2date(t)
        self.assertTrue(d.shape == t.shape)
        d_check = "1582-10-05 00:00:001582-10-05 12:00:001582-10-06 00:00:001582-10-06 12:00:001582-10-07 00:00:001582-10-07 12:00:001582-10-08 00:00:001582-10-08 12:00:001582-10-09 00:00:001582-10-09 12:00:001582-10-10 00:00:001582-10-10 12:00:001582-10-11 00:00:001582-10-11 12:00:001582-10-12 00:00:001582-10-12 12:00:001582-10-13 00:00:001582-10-13 12:00:001582-10-14 00:00:001582-10-14 12:00:00"
        d2 = [str(dd) for dd in d.flat]
        self.assertTrue(d_check == ''.join(d2))
        # test proleptic gregorian calendar.
        self.assertTrue(self.cdftime_pg.units == 'seconds')
        self.assertTrue(str(self.cdftime_pg.origin) == '0001-01-01 00:00:00')
        self.assertTrue(
            self.cdftime_pg.unit_string == 'seconds since 0001-01-01 00:00:00')
        self.assertTrue(self.cdftime_pg.calendar == 'proleptic_gregorian')
        # check date2num method.
        d = datetime(1990, 5, 5, 2, 17)
        t1 = self.cdftime_pg.date2num(d)
        self.assertTrue(np.around(t1) == 62777470620.0)
        # check num2date method.
        d2 = self.cdftime_pg.num2date(t1)
        self.assertTrue(d.strftime(dateformat) == d2.strftime(dateformat))
        # check day of year.
        ndayr = d.timetuple()[7]
        self.assertTrue(ndayr == 125)
        # check noleap calendar.
        # this is a non-existant date, should raise ValueError.
        self.assertRaises(
            ValueError, utime, 'days since 1600-02-29 00:00:00', calendar='noleap')
        self.assertTrue(self.cdftime_noleap.units == 'days')
        self.assertTrue(
            str(self.cdftime_noleap.origin) == '1600-02-28 00:00:00')
        self.assertTrue(
            self.cdftime_noleap.unit_string == 'days since 1600-02-28 00:00:00')
        self.assertTrue(self.cdftime_noleap.calendar == 'noleap')
        assert_almost_equal(
            self.cdftime_noleap.date2num(self.cdftime_noleap.origin), 0.0)
        # check date2num method.
        d1 = datetime(2000, 2, 28)
        d2 = datetime(1600, 2, 28)
        t1 = self.cdftime_noleap.date2num(d1)
        t2 = self.cdftime_noleap.date2num(d2)
        assert_almost_equal(t1, 400 * 365.)
        assert_almost_equal(t2, 0.)
        t12 = self.cdftime_noleap.date2num([d1, d2])
        assert_almost_equal(t12, [400 * 365., 0])
        # check num2date method.
        d2 = self.cdftime_noleap.num2date(t1)
        self.assertTrue(str(d1) == str(d2))
        # check day of year.
        ndayr = d2.timetuple()[7]
        self.assertTrue(ndayr == 59)
        # non-existant date, should raise ValueError.
        date = datetime(2000, 2, 29)
        self.assertRaises(ValueError, self.cdftime_noleap.date2num, date)
        # check all_leap calendar.
        self.assertTrue(self.cdftime_leap.units == 'days')
        self.assertTrue(
            str(self.cdftime_leap.origin) == '1600-02-29 00:00:00')
        self.assertTrue(
            self.cdftime_leap.unit_string == 'days since 1600-02-29 00:00:00')
        self.assertTrue(self.cdftime_leap.calendar == 'all_leap')
        assert_almost_equal(
            self.cdftime_leap.date2num(self.cdftime_leap.origin), 0.0)
        # check date2num method.
        d1 = datetime(2000, 2, 29)
        d2 = datetime(1600, 2, 29)
        t1 = self.cdftime_leap.date2num(d1)
        t2 = self.cdftime_leap.date2num(d2)
        assert_almost_equal(t1, 400 * 366.)
        assert_almost_equal(t2, 0.)
        # check num2date method.
        d2 = self.cdftime_leap.num2date(t1)
        self.assertTrue(str(d1) == str(d2))
        # check day of year.
        ndayr = d2.timetuple()[7]
        self.assertTrue(ndayr == 60)
        # double check date2num,num2date methods.
        d = datetime(2000, 12, 31)
        t1 = self.cdftime_mixed.date2num(d)
        d2 = self.cdftime_mixed.num2date(t1)
        self.assertTrue(str(d) == str(d2))
        ndayr = d2.timetuple()[7]
        self.assertTrue(ndayr == 366)
        # check 360_day calendar.
        self.assertTrue(self.cdftime_360day.units == 'days')
        self.assertTrue(
            str(self.cdftime_360day.origin) == '1600-02-30 00:00:00')
        self.assertTrue(
            self.cdftime_360day.unit_string == 'days since 1600-02-30 00:00:00')
        self.assertTrue(self.cdftime_360day.calendar == '360_day')
        assert_almost_equal(
            self.cdftime_360day.date2num(self.cdftime_360day.origin), 0.0)
        # check date2num,num2date methods.
        # use datetime from cftime, since this date doesn't
        # exist in "normal" calendars.
        d = datetimex(2000, 2, 30, calendar='')
        t1 = self.cdftime_360day.date2num(d)
        assert_almost_equal(t1, 360 * 400.)
        d2 = self.cdftime_360day.num2date(t1)
        assert_equal(str(d), str(d2))
        # check day of year.
        d = datetime(2001, 12, 30)
        t = self.cdftime_360day.date2num(d)
        assert_almost_equal(t, 144660.0)
        date = self.cdftime_360day.num2date(t)
        self.assertTrue(str(d) == str(date))
        ndayr = date.timetuple()[7]
        self.assertTrue(ndayr == 360)
        # Check fraction
        d = datetime(1969, 12, 30, 12)
        t = self.cdftime_360day.date2num(d)
        date = self.cdftime_360day.num2date(t)
        assert_equal(str(d), str(date))
        # test proleptic julian calendar.
        d = datetime(1858, 11, 17, 12)
        t = self.cdftime_jul.date2num(d)
        assert_almost_equal(t, 7528932.0)
        d1 = datetime(1582, 10, 4, 23)
        d2 = datetime(1582, 10, 15, 0)
        assert_almost_equal(
            self.cdftime_jul.date2num(d1) + 241.0, self.cdftime_jul.date2num(d2))
        date = self.cdftime_jul.num2date(t)
        self.assertTrue(str(d) == str(date))
        # test julian day from date, date from julian day
        d = cftime.datetime(1858, 11, 17, calendar='standard')
        # 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)
        mjd1z = dz.toordinal(fractional=True)
        assert_almost_equal(mjd1, 2400000.5)
        assert_almost_equal(mjd1z, 2400000.5)
        # fromordinal (same as old DateFromJulianDay)
        date1 = cftime.datetime.fromordinal(mjd1)
        self.assertTrue(str(date1) == str(d))
        # test iso 8601 units string
        d = datetime(1970, 1, 1, 1)
        t = self.cdftime_iso.date2num(d)
        assert_equal(np.around(t), 3600)
        # test fix for issue 75 (seconds hit 60 at end of month,
        # day goes out of range).
        t = 733499.0
        d = num2date(t, units='days since 0001-01-01 00:00:00')
        assert_equal(d.strftime(dateformat), '2009-04-01 00:00:00')
        # test edge case of issue 75 for numerical problems
        for t in (733498.999, 733498.9999, 733498.99999, 733498.999999, 733498.9999999):
            d = num2date(t, units='days since 0001-01-01 00:00:00')
            t2 = date2num(d, units='days since 0001-01-01 00:00:00')
            assert(abs(t2 - t) < 1e-5)  # values should be less than second
        # Check equality testing
        d1 = datetimex(1979, 6, 21, 9, 23, 12, calendar='standard')
        d2 = datetime(1979, 6, 21, 9, 23, 12)
        assert(d1 == d2)
        # check timezone offset
        d = datetime(2012, 2, 29, 15)
        # mixed_tz is -6 hours from UTC, mixed is UTC so
        # difference in elapsed time is -6 hours.
        assert(self.cdftime_mixed_tz.date2num(
            d) - self.cdftime_mixed.date2num(d) == -6)

        # Check comparisons with Python datetime types

        # Note that d1 has to use the proleptic Gregorian calendar to
        # be comparable to d2: datetime.datetime uses the proleptic
        # Gregorian calendar and year 1000 is before the
        # Julian/Gregorian transition (1582-10-15).
        d1 = num2date(0, 'days since 1000-01-01', 'proleptic_gregorian')

        d2 = datetime(2000, 1, 1)

        # The date d3 is well after the Julian/Gregorian transition
        # and so this Gregorian date can be compared to the proleptic
        # Gregorian date d2.
        d3 = num2date(0, 'days since 3000-01-01', 'standard')
        assert d1 < d2
        assert d2 < d3

        # check all comparisons
        assert d1 != d2
        assert d1 <= d2
        assert d2 > d1
        assert d2 >= d1

        # check datetime hash
        d1 = datetimex(1995, 1, 1)
        d2 = datetime(1995, 1, 1)
        d3 = datetimex(2001, 2, 20)
        assert hash(d1) == hash(d1)
        assert hash(d1) == hash(d2)
        assert hash(d1) != hash(d3)
        assert hash(d3) == hash(d3)

        # check datetime immutability
        # using assertRaises as a context manager
        # only works with python >= 2.7 (issue #497).
        immutability_tests = {"year": 1999,
                              "month": 6,
                              "day": 5,
                              "hour": 10,
                              "minute": 33,
                              "second": 45,
                              "dayofwk": 1,
                              "dayofyr": 52,
                              "format": '%Y'}

        for name, value in immutability_tests.items():
            self.assertRaises(AttributeError, setattr, d1, name, value)

        # Check leading white space
        self.assertEqual(
            str(self.cdftime_leading_space.origin), '0850-01-01 00:00:00')

        #issue 330
        units = "seconds since 1970-01-01T00:00:00Z"
        t = utime(units)
        for n in range(10):
            assert n == int(round(t.date2num(t.num2date(n))))

        #issue 344
        units = 'hours since 2013-12-12T12:00:00'
        assert(1.0 == date2num(num2date(1.0, units), units))

        # test rountrip accuracy
        # also tests error found in issue #349
        dateref = datetime(2015,2,28,12)
        verbose = True # print out max error diagnostics
        ntimes = 101
        def roundtrip(delta,eps,units):
            times1 = date2num(dateref,units,calendar=calendar)
            times1 += delta*np.arange(0,ntimes)
            dates1 = num2date(times1,units,calendar=calendar)
            times2 = date2num(dates1,units,calendar=calendar)
            dates2 = num2date(times2,units,calendar=calendar)
            err = np.abs(times1 - times2)
            assert(np.all(err<eps))
            dates1 = [date.strftime(dateformat) for date in dates1]
            dates2 = [date.strftime(dateformat) for date in dates2]
            assert(dates1==dates2)
            return err.max()
        for calendar in calendars:
            eps = 1.; delta = 1.
            units = 'microseconds since 2000-01-30 01:01:01'
            maxerr = roundtrip(eps,delta,units)
            if verbose:
                print('calendar = %s max abs err (microsecs) = %s eps = %s' % \
                     (calendar,maxerr,eps))
            eps = 0.001; delta = 0.001
            units = 'milliseconds since 2000-01-30 01:01:01'
            maxerr = roundtrip(eps,delta,units)
            if verbose:
                print('calendar = %s max abs err (millisecs) = %s eps = %s' % \
                     (calendar,maxerr,eps))
            eps = 1.e-5; delta = 0.1
            units = 'seconds since 0001-01-30 01:01:01'
            maxerr = roundtrip(eps,delta,units)
            if verbose:
                print('calendar = %s max abs err (secs) = %s eps = %s' % \
                     (calendar,maxerr,eps))
            eps = 1.e-6; delta = 0.01
            units = 'minutes since 0001-01-30 01:01:01'
            maxerr = roundtrip(eps,delta,units)
            if verbose:
                print('calendar = %s max abs err (mins) = %s eps = %s' % \
                     (calendar,maxerr,eps))
            eps = 1.e-8; delta = 0.001
            units = 'hours since 0001-01-30 01:01:01'
            maxerr = roundtrip(eps,delta,units)
            if verbose:
                print('calendar = %s max abs err (hours) = %s eps = %s' % \
                     (calendar,maxerr,eps))
            eps = 1.e-9; delta = 0.00001
            units = 'days since 0001-01-30 01:01:01'
            maxerr = roundtrip(eps,delta,units)
            if verbose:
                print('calendar = %s max abs err (days) = %s eps = %s' % \
                     (calendar,maxerr,eps))

        # issue 353
        assert (num2date(0, 'hours since 2000-01-01 0') ==
                datetime(2000,1,1,0))

        # issue 354
        num1 = np.array([[0, 1], [2, 3]])
        num2 = np.array([[0, 1], [2, 3]])
        dates1 = num2date(num1, 'days since 0001-01-01')
        dates2 = num2date(num2, 'days since 2001-01-01')
        assert( dates1.shape == (2,2) )
        assert( dates2.shape == (2,2) )
        num1b = date2num(dates1, 'days since 0001-01-01')
        num2b = date2num(dates2, 'days since 2001-01-01')
        assert( num1b.shape == (2,2) )
        assert( num2b.shape == (2,2) )
        assert_almost_equal(num1,num1b)
        assert_almost_equal(num2,num2b)

        # issue 357 (make sure time zone offset in units done correctly)
        # Denver time, 7 hours behind UTC
        units = 'hours since 1682-10-15 -07:00 UTC'
        # date after gregorian switch, python datetime used
        date = datetime(1682,10,15) # assumed UTC
        num = date2num(date,units)
        # UTC is 7 hours ahead of units, so num should be -7
        assert (num == -7)
        assert (num2date(num, units) == date)
        units = 'hours since 1482-10-15 -07:00 UTC'
        # date before gregorian switch, cftime datetime used
        date = datetime(1482,10,15)
        num = date2num(date,units)
        date2 = num2date(num, units)
        assert (num == -7)
        assert (date2.year == date.year)
        assert (date2.month == date.month)
        assert (date2.day == date.day)
        assert (date2.hour == date.hour)
        assert (date2.minute == date.minute)
        assert (date2.second == date.second)

        # issue 362: case insensitive calendars
        self.assertTrue(self.cdftime_mixed_capcal.calendar == 'standard')
        self.assertTrue(self.cdftime_noleap_capcal.calendar == 'noleap')
        d = datetime(2015, 3, 4, 12, 18, 30)
        units = 'days since 0001-01-01'
        for cap_cal, low_cal in (('STANDARD', 'standard'),
                                 ('NoLeap', 'noleap'),
                                 ('Gregorian', 'gregorian'),
                                 ('ALL_LEAP', 'all_leap')):
            d1 = date2num(d, units, cap_cal)
            d2 = date2num(d, units, low_cal)
            self.assertEqual(d1, d2)
            self.assertEqual(num2date(d1, units, cap_cal),
                             num2date(d1, units, low_cal))
        # issue 415
        t = datetimex(2001, 12, 1, 2, 3, 4)
        self.assertEqual(t, copy.deepcopy(t))

        # issue 442
        units = "days since 0000-01-01 00:00:00"
        # this should fail (year zero not allowed with real-world calendars)
        try:
            date2num(datetime(1, 1, 1), units, calendar='standard')
        except ValueError:
            pass
        # this should not fail (year zero allowed in 'fake' calendars)
        t = date2num(datetime(1, 1, 1), units, calendar='360_day')
        self.assertAlmostEqual(t,360)
        d = num2date(t, units, calendar='360_day')
        self.assertEqual(d, Datetime360Day(1,1,1))
        d = num2date(0, units, calendar='360_day')
        self.assertEqual(d, Datetime360Day(0,1,1))

        # issue 685: wrong time zone conversion
        # 'The following times all refer to the same moment: "18:30Z", "22:30+04", "1130-0700", and "15:00-03:30'
        # (https://en.wikipedia.org/w/index.php?title=ISO_8601&oldid=787811367#Time_offsets_from_UTC)
        # test num2date
        utc_date = datetime(2000,1,1,18,30)
        for units in ("hours since 2000-01-01 22:30+04:00", "hours since 2000-01-01 11:30-07:00", "hours since 2000-01-01 15:00-03:30"):
            d = num2date(0, units, calendar="standard")
            #self.assertEqual(d, utc_date)
            # tolerance of 1.e-3 secs
            assert(np.abs((d-utc_date).total_seconds()) < 1.e-3)
            # also test with negative values to cover 2nd code path
            d = num2date(-1, units, calendar="standard")
            self.assertEqual(d, utc_date - timedelta(hours=1))

            n = date2num(utc_date, units, calendar="standard")
            # n should always be 0 as all units refer to the same point in time
            self.assertEqual(n, 0)
        # explicitly test 2nd code path for date2num
        units = "hours since 2000-01-01 22:30+04:00"
        n = date2num(utc_date, units, calendar="julian")
        # n should always be 0 as all units refer to the same point in time
        assert_almost_equal(n, 0)

        # list around missing dates in Gregorian calendar
        # scalar
        units = 'days since 0001-01-01 12:00:00'
        t1 = date2num(datetime(1582, 10, 4), units, calendar='gregorian')
        t2 = date2num(datetime(1582, 10, 15), units, calendar='gregorian')
        self.assertEqual(t1+1, t2)
        # list
        t1, t2 = date2num([datetime(1582, 10, 4), datetime(1582, 10, 15)], units, calendar='gregorian')
        self.assertEqual(t1+1, t2)
        t1, t2 = date2num([datetime(1582, 10, 4), datetime(1582, 10, 15)], units, calendar='standard')
        self.assertEqual(t1+1, t2)
        # this should fail: days missing in Gregorian calendar
        try:
            t1, t2, t3 = date2num([datetime(1582, 10, 4), datetime(1582, 10, 10), datetime(1582, 10, 15)], units, calendar='standard')
        except ValueError:
            pass
        # test fix for issue #596 - julian day calculations wrong for negative years,
        # caused incorrect rountrip num2date(date2num(date)) roundtrip for dates with year
        # < 0.
        u = utime("seconds since 1-1-1",calendar='julian')
        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(cftime.datetime.toordinal(date1,fractional=True), 1721057.5)
        # issue 596 - negative years fail in utime.num2date
        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)
        assert (d.hour == 0)
        # test fix for issue #659 (proper treatment of negative time values)
        units = 'days since 1800-01-01 00:00:0.0'
        d = num2date(-657073, units, calendar='standard')
        assert (d.year == 1)
        assert (d.month == 1)
        assert (d.day == 1)
        assert (d.hour == 0)
        # cftime issue #134
        d = num2date(-657071, units, calendar='proleptic_gregorian',
                     only_use_cftime_datetimes=False,only_use_python_datetimes=True)
        assert(d==datetime(1,1,1,0))
        self.assertRaises(ValueError, num2date, \
        -657072, units, calendar='proleptic_gregorian',
                     only_use_cftime_datetimes=False,only_use_python_datetimes=True)
        # issue 685: wrong time zone conversion
        # 'The following times all refer to the same moment: "18:30Z", "22:30+04", "1130-0700", and "15:00-03:30'
        # (https://en.wikipedia.org/w/index.php?title=ISO_8601&oldid=787811367#Time_offsets_from_UTC)
        # test num2date
        utc_date = datetime(2000,1,1,18,30)
        for units in ("hours since 2000-01-01 22:30+04:00", "hours since 2000-01-01 11:30-07:00", "hours since 2000-01-01 15:00-03:30"):
            d = num2date(0, units, calendar="standard")
            assert(np.abs((d-utc_date).total_seconds()) < 1.e-3)
            # also test with negative values to cover 2nd code path
            d = num2date(-1, units, calendar="standard")
            assert(np.abs((d - \
                (utc_date-timedelta(hours=1))).total_seconds()) < 1.e-3)

            n = date2num(utc_date, units, calendar="standard")
            # n should always be 0 as all units refer to the same point in time
            self.assertEqual(n, 0)
        # explicitly test 2nd code path for date2num
        units = "hours since 2000-01-01 22:30+04:00"
        n = date2num(utc_date, units, calendar="julian")
        # n should always be 0 as all units refer to the same point in time
        assert_almost_equal(n, 0)
        # cftime issue #49
        d = cftime.datetime.fromordinal(2450022.5, calendar="standard")
        assert (d.year == 1995)
        assert (d.month == 11)
        assert (d.day == 1)
        assert (d.hour == 0)
        assert (d.minute == 0)
        assert (d.second == 0)
        # cftime issue #52
        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)
        assert (d1.dayofwk == d2.dayofwk == 5)
        assert (d1.dayofyr == d2.dayofyr == 60)
        d1 = DatetimeGregorian(2020,2,29,23,59,59)
        d2 = real_datetime(2020,2,29,23,59,59)
        assert (d1.dayofwk == d2.dayofwk == 5)
        assert (d1.dayofyr == d2.dayofyr == 60)
        d1 = DatetimeGregorian(2020,2,28,23,59,59)
        d2 = real_datetime(2020,2,28,23,59,59)
        assert (d1.dayofwk == d2.dayofwk == 4)
        assert (d1.dayofyr == d2.dayofyr == 59)
        d1 = DatetimeGregorian(1700,1,1)
        d2 = real_datetime(1700,1,1)
        assert (d1.dayofwk == d2.dayofwk == 4)
        assert (d1.dayofyr == d2.dayofyr == 1)
        # last day of Julian Calendar (Thursday)
        d1 = DatetimeJulian(1582, 10, 4, 12)
        d2 = DatetimeGregorian(1582, 10, 4, 12)
        assert (d1.dayofwk == d2.dayofwk == 3)
        assert (d1.dayofyr == d2.dayofyr == 277)
        # Monday in proleptic gregorian calendar
        d1 = DatetimeProlepticGregorian(1582, 10, 4, 12)
        d2 = real_datetime(1582,10,4,12)
        assert (d1.dayofwk == d2.dayofwk == 0)
        assert (d1.dayofyr == d2.dayofyr == 277)

        # issue 173: Return the default values of dayofwk and dayofyr
        # when calendar is ''
        d1 = datetimex(1582, 10, 4, 12, calendar='')
        assert (d1.dayofwk == d1.dayofyr == -1)
        d1 = datetimex(2020, 5, 20, calendar='')
        assert (d1.dayofwk == d1.dayofyr == -1)
        d1 = datetimex(2020, 5, 20, dayofwk=-2, dayofyr=-3, calendar='')
        assert (d1.dayofwk == -2)
        assert (d1.dayofyr == -3)
        d1 = datetimex(2020, 5, 20, dayofwk=8, dayofyr=9, calendar='')
        assert (d1.dayofwk == 8)
        assert (d1.dayofyr == 9)
        
        # issue 71: negative reference years
        # https://coastwatch.pfeg.noaa.gov/erddap/convert/time.html
        # 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
        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')
        self.assertEqual(d, Datetime360Day(0,2,1))
        t = date2num(d, 'months since 0000-01-01 00:00:00', calendar='360_day')
        self.assertEqual(t, 1)
        # check that exception is raised if 'months since' used with
        # anything but the 360_day calendar.
        self.assertRaises(ValueError, num2date, \
             1, 'months since 01-01-01',calendar='standard')
        self.assertRaises(ValueError, utime, \
            'months since 01-01-01', calendar='standard')
        # issue #78 - extra digits due to roundoff
        assert(cftime.date2num(cftime.datetime(1, 12, 1, 0, 0, 0, 0, -1, 1), units='days since 01-01-01',calendar='noleap')  == 334.0)
        assert(cftime.date2num(cftime.num2date(1.0,units='days since 01-01-01',calendar='noleap'),units='days since 01-01-01',calendar='noleap') == 1.0)
        assert(cftime.date2num(cftime.DatetimeNoLeap(1980, 1, 1, 0, 0, 0, 0, 6, 1),'days since 1970-01-01','noleap') == 3650.0)
        # issue #126
        d = cftime.DatetimeProlepticGregorian(1, 1, 1)
        assert(cftime.date2num(d, 'days since 0001-01-01',\
            'proleptic_gregorian') == 0.0)
        # issue #140 (fractional seconds in reference date)
        d = datetime.strptime('2018-01-23 09:27:10.950000',"%Y-%m-%d %H:%M:%S.%f")
        units = 'seconds since 2018-01-23 09:31:42.94'
        assert(cftime.date2num(d, units) == -271.99)
        # issue 143 - same answer for arrays vs scalars.
        units = 'seconds since 1970-01-01 00:00:00'
        times_in = [1261440000.0, 1261440001.0, 1261440002.0, 1261440003.0,
                    1261440004.0, 1261440005.0]
        times_out1 = cftime.num2date(times_in, units)
        times_out2 = []
        for time_in in times_in:
            times_out2.append(cftime.num2date(time_in, units))
        dates1 = [str(d) for d in times_out1]
        dates2 = [str(d) for d in times_out2]
        assert(dates1 == dates2)
        # issue #143 formatting of microseconds
        d = cftime.num2date(1261440000.015625,units)
        # on windows only 100 ms precision
        assert(str(d)[0:24] == '2009-12-22 00:00:00.0156')
        # issue #152 add isoformat()
        assert(d.isoformat()[0:24] == '2009-12-22T00:00:00.0156')
        assert(d.isoformat(sep=' ')[0:24] == '2009-12-22 00:00:00.0156')
        assert(d.isoformat(sep=' ',timespec='milliseconds') == '2009-12-22 00:00:00.015')
        assert(d.isoformat(sep=' ',timespec='seconds') == '2009-12-22 00:00:00')
        assert(d.isoformat(sep=' ',timespec='minutes') == '2009-12-22 00:00')
        assert(d.isoformat(sep=' ',timespec='hours') == '2009-12-22 00')
        # issue #165: make sure python datetime returned
        d=num2date(0,units="seconds since 2000-01-01 00:00:00",only_use_cftime_datetimes=False)
        assert isinstance(d, datetime)
        # issue #169: cftime.datetime has no calendar attribute, causing dayofwk,dayofyr methods
        # to fail.
        c = cftime.datetime(*cftime._parse_date('7480-01-01 00:00:00'),calendar='standard')
        assert(c.strftime() == '7480-01-01 00:00:00')
        # issue #175: masked values not treated properly in num2date
        times = np.ma.masked_array([-3956.7499999995343,-999999999999],mask=[False,True])
        units='days since 1858-11-17 00:00:00'
        dates = num2date(times, units=units, calendar='standard',\
        only_use_cftime_datetimes=False, only_use_python_datetimes=True)
        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, 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
        refdate = DatetimeGregorian(292277,10,24,0,0,1)
        for unit in ['microseconds','milliseconds','seconds']:
            units = '%s since 01-01-01' % unit
            time = 292471*365*86400*(1000000//int(UNIT_CONVERSION_FACTORS[unit])) + 1000000//int(UNIT_CONVERSION_FACTORS[unit])
            date = num2date(time,units,calendar=calendar)
            assert(date == refdate)
            # check round-trip
            time2 = date2num(date,units,calendar=calendar)
            date2 = num2date(time2,units,calendar=calendar)
            # windows doesn't have a longdouble type (it defaults to float64)
            if np.finfo(np.longdouble).precision == 18:
                assert(date2 == refdate)
# microsecond roundtrip accuracy preserved over time ranges of 286 years
# (float64 can only represent integers exactly up to 2**53-1)
        refdate=DatetimeGregorian(286,6,3,23,47,34,740992)
        for unit in ['microseconds','milliseconds','seconds','hours','days']:
            units = '%s since 01-01-01' % unit
            time = (2**53 - 1)*(1/UNIT_CONVERSION_FACTORS[unit]) + 1/UNIT_CONVERSION_FACTORS[unit]
            date = num2date(time,units,calendar=calendar)
            assert(date == refdate)
            # check round-trip
            time2 = date2num(date,units,calendar=calendar)
            date2 = num2date(time2,units,calendar=calendar)
            assert(date2 == refdate)
# issue #185: date2num should work the numpy scalar array of dates (1.2.0 regression)
        dates = np.array(datetime(2010, 2, 2, 0, 0))
        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)
        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) # python datetime
        units = 'days since -4713-01-01 12:00'
        cal = 'proleptic_gregorian'
        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.
        for cal in calendars:
            dt = cftime.datetime(2020, 1, 1, calendar=cal)
            dt += timedelta(hours=1)
            assert(str(dt) == '2020-01-01 01:00:00')
# issue #193 - years with more than four digits in reference date
        assert(cftime.date2num(cftime.datetime(18000, 12, 1, 0, 0), 'days since 18000-1-1', '360_day') == 330.0)
        # julian day not including year zero
        d = cftime.datetime(2020, 12, 1, 12, calendar='julian')
        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:
        # 'cftime._cftime.DatetimeGregorian' object has no attribute 'view')
        m = np.ma.asarray(
            [cftime.DatetimeGregorian(2014, 8, 1, 12, 0, 0, 0)]
            )
        assert(
             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):

    class TestTime:

        """Fake a netCDF time variable."""

        def __init__(self, start, n, step, units, calendar='standard'):
            """Create an object that fakes a netCDF time variable.

            Internally, this object has a _data array attribute whose values
            corresponds to dates in the given units and calendar. `start`, `n`
            and `step` define the starting date, the length of the array and
            the distance between each date (in units).

            """
            self.units = units
            self.calendar = calendar
            t0 = date2num(start, units, calendar)
            self._data = (t0 + np.arange(n) * step).astype('float')
            self.dtype = np.float

        def __getitem__(self, item):
            return self._data[item]

        def __len__(self):
            return len(self._data)

        def shape():
            def fget(self):
                return self._data.shape
            return locals()

        shape = property(**shape())

    def setUp(self):
        self.standardtime = self.TestTime(datetime(1950, 1, 1), 366, 24,
                                          'hours since 1900-01-01', 'standard')

        self.time_vars = {}
        self.time_vars['time'] = CFTimeVariable(
            values=self.standardtime,
            units='hours since 1900-01-01')

        self.first_timestamp = datetime(2000, 1, 1)
        units = 'days since 1901-01-01'
        self.time_vars['time2'] = CFTimeVariable(
            values=date2num([self.first_timestamp], units),
            units=units)

        ntimes = 21

        units = "seconds since 1970-01-01 00:00:00"
        date = datetime(2037, 1, 1, 0)
        dates = [date]
        for ndate in range(ntimes-1):
            date += (ndate+1)*timedelta(hours=1)
            dates.append(date)
        self.time_vars['time3'] = CFTimeVariable(
            values=date2num(dates, units),
            units=units)

    def test_tz_aware(self):
        """implicit test of date2num"""
        dutc = datetime(1950, 2, 1, 0, tzinfo=utc)
        t1 = date2index(dutc, self.standardtime)
        assert_equal(t1, 31)
        dest = datetime(1950, 1, 31, 19, tzinfo=est)
        t2 = date2index(dest, self.standardtime)
        assert_equal(t2, 31)

    def test_simple(self):
        t = date2index(datetime(1950, 2, 1), self.standardtime)
        assert_equal(t, 31)

    def test_singletime(self):
        # issue 215 test (date2index with time variable length == 1)
        time2 = self.time_vars['time2']
        result_index = date2index(self.first_timestamp, time2, select="exact")
        assert_equal(result_index, 0)

    def test_list(self):
        t = date2index(
            [datetime(1950, 2, 1), datetime(1950, 2, 3)], self.standardtime)
        assert_equal(t, [31, 33])

    def test_nonuniform(self):
        """Check that the fallback mechanism works. """
        nutime = self.TestTime(datetime(1950, 1, 1), 366, 24,
                               'hours since 1900-01-01', 'standard')

        # Let's remove the second entry, so that the computed stride is not
        # representative and the bisection method is needed.
        nutime._data = nutime._data[np.r_[0, slice(2, 200)]]

        t = date2index(datetime(1950, 2, 1), nutime)
        assert_equal(t, 30)

        t = date2index([datetime(1950, 2, 1), datetime(1950, 2, 3)], nutime)
        assert_equal(t, [30, 32])

    def test_failure(self):
        nutime = self.TestTime(datetime(1950, 1, 1), 366, 24,
                               'hours since 1900-01-01', 'standard')
        try:
            t = date2index(datetime(1949, 2, 1), nutime)
        except ValueError:
            pass
        else:
            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', calendar)

        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])

        t = date2index(dates, nutime, select='after')
        assert_equal(t, [2, 2, 3])

        t = date2index(dates, nutime, select='nearest')
        assert_equal(t, [1, 2, 3])

    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)]

        t = date2index(dates, nutime, select='before')
        assert_equal(t, [1, 2, 2])

        t = date2index(dates, nutime, select='after')
        assert_equal(t, [2, 2, 3])

        t = date2index(dates, nutime, select='nearest')
        assert_equal(t, [1, 2, 3])

        # Test dates outside the support with select
        t = date2index(datetime(1949, 12, 1), nutime, select='nearest')
        assert_equal(t, 0)

        t = date2index(datetime(1978, 1, 1), nutime, select='nearest')
        assert_equal(t, 365)

        # Test dates outside the support with before
        self.assertRaises(
            ValueError, date2index, datetime(1949, 12, 1), nutime, select='before')

        t = date2index(datetime(1978, 1, 1), nutime, select='before')
        assert_equal(t, 365)

        # Test dates outside the support with after
        t = date2index(datetime(1949, 12, 1), nutime, select='after')
        assert_equal(t, 0)

        self.assertRaises(
            ValueError, date2index, datetime(1978, 1, 1), nutime, select='after')
        # test microsecond and millisecond units
        unix_epoch = "milliseconds since 1970-01-01T00:00:00Z"
        d = datetime(2038, 1, 19, 3, 14, 7)
        millisecs = int(
            date2num(d, unix_epoch, calendar='proleptic_gregorian'))
        assert_equal(millisecs, (2 ** 32 / 2 - 1) * 1000)
        unix_epoch = "microseconds since 1970-01-01T00:00:00Z"
        microsecs = int(date2num(d, unix_epoch))
        assert_equal(microsecs, (2 ** 32 / 2 - 1) * 1000000)
        # test microsecond accuracy in date2num/num2date roundtrip
        # note: microsecond accuracy lost for time intervals greater
        # than about 270 years.
        units = 'microseconds since 1776-07-04 00:00:00-12:00'
        dates = [datetime(1962, 10, 27, 6, 1, 30, 9001),
                 datetime(1993, 11, 21, 12, 5, 25, 999),
                 datetime(1995, 11, 25, 18, 7, 59, 999999)]
        times2 = date2num(dates, units)
        dates2 = num2date(times2, units)
        datediff = abs(dates-dates2)
        for diff in datediff:
            assert(diff.microseconds < 100) # tolerance of 100 ms

    def test_issue444(self):
        # make sure integer overflow not causing error in
        # calculation of nearest index when sum of adjacent
        # time values won't fit in 32 bits.
        query_time = datetime(2037, 1, 3, 21, 12)
        index = date2index(query_time, self.time_vars['time3'],
                           select='nearest')
        assert(index == 11)


class issue584TestCase(unittest.TestCase):
    """Regression tests for issue #584."""
    converters = None

    def setUp(self):
        self.converters = {"360_day": utime("days since 1-1-1", "360_day"),
                           "noleap": utime("days since 1-1-1", "365_day")}

    def test_roundtrip(self):
        "Test roundtrip conversion (num2date <-> date2num) using 360_day and 365_day calendars."

        for datetime_class in [Datetime360Day, DatetimeNoLeap]:
            # Pick a date and time outside of the range of the Julian calendar.
            date = datetime_class(-5000, 1, 1, 12)

            converter = self.converters[date.calendar]
            self.assertEqual(date, converter.num2date(converter.date2num(date)))

    def test_dayofwk(self):
        "Test computation of dayofwk in the 365_day calendar."

        converter = self.converters["noleap"]

        # Pick the date corresponding to the Julian day of 1.0 to test
        # the transition from positive to negative Julian days.
        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)
        for delta_year in range(1, 101): # 100 years cover several 7-year cycles
            date = converter.num2date(julian_day - delta_year * 365)

            # test that the day of the week changes by one every year (except
            # for wrapping around every 7 years, of course)
            if date.dayofwk == 6:
                self.assertEqual(old_date.dayofwk, 0)
            else:
                self.assertEqual(old_date.dayofwk - date.dayofwk, 1)

            old_date = date


class DateTime(unittest.TestCase):
    def setUp(self):
        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)
        # first day of the Gregorian calendar in the mixed Julian/Gregorian calendar
        self.date5_gregorian = DatetimeGregorian(1582, 10, 15)

        self.date6_proleptic_gregorian = DatetimeProlepticGregorian(1582, 10, 15)

        self.date7_360_day = Datetime360Day(2000, 1, 1)

        self.date8_julian = DatetimeJulian(1582, 10, 4)

        # a datetime.datetime instance (proleptic Gregorian calendar)
        self.datetime_date1 = datetime(1969,  7, 21, 12)

        self.delta = timedelta(hours=25)

    def test_add(self):
        dt = self.date1_365_day
        # datetime + timedelta
        self.assertEqual(dt + self.delta, # add 25 hours
                         dt.replace(day=dt.day + 1, hour=dt.hour + 1))

        # timedelta + datetime
        self.assertEqual(self.delta + dt, # add 25 hours
                         dt.replace(day=dt.day + 1, hour=dt.hour + 1))

        # test the Julian/Gregorian transition
        self.assertEqual(self.date4_gregorian + self.delta,
                         DatetimeGregorian(1582, 10, 15, 1))

        # The Julian calendar has no invalid dates
        self.assertEqual(self.date8_julian + self.delta,
                         DatetimeJulian(1582, 10, 5, 1))

        # Test going over the year boundary.
        self.assertEqual(DatetimeGregorian(2000, 11, 1) + timedelta(days=30 + 31),
                         DatetimeGregorian(2001, 1, 1))

        # Year 2000 is a leap year.
        self.assertEqual(DatetimeGregorian(2000, 1, 1) + timedelta(days=31 + 29),
                         DatetimeGregorian(2000, 3, 1))

        # Test the 366_day calendar.
        self.assertEqual(DatetimeAllLeap(1, 1, 1) + timedelta(days=366 * 10 + 31),
                         DatetimeAllLeap(11, 2, 1))

        # The Gregorian calendar has no year zero.
        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

        def invalid_add_2():
            1 + self.date1_365_day

        for func in [invalid_add_1, invalid_add_2]:
            self.assertRaises(TypeError, func)

    def test_sub(self):
        # subtracting a timedelta
        previous_day = self.date1_365_day - self.delta
        self.assertEqual(previous_day.day, self.date1_365_day.day - 1)

        def total_seconds(td):
            """Equivalent to td.total_seconds() on Python >= 2.7. See
            https://docs.python.org/2/library/datetime.html#datetime.timedelta.total_seconds
            """
            return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6

        # sutracting two cftime.datetime instances
        delta = self.date2_365_day - self.date1_365_day
        # date1 and date2 are exactly one day apart
        self.assertEqual(total_seconds(delta), 86400)

        # subtracting cftime.datetime from datetime.datetime
        delta = self.datetime_date1 - self.date3_gregorian
        # real_date2 and real_date1 are exactly one day apart
        self.assertEqual(total_seconds(delta), 86400)

        # subtracting datetime.datetime from cftime.datetime
        delta = self.date3_gregorian - self.datetime_date1
        # real_date2 and real_date1 are exactly one day apart
        self.assertEqual(total_seconds(delta), -86400)

        # Test the Julian/Gregorian transition.
        self.assertEqual(self.date5_gregorian - self.delta,
                         DatetimeGregorian(1582, 10, 3, 23))

        # The proleptic Gregorian calendar does not have invalid dates.
        self.assertEqual(self.date6_proleptic_gregorian - self.delta,
                         DatetimeProlepticGregorian(1582, 10, 13, 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.

        self.assertEqual(self.date7_360_day - timedelta(days=2000 * 360),
                         Datetime360Day(0, 1, 1))

        # Test going over the year boundary.
        self.assertEqual(DatetimeGregorian(2000, 3, 1) - timedelta(days=29 + 31 + 31),
                         DatetimeGregorian(1999, 12, 1))

        # Year 2000 is a leap year.
        self.assertEqual(DatetimeGregorian(2000, 3, 1) - self.delta,
                         DatetimeGregorian(2000, 2, 28, 23))

        def invalid_sub_1():
            self.date1_365_day - 1

        def invalid_sub_2():
            1 - self.date1_365_day

        def invalid_sub_3():
            self.date1_365_day - self.datetime_date1

        def invalid_sub_4():
            self.datetime_date1 - self.date1_365_day

        def invalid_sub_5():
            self.date3_gregorian - self.date1_365_day

        def invalid_sub_6():
            self.date3_gregorian - self.date3_gregorian_yearzero

        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)
        self.assertEqual(self.date1_365_day.replace(month=3).month, 3)
        self.assertEqual(self.date1_365_day.replace(day=3).day, 3)
        self.assertEqual(self.date1_365_day.replace(hour=3).hour, 3)
        self.assertEqual(self.date1_365_day.replace(minute=3).minute, 3)
        self.assertEqual(self.date1_365_day.replace(second=3).second, 3)
        self.assertEqual(self.date1_365_day.replace(microsecond=3).microsecond, 3)
        d = datetimex(2000, 2, 3, calendar='noleap')
        self.assertEqual(d.replace(year=1999).calendar, 'noleap')
        d = datetimex(2000, 2, 3, calendar='')
        self.assertEqual(d.replace(year=1999).calendar, '')

    def test_pickling(self):
        "Test reversibility of pickling."
        import pickle

        date = Datetime360Day(year=1, month=2, day=3, hour=4, minute=5, second=6, microsecond=7)
        self.assertEqual(date, pickle.loads(pickle.dumps(date)))

    def test_misc(self):
        "Miscellaneous tests."
        # make sure repr succeeds
        repr(self.date1_365_day)

        # make sure strftime without a format string works
        self.assertEqual(self.date3_gregorian.strftime(None),
                         "1969-07-20 12:00:00")

        def invalid_year():
            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

        def invalid_day():
            DatetimeGregorian(1, 1, 32) + self.delta

        def invalid_gregorian_date():
            DatetimeGregorian(1582, 10, 5) + self.delta

        for func in [invalid_year, invalid_month, invalid_day, invalid_gregorian_date]:
            self.assertRaises(ValueError, func)

    def test_richcmp(self):
        # compare datetime and datetime
        self.assertTrue(self.date1_365_day == self.date1_365_day)
        self.assertFalse(self.date1_365_day == self.date2_365_day)
        self.assertTrue(self.date1_365_day < self.date2_365_day)
        self.assertFalse(self.date1_365_day < self.date1_365_day)
        self.assertTrue(self.date2_365_day > self.date1_365_day)
        self.assertFalse(self.date1_365_day > self.date2_365_day)
        # compare real_datetime and datetime
        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"
            self.date1_365_day > self.date3_gregorian

        def not_comparable_2():
            "compare a datetime instance with a non-standard calendar to real_datetime"
            self.date2_365_day > self.datetime_date1

        def not_comparable_3():
            "compare datetime.datetime to cftime.datetime with a non-gregorian calendar"
            self.datetime_date1 > self.date2_365_day

        def not_comparable_4():
            "compare a datetime instance to non-datetime"
            self.date1_365_day > 0

        def not_comparable_5():
            "compare non-datetime to a datetime instance"
            0 < self.date1_365_day

        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,
                        reason='python2 specific, non-comparable test')
    def test_richcmp_py2(self):
        class Rich(object):
            """Dummy class with traditional rich comparison support."""
            def __lt__(self, other):
                raise NotImplementedError('__lt__')
            def __le__(self, other):
                raise NotImplementedError('__le__')
            def __eq__(self, other):
                raise NotImplementedError('__eq__')
            def __ne__(self, other):
                raise NotImplementedError('__ne__')
            def __gt__(self, other):
                raise NotImplementedError('__gt__')
            def __ge__(self, other):
                raise NotImplementedError('__ge__')

        class CythonRich(object):
            """Dummy class with spoof cython rich comparison support."""
            def __richcmp__(self, other):
                """
                This method is never called. However it is introspected
                by the cftime.datetime.__richcmp__ method, which will then
                return NotImplemented, causing Python to call this classes
                __cmp__ method as a back-stop, and hence spoofing the
                cython specific rich comparison behaviour.
                """
                pass
            def __cmp__(self, other):
                raise NotImplementedError('__richcmp__')

        class Pass(object):
            """Dummy class with no rich comparison support whatsoever."""
            pass

        class Pass___cmp__(object):
            """Dummy class that delegates all comparisons."""
            def __cmp__(self, other):
                return NotImplemented

        # Test LHS operand comparison operator processing.
        for op, expected in [(operator.gt, '__lt__'), (operator.ge, '__le__'),
                             (operator.eq, '__eq__'), (operator.ne, '__ne__'),
                             (operator.lt, '__gt__'), (operator.le, '__ge__')]:
            with self.assertRaisesRegex(NotImplementedError, expected):
                op(self.date1_365_day, Rich())

            with self.assertRaisesRegex(NotImplementedError, '__richcmp__'):
                op(self.date1_365_day, CythonRich())

        # Test RHS operand comparison operator processing.
        for op in [operator.gt, operator.ge, operator.eq, operator.ne,
                   operator.lt, operator.le]:
            with self.assertRaisesRegex(TypeError, 'cannot compare'):
                op(Pass(), self.date1_365_day)

            with self.assertRaisesRegex(TypeError, 'cannot compare'):
                op(Pass___cmp__(), self.date1_365_day)


class issue17TestCase(unittest.TestCase):
    """Regression tests for issue #17/#669."""
    # issue 17 / 699: timezone formats not supported correctly
    # valid timezone formats are: +-hh, +-hh:mm, +-hhmm

    def setUp(self):
        pass

    def test_parse_date_tz(self):
        "Test timezone parsing in _parse_date"

        # these should succeed and are ISO8601 compliant
        expected_parsed_date = (2017, 5, 1, 0, 0, 0, 0, 60.0)
        for datestr in ("2017-05-01 00:00+01:00", "2017-05-01 00:00+0100", "2017-05-01 00:00+01"):
            d = _parse_date(datestr)
            assert_equal(d, expected_parsed_date)
        # some more tests with non-zero minutes, should all be ISO compliant and work
        expected_parsed_date = (2017, 5, 1, 0, 0, 0, 0, 85.0)
        for datestr in ("2017-05-01 00:00+01:25", "2017-05-01 00:00+0125"):
            d = _parse_date(datestr)
            assert_equal(d, expected_parsed_date)
        # these are NOT ISO8601 compliant and should not even be parseable but will be parsed with timezone anyway
        # because, due to support of other legacy time formats, they are difficult to reject
        # ATTENTION: only the hours part of this will be parsed, single-digit minutes will be ignored!
        expected_parsed_date = (2017, 5, 1, 0, 0, 0, 0, 60.0)
        for datestr in ("2017-05-01 00:00+01:0", "2017-05-01 00:00+01:", "2017-05-01 00:00+01:5"):
            d = _parse_date(datestr)
            assert_equal(d, expected_parsed_date)
        # these should not even be parseable as datestrings but are parseable anyway with ignored timezone
        # this is because the module also supports some legacy, non-standard time strings
        expected_parsed_date = (2017, 5, 1, 0, 0, 0, 0, 0.0)
        for datestr in ("2017-05-01 00:00+1",):
            d = _parse_date(datestr)
            assert_equal(d, expected_parsed_date)


class issue57TestCase(unittest.TestCase):
    """Regression tests for issue #57."""
    # issue 57: cftime._cftime._dateparse returns quite opaque error messages that make it difficult to
    # track down the source of problem
    def setUp(self):
        pass

    def test_parse_incorrect_unitstring(self):
        for datestr in ("days since2017-05-01 ", "dayssince 2017-05-01 00:00", "days snce 2017-05-01 00:00", "days_since_2017-05-01 00:00",
            "days_since_2017-05-01_00:00"):
            self.assertRaises(
                ValueError, cftime._cftime._dateparse, datestr, 'standard')

            self.assertRaises(
                ValueError, cftime._cftime.num2date, 1, datestr, 'standard')

            self.assertRaises(
                ValueError, cftime._cftime.date2num, datetime(1900, 1, 1, 0), datestr, 'standard')


_DATE_TYPES = [DatetimeNoLeap, DatetimeAllLeap, DatetimeJulian, Datetime360Day,
               DatetimeGregorian, DatetimeProlepticGregorian]


@pytest.fixture(params=_DATE_TYPES)
def date_type(request):
    return request.param


@pytest.fixture(params=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
def month(request):
    return request.param


@pytest.fixture
def days_per_month_non_leap_year(date_type, month):
    if date_type is Datetime360Day:
        return [-1, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30][month]
    if date_type is DatetimeAllLeap:
        return [-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]
    else:
        return [-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]


@pytest.fixture
def days_per_month_leap_year(date_type, month):
    if date_type is Datetime360Day:
        return [-1, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30][month]
    if date_type in [DatetimeGregorian, DatetimeProlepticGregorian,
                     DatetimeJulian, DatetimeAllLeap]:
        return [-1, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]
    else:
        return [-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]


def test_zero_year(date_type):
    # 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):
    with pytest.raises(ValueError):
        date_type(1, 0, 1)

    with pytest.raises(ValueError):
        date_type(1, 13, 1)


def test_invalid_day_non_leap_year(
        date_type, month, days_per_month_non_leap_year):
    with pytest.raises(ValueError):
        date_type(1, month, days_per_month_non_leap_year + 1)


def test_invalid_day_leap_year(date_type, month, days_per_month_leap_year):
    with pytest.raises(ValueError):
        date_type(2000, month, days_per_month_leap_year + 1)


def test_invalid_day_lower_bound(date_type, month):
    with pytest.raises(ValueError):
        date_type(1, month, 0)


def test_valid_day_non_leap_year(
        date_type, month, days_per_month_non_leap_year):
    date_type(1, month, 1)
    date_type(1, month, days_per_month_non_leap_year)


def test_valid_day_leap_year(
        date_type, month, days_per_month_leap_year):
    date_type(2000, month, 1)
    date_type(2000, month, days_per_month_leap_year)


_INVALID_SUB_DAY_TESTS = {
    'lower-bound-hour': (1, 1, 1, -1),
    'upper-bound-hour': (1, 1, 1, 24),
    'lower-bound-minute': (1, 1, 1, 1, -1),
    'upper-bound-minute': (1, 1, 1, 1, 60),
    'lower-bound-second': (1, 1, 1, 1, 1, -1),
    'upper-bound-second': (1, 1, 1, 1, 1, 60),
    'lower-bound-microsecond': (1, 1, 1, 1, 1, 1, -1),
    'upper-bound-microsecond': (1, 1, 1, 1, 1, 1, 1000000)
}


@pytest.mark.parametrize('date_args', list(_INVALID_SUB_DAY_TESTS.values()),
                         ids=list(_INVALID_SUB_DAY_TESTS.keys()))
def test_invalid_sub_day_reso_dates(date_type, date_args):
    with pytest.raises(ValueError):
        date_type(*date_args)


_VALID_SUB_DAY_TESTS = {
    'lower-bound-hour': (1, 1, 1, 0),
    'upper-bound-hour': (1, 1, 1, 23),
    'lower-bound-minute': (1, 1, 1, 1, 0),
    'upper-bound-minute': (1, 1, 1, 1, 59),
    'lower-bound-second': (1, 1, 1, 1, 1, 0),
    'upper-bound-second': (1, 1, 1, 1, 1, 59),
    'lower-bound-microsecond': (1, 1, 1, 1, 1, 1, 0),
    'upper-bound-microsecond': (1, 1, 1, 1, 1, 1, 999999)
}


@pytest.mark.parametrize('date_args', list(_VALID_SUB_DAY_TESTS.values()),
                         ids=list(_VALID_SUB_DAY_TESTS.keys()))
def test_valid_sub_day_reso_dates(date_type, date_args):
    date_type(*date_args)


@pytest.mark.parametrize(
    'date_args',
    [(1582, 10, 5), (1582, 10, 14)], ids=['lower-bound', 'upper-bound'])
def test_invalid_julian_gregorian_mixed_dates(date_type, date_args):
    if date_type is DatetimeGregorian:
        with pytest.raises(ValueError):
            date_type(*date_args)
    else:
        date_type(*date_args)


@pytest.mark.parametrize(
    'date_args',
    [(1582, 10, 4), (1582, 10, 15)], ids=['lower-bound', 'upper-bound'])
def test_valid_julian_gregorian_mixed_dates(date_type, date_args):
    date_type(*date_args)


@pytest.mark.parametrize(
    'date_args',
    [(1, 2, 3, 4, 5, 6), (10, 2, 3, 4, 5, 6), (100, 2, 3, 4, 5, 6),
     (1000, 2, 3, 4, 5, 6),
     (2000, 1, 1, 12, 34, 56, 123456)],
    ids=['1', '10', '100', '1000', '2000'])
def test_str_matches_datetime_str(date_type, date_args):
    assert str(date_type(*date_args)) == str(datetime(*date_args))


_EXPECTED_DATE_TYPES = {'noleap': DatetimeNoLeap,
                        '365_day': DatetimeNoLeap,
                        '360_day': Datetime360Day,
                        'julian': DatetimeJulian,
                        'all_leap': DatetimeAllLeap,
                        '366_day': DatetimeAllLeap,
                        'gregorian': DatetimeGregorian,
                        'proleptic_gregorian': DatetimeProlepticGregorian,
                        'standard': DatetimeGregorian}


@pytest.mark.parametrize(
    ['calendar', 'expected_date_type'],
    list(_EXPECTED_DATE_TYPES.items())
)
def test_num2date_only_use_cftime_datetimes_negative_years(
        calendar, expected_date_type):
    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))


@pytest.mark.parametrize(
    ['calendar', 'expected_date_type'],
    list(_EXPECTED_DATE_TYPES.items())
)
def test_num2date_only_use_cftime_datetimes_pre_gregorian(
        calendar, expected_date_type):
    result = num2date(1., units='days since 0001-01-01', calendar=calendar,
                      only_use_cftime_datetimes=True)
    assert isinstance(result, datetimex)
    assert (result.calendar == adjust_calendar(calendar))


@pytest.mark.parametrize(
    ['calendar', 'expected_date_type'],
    list(_EXPECTED_DATE_TYPES.items())
)
def test_num2date_only_use_cftime_datetimes_post_gregorian(
        calendar, expected_date_type):
    result = num2date(0., units='days since 1582-10-15', calendar=calendar,
                      only_use_cftime_datetimes=True)
    assert isinstance(result, datetimex)
    assert (result.calendar == adjust_calendar(calendar))


def test_repr():
    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='', 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)
    assert date.dayofyr == 1
    assert date.replace(day=2).dayofyr == 2


def test_dayofwk_after_replace(date_type):
    date = date_type(1, 1, 1)
    original_dayofwk = date.dayofwk
    expected = (original_dayofwk + 1) % 7
    result = date.replace(day=2).dayofwk
    assert result == expected


def test_daysinmonth_non_leap(date_type, month, days_per_month_non_leap_year):
    date = date_type(1, month, 1)
    assert date.daysinmonth == days_per_month_non_leap_year


def test_daysinmonth_leap(date_type, month, days_per_month_leap_year):
    date = date_type(2000, month, 1)
    assert date.daysinmonth == days_per_month_leap_year


@pytest.mark.parametrize('argument', ['dayofyr', 'dayofwk'])
def test_replace_dayofyr_or_dayofwk_error(date_type, argument):
    with pytest.raises(ValueError):
        date_type(1, 1, 1).replace(**{argument: 3})


def test_dayofyr_after_timedelta_addition(date_type):
    initial_date = date_type(1, 1, 2)
    date_after_timedelta_addition = initial_date + timedelta(days=1)
    assert initial_date.dayofyr == 2
    assert date_after_timedelta_addition.dayofyr == 3


def test_exact_datetime_difference(date_type):
    b = date_type(2000, 1, 2, 0, 0, 0, 5)
    a = date_type(2000, 1, 2)
    result = b - a
    expected = timedelta(microseconds=5)
    assert result == expected


_SHAPES = [(4, ), (2, 2)]
_MICROSECOND_UNITS = ["microseconds", "microsecond", "microsec", "microsecs"]
_MILLISECOND_UNITS = ["milliseconds", "millisecond", "millisec", "millisecs", "ms"]
_SECOND_UNITS = ["seconds", "second", "sec", "secs", "s"]
_MINUTE_UNITS = ["minutes", "minute", "min", "mins"]
_HOUR_UNITS = ["hours", "hour", "hr", "hrs", "h"]
_DAY_UNITS = ["day", "days", "d"]
_MONTH_UNITS = ["month", "months"]
_DTYPES = [np.dtype("int64"), np.dtype("float64")]
_STANDARD_CALENDARS = [
    "standard",
    "gregorian",
    "proleptic_gregorian"
]
_REAL_WORLD_CALENDARS = [
    "julian",
    "standard",
    "gregorian",
    "proleptic_gregorian"
]
_ARTIFICIAL_CALENDARS = ["noleap", "all_leap", "360_day"]


@pytest.fixture(params=_SHAPES)
def shape(request):
    return request.param


@pytest.fixture(params=_DTYPES)
def dtype(request):
    return request.param


@pytest.fixture(params=list(_EXPECTED_DATE_TYPES.keys()))
def calendar(request):
    return request.param


@pytest.mark.parametrize("unit", _MICROSECOND_UNITS)
def test_num2date_microsecond_units(calendar, unit, shape, dtype):
    date_type = _EXPECTED_DATE_TYPES[calendar]
    expected = np.array([date_type(2000, 1, 1, 0, 0, 0, 1),
                         date_type(2000, 1, 1, 0, 0, 0, 2),
                         date_type(2000, 1, 1, 0, 0, 0, 3),
                         date_type(2000, 1, 1, 0, 0, 0, 4)]).reshape(shape)
    numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype)
    units = "{} since 2000-01-01".format(unit)
    result = num2date(numeric_times, units=units, calendar=calendar)
    np.testing.assert_equal(result, expected)


@pytest.mark.parametrize("unit", _MILLISECOND_UNITS)
def test_num2date_millisecond_units(calendar, unit, shape, dtype):
    date_type = _EXPECTED_DATE_TYPES[calendar]
    expected = np.array([date_type(2000, 1, 1, 0, 0, 0, 1000),
                         date_type(2000, 1, 1, 0, 0, 0, 2000),
                         date_type(2000, 1, 1, 0, 0, 0, 3000),
                         date_type(2000, 1, 1, 0, 0, 0, 4000)]).reshape(shape)
    numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype)
    units = "{} since 2000-01-01".format(unit)
    result = num2date(numeric_times, units=units, calendar=calendar)
    np.testing.assert_equal(result, expected)


@pytest.mark.parametrize("unit", _SECOND_UNITS)
def test_num2date_second_units(calendar, unit, shape, dtype):
    date_type = _EXPECTED_DATE_TYPES[calendar]
    expected = np.array([date_type(2000, 1, 1, 0, 0, 1, 0),
                         date_type(2000, 1, 1, 0, 0, 2, 0),
                         date_type(2000, 1, 1, 0, 0, 3, 0),
                         date_type(2000, 1, 1, 0, 0, 4, 0)]).reshape(shape)
    numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype)
    units = "{} since 2000-01-01".format(unit)
    result = num2date(numeric_times, units=units, calendar=calendar)
    np.testing.assert_equal(result, expected)


@pytest.mark.parametrize("unit", _MINUTE_UNITS)
def test_num2date_minute_units(calendar, unit, shape, dtype):
    date_type = _EXPECTED_DATE_TYPES[calendar]
    expected = np.array([date_type(2000, 1, 1, 0, 1, 0, 0),
                         date_type(2000, 1, 1, 0, 2, 0, 0),
                         date_type(2000, 1, 1, 0, 3, 0, 0),
                         date_type(2000, 1, 1, 0, 4, 0, 0)]).reshape(shape)
    numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype)
    units = "{} since 2000-01-01".format(unit)
    result = num2date(numeric_times, units=units, calendar=calendar)
    np.testing.assert_equal(result, expected)


@pytest.mark.parametrize("unit", _HOUR_UNITS)
def test_num2date_hour_units(calendar, unit, shape, dtype):
    date_type = _EXPECTED_DATE_TYPES[calendar]
    expected = np.array([date_type(2000, 1, 1, 1, 0, 0, 0),
                         date_type(2000, 1, 1, 2, 0, 0, 0),
                         date_type(2000, 1, 1, 3, 0, 0, 0),
                         date_type(2000, 1, 1, 4, 0, 0, 0)]).reshape(shape)
    numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype)
    units = "{} since 2000-01-01".format(unit)
    result = num2date(numeric_times, units=units, calendar=calendar)
    np.testing.assert_equal(result, expected)


@pytest.mark.parametrize("unit", _DAY_UNITS)
def test_num2date_day_units(calendar, unit, shape, dtype):
    date_type = _EXPECTED_DATE_TYPES[calendar]
    expected = np.array([date_type(2000, 1, 2, 0, 0, 0, 0),
                         date_type(2000, 1, 3, 0, 0, 0, 0),
                         date_type(2000, 1, 4, 0, 0, 0, 0),
                         date_type(2000, 1, 5, 0, 0, 0, 0)]).reshape(shape)
    numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype)
    units = "{} since 2000-01-01".format(unit)
    result = num2date(numeric_times, units=units, calendar=calendar)
    np.testing.assert_equal(result, expected)


@pytest.mark.parametrize("unit", _MONTH_UNITS)
def test_num2date_month_units(calendar, unit, shape, dtype):
    date_type = _EXPECTED_DATE_TYPES[calendar]
    expected = np.array([date_type(2000, 2, 1, 0, 0, 0, 0),
                         date_type(2000, 3, 1, 0, 0, 0, 0),
                         date_type(2000, 4, 1, 0, 0, 0, 0),
                         date_type(2000, 5, 1, 0, 0, 0, 0)]).reshape(shape)
    numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype)
    units = "{} since 2000-01-01".format(unit)

    if calendar != "360_day":
        with pytest.raises(ValueError):
            num2date(numeric_times, units=units, calendar=calendar)
    else:
        result = num2date(numeric_times, units=units, calendar=calendar)
        np.testing.assert_equal(result, expected)


def test_num2date_only_use_python_datetimes(calendar, shape, dtype):
    date_type = real_datetime
    expected = np.array([date_type(2000, 1, 2, 0, 0, 0, 0),
                         date_type(2000, 1, 3, 0, 0, 0, 0),
                         date_type(2000, 1, 4, 0, 0, 0, 0),
                         date_type(2000, 1, 5, 0, 0, 0, 0)]).reshape(shape)
    numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype)
    units = "days since 2000-01-01"
    if calendar not in _STANDARD_CALENDARS:
        with pytest.raises(ValueError):
            num2date(numeric_times, units=units, calendar=calendar,
                           only_use_python_datetimes=True,
                           only_use_cftime_datetimes=False)
    else:
        result = num2date(numeric_times, units=units, calendar=calendar,
                                only_use_python_datetimes=True,
                                only_use_cftime_datetimes=False)
        np.testing.assert_equal(result, expected)


def test_num2date_use_pydatetime_if_possible(calendar, shape, dtype):
    if calendar not in _STANDARD_CALENDARS:
        date_type = _EXPECTED_DATE_TYPES[calendar]
    else:
        date_type = real_datetime

    expected = np.array([date_type(2000, 1, 2, 0, 0, 0, 0),
                         date_type(2000, 1, 3, 0, 0, 0, 0),
                         date_type(2000, 1, 4, 0, 0, 0, 0),
                         date_type(2000, 1, 5, 0, 0, 0, 0)]).reshape(shape)
    numeric_times = np.array([1, 2, 3, 4]).reshape(shape).astype(dtype)
    units = "days since 2000-01-01"
    result = num2date(numeric_times, units=units, calendar=calendar,
                            only_use_python_datetimes=False,
                            only_use_cftime_datetimes=False)
    np.testing.assert_equal(result, expected)


@pytest.mark.parametrize(
    ["standard_calendar", "breakpoint"],
    [("proleptic_gregorian", "{}-12-31T23:59:59.999999".format(MINYEAR - 1)),
     ("gregorian", "1582-10-15"),
     ("standard", "1582-10-15")]
)
def test_num2date_only_use_python_datetimes_invalid_basedate(
    standard_calendar,
    breakpoint
):
    units = "days since {}".format(breakpoint)
    numeric_times = np.array([1, 2, 3, 4])
    with pytest.raises(ValueError):
        num2date(
            numeric_times,
            units=units,
            calendar=standard_calendar,
            only_use_python_datetimes=True,
            only_use_cftime_datetimes=False
        )


@pytest.mark.parametrize("real_world_calendar", _REAL_WORLD_CALENDARS)
def test_num2date_invalid_zero_reference_year(real_world_calendar):
    units = "days since 0000-01-01"
    numeric_times = np.array([1, 2, 3, 4])
    with pytest.raises(ValueError, match="zero not allowed as a reference"):
        num2date(
            numeric_times,
            units=units,
            calendar=real_world_calendar
        )


@pytest.mark.parametrize("artificial_calendar", _ARTIFICIAL_CALENDARS)
def test_num2date_valid_zero_reference_year(artificial_calendar):
    units = "days since 0000-01-01"
    numeric_times = np.array([1, 2, 3, 4])
    num2date(numeric_times, units=units, calendar=artificial_calendar)


def test_num2date_masked_array(calendar):
    date_type = _EXPECTED_DATE_TYPES[calendar]
    expected = np.array([date_type(2000, 1, 1, 1, 0, 0, 0),
                         date_type(2000, 1, 1, 2, 0, 0, 0),
                         date_type(2000, 1, 1, 3, 0, 0, 0),
                         date_type(2000, 1, 1, 4, 0, 0, 0)])
    mask = [False, False, True, False]
    expected = np.ma.masked_array(expected, mask=mask)
    numeric_times = np.ma.masked_array([1, 2, 3, 4], mask=mask)
    units = "hours since 2000-01-01"
    result = num2date(numeric_times, units=units, calendar=calendar)
    np.testing.assert_equal(result, expected)


def test_num2date_out_of_range():
    numeric_times = 12 * np.array([200000, 400000, 600000])
    units = "months since 2000-01-01"
    with pytest.raises(OverflowError, match="time values outside range of 64 bit signed integers"):
        num2date(numeric_times, units=units, calendar="360_day")


def test_num2date_list_input(calendar):
    date_type = _EXPECTED_DATE_TYPES[calendar]
    expected = np.array([date_type(2000, 1, 1, 1, 0, 0, 0),
                         date_type(2000, 1, 1, 2, 0, 0, 0),
                         date_type(2000, 1, 1, 3, 0, 0, 0),
                         date_type(2000, 1, 1, 4, 0, 0, 0)])
    numeric_times = [1, 2, 3, 4]
    units = "hours since 2000-01-01"
    result = num2date(numeric_times, units=units, calendar=calendar)
    np.testing.assert_equal(result, expected)


def test_num2date_integer_upcast_required():
    numeric_times = np.array([30, 60, 90, 120], dtype=np.int32)
    units = "minutes since 2000-01-01"
    expected = np.array([
        Datetime360Day(2000, 1, 1, 0, 30, 0),
        Datetime360Day(2000, 1, 1, 1, 0, 0),
        Datetime360Day(2000, 1, 1, 1, 30, 0),
        Datetime360Day(2000, 1, 1, 2, 0, 0)
    ])
    result = num2date(numeric_times, units=units, calendar="360_day")
    np.testing.assert_equal(result, expected)


@pytest.mark.parametrize(
    "encoding_units", 
    ["microseconds", "milliseconds", "seconds", "minutes", "hours", "days"]
)
@pytest.mark.parametrize(
    "freq",
    [
        timedelta(microseconds=1),
        timedelta(microseconds=1000), 
        timedelta(seconds=1), 
        timedelta(minutes=1), 
        timedelta(hours=1), 
        timedelta(days=1)
    ], 
    ids=lambda x: f"{x!r}"
)
def test_date2num_num2date_roundtrip(encoding_units, freq, calendar):
    date_type = _EXPECTED_DATE_TYPES[calendar]
    lengthy_timedelta = timedelta(days=291000 * 360)
    times = np.array(
        [
            date_type(1, 1, 1), 
            date_type(1, 1, 1) + lengthy_timedelta, 
            date_type(1, 1, 1) + lengthy_timedelta + freq
        ]
    )
    units = f"{encoding_units} since 0001-01-01"
    encoded = date2num(times, units=units, calendar=calendar)
    decoded = num2date(encoded, units=units, calendar=calendar)
    encoding_units_as_timedelta = timedelta(microseconds=UNIT_CONVERSION_FACTORS[encoding_units])

    if freq >= encoding_units_as_timedelta:
        assert encoded.dtype == np.int64
        np.testing.assert_equal(decoded, times)
    else:
        assert encoded.dtype == np.float64
        tolerance = timedelta(microseconds=2000)
        meets_tolerance = np.abs(decoded - times) <= tolerance
        assert np.all(meets_tolerance)


if __name__ == '__main__':
    unittest.main()
