##############################################################################
#
# Copyright (c) 2003 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################

import math
import os
import pickle
import platform
import sys
import time
import unittest
from datetime import date
from datetime import datetime
from datetime import timedelta
from datetime import tzinfo

import pytz

from DateTime import DateTime
from DateTime.DateTime import _findLocalTimeZoneName


try:
    __file__
except NameError:   # pragma: no cover
    f = sys.argv[0]
else:
    f = __file__

IS_PYPY = getattr(platform, 'python_implementation', lambda: None)() == 'PyPy'

DATADIR = os.path.dirname(os.path.abspath(f))
del f

ZERO = timedelta(0)


class FixedOffset(tzinfo):
    """Fixed offset in minutes east from UTC."""

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

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

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

    def dst(self, dt):
        return ZERO


class DateTimeTests(unittest.TestCase):

    def _compare(self, dt1, dt2):
        '''Compares the internal representation of dt1 with
        the representation in dt2.  Allows sub-millisecond variations.
        Primarily for testing.'''
        self.assertEqual(round(dt1._t, 3), round(dt2._t, 3))
        self.assertEqual(round(dt1._d, 9), round(dt2._d, 9))
        self.assertEqual(round(dt1.time, 9), round(dt2.time, 9))
        self.assertEqual(dt1.millis(), dt2.millis())
        self.assertEqual(dt1._micros, dt2._micros)

    def testBug1203(self):
        # 01:59:60 occurred in old DateTime
        dt = DateTime(7200, 'GMT')
        self.assertTrue(str(dt).find('60') < 0, dt)

    def testDSTInEffect(self):
        # Checks GMT offset for a DST date in the US/Eastern time zone
        dt = DateTime(2000, 5, 9, 15, 0, 0, 'US/Eastern')
        self.assertEqual(dt.toZone('GMT').hour(), 19,
                         (dt, dt.toZone('GMT')))

    def testDSTNotInEffect(self):
        # Checks GMT offset for a non-DST date in the US/Eastern time zone
        dt = DateTime(2000, 11, 9, 15, 0, 0, 'US/Eastern')
        self.assertEqual(dt.toZone('GMT').hour(), 20,
                         (dt, dt.toZone('GMT')))

    def testAddPrecision(self):
        # Precision of serial additions
        dt = DateTime()
        self.assertEqual(str(dt + 0.10 + 3.14 + 6.76 - 10), str(dt),
                         dt)
        # checks problem reported in
        # https://github.com/zopefoundation/DateTime/issues/41
        dt = DateTime(2038, 10, 7, 8, 52, 44.959840, "UTC")
        self.assertEqual(str(dt + 0.10 + 3.14 + 6.76 - 10), str(dt),
                         dt)

    def testConsistentSecondMicroRounding(self):
        dt = DateTime(2038, 10, 7, 8, 52, 44.9598398, "UTC")
        self.assertEqual(int(dt.second() * 1000000),
                         dt.micros() % 60000000)

    def testConstructor3(self):
        # Constructor from date/time string
        dt = DateTime()
        dt1s = '%d/%d/%d %d:%d:%f %s' % (
            dt.year(),
            dt.month(),
            dt.day(),
            dt.hour(),
            dt.minute(),
            dt.second(),
            dt.timezone())
        dt1 = DateTime(dt1s)
        # Compare representations as it's the
        # only way to compare the dates to the same accuracy
        self.assertEqual(repr(dt), repr(dt1))

    def testConstructor4(self):
        # Constructor from time float
        dt = DateTime()
        dt1 = DateTime(float(dt))
        self._compare(dt, dt1)

    def testConstructor5(self):
        # Constructor from time float and timezone
        dt = DateTime()
        dt1 = DateTime(float(dt), dt.timezone())
        self.assertEqual(str(dt), str(dt1), (dt, dt1))
        dt1 = DateTime(float(dt), str(dt.timezone()))
        self.assertEqual(str(dt), str(dt1), (dt, dt1))

    def testConstructor6(self):
        # Constructor from year and julian date
        # This test must normalize the time zone, or it *will* break when
        # DST changes!
        dt1 = DateTime(2000, 5.500000578705)
        dt = DateTime('2000/1/5 12:00:00.050 pm %s' % dt1.localZone())
        self._compare(dt, dt1)

    def testConstructor7(self):
        # Constructor from parts
        dt = DateTime()
        dt1 = DateTime(
            dt.year(),
            dt.month(),
            dt.day(),
            dt.hour(),
            dt.minute(),
            dt.second(),
            dt.timezone())
        # Compare representations as it's the
        # only way to compare the dates to the same accuracy
        self.assertEqual(repr(dt), repr(dt1))

    def testDayOfWeek(self):
        # Compare to the datetime.date value to make it locale independent
        expected = date(2000, 6, 16).strftime('%A')
        # strftime() used to always be passed a day of week of 0
        dt = DateTime('2000/6/16')
        s = dt.strftime('%A')
        self.assertEqual(s, expected, (dt, s))

    def testOldDate(self):
        # Fails when an 1800 date is displayed with negative signs
        dt = DateTime('1830/5/6 12:31:46.213 pm')
        dt1 = dt.toZone('GMT+6')
        self.assertTrue(str(dt1).find('-') < 0, (dt, dt1))

    def testSubtraction(self):
        # Reconstruction of a DateTime from its parts, with subtraction
        # this also tests the accuracy of addition and reconstruction
        dt = DateTime()
        dt1 = dt - 3.141592653
        dt2 = DateTime(
            dt.year(),
            dt.month(),
            dt.day(),
            dt.hour(),
            dt.minute(),
            dt.second())
        dt3 = dt2 - 3.141592653
        self.assertEqual(dt1, dt3, (dt, dt1, dt2, dt3))

    def testTZ1add(self):
        # Time zone manipulation: add to a date
        dt = DateTime('1997/3/8 1:45am GMT-4')
        dt1 = DateTime('1997/3/9 1:45pm GMT+8')
        self.assertTrue((dt + 1.0).equalTo(dt1))

    def testTZ1sub(self):
        # Time zone manipulation: subtract from a date
        dt = DateTime('1997/3/8 1:45am GMT-4')
        dt1 = DateTime('1997/3/9 1:45pm GMT+8')
        self.assertTrue((dt1 - 1.0).equalTo(dt))

    def testTZ1diff(self):
        # Time zone manipulation: diff two dates
        dt = DateTime('1997/3/8 1:45am GMT-4')
        dt1 = DateTime('1997/3/9 1:45pm GMT+8')
        self.assertEqual(dt1 - dt, 1.0, (dt, dt1))

    def test_compare_methods(self):
        # Compare two dates using several methods
        dt = DateTime('1997/1/1')
        dt1 = DateTime('1997/2/2')
        self.assertTrue(dt1.greaterThan(dt))
        self.assertTrue(dt1.greaterThanEqualTo(dt))
        self.assertTrue(dt.lessThan(dt1))
        self.assertTrue(dt.lessThanEqualTo(dt1))
        self.assertTrue(dt.notEqualTo(dt1))
        self.assertFalse(dt.equalTo(dt1))
        # Compare a date to float
        dt = DateTime(1.0)
        self.assertTrue(dt == DateTime(1.0))  # testing __eq__
        self.assertFalse(dt != DateTime(1.0))  # testing __ne__
        self.assertFalse(dt.greaterThan(1.0))
        self.assertTrue(dt.greaterThanEqualTo(1.0))
        self.assertFalse(dt.lessThan(1.0))
        self.assertTrue(dt.lessThanEqualTo(1.0))
        self.assertFalse(dt.notEqualTo(1.0))
        self.assertTrue(dt.equalTo(1.0))
        # Compare a date to int
        dt = DateTime(1)
        self.assertEqual(dt, DateTime(1.0))
        self.assertTrue(dt == DateTime(1))  # testing __eq__
        self.assertFalse(dt != DateTime(1))  # testing __ne__
        self.assertFalse(dt.greaterThan(1))
        self.assertTrue(dt.greaterThanEqualTo(1))
        self.assertFalse(dt.lessThan(1))
        self.assertTrue(dt.lessThanEqualTo(1))
        self.assertFalse(dt.notEqualTo(1))
        self.assertTrue(dt.equalTo(1))
        # Compare a date to string; there is no implicit type conversion
        # but behavior if consistent as when comparing, for example, an int
        # and a string.
        dt = DateTime("2023")
        self.assertFalse(dt == "2023")  # testing __eq__
        self.assertTrue(dt != "2023")  # testing __ne__
        self.assertRaises(TypeError, dt.greaterThan, "2023")
        self.assertRaises(TypeError, dt.greaterThanEqualTo, "2023")
        self.assertRaises(TypeError, dt.lessThan, "2023")
        self.assertRaises(TypeError, dt.lessThanEqualTo, "2023")
        self.assertTrue(dt.notEqualTo("2023"))
        self.assertFalse(dt.equalTo("2023"))

    def test_compare_methods_none(self):
        # Compare a date to None
        for dt in (DateTime('1997/1/1'), DateTime(0)):
            self.assertTrue(dt.greaterThan(None))
            self.assertTrue(dt.greaterThanEqualTo(None))
            self.assertFalse(dt.lessThan(None))
            self.assertFalse(dt.lessThanEqualTo(None))
            self.assertTrue(dt.notEqualTo(None))
            self.assertFalse(dt.equalTo(None))

    def test_pickle(self):
        dt = DateTime()
        data = pickle.dumps(dt, 1)
        new = pickle.loads(data)
        for key in DateTime.__slots__:
            self.assertEqual(getattr(dt, key), getattr(new, key))

    def test_pickle_with_tz(self):
        dt = DateTime('2002/5/2 8:00am GMT+8')
        data = pickle.dumps(dt, 1)
        new = pickle.loads(data)
        for key in DateTime.__slots__:
            self.assertEqual(getattr(dt, key), getattr(new, key))

    def test_pickle_asdatetime_with_tz(self):
        dt = DateTime('2002/5/2 8:00am GMT+8')
        data = pickle.dumps(dt.asdatetime(), 1)
        new = DateTime(pickle.loads(data))
        for key in DateTime.__slots__:
            self.assertEqual(getattr(dt, key), getattr(new, key))

    def test_pickle_with_numerical_tz(self):
        for dt_str in ('2007/01/02 12:34:56.789 +0300',
                       '2007/01/02 12:34:56.789 +0430',
                       '2007/01/02 12:34:56.789 -1234'):
            dt = DateTime(dt_str)
            data = pickle.dumps(dt, 1)
            new = pickle.loads(data)
            for key in DateTime.__slots__:
                self.assertEqual(getattr(dt, key), getattr(new, key))

    def test_pickle_with_micros(self):
        dt = DateTime('2002/5/2 8:00:14.123 GMT+8')
        data = pickle.dumps(dt, 1)
        new = pickle.loads(data)
        for key in DateTime.__slots__:
            self.assertEqual(getattr(dt, key), getattr(new, key))

    def test_pickle_old(self):
        dt = DateTime('2002/5/2 8:00am GMT+0')
        data = (
            '(cDateTime.DateTime\nDateTime\nq\x01Noq\x02}q\x03(U\x05'
            '_amonq\x04U\x03Mayq\x05U\x05_adayq\x06U\x03Thuq\x07U\x05_pmonq'
            '\x08h\x05U\x05_hourq\tK\x08U\x05_fmonq\nh\x05U\x05_pdayq\x0bU'
            '\x04Thu.q\x0cU\x05_fdayq\rU\x08Thursdayq\x0eU\x03_pmq\x0fU\x02amq'
            '\x10U\x02_tq\x11GA\xcehy\x00\x00\x00\x00U\x07_minuteq\x12K\x00U'
            '\x07_microsq\x13L1020326400000000L\nU\x02_dq\x14G@\xe2\x12j\xaa'
            '\xaa\xaa\xabU\x07_secondq\x15G\x00\x00\x00\x00\x00\x00\x00\x00U'
            '\x03_tzq\x16U\x05GMT+0q\x17U\x06_monthq\x18K\x05U'
            '\x0f_timezone_naiveq\x19I00\nU\x04_dayq\x1aK\x02U\x05_yearq'
            '\x1bM\xd2\x07U\x08_nearsecq\x1cG\x00\x00\x00\x00\x00\x00\x00'
            '\x00U\x07_pmhourq\x1dK\x08U\n_dayoffsetq\x1eK\x04U\x04timeq'
            '\x1fG?\xd5UUUV\x00\x00ub.')
        data = data.encode('latin-1')
        new = pickle.loads(data)
        for key in DateTime.__slots__:
            self.assertEqual(getattr(dt, key), getattr(new, key))

    def test_pickle_old_without_micros(self):
        dt = DateTime('2002/5/2 8:00am GMT+0')
        data = (
            '(cDateTime.DateTime\nDateTime\nq\x01Noq\x02}q\x03(U\x05'
            '_amonq\x04U\x03Mayq\x05U\x05_adayq\x06U\x03Thuq\x07U\x05_pmonq'
            '\x08h\x05U\x05_hourq\tK\x08U\x05_fmonq\nh\x05U\x05_pdayq\x0bU'
            '\x04Thu.q\x0cU\x05_fdayq\rU\x08Thursdayq\x0eU\x03_pmq\x0fU'
            '\x02amq\x10U\x02_tq\x11GA\xcehy\x00\x00\x00\x00U\x07_minuteq'
            '\x12K\x00U\x02_dq\x13G@\xe2\x12j\xaa\xaa\xaa\xabU\x07_secondq'
            '\x14G\x00\x00\x00\x00\x00\x00\x00\x00U\x03_tzq\x15U\x05GMT+0q'
            '\x16U\x06_monthq\x17K\x05U\x0f_timezone_naiveq\x18I00\nU'
            '\x04_dayq\x19K\x02U\x05_yearq\x1aM\xd2\x07U\x08_nearsecq'
            '\x1bG\x00\x00\x00\x00\x00\x00\x00\x00U\x07_pmhourq\x1cK\x08U'
            '\n_dayoffsetq\x1dK\x04U\x04timeq\x1eG?\xd5UUUV\x00\x00ub.')
        data = data.encode('latin-1')
        new = pickle.loads(data)
        for key in DateTime.__slots__:
            self.assertEqual(getattr(dt, key), getattr(new, key))

    def test_pickle_dates_after_2038(self):
        dt = DateTime('2039/09/02 07:07:6.235027 GMT+1')
        data = pickle.dumps(dt, 1)
        new = pickle.loads(data)
        for key in DateTime.__slots__:
            self.assertEqual(getattr(dt, key), getattr(new, key))

    def test_pickle_old_with_micros_as_float(self):
        dt = DateTime('2002/5/2 8:00am GMT+0')
        data = (
            'ccopy_reg\n_reconstructor\nq\x00(cDateTime.DateTime\nDateTime'
            '\nq\x01c__builtin__\nobject\nq\x02Ntq\x03Rq\x04(GA\xcehy\x00\x00'
            '\x00\x00I00\nX\x05\x00\x00\x00GMT+0q\x05tq\x06b.')
        data = data.encode('latin-1')
        new = pickle.loads(data)
        for key in DateTime.__slots__:
            self.assertEqual(getattr(dt, key), getattr(new, key))

    def testTZ2(self):
        # Time zone manipulation test 2
        dt = DateTime()
        dt1 = dt.toZone('GMT')
        s = dt.second()
        s1 = dt1.second()
        self.assertEqual(s, s1, (dt, dt1, s, s1))

    def testTZDiffDaylight(self):
        # Diff dates across daylight savings dates
        dt = DateTime('2000/6/8 1:45am US/Eastern')
        dt1 = DateTime('2000/12/8 12:45am US/Eastern')
        self.assertEqual(dt1 - dt, 183, (dt, dt1, dt1 - dt))

    def testY10KDate(self):
        # Comparison of a Y10K date and a Y2K date
        dt = DateTime('10213/09/21')
        dt1 = DateTime(2000, 1, 1)

        dsec = (dt.millis() - dt1.millis()) / 1000.0
        ddays = math.floor((dsec / 86400.0) + 0.5)

        self.assertEqual(ddays, 3000000, ddays)

    def test_tzoffset(self):
        # Test time-zone given as an offset

        # GMT
        dt = DateTime('Tue, 10 Sep 2001 09:41:03 GMT')
        self.assertEqual(dt.tzoffset(), 0)

        # Timezone by name, a timezone that hasn't got daylightsaving.
        dt = DateTime('Tue, 2 Mar 2001 09:41:03 GMT+3')
        self.assertEqual(dt.tzoffset(), 10800)

        # Timezone by name, has daylightsaving but is not in effect.
        dt = DateTime('Tue, 21 Jan 2001 09:41:03 PST')
        self.assertEqual(dt.tzoffset(), -28800)

        # Timezone by name, with daylightsaving in effect
        dt = DateTime('Tue, 24 Aug 2001 09:41:03 PST')
        self.assertEqual(dt.tzoffset(), -25200)

        # A negative numerical timezone
        dt = DateTime('Tue, 24 Jul 2001 09:41:03 -0400')
        self.assertEqual(dt.tzoffset(), -14400)

        # A positive numerical timzone
        dt = DateTime('Tue, 6 Dec 1966 01:41:03 +0200')
        self.assertEqual(dt.tzoffset(), 7200)

        # A negative numerical timezone with minutes.
        dt = DateTime('Tue, 24 Jul 2001 09:41:03 -0637')
        self.assertEqual(dt.tzoffset(), -23820)

        # A positive numerical timezone with minutes.
        dt = DateTime('Tue, 24 Jul 2001 09:41:03 +0425')
        self.assertEqual(dt.tzoffset(), 15900)

    def testISO8601(self):
        # ISO8601 reference dates
        ref0 = DateTime('2002/5/2 8:00am GMT')
        ref1 = DateTime('2002/5/2 8:00am US/Eastern')
        ref2 = DateTime('2006/11/6 10:30 GMT')
        ref3 = DateTime('2004/06/14 14:30:15 GMT-3')
        ref4 = DateTime('2006/01/01 GMT')

        # Basic tests
        # Though this is timezone naive and according to specification should
        # be interpreted in the local timezone, to preserve backwards
        # compatibility with previously expected behaviour.
        isoDt = DateTime('2002-05-02T08:00:00')
        self.assertTrue(ref0.equalTo(isoDt))
        isoDt = DateTime('2002-05-02T08:00:00Z')
        self.assertTrue(ref0.equalTo(isoDt))
        isoDt = DateTime('2002-05-02T08:00:00+00:00')
        self.assertTrue(ref0.equalTo(isoDt))
        isoDt = DateTime('2002-05-02T08:00:00-04:00')
        self.assertTrue(ref1.equalTo(isoDt))
        isoDt = DateTime('2002-05-02 08:00:00-04:00')
        self.assertTrue(ref1.equalTo(isoDt))

        # Bug 1386: the colon in the timezone offset is optional
        isoDt = DateTime('2002-05-02T08:00:00-0400')
        self.assertTrue(ref1.equalTo(isoDt))

        # Bug 2191: date reduced formats
        isoDt = DateTime('2006-01-01')
        self.assertTrue(ref4.equalTo(isoDt))
        isoDt = DateTime('200601-01')
        self.assertTrue(ref4.equalTo(isoDt))
        isoDt = DateTime('20060101')
        self.assertTrue(ref4.equalTo(isoDt))
        isoDt = DateTime('2006-01')
        self.assertTrue(ref4.equalTo(isoDt))
        isoDt = DateTime('200601')
        self.assertTrue(ref4.equalTo(isoDt))
        isoDt = DateTime('2006')
        self.assertTrue(ref4.equalTo(isoDt))

        # Bug 2191: date/time separators are also optional
        isoDt = DateTime('20020502T08:00:00')
        self.assertTrue(ref0.equalTo(isoDt))
        isoDt = DateTime('2002-05-02T080000')
        self.assertTrue(ref0.equalTo(isoDt))
        isoDt = DateTime('20020502T080000')
        self.assertTrue(ref0.equalTo(isoDt))

        # Bug 2191: timezones with only one digit for hour
        isoDt = DateTime('20020502T080000+0')
        self.assertTrue(ref0.equalTo(isoDt))
        isoDt = DateTime('20020502 080000-4')
        self.assertTrue(ref1.equalTo(isoDt))
        isoDt = DateTime('20020502T080000-400')
        self.assertTrue(ref1.equalTo(isoDt))
        isoDt = DateTime('20020502T080000-4:00')
        self.assertTrue(ref1.equalTo(isoDt))

        # Bug 2191: optional seconds/minutes
        isoDt = DateTime('2002-05-02T0800')
        self.assertTrue(ref0.equalTo(isoDt))
        isoDt = DateTime('2002-05-02T08')
        self.assertTrue(ref0.equalTo(isoDt))

        # Bug 2191: week format
        isoDt = DateTime('2002-W18-4T0800')
        self.assertTrue(ref0.equalTo(isoDt))
        isoDt = DateTime('2002-W184T0800')
        self.assertTrue(ref0.equalTo(isoDt))
        isoDt = DateTime('2002W18-4T0800')
        self.assertTrue(ref0.equalTo(isoDt))
        isoDt = DateTime('2002W184T08')
        self.assertTrue(ref0.equalTo(isoDt))
        isoDt = DateTime('2004-W25-1T14:30:15-03:00')
        self.assertTrue(ref3.equalTo(isoDt))
        isoDt = DateTime('2004-W25T14:30:15-03:00')
        self.assertTrue(ref3.equalTo(isoDt))

        # Bug 2191: day of year format
        isoDt = DateTime('2002-122T0800')
        self.assertTrue(ref0.equalTo(isoDt))
        isoDt = DateTime('2002122T0800')
        self.assertTrue(ref0.equalTo(isoDt))

        # Bug 2191: hours/minutes fractions
        isoDt = DateTime('2006-11-06T10.5')
        self.assertTrue(ref2.equalTo(isoDt))
        isoDt = DateTime('2006-11-06T10,5')
        self.assertTrue(ref2.equalTo(isoDt))
        isoDt = DateTime('20040614T1430.25-3')
        self.assertTrue(ref3.equalTo(isoDt))
        isoDt = DateTime('2004-06-14T1430,25-3')
        self.assertTrue(ref3.equalTo(isoDt))
        isoDt = DateTime('2004-06-14T14:30.25-3')
        self.assertTrue(ref3.equalTo(isoDt))
        isoDt = DateTime('20040614T14:30,25-3')
        self.assertTrue(ref3.equalTo(isoDt))

        # ISO8601 standard format
        iso8601_string = '2002-05-02T08:00:00-04:00'
        iso8601DT = DateTime(iso8601_string)
        self.assertEqual(iso8601_string, iso8601DT.ISO8601())

        # ISO format with no timezone
        isoDt = DateTime('2006-01-01 00:00:00')
        self.assertTrue(ref4.equalTo(isoDt))

    def testJulianWeek(self):
        # Check JulianDayWeek function
        fn = os.path.join(DATADIR, 'julian_testdata.txt')
        with open(fn) as fd:
            lines = fd.readlines()
        for line in lines:
            d = DateTime(line[:10])
            result_from_mx = tuple(map(int, line[12:-2].split(',')))
            self.assertEqual(result_from_mx[1], d.week())

    def testCopyConstructor(self):
        d = DateTime('2004/04/04')
        self.assertEqual(DateTime(d), d)
        self.assertEqual(str(DateTime(d)), str(d))
        d2 = DateTime('1999/04/12 01:00:00')
        self.assertEqual(DateTime(d2), d2)
        self.assertEqual(str(DateTime(d2)), str(d2))

    def testCopyConstructorPreservesTimezone(self):
        # test for https://bugs.launchpad.net/zope2/+bug/200007
        # This always worked in the local timezone, so we need at least
        # two tests with different zones to be sure at least one of them
        # is not local.
        d = DateTime('2004/04/04')
        self.assertEqual(DateTime(d).timezone(), d.timezone())
        d2 = DateTime('2008/04/25 12:00:00 EST')
        self.assertEqual(DateTime(d2).timezone(), d2.timezone())
        self.assertEqual(str(DateTime(d2)), str(d2))
        d3 = DateTime('2008/04/25 12:00:00 PST')
        self.assertEqual(DateTime(d3).timezone(), d3.timezone())
        self.assertEqual(str(DateTime(d3)), str(d3))

    def testRFC822(self):
        # rfc822 conversion
        dt = DateTime('2002-05-02T08:00:00+00:00')
        self.assertEqual(dt.rfc822(), 'Thu, 02 May 2002 08:00:00 +0000')

        dt = DateTime('2002-05-02T08:00:00+02:00')
        self.assertEqual(dt.rfc822(), 'Thu, 02 May 2002 08:00:00 +0200')

        dt = DateTime('2002-05-02T08:00:00-02:00')
        self.assertEqual(dt.rfc822(), 'Thu, 02 May 2002 08:00:00 -0200')

        # Checking that conversion from local time is working.
        dt = DateTime()
        dts = dt.rfc822().split(' ')
        times = dts[4].split(':')
        _isDST = time.localtime(time.time())[8]
        if _isDST:
            offset = time.altzone
        else:
            offset = time.timezone
        self.assertEqual(dts[0], dt.aDay() + ',')
        self.assertEqual(int(dts[1]), dt.day())
        self.assertEqual(dts[2], dt.aMonth())
        self.assertEqual(int(dts[3]), dt.year())
        self.assertEqual(int(times[0]), dt.h_24())
        self.assertEqual(int(times[1]), dt.minute())
        self.assertEqual(int(times[2]), int(dt.second()))
        self.assertEqual(dts[5], "%+03d%02d" % divmod((-offset / 60), 60))

    def testInternationalDateformat(self):
        for year in (1990, 2001, 2020):
            for month in (1, 12):
                for day in (1, 12, 28, 31):
                    try:
                        d_us = DateTime("%d/%d/%d" % (year, month, day))
                    except Exception:
                        continue

                    d_int = DateTime("%d.%d.%d" % (day, month, year),
                                     datefmt="international")
                    self.assertEqual(d_us, d_int)

                    d_int = DateTime("%d/%d/%d" % (day, month, year),
                                     datefmt="international")
                    self.assertEqual(d_us, d_int)

    def test_intl_format_hyphen(self):
        d_jan = DateTime('2011-01-11 GMT')
        d_nov = DateTime('2011-11-01 GMT')
        d_us = DateTime('11-01-2011 GMT')
        d_int = DateTime('11-01-2011 GMT', datefmt="international")
        self.assertNotEqual(d_us, d_int)
        self.assertEqual(d_us, d_nov)
        self.assertEqual(d_int, d_jan)

    def test_calcTimezoneName(self):
        from DateTime.interfaces import TimeError
        timezone_dependent_epoch = 2177452800
        try:
            DateTime()._calcTimezoneName(timezone_dependent_epoch, 0)
        except TimeError:
            self.fail('Zope Collector issue #484 (negative time bug): '
                      'TimeError raised')

    def testStrftimeTZhandling(self):
        # strftime timezone testing
        # This is a test for collector issue #1127
        format = '%Y-%m-%d %H:%M %Z'
        dt = DateTime('Wed, 19 Nov 2003 18:32:07 -0215')
        dt_string = dt.strftime(format)
        dt_local = dt.toZone(_findLocalTimeZoneName(0))
        dt_localstring = dt_local.strftime(format)
        self.assertEqual(dt_string, dt_localstring)

    def testStrftimeFarDates(self):
        # Checks strftime in dates <= 1900 or >= 2038
        dt = DateTime('1900/01/30')
        self.assertEqual(dt.strftime('%d/%m/%Y'), '30/01/1900')
        dt = DateTime('2040/01/30')
        self.assertEqual(dt.strftime('%d/%m/%Y'), '30/01/2040')

    def testZoneInFarDates(self):
        # Checks time zone in dates <= 1900 or >= 2038
        dt1 = DateTime('2040/01/30 14:33 GMT+1')
        dt2 = DateTime('2040/01/30 11:33 GMT-2')
        self.assertEqual(dt1.strftime('%d/%m/%Y %H:%M'),
                         dt2.strftime('%d/%m/%Y %H:%M'))

    @unittest.skipIf(
        IS_PYPY,
        "Using Non-Ascii characters for strftime doesn't work in PyPy"
        "https://bitbucket.org/pypy/pypy/issues/2161/pypy3-strftime-does-not-accept-unicode"  # noqa: E501 line too long
    )
    def testStrftimeStr(self):
        dt = DateTime('2002-05-02T08:00:00+00:00')
        uchar = b'\xc3\xa0'.decode('utf-8')
        ok = dt.strftime('Le %d/%m/%Y a %Hh%M').replace('a', uchar)
        ustr = b'Le %d/%m/%Y \xc3\xa0 %Hh%M'.decode('utf-8')
        self.assertEqual(dt.strftime(ustr), ok)

    def testTimezoneNaiveHandling(self):
        # checks that we assign timezone naivity correctly
        dt = DateTime('2007-10-04T08:00:00+00:00')
        self.assertFalse(dt.timezoneNaive(),
                         'error with naivity handling in __parse_iso8601')
        dt = DateTime('2007-10-04T08:00:00Z')
        self.assertFalse(dt.timezoneNaive(),
                         'error with naivity handling in __parse_iso8601')
        dt = DateTime('2007-10-04T08:00:00')
        self.assertTrue(dt.timezoneNaive(),
                        'error with naivity handling in __parse_iso8601')
        dt = DateTime('2007/10/04 15:12:33.487618 GMT+1')
        self.assertFalse(dt.timezoneNaive(),
                         'error with naivity handling in _parse')
        dt = DateTime('2007/10/04 15:12:33.487618')
        self.assertTrue(dt.timezoneNaive(),
                        'error with naivity handling in _parse')
        dt = DateTime()
        self.assertFalse(dt.timezoneNaive(),
                         'error with naivity for current time')
        s = '2007-10-04T08:00:00'
        dt = DateTime(s)
        self.assertEqual(s, dt.ISO8601())
        s = '2007-10-04T08:00:00+00:00'
        dt = DateTime(s)
        self.assertEqual(s, dt.ISO8601())

    def testConversions(self):
        sdt0 = datetime.now()  # this is a timezone naive datetime
        dt0 = DateTime(sdt0)
        self.assertTrue(dt0.timezoneNaive(), (sdt0, dt0))
        sdt1 = datetime(2007, 10, 4, 18, 14, 42, 580, pytz.utc)
        dt1 = DateTime(sdt1)
        self.assertFalse(dt1.timezoneNaive(), (sdt1, dt1))

        # convert back
        sdt2 = dt0.asdatetime()
        self.assertEqual(sdt0, sdt2)
        sdt3 = dt1.utcdatetime()  # this returns a timezone naive datetime
        self.assertEqual(sdt1.hour, sdt3.hour)

        dt4 = DateTime('2007-10-04T10:00:00+05:00')
        sdt4 = datetime(2007, 10, 4, 5, 0)
        self.assertEqual(dt4.utcdatetime(), sdt4)
        self.assertEqual(dt4.asdatetime(), sdt4.replace(tzinfo=pytz.utc))

        dt5 = DateTime('2007-10-23 10:00:00 US/Eastern')
        tz = pytz.timezone('US/Eastern')
        sdt5 = datetime(2007, 10, 23, 10, 0, tzinfo=tz)
        dt6 = DateTime(sdt5)
        self.assertEqual(dt5.asdatetime(), sdt5)
        self.assertEqual(dt6.asdatetime(), sdt5)
        self.assertEqual(dt5, dt6)
        self.assertEqual(dt5.asdatetime().tzinfo, tz)
        self.assertEqual(dt6.asdatetime().tzinfo, tz)

    def testBasicTZ(self):
        # psycopg2 supplies it's own tzinfo instances, with no `zone` attribute
        tz = FixedOffset(60, 'GMT+1')
        dt1 = datetime(2008, 8, 5, 12, 0, tzinfo=tz)
        DT = DateTime(dt1)
        dt2 = DT.asdatetime()
        offset1 = dt1.tzinfo.utcoffset(dt1)
        offset2 = dt2.tzinfo.utcoffset(dt2)
        self.assertEqual(offset1, offset2)

    def testEDTTimezone(self):
        # should be able to parse EDT timezones:  see lp:599856.
        dt = DateTime("Mon, 28 Jun 2010 10:12:25 EDT")
        self.assertEqual(dt.Day(), 'Monday')
        self.assertEqual(dt.day(), 28)
        self.assertEqual(dt.Month(), 'June')
        self.assertEqual(dt.timezone(), 'GMT-4')

    def testParseISO8601(self):
        parsed = DateTime()._parse_iso8601('2010-10-10')
        self.assertEqual(parsed, (2010, 10, 10, 0, 0, 0, 'GMT+0000'))

    def test_interface(self):
        from DateTime.interfaces import IDateTime
        self.assertTrue(IDateTime.providedBy(DateTime()))

    def test_security(self):
        dt = DateTime()
        self.assertEqual(dt.__roles__, None)
        self.assertEqual(dt.__allow_access_to_unprotected_subobjects__, 1)

    def test_format(self):
        dt = DateTime(1968, 3, 10, 23, 45, 0, 'Europe/Vienna')
        fmt = '%d.%m.%Y %H:%M'
        result = dt.strftime(fmt)
        unformatted_result = '1968/03/10 23:45:00 Europe/Vienna'
        self.assertEqual(result, f'{dt:%d.%m.%Y %H:%M}')
        self.assertEqual(unformatted_result, f'{dt}')
        self.assertEqual(unformatted_result, f'{dt}')
        self.assertEqual(result, f'{dt:{fmt}}')
        self.assertEqual(unformatted_result, f'{dt:}')
        self.assertEqual(unformatted_result, f'{dt}')


def test_suite():
    import doctest
    return unittest.TestSuite([
        unittest.defaultTestLoader.loadTestsFromTestCase(DateTimeTests),
        doctest.DocFileSuite('DateTime.txt', package='DateTime'),
        doctest.DocFileSuite('pytz.txt', package='DateTime'),
    ])
