# -*- coding: utf-8 -*-
# ----------------------------------------------------------------------------
# Copyright (C) 2013-2018 British Crown (Met Office) & Contributors.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
# ----------------------------------------------------------------------------

"""This tests the ISO 8601 parsing and data model functionality."""

import copy
import multiprocessing
import unittest
import mock

from . import data
from . import dumpers
from . import parsers
from . import parser_spec
from . import util
from . import timezone


def get_timeduration_tests():
    """Yield tests for the duration class."""
    tests = {
        "get_days_and_seconds": [
            ([], {"hours": 25}, (1, 3600)),
            ([], {"seconds": 59}, (0, 59)),
            ([], {"minutes": 10}, (0, 600)),
            ([], {"days": 5, "minutes": 2}, (5, 120)),
            ([], {"hours": 2, "minutes": 5, "seconds": 11.5}, (0, 7511.5)),
            ([], {"hours": 23, "minutes": 1446}, (1, 83160))
        ],
        "get_seconds": [
            ([], {"hours": 25}, 90000),
            ([], {"seconds": 59}, 59),
            ([], {"minutes": 10}, 600),
            ([], {"days": 5, "minutes": 2}, 432120),
            ([], {"hours": 2, "minutes": 5, "seconds": 11.5}, 7511.5),
            ([], {"hours": 23, "minutes": 1446}, 169560)
        ]
    }
    for method, method_tests in tests.items():
        for method_args, test_props, ctrl_results in method_tests:
            yield test_props, method, method_args, ctrl_results


def get_timedurationparser_tests():
    """Yield tests for the duration parser."""
    test_expressions = {
        "P3Y": {"years": 3},
        "P90Y": {"years": 90},
        "P1Y2M": {"years": 1, "months": 2},
        "P20Y2M": {"years": 20, "months": 2},
        "P2M": {"months": 2},
        "P52M": {"months": 52},
        "P20Y10M2D": {"years": 20, "months": 10, "days": 2},
        "P1Y3D": {"years": 1, "days": 3},
        "P4M1D": {"months": 4, "days": 1},
        "P3Y404D": {"years": 3, "days": 404},
        "P30Y2D": {"years": 30, "days": 2},
        "PT6H": {"hours": 6},
        "PT1034H": {"hours": 1034},
        "P3YT4H2M": {"years": 3, "hours": 4, "minutes": 2},
        "P30Y2DT10S": {"years": 30, "days": 2, "seconds": 10},
        "PT2S": {"seconds": 2},
        "PT2.5S": {"seconds": 2.5},
        "PT2,5S": {"seconds": 2.5},
        "PT5.5023H": {"hours": 5.5023},
        "PT5,5023H": {"hours": 5.5023},
        "P5W": {"weeks": 5},
        "P100W": {"weeks": 100},
        "P0004-03-02T01": {"years": 4, "months": 3, "days": 2,
                           "hours": 1},
        "P0004-03-00": {"years": 4, "months": 3},
        "P0004-078": {"years": 4, "days": 78},
        "P0004-078T10,5": {"years": 4, "days": 78, "hours": 10.5},
        "P00000020T133702": {"days": 20, "hours": 13, "minutes": 37,
                             "seconds": 2},
        "-P3YT4H2M": {"years": -3, "hours": -4, "minutes": -2},
        "-PT5M": {"minutes": -5},
        "-P7Y": {"years": -7, "hours": 0}
    }
    for expression, ctrl_result in test_expressions.items():
        ctrl_data = str(data.Duration(**ctrl_result))
        yield expression, ctrl_data


def get_timedurationdumper_tests():
    """Yield tests for the duration dumper."""
    test_expressions = {
        "P3Y": {"years": 3},
        "P90Y": {"years": 90},
        "P1Y2M": {"years": 1, "months": 2},
        "P20Y2M": {"years": 20, "months": 2},
        "P2M": {"months": 2},
        "P52M": {"months": 52},
        "P20Y10M2D": {"years": 20, "months": 10, "days": 2},
        "P1Y3D": {"years": 1, "days": 3},
        "P4M1D": {"months": 4, "days": 1},
        "P3Y404D": {"years": 3, "days": 404},
        "P30Y2D": {"years": 30, "days": 2},
        "PT6H": {"hours": 6},
        "PT1034H": {"hours": 1034},
        "P3YT4H2M": {"years": 3, "hours": 4, "minutes": 2},
        "P30Y2DT10S": {"years": 30, "days": 2, "seconds": 10},
        "PT2S": {"seconds": 2},
        "PT2,5S": {"seconds": 2.5},
        "PT5,5023H": {"hours": 5.5023},
        "P5W": {"weeks": 5},
        "P100W": {"weeks": 100},
        "-P3YT4H2M": {"years": -3, "hours": -4, "minutes": -2},
        "-PT5M": {"minutes": -5},
        "-P7Y": {"years": -7, "hours": 0},
        "PT1H": {"seconds": 3600, "standardize": True},
        "P1DT5M": {"minutes": 1445, "standardize": True},
        "PT59S": {"seconds": 59, "standardize": True},
        "PT1H4M56S": {"minutes": 10, "seconds": 3296, "standardize": True},
    }
    for expression, ctrl_result in test_expressions.items():
        yield expression, ctrl_result


def get_timepoint_dumper_tests():
    """Yield tests for custom timepoint dumps."""
    return [
        (
            {"year": 44, "month_of_year": 1, "day_of_month": 4,
             "hour_of_day": 5, "minute_of_hour": 1, "second_of_minute": 2,
             "time_zone_hour": 0, "time_zone_minute": 0},
            [("CCYY-MMDDThhmmZ", "0044-0104T0501Z"),
             ("YYDDDThh:mm:ss", "44004T05:01:02"),
             ("WwwD", "W011"),
             ("CCDDDThh*ss-0600", "00003T23*02-0600"),
             (u"+XCCYY-MM-DDThh:mm:ss-11:45",
              "+000044-01-03T17:16:02-11:45"),
             (u"+XCCYYMM-DDThh-01:00", "+00004401-04T04-01:00"),
             (u"+XCCYYMM-DDThh+13:00", "+00004401-04T18+13:00"),
             (u"+XCCYYMM-DDThh-0100", "+00004401-04T04-0100"),
             (u"+XCCYYMM-DDThh+1300", "+00004401-04T18+1300"),
             (u"+XCCYYMMDDThh-0100", "+0000440104T04-0100"),
             (u"+XCCYYMMDDThh+13", "+0000440104T18+13"),
             (u"+XCCYYMMDDThh+hhmm", "+0000440104T05+0000"),
             (u"+XCCYY-MM-DDThh:mm:ss+hh:mm",
              "+000044-01-04T05:01:02+00:00"),
             ("DD/MM/CCYY is a silly format", "04/01/0044 is a silly format"),
             ("ThhZ", "T05Z"),
             ("%Y-%m-%dT%H:%M", "0044-01-04T05:01")]
        ),
        (
            {"year": 500200, "month_of_year": 7, "day_of_month": 28,
             "expanded_year_digits": 2, "hour_of_day": 0,
             "hour_of_day_decimal": 0.4356, "time_zone_hour": -8,
             "time_zone_minute": -30},
            [("+XCCYY-MMDDThhmmZ", "+500200-0728T0856Z"),
             ("+XCCYYDDDThh:mm:ss", "+500200209T00:26:08"),
             ("WwwD", "W311"),
             ("+XCCDDDThh*ss-0600", "+5002209T02*08-0600"),
             (u"+XCCYY-MM-DDThh:mm:ss-11:45",
              "+500200-07-27T21:11:08-11:45"),
             (u"+XCCYYMM-DDThhmm-01:00", "+50020007-28T0756-01:00"),
             (u"+XCCYYMM-DDThhmm+13:00", "+50020007-28T2156+13:00"),
             (u"+XCCYYMM-DDThhmm-0100", "+50020007-28T0756-0100"),
             (u"+XCCYYMM-DDThhmm+1300", "+50020007-28T2156+1300"),
             (u"+XCCYYMMDDThhmm-0100", "+5002000728T0756-0100"),
             (u"+XCCYYMMDDThhmm+13", "+5002000728T2156+13"),
             (u"+XCCYYMMDDThh+hhmm", "+5002000728T00-0830"),
             (u"+XCCYYWwwDThhmm+hh", "+500200W311T0026-08"),
             (u"+XCCYYDDDThhmm+hh", "+500200209T0026-08"),
             (u"+XCCYY-MM-DDThh:mm:ss+hh:mm",
              "+500200-07-28T00:26:08-08:30"),
             (u"+XCCYY-MM-DDThh:mm:ssZ", "+500200-07-28T08:56:08Z"),
             ("DD/MM/+XCCYY is a silly format",
              "28/07/+500200 is a silly format"),
             ("ThhmmZ", "T0856Z"),
             ("%m-%dT%H:%M", "07-28T00:26")]
        ),
        (
            {"year": -56, "day_of_year": 318, "expanded_year_digits": 2,
             "hour_of_day": 5, "minute_of_hour": 1, "time_zone_hour": 6},
            [("+XCCYY-MMDDThhmmZ", "-000056-1112T2301Z"),
             ("+XCCYYDDDThh:mm:ss", "-000056318T05:01:00"),
             ("WwwD", "W461"),
             ("+XCCDDDThh*ss-0600", "-0000317T17*00-0600"),
             (u"+XCCYY-MM-DDThh:mm:ss-11:45",
              "-000056-11-12T11:16:00-11:45"),
             (u"+XCCYYMM-DDThhmm-01:00", "-00005611-12T2201-01:00"),
             (u"+XCCYYMM-DDThhmm+13:00", "-00005611-13T1201+13:00"),
             (u"+XCCYYMM-DDThhmm-0100", "-00005611-12T2201-0100"),
             (u"+XCCYYMM-DDThhmm+1300", "-00005611-13T1201+1300"),
             (u"+XCCYYMMDDThhmm-0100", "-0000561112T2201-0100"),
             (u"+XCCYYMMDDThhmm+13", "-0000561113T1201+13"),
             (u"+XCCYYMMDDThh+hhmm", "-0000561113T05+0600"),
             (u"+XCCYYWwwDThhmm+hh", "-000056W461T0501+06"),
             (u"+XCCYYDDDThhmm+hh", "-000056318T0501+06"),
             (u"+XCCYY-MM-DDThh:mm:ss+hh:mm",
              "-000056-11-13T05:01:00+06:00"),
             (u"+XCCYY-MM-DDThh:mm:ssZ", "-000056-11-12T23:01:00Z"),
             ("DD/MM/+XCCYY is a silly format",
              "13/11/-000056 is a silly format"),
             ("ThhmmZ", "T2301Z"),
             ("%m-%dT%H:%M", "11-13T05:01")]
        ),
        (
            {"year": 1000, "week_of_year": 1, "day_of_week": 1,
             "time_zone_hour": 0},
            [("CCYY-MMDDThhmmZ", "0999-1230T0000Z"),
             ("CCYY-DDDThhmmZ", "0999-364T0000Z"),
             ("CCYY-Www-DThhmm+0200", "1000-W01-1T0200+0200"),
             ("CCYY-Www-DThhmm-0200", "0999-W52-7T2200-0200"),
             ("%Y-%m-%dT%H:%M", "0999-12-30T00:00")]
        ),
        (
            {"year": 999, "day_of_year": 364, "time_zone_hour": 0},
            [("CCYY-MMDDThhmmZ", "0999-1230T0000Z"),
             ("CCYY-DDDThhmmZ", "0999-364T0000Z"),
             ("CCYY-Www-DThhmm+0200", "1000-W01-1T0200+0200"),
             ("CCYY-Www-DThhmm-0200", "0999-W52-7T2200-0200"),
             ("%Y-%m-%dT%H:%M", "0999-12-30T00:00")]
        )
    ]


def get_timepointdumper_failure_tests():
    """Yield tests that raise exceptions for custom time point dumps."""
    bounds_error = dumpers.TimePointDumperBoundsError
    return [
        (
            {"year": 10000, "month_of_year": 1, "day_of_month": 4,
             "time_zone_hour": 0, "time_zone_minute": 0},
            [("CCYY-MMDDThhmmZ", bounds_error, 0),
             ("%Y-%m-%dT%H:%M", bounds_error, 0)]
        ),
        (
            {"year": -10000, "month_of_year": 1, "day_of_month": 4,
             "time_zone_hour": 0, "time_zone_minute": 0},
            [("CCYY-MMDDThhmmZ", bounds_error, 0),
             ("%Y-%m-%dT%H:%M", bounds_error, 0)]
        ),
        (
            {"year": 10000, "month_of_year": 1, "day_of_month": 4,
             "time_zone_hour": 0, "time_zone_minute": 0},
            [("CCYY-MMDDThhmmZ", bounds_error, 2)]
        ),
        (
            {"year": -10000, "month_of_year": 1, "day_of_month": 4,
             "time_zone_hour": 0, "time_zone_minute": 0},
            [("CCYY-MMDDThhmmZ", bounds_error, 2)]
        ),
        (
            {"year": 1000000, "month_of_year": 1, "day_of_month": 4,
             "time_zone_hour": 0, "time_zone_minute": 0},
            [("+XCCYY-MMDDThhmmZ", bounds_error, 2)]
        ),
        (
            {"year": -1000000, "month_of_year": 1, "day_of_month": 4,
             "time_zone_hour": 0, "time_zone_minute": 0},
            [("+XCCYY-MMDDThhmmZ", bounds_error, 2)]
        )
    ]


def get_timepointparser_tests(allow_only_basic=False,
                              allow_truncated=False,
                              skip_time_zones=False):
    """Yield tests for the time point parser."""
    # Note: test dates assume 2 expanded year digits.
    test_date_map = {
        "basic": {
            "complete": {
                "00440104": {"year": 44, "month_of_year": 1,
                             "day_of_month": 4},
                "+5002000830": {"year": 500200, "month_of_year": 8,
                                "day_of_month": 30,
                                "expanded_year_digits": 2},
                "-0000561113": {"year": -56, "month_of_year": 11,
                                "day_of_month": 13,
                                "expanded_year_digits": 2},
                "-1000240210": {"year": -100024, "month_of_year": 2,
                                "day_of_month": 10,
                                "expanded_year_digits": 2},
                "1967056": {"year": 1967, "day_of_year": 56},
                "+123456078": {"year": 123456, "day_of_year": 78,
                               "expanded_year_digits": 2},
                "-004560134": {"year": -4560, "day_of_year": 134,
                               "expanded_year_digits": 2},
                "1001W011": {"year": 1001, "week_of_year": 1,
                             "day_of_week": 1},
                "+000001W457": {"year": 1, "week_of_year": 45,
                                "day_of_week": 7,
                                "expanded_year_digits": 2},
                "-010001W053": {"year": -10001, "week_of_year": 5,
                                "day_of_week": 3, "expanded_year_digits": 2}
            },
            "reduced": {
                "4401-03": {"year": 4401, "month_of_year": 3},
                "1982": {"year": 1982},
                "19": {"year": 1900},
                "+056789-01": {"year": 56789, "month_of_year": 1,
                               "expanded_year_digits": 2},
                "-000001-12": {"year": -1, "month_of_year": 12,
                               "expanded_year_digits": 2},
                "-789123": {"year": -789123, "expanded_year_digits": 2},
                "+450001": {"year": 450001, "expanded_year_digits": 2},
                # The following cannot be parsed - looks like truncated -YYMM.
                #  "-0023": {"year": -2300, "expanded_year_digits": 2},
                "+5678": {"year": 567800, "expanded_year_digits": 2},
                "1765W04": {"year": 1765, "week_of_year": 4},
                "+001765W44": {"year": 1765, "week_of_year": 44,
                               "expanded_year_digits": 2},
                "-123321W50": {"year": -123321, "week_of_year": 50,
                               "expanded_year_digits": 2}
            },
            "truncated": {
                "-9001": {"year": 90, "month_of_year": 1,
                          "truncated": True,
                          "truncated_property": "year_of_century"},
                "960328": {"year": 96, "month_of_year": 3,
                           "day_of_month": 28,
                           "truncated": True,
                           "truncated_property": "year_of_century"},
                "-90": {"year": 90, "truncated": True,
                        "truncated_property": "year_of_century"},
                "--0501": {"month_of_year": 5, "day_of_month": 1,
                           "truncated": True},
                "--12": {"month_of_year": 12, "truncated": True},
                "---30": {"day_of_month": 30, "truncated": True},
                "98354": {"year": 98, "day_of_year": 354, "truncated": True,
                          "truncated_property": "year_of_century"},
                "-034": {"day_of_year": 34, "truncated": True},
                "00W031": {"year": 0, "week_of_year": 3, "day_of_week": 1,
                           "truncated": True,
                           "truncated_property": "year_of_century"},
                "99W34": {"year": 99, "week_of_year": 34, "truncated": True,
                          "truncated_property": "year_of_century"},
                "-1W02": {"year": 1, "week_of_year": 2,
                          "truncated": True,
                          "truncated_property": "year_of_decade"},
                "-W031": {"week_of_year": 3, "day_of_week": 1,
                          "truncated": True},
                "-W32": {"week_of_year": 32, "truncated": True},
                "-W-1": {"day_of_week": 1, "truncated": True}
            }
        },
        "extended": {
            "complete": {
                "0044-01-04": {"year": 44, "month_of_year": 1,
                               "day_of_month": 4},
                "+500200-08-30": {"year": 500200, "month_of_year": 8,
                                  "day_of_month": 30,
                                  "expanded_year_digits": 2},
                "-000056-11-13": {"year": -56, "month_of_year": 11,
                                  "day_of_month": 13,
                                  "expanded_year_digits": 2},
                "-100024-02-10": {"year": -100024, "month_of_year": 2,
                                  "day_of_month": 10,
                                  "expanded_year_digits": 2},
                "1967-056": {"year": 1967, "day_of_year": 56},
                "+123456-078": {"year": 123456, "day_of_year": 78,
                                "expanded_year_digits": 2},
                "-004560-134": {"year": -4560, "day_of_year": 134,
                                "expanded_year_digits": 2},
                "1001-W01-1": {"year": 1001, "week_of_year": 1,
                               "day_of_week": 1},
                "+000001-W45-7": {"year": 1, "week_of_year": 45,
                                  "day_of_week": 7,
                                  "expanded_year_digits": 2},
                "-010001-W05-3": {"year": -10001, "week_of_year": 5,
                                  "day_of_week": 3,
                                  "expanded_year_digits": 2}
            },
            "reduced": {
                "4401-03": {"year": 4401, "month_of_year": 3},
                "1982": {"year": 1982},
                "19": {"year": 1900},
                "+056789-01": {"year": 56789, "month_of_year": 1,
                               "expanded_year_digits": 2},
                "-000001-12": {"year": -1, "month_of_year": 12,
                               "expanded_year_digits": 2},
                "-789123": {"year": -789123, "expanded_year_digits": 2},
                "+450001": {"year": 450001, "expanded_year_digits": 2},
                # The following cannot be parsed - looks like truncated -YYMM.
                #  "-0023": {"year": -2300, "expanded_year_digits": 2},
                "+5678": {"year": 567800, "expanded_year_digits": 2},
                "1765-W04": {"year": 1765, "week_of_year": 4},
                "+001765-W44": {"year": 1765, "week_of_year": 44,
                                "expanded_year_digits": 2},
                "-123321-W50": {"year": -123321, "week_of_year": 50,
                                "expanded_year_digits": 2}
            },
            "truncated": {
                "-9001": {"year": 90, "month_of_year": 1,
                          "truncated": True,
                          "truncated_property": "year_of_century"},
                "96-03-28": {"year": 96, "month_of_year": 3,
                             "day_of_month": 28,
                             "truncated": True,
                             "truncated_property": "year_of_century"},
                "-90": {"year": 90, "truncated": True,
                        "truncated_property": "year_of_century"},
                "--05-01": {"month_of_year": 5, "day_of_month": 1,
                            "truncated": True},
                "--12": {"month_of_year": 12, "truncated": True},
                "---30": {"day_of_month": 30, "truncated": True},
                "98-354": {"year": 98, "day_of_year": 354, "truncated": True,
                           "truncated_property": "year_of_century"},
                "-034": {"day_of_year": 34, "truncated": True},
                "00-W03-1": {"year": 0, "week_of_year": 3, "day_of_week": 1,
                             "truncated": True,
                             "truncated_property": "year_of_century"},
                "99-W34": {"year": 99, "week_of_year": 34, "truncated": True,
                           "truncated_property": "year_of_century"},
                "-1-W02": {"year": 1, "week_of_year": 2,
                           "truncated": True,
                           "truncated_property": "year_of_decade"},
                "-W03-1": {"week_of_year": 3, "day_of_week": 1,
                           "truncated": True},
                "-W32": {"week_of_year": 32, "truncated": True},
                "-W-1": {"day_of_week": 1, "truncated": True}
            }
        }
    }
    test_time_map = {
        "basic": {
            "complete": {
                "050102": {"hour_of_day": 5, "minute_of_hour": 1,
                           "second_of_minute": 2},
                "235902,345": {"hour_of_day": 23, "minute_of_hour": 59,
                               "second_of_minute": 2,
                               "second_of_minute_decimal": 0.345},
                "235902.345": {"hour_of_day": 23, "minute_of_hour": 59,
                               "second_of_minute": 2,
                               "second_of_minute_decimal": 0.345},
                "1201,4": {"hour_of_day": 12, "minute_of_hour": 1,
                           "minute_of_hour_decimal": 0.4},
                "1201.4": {"hour_of_day": 12, "minute_of_hour": 1,
                           "minute_of_hour_decimal": 0.4},
                "00,4356": {"hour_of_day": 0,
                            "hour_of_day_decimal": 0.4356},
                "00.4356": {"hour_of_day": 0,
                            "hour_of_day_decimal": 0.4356}
            },
            "reduced": {
                "0203": {"hour_of_day": 2, "minute_of_hour": 3},
                "17": {"hour_of_day": 17}
            },
            "truncated": {
                "-5612": {"minute_of_hour": 56, "second_of_minute": 12,
                          "truncated": True},
                "-12": {"minute_of_hour": 12, "truncated": True},
                "--45": {"second_of_minute": 45, "truncated": True},
                "-1234,45": {"minute_of_hour": 12, "second_of_minute": 34,
                             "second_of_minute_decimal": 0.45,
                             "truncated": True},
                "-1234.45": {"minute_of_hour": 12, "second_of_minute": 34,
                             "second_of_minute_decimal": 0.45,
                             "truncated": True},
                "-34,2": {"minute_of_hour": 34, "minute_of_hour_decimal": 0.2,
                          "truncated": True},
                "-34.2": {"minute_of_hour": 34, "minute_of_hour_decimal": 0.2,
                          "truncated": True},
                "--59,99": {"second_of_minute": 59,
                            "second_of_minute_decimal": 0.99,
                            "truncated": True},
                "--59.99": {"second_of_minute": 59,
                            "second_of_minute_decimal": 0.99,
                            "truncated": True}
            }
        },
        "extended": {
            "complete": {
                "05:01:02": {"hour_of_day": 5, "minute_of_hour": 1,
                             "second_of_minute": 2},
                "23:59:02,345": {"hour_of_day": 23, "minute_of_hour": 59,
                                 "second_of_minute": 2,
                                 "second_of_minute_decimal": 0.345},
                "23:59:02.345": {"hour_of_day": 23, "minute_of_hour": 59,
                                 "second_of_minute": 2,
                                 "second_of_minute_decimal": 0.345},
                "12:01,4": {"hour_of_day": 12, "minute_of_hour": 1,
                            "minute_of_hour_decimal": 0.4},
                "12:01.4": {"hour_of_day": 12, "minute_of_hour": 1,
                            "minute_of_hour_decimal": 0.4},
                "00,4356": {"hour_of_day": 0, "hour_of_day_decimal": 0.4356},
                "00.4356": {"hour_of_day": 0, "hour_of_day_decimal": 0.4356}
            },
            "reduced": {
                "02:03": {"hour_of_day": 2, "minute_of_hour": 3},
                "17": {"hour_of_day": 17}
            },
            "truncated": {
                "-56:12": {"minute_of_hour": 56, "second_of_minute": 12,
                           "truncated": True},
                "-12": {"minute_of_hour": 12, "truncated": True},
                "--45": {"second_of_minute": 45, "truncated": True},
                "-12:34,45": {"minute_of_hour": 12, "second_of_minute": 34,
                              "second_of_minute_decimal": 0.45,
                              "truncated": True},
                "-12:34.45": {"minute_of_hour": 12, "second_of_minute": 34,
                              "second_of_minute_decimal": 0.45,
                              "truncated": True},
                "-34,2": {"minute_of_hour": 34, "minute_of_hour_decimal": 0.2,
                          "truncated": True},
                "-34.2": {"minute_of_hour": 34, "minute_of_hour_decimal": 0.2,
                          "truncated": True},
                "--59,99": {"second_of_minute": 59,
                            "second_of_minute_decimal": 0.99,
                            "truncated": True},
                "--59.99": {"second_of_minute": 59,
                            "second_of_minute_decimal": 0.99,
                            "truncated": True}
            }
        }
    }
    test_time_zone_map = {
        "basic": {
            "Z": {"time_zone_hour": 0, "time_zone_minute": 0},
            "+01": {"time_zone_hour": 1},
            "-05": {"time_zone_hour": -5},
            "+2301": {"time_zone_hour": 23, "time_zone_minute": 1},
            "-1230": {"time_zone_hour": -12, "time_zone_minute": -30}
        },
        "extended": {
            "Z": {"time_zone_hour": 0, "time_zone_minute": 0},
            "+01": {"time_zone_hour": 1},
            "-05": {"time_zone_hour": -5},
            "+23:01": {"time_zone_hour": 23, "time_zone_minute": 1},
            "-12:30": {"time_zone_hour": -12, "time_zone_minute": -30}
        }
    }
    format_ok_keys = ["basic", "extended"]
    if allow_only_basic:
        format_ok_keys = ["basic"]
    date_combo_ok_keys = ["complete"]
    if allow_truncated:
        date_combo_ok_keys = ["complete", "truncated"]
    time_combo_ok_keys = ["complete", "reduced"]
    time_designator = parser_spec.TIME_DESIGNATOR
    for format_type in format_ok_keys:
        date_format_tests = test_date_map[format_type]
        time_format_tests = test_time_map[format_type]
        time_zone_format_tests = test_time_zone_map[format_type]
        for date_key in date_format_tests:
            if not allow_truncated and date_key == "truncated":
                continue
            for date_expr, info in date_format_tests[date_key].items():
                yield date_expr, info
        for date_key in date_combo_ok_keys:
            date_tests = date_format_tests[date_key]
            # Add a blank date for time-only testing.
            for date_expr, info in date_tests.items():
                for time_key in time_combo_ok_keys:
                    time_items = time_format_tests[time_key].items()
                    for time_expr, time_info in time_items:
                        combo_expr = (
                            date_expr +
                            time_designator +
                            time_expr
                        )
                        combo_info = {}
                        for key, value in info.items() + time_info.items():
                            combo_info[key] = value
                        yield combo_expr, combo_info
                        if skip_time_zones:
                            continue
                        time_zone_items = time_zone_format_tests.items()
                        for time_zone_expr, time_zone_info in time_zone_items:
                            tz_expr = combo_expr + time_zone_expr
                            tz_info = {}
                            for key, value in (combo_info.items() +
                                               time_zone_info.items()):
                                tz_info[key] = value
                            yield tz_expr, tz_info
        if not allow_truncated:
            continue
        for time_key in time_format_tests:
            time_tests = time_format_tests[time_key]
            for time_expr, time_info in time_tests.items():
                combo_expr = (
                    time_designator +
                    time_expr
                )
                # Add truncated (no date).
                combo_info = {"truncated": True}
                for key, value in time_info.items():
                    combo_info[key] = value
                yield combo_expr, combo_info
                if skip_time_zones:
                    continue
                time_zone_items = time_zone_format_tests.items()
                for time_zone_expr, time_zone_info in time_zone_items:
                    tz_expr = combo_expr + time_zone_expr
                    tz_info = {}
                    for key, value in (combo_info.items() +
                                       time_zone_info.items()):
                        tz_info[key] = value
                    yield tz_expr, tz_info


def get_truncated_property_tests():
    """
    Tests for largest truncated and
    smallest missing property names
    """
    test_timepoints = {
        "-9001": {"year": 90,
                  "month_of_year": 1,
                  "largest_truncated_property_name": "year_of_century",
                  "smallest_missing_property_name": "century"},
        "20960328": {"year": 96,
                     "month_of_year": 3,
                     "day_of_month": 28,
                     "largest_truncated_property_name": None,
                     "smallest_missing_property_name": None},
        "-90": {"year": 90,
                "largest_truncated_property_name": "year_of_century",
                "smallest_missing_property_name": "century"},
        "--0501": {"month_of_year": 5, "day_of_month": 1,
                   "largest_truncated_property_name": "month_of_year",
                   "smallest_missing_property_name": "year_of_century"},
        "--12": {"month_of_year": 12,
                 "largest_truncated_property_name": "month_of_year",
                 "smallest_missing_property_name": "year_of_century"},
        "---30": {"day_of_month": 30,
                  "largest_truncated_property_name": "day_of_month",
                  "smallest_missing_property_name": "month_of_year"},
        "98354": {"year": 98,
                  "day_of_year": 354,
                  "largest_truncated_property_name": "year_of_century",
                  "smallest_missing_property_name": "century"},
        "-034": {"day_of_year": 34,
                 "largest_truncated_property_name": "day_of_year",
                 "smallest_missing_property_name": "year_of_century"},
        "00W031": {"year": 0,
                   "week_of_year": 3,
                   "day_of_week": 1,
                   "largest_truncated_property_name": "year_of_century",
                   "smallest_missing_property_name": "century"},
        "99W34": {"year": 99,
                  "week_of_year": 34,
                  "largest_truncated_property_name": "year_of_century",
                  "smallest_missing_property_name": "century"},
        "-1W02": {"year": 1,
                  "week_of_year": 2,
                  "largest_truncated_property_name": "year_of_decade",
                  "smallest_missing_property_name": "decade_of_century"},
        "-W031": {"week_of_year": 3,
                  "day_of_week": 1,
                  "largest_truncated_property_name": "week_of_year",
                  "smallest_missing_property_name": "year_of_century"},
        "-W32": {"week_of_year": 32,
                 "largest_truncated_property_name": "week_of_year",
                 "smallest_missing_property_name": "year_of_century"},
        "-W-1": {"day_of_week": 1,
                 "largest_truncated_property_name": "day_of_week",
                 "smallest_missing_property_name": "week_of_year"},
        "T04:30": {"hour_of_day": 4,
                   "minute_of_hour": 30,
                   "largest_truncated_property_name": "hour_of_day",
                   "smallest_missing_property_name": "day_of_month"},
        "T19": {"hour_of_day": 19,
                "largest_truncated_property_name": "hour_of_day",
                "smallest_missing_property_name": "day_of_month"},
        "T-56:12": {"minute_of_hour": 56,
                    "second_of_minute": 12,
                    "largest_truncated_property_name": "minute_of_hour",
                    "smallest_missing_property_name": "hour_of_day"},
        "T-12": {"minute_of_hour": 12,
                 "largest_truncated_property_name": "minute_of_hour",
                 "smallest_missing_property_name": "hour_of_day"},
        "T--45": {"second_of_minute": 45,
                  "largest_truncated_property_name": "second_of_minute",
                  "smallest_missing_property_name": "minute_of_hour"},
        "T-12:34.45": {"minute_of_hour": 12,
                       "second_of_minute": 34,
                       "second_of_minute_decimal": 0.45,
                       "largest_truncated_property_name": "minute_of_hour",
                       "smallest_missing_property_name": "hour_of_day"},
        "T-34,2": {"minute_of_hour": 34,
                   "minute_of_hour_decimal": 0.2,
                   "largest_truncated_property_name": "minute_of_hour",
                   "smallest_missing_property_name": "hour_of_day"},
        "T--59.99": {"second_of_minute": 59,
                     "second_of_minute_decimal": 0.99,
                     "largest_truncated_property_name": "second_of_minute",
                     "smallest_missing_property_name": "minute_of_hour"}}
    return test_timepoints


def get_timepoint_subtract_tests():
    """Yield tests for subtracting one timepoint from another."""
    return [
        (
            {"year": 44, "month_of_year": 1, "day_of_month": 4,
             "hour_of_day": 5, "minute_of_hour": 1, "second_of_minute": 2,
             "time_zone_hour": 0, "time_zone_minute": 0},
            {"year": 41, "month_of_year": 12, "day_of_month": 2,
             "hour_of_day": 4, "minute_of_hour": 23, "second_of_minute": 1,
             "time_zone_hour": 3, "time_zone_minute": 20},
            "P763DT3H58M1S"
        ),
        (
            {"year": 41, "month_of_year": 12, "day_of_month": 2,
             "hour_of_day": 4, "minute_of_hour": 23, "second_of_minute": 1,
             "time_zone_hour": 3, "time_zone_minute": 20},
            {"year": 44, "month_of_year": 1, "day_of_month": 4,
             "hour_of_day": 5, "minute_of_hour": 1, "second_of_minute": 2,
             "time_zone_hour": 0, "time_zone_minute": 0},
            "-P763DT3H58M1S"
        ),
        (
            {"year": 1991, "month_of_year": 6, "day_of_month": 3,
             "hour_of_day": 0, "time_zone_hour": 0, "time_zone_minute": 0},
            {"year": 1991, "month_of_year": 5, "day_of_month": 4,
             "hour_of_day": 5, "time_zone_hour": 0, "time_zone_minute": 0},
            "P29DT19H"
        ),
        (
            {"year": 1969, "month_of_year": 7, "day_of_month": 20,
             "hour_of_day": 20, "time_zone_hour": 0, "time_zone_minute": 0},
            {"year": 1969, "month_of_year": 7, "day_of_month": 20,
             "hour_of_day": 19, "time_zone_hour": 0, "time_zone_minute": 0},
            "PT1H"
        ),

        (
            {"year": 1969, "month_of_year": 7, "day_of_month": 20,
             "hour_of_day": 19, "time_zone_hour": 0, "time_zone_minute": 0},
            {"year": 1969, "month_of_year": 7, "day_of_month": 20,
             "hour_of_day": 20, "time_zone_hour": 0, "time_zone_minute": 0},
            "-PT1H"
        ),
        (
            {"year": 1991, "month_of_year": 5, "day_of_month": 4,
             "hour_of_day": 5, "time_zone_hour": 0, "time_zone_minute": 0},
            {"year": 1991, "month_of_year": 6, "day_of_month": 3,
             "hour_of_day": 0, "time_zone_hour": 0, "time_zone_minute": 0},
            "-P29DT19H"
        ),
        (
            {"year": 2014, "month_of_year": 1, "day_of_month": 1,
             "hour_of_day": 0, "time_zone_hour": 0, "time_zone_minute": 0},
            {"year": 2013, "month_of_year": 12, "day_of_month": 31,
             "hour_of_day": 23, "time_zone_hour": 0, "time_zone_minute": 0},
            "PT1H"
        ),
        (
            {"year": 2013, "month_of_year": 12, "day_of_month": 31,
             "hour_of_day": 23, "time_zone_hour": 0, "time_zone_minute": 0},
            {"year": 2014, "month_of_year": 1, "day_of_month": 1,
             "hour_of_day": 0, "time_zone_hour": 0, "time_zone_minute": 0},
            "-PT1H"
        ),
        (
            {"year": 2014, "month_of_year": 1, "day_of_month": 1,
             "hour_of_day": 0, "time_zone_hour": 0, "time_zone_minute": 0},
            {"year": 2013, "month_of_year": 12, "day_of_month": 1,
             "hour_of_day": 0, "time_zone_hour": 0, "time_zone_minute": 0},
            "P31D"
        ),
        (
            {"year": 2013, "month_of_year": 12, "day_of_month": 1,
             "hour_of_day": 0, "time_zone_hour": 0, "time_zone_minute": 0},
            {"year": 2014, "month_of_year": 1, "day_of_month": 1,
             "hour_of_day": 0, "time_zone_hour": 0, "time_zone_minute": 0},
            "-P31D"
        ),
        (
            {"year": 44, "month_of_year": 1, "day_of_month": 4,
             "hour_of_day": 5, "minute_of_hour": 1, "second_of_minute": 2,
             "time_zone_hour": 0, "time_zone_minute": 0},
            {"year": 41, "month_of_year": 12, "day_of_month": 2,
             "hour_of_day": 13, "minute_of_hour": 23, "second_of_minute": 1,
             "time_zone_hour": 3, "time_zone_minute": 20},
            "P762DT18H58M1S"
        ),
        (
            {"year": 41, "month_of_year": 12, "day_of_month": 2,
             "hour_of_day": 13, "minute_of_hour": 23, "second_of_minute": 1,
             "time_zone_hour": 3, "time_zone_minute": 20},
            {"year": 44, "month_of_year": 1, "day_of_month": 4,
             "hour_of_day": 5, "minute_of_hour": 1, "second_of_minute": 2,
             "time_zone_hour": 0, "time_zone_minute": 0},
            "-P762DT18H58M1S"
        ),
    ]


def get_duration_subtract_tests():
    """Yield tests for subtracting a duration from a timepoint."""
    return [
        (
            {"year": 2010, "day_of_year": 65,
             # "month_of_year": 3, "day_of_month": 6,
             "hour_of_day": 12, "minute_of_hour": 0, "second_of_minute": 0,
             "time_zone_hour": 0, "time_zone_minute": 0},
            "P6Y",
            {"year": 2004,  # "day_of_year": 65,
             "month_of_year": 3, "day_of_month": 5,
             "hour_of_day": 12, "minute_of_hour": 0, "second_of_minute": 0,
             "time_zone_hour": 0, "time_zone_minute": 0}
        ),
        (
            {"year": 2010, "week_of_year": 10, "day_of_week": 3,
             # "month_of_year": 3, "day_of_month": 10,
             "hour_of_day": 12, "minute_of_hour": 0, "second_of_minute": 0,
             "time_zone_hour": 0, "time_zone_minute": 0},
            "P6Y",
            {"year": 2004,  # "week_of_year": 10, "day_of_week": 3,
             "month_of_year": 3, "day_of_month": 3,
             "hour_of_day": 12, "minute_of_hour": 0, "second_of_minute": 0,
             "time_zone_hour": 0, "time_zone_minute": 0}
        ),
    ]


def get_timerecurrence_expansion_tests():
    """Return test expansion expressions for data.TimeRecurrence."""
    return [
        ("R3/1001-W01-1T00:00:00Z/1002-W52-6T00:00:00-05:30",
         ["1001-W01-1T00:00:00Z", "1001-W53-3T14:45:00Z",
          "1002-W52-6T05:30:00Z"]),
        ("R3/P700D/1957-W01-1T06,5Z",
         ["1953-W10-1T06,5Z", "1955-W05-1T06,5Z", "1957-W01-1T06,5Z"]),
        ("R3/P5DT2,5S/1001-W11-1T00:30:02,5-02:00",
         ["1001-W09-5T00:29:57,5-02:00", "1001-W10-3T00:30:00-02:00",
          "1001-W11-1T00:30:02,5-02:00"]),
        ("R/+000001W457T060000Z/P4M1D",
         ["+000001-W45-7T06:00:00Z", "+000002-W11-2T06:00:00Z",
          "+000002-W28-6T06:00:00Z"]),
        ("R/P4M1DT6M/+002302-002T06:00:00-00:30",
         ["+002302-002T06:00:00-00:30", "+002301-244T05:54:00-00:30",
          "+002301-120T05:48:00-00:30"]),
        ("R/P30Y2DT15H/-099994-02-12T17:00:00-02:30",
         ["-099994-02-12T17:00:00-02:30", "-100024-02-10T02:00:00-02:30",
          "-100054-02-07T11:00:00-02:30"]),
        ("R/-100024-02-10T17:00:00-12:30/PT5.5H",
         ["-100024-02-10T17:00:00-12:30", "-100024-02-10T22:30:00-12:30",
          "-100024-02-11T04:00:00-12:30"])
    ]


def get_timerecurrence_expansion_tests_for_alt_calendar(calendar_mode):
    """Return alternate calendar tests for data.TimeRecurrence."""
    if calendar_mode == "360":
        return get_timerecurrence_expansion_tests_360()
    if calendar_mode == "365":
        return get_timerecurrence_expansion_tests_365()
    if calendar_mode == "366":
        return get_timerecurrence_expansion_tests_366()


def get_timerecurrence_expansion_tests_360():
    """Return test expansion expressions for data.TimeRecurrence."""
    return [
        ("R13/1984-01-30T00Z/P1M",
         ["1984-01-30T00:00:00Z", "1984-02-30T00:00:00Z",
          "1984-03-30T00:00:00Z", "1984-04-30T00:00:00Z",
          "1984-05-30T00:00:00Z", "1984-06-30T00:00:00Z",
          "1984-07-30T00:00:00Z", "1984-08-30T00:00:00Z",
          "1984-09-30T00:00:00Z", "1984-10-30T00:00:00Z",
          "1984-11-30T00:00:00Z", "1984-12-30T00:00:00Z",
          "1985-01-30T00:00:00Z"]),
        ("R2/1984-01-30T00Z/P1D",
         ["1984-01-30T00:00:00Z", "1984-02-01T00:00:00Z"]),
        ("R2/P1D/1984-02-01T00Z",
         ["1984-01-30T00:00:00Z", "1984-02-01T00:00:00Z"]),
        ("R2/P1D/1984-01-01T00Z",
         ["1983-12-30T00:00:00Z", "1984-01-01T00:00:00Z"]),
        ("R2/1983-12-30T00Z/P1D",
         ["1983-12-30T00:00:00Z", "1984-01-01T00:00:00Z"]),
        ("R2/P1D/2005-01-01T00Z",
         ["2004-12-30T00:00:00Z", "2005-01-01T00:00:00Z"]),
        ("R2/2003-12-30T00Z/P1D",
         ["2003-12-30T00:00:00Z", "2004-01-01T00:00:00Z"]),
        ("R2/P1D/2004-01-01T00Z",
         ["2003-12-30T00:00:00Z", "2004-01-01T00:00:00Z"]),
        ("R2/2004-12-30T00Z/P1D",
         ["2004-12-30T00:00:00Z", "2005-01-01T00:00:00Z"]),
        ("R3/P1Y/2005-02-30T00Z",
         ["2003-02-30T00:00:00Z", "2004-02-30T00:00:00Z",
          "2005-02-30T00:00:00Z"]),
        ("R3/2003-02-30T00Z/P1Y",
         ["2003-02-30T00:00:00Z", "2004-02-30T00:00:00Z",
          "2005-02-30T00:00:00Z"]),
    ]


def get_timerecurrence_expansion_tests_365():
    """Return test expansion expressions for data.TimeRecurrence."""
    return [
        ("R13/1984-01-30T00Z/P1M",
         ["1984-01-30T00:00:00Z", "1984-02-28T00:00:00Z",
          "1984-03-28T00:00:00Z", "1984-04-28T00:00:00Z",
          "1984-05-28T00:00:00Z", "1984-06-28T00:00:00Z",
          "1984-07-28T00:00:00Z", "1984-08-28T00:00:00Z",
          "1984-09-28T00:00:00Z", "1984-10-28T00:00:00Z",
          "1984-11-28T00:00:00Z", "1984-12-28T00:00:00Z",
          "1985-01-28T00:00:00Z"]),
        ("R13/1985-01-30T00Z/P1M",
         ["1985-01-30T00:00:00Z", "1985-02-28T00:00:00Z",
          "1985-03-28T00:00:00Z", "1985-04-28T00:00:00Z",
          "1985-05-28T00:00:00Z", "1985-06-28T00:00:00Z",
          "1985-07-28T00:00:00Z", "1985-08-28T00:00:00Z",
          "1985-09-28T00:00:00Z", "1985-10-28T00:00:00Z",
          "1985-11-28T00:00:00Z", "1985-12-28T00:00:00Z",
          "1986-01-28T00:00:00Z"]),
        ("R2/1984-01-30T00Z/P1D",
         ["1984-01-30T00:00:00Z", "1984-01-31T00:00:00Z"]),
        ("R2/P1D/1984-02-01T00Z",
         ["1984-01-31T00:00:00Z", "1984-02-01T00:00:00Z"]),
        ("R2/P1D/1984-01-01T00Z",
         ["1983-12-31T00:00:00Z", "1984-01-01T00:00:00Z"]),
        ("R2/1983-12-30T00Z/P1D",
         ["1983-12-30T00:00:00Z", "1983-12-31T00:00:00Z"]),
        ("R2/2000-02-28T00Z/P1Y1D",
         ["2000-02-28T00:00:00Z", "2001-03-01T00:00:00Z"]),
        ("R2/2001-02-28T00Z/P1Y1D",
         ["2001-02-28T00:00:00Z", "2002-03-01T00:00:00Z"]),
    ]


def get_timerecurrence_expansion_tests_366():
    """Return test expansion expressions for data.TimeRecurrence."""
    return [
        ("R13/1984-01-30T00Z/P1M",
         ["1984-01-30T00:00:00Z", "1984-02-29T00:00:00Z",
          "1984-03-29T00:00:00Z", "1984-04-29T00:00:00Z",
          "1984-05-29T00:00:00Z", "1984-06-29T00:00:00Z",
          "1984-07-29T00:00:00Z", "1984-08-29T00:00:00Z",
          "1984-09-29T00:00:00Z", "1984-10-29T00:00:00Z",
          "1984-11-29T00:00:00Z", "1984-12-29T00:00:00Z",
          "1985-01-29T00:00:00Z"]),
        ("R13/1985-01-30T00Z/P1M",
         ["1985-01-30T00:00:00Z", "1985-02-29T00:00:00Z",
          "1985-03-29T00:00:00Z", "1985-04-29T00:00:00Z",
          "1985-05-29T00:00:00Z", "1985-06-29T00:00:00Z",
          "1985-07-29T00:00:00Z", "1985-08-29T00:00:00Z",
          "1985-09-29T00:00:00Z", "1985-10-29T00:00:00Z",
          "1985-11-29T00:00:00Z", "1985-12-29T00:00:00Z",
          "1986-01-29T00:00:00Z"]),
        ("R2/1984-01-30T00Z/P1D",
         ["1984-01-30T00:00:00Z", "1984-01-31T00:00:00Z"]),
        ("R2/P1D/1984-02-01T00Z",
         ["1984-01-31T00:00:00Z", "1984-02-01T00:00:00Z"]),
        ("R2/P1D/1984-01-01T00Z",
         ["1983-12-31T00:00:00Z", "1984-01-01T00:00:00Z"]),
        ("R2/1983-12-30T00Z/P1D",
         ["1983-12-30T00:00:00Z", "1983-12-31T00:00:00Z"]),
        ("R2/1999-02-28T00Z/P1Y1D",
         ["1999-02-28T00:00:00Z", "2000-02-29T00:00:00Z"]),
        ("R2/2000-02-28T00Z/P1Y1D",
         ["2000-02-28T00:00:00Z", "2001-02-29T00:00:00Z"]),
        ("R2/2001-02-28T00Z/P1Y1D",
         ["2001-02-28T00:00:00Z", "2002-02-29T00:00:00Z"]),
    ]


def get_timerecurrence_membership_tests():
    """Return test membership expressions for data.TimeRecurrence."""
    return [
        ("R3/1001-W01-1T00:00:00Z/1002-W52-6T00:00:00-05:30",
         [("1001-W01-1T00:00:00Z", True),
          ("1000-12-29T00:00:00Z", True),
          ("0901-07-08T12:45:00Z", False),
          ("1001-W01-2T00:00:00Z", False),
          ("1001-W53-3T14:45:00Z", True),
          ("1002-W52-6T05:30:00Z", True),
          ("1002-W52-6T03:30:00-02:00", True),
          ("1002-W52-6T07:30:00+02:00", True),
          ("10030101T00Z", False)]),
        ("R3/P700D/1957-W01-1T06,5Z",
         [("1953-W10-1T06,5Z", True),
          ("1953-03-02T06,5Z", True),
          ("1952-03-02T06,5Z", False),
          ("1955-W05-1T06,5Z", True),
          ("1957-W01-1T06,5Z", True),
          ("1956-366T06,5Z", True),
          ("1956-356T04,5Z", False)]),
    ]


def get_timerecurrenceparser_tests():
    """Yield tests for the time recurrence parser."""
    test_points = ["-100024-02-10T17:00:00-12:30",
                   "+000001-W45-7T06Z", "1001W011",
                   "1955W051T06,5Z", "1999-06-01",
                   "1967-056", "+5002000830T235902,345",
                   "1765-W04"]
    for reps in [None, 1, 2, 3, 10]:
        if reps is None:
            reps_string = ""
        else:
            reps_string = str(reps)
        point_parser = parsers.TimePointParser()
        duration_parser = parsers.DurationParser()
        for point_expr in test_points:
            duration_tests = get_timedurationparser_tests()
            start_point = point_parser.parse(point_expr)
            for duration_expr, _ in duration_tests:
                if duration_expr.startswith("-P"):
                    # Our negative durations are not supported in recurrences.
                    continue
                duration = duration_parser.parse(duration_expr)
                end_point = start_point + duration
                if reps is not None:
                    expr_1 = ("R" + reps_string + "/" + str(start_point) +
                              "/" + str(end_point))
                    yield expr_1, {"repetitions": reps,
                                   "start_point": start_point,
                                   "end_point": end_point}
                expr_3 = ("R" + reps_string + "/" + str(start_point) +
                          "/" + str(duration))
                yield expr_3, {"repetitions": reps,
                               "start_point": start_point,
                               "duration": duration}
                expr_4 = ("R" + reps_string + "/" + str(duration) + "/" +
                          str(end_point))
                yield expr_4, {"repetitions": reps, "duration": duration,
                               "end_point": end_point}


def get_local_time_zone_hours_minutes():
    """Provide an independent method of getting the local time zone."""
    import datetime
    utc_offset = datetime.datetime.now() - datetime.datetime.utcnow()
    # datetime.timedelta represents -21 microseconds as -1 day,
    # +86399 seconds, +999979 microseconds. This is not nice.
    utc_offset_seconds = utc_offset.seconds + 86400 * utc_offset.days
    utc_offset_hours = (utc_offset_seconds + 1800) // 3600
    utc_offset_minutes = (
        ((utc_offset_seconds - 3600 * utc_offset_hours) + 30) // 60
    )
    return utc_offset_hours, utc_offset_minutes


class TestSuite(unittest.TestCase):
    """Test the functionality of parsers and data model manipulation."""

    def assertEqual(self, test, control, info=None):
        """Override the assertEqual method to provide more information."""
        superinfo = None
        if info is not None:
            superinfo = (
                "Source %s produced:\n'%s'\nshould be:\n'%s'" %
                (info, test, control))
        super(TestSuite, self).assertEqual(test, control, superinfo)

    def test_days_in_year_range(self):
        """Test the summing-over-days-in-year-range shortcut code."""
        for start_year in range(-401, 2):
            for end_year in range(start_year, 2):
                test_days = data.get_days_in_year_range(
                    start_year, end_year)
                control_days = 0
                for year in xrange(start_year, end_year + 1):
                    control_days += data.get_days_in_year(year)
                self.assertEqual(
                    control_days, test_days, "days in %s to %s" % (
                        start_year, end_year)
                )

    def test_largest_truncated_property_name(self):
        """Test the largest truncated property name."""

        parser = parsers.TimePointParser(
            allow_truncated=True)

        truncated_property_tests = get_truncated_property_tests()
        for expression in truncated_property_tests.keys():
            try:
                test_data = parser.parse(expression)
            except parsers.ISO8601SyntaxError as syn_exc:
                raise ValueError("Parsing failed for {0}: {1}".format(
                    expression, syn_exc))

            self.assertEqual(
                test_data.get_largest_truncated_property_name(),
                truncated_property_tests[expression]
                ["largest_truncated_property_name"],
                info=expression)

    def test_smallest_missing_property_name(self):
        """Test the smallest missing property name."""

        parser = parsers.TimePointParser(
            allow_truncated=True)

        truncated_property_tests = get_truncated_property_tests()
        for expression in truncated_property_tests.keys():
            try:
                test_data = parser.parse(expression)
            except parsers.ISO8601SyntaxError as syn_exc:
                raise ValueError("Parsing failed for {0}: {1}".format(
                    expression, syn_exc))

            self.assertEqual(
                test_data.get_smallest_missing_property_name(),
                truncated_property_tests[expression]
                ["smallest_missing_property_name"],
                info=expression)

    def test_timeduration(self):
        """Test the duration class methods."""
        for test_props, method, method_args, ctrl_results in (
                get_timeduration_tests()):
            duration = data.Duration(**test_props)
            duration_method = getattr(duration, method)
            test_results = duration_method(*method_args)
            self.assertEqual(
                test_results, ctrl_results,
                "%s -> %s(%s)" % (test_props, method, method_args)
            )

    def test_timeduration_parser(self):
        """Test the duration parsing."""
        parser = parsers.DurationParser()
        for expression, ctrl_result in get_timedurationparser_tests():
            try:
                test_result = str(parser.parse(expression))
            except parsers.ISO8601SyntaxError:
                raise ValueError(
                    "DurationParser test failed to parse '%s'" %
                    expression
                )
            self.assertEqual(test_result, ctrl_result, expression)

    def test_timeduration_dumper(self):
        """Test the duration dumping."""
        for ctrl_expression, test_props in get_timedurationdumper_tests():
            duration = data.Duration(**test_props)
            test_expression = str(duration)
            self.assertEqual(test_expression, ctrl_expression,
                             str(test_props))

    def test_timeduration_add_week(self):
        """Test the duration not in weeks add duration in weeks."""
        self.assertEqual(
            str(data.Duration(days=7) + data.Duration(weeks=1)),
            "P14D")

    @staticmethod
    def test_timepoint():
        """Test the time point data model (takes a while)."""
        pool = multiprocessing.Pool(processes=4)
        pool.map_async(test_timepoint_at_year, range(1801, 2403)).get()

    def test_timepoint_plus_float_time_duration_day_of_month_type(self):
        """Test (TimePoint + Duration).day_of_month is an int."""
        time_point = data.TimePoint(year=2000) + data.Duration(seconds=1.0)
        self.assertEqual(type(time_point.day_of_month), int)

    def test_timepoint_subtract(self):
        """Test subtracting one time point from another."""
        for test_props1, test_props2, ctrl_string in (
                get_timepoint_subtract_tests()):
            point1 = data.TimePoint(**test_props1)
            point2 = data.TimePoint(**test_props2)
            test_string = str(point1 - point2)
            self.assertEqual(test_string, ctrl_string,
                             "%s - %s" % (point1, point2))

    def test_duration_subtract(self):
        """Test subtracting a duration from a timepoint."""
        parser = parsers.DurationParser()
        for my_timepoint, my_duration, my_result in (
                get_duration_subtract_tests()):
            start_point = data.TimePoint(**my_timepoint)
            test_duration = parser.parse(my_duration)
            end_point = data.TimePoint(**my_result)
            test_subtract = (start_point - test_duration).to_calendar_date()
            self.assertEqual(str(test_subtract), str(end_point),
                             "%s - %s" % (start_point, test_duration))

    def test_timepoint_time_zone(self):
        """Test the time zone handling of timepoint instances."""
        year = 2000
        month_of_year = 1
        day_of_month = 1
        utc_offset_hours, utc_offset_minutes = (
            get_local_time_zone_hours_minutes()
        )
        for hour_of_day in range(24):
            for minute_of_hour in [0, 30]:
                test_dates = [
                    data.TimePoint(
                        year=year,
                        month_of_year=month_of_year,
                        day_of_month=day_of_month,
                        hour_of_day=hour_of_day,
                        minute_of_hour=minute_of_hour
                    )
                ]
                test_dates.append(test_dates[0].copy())
                test_dates.append(test_dates[0].copy())
                test_dates.append(test_dates[0].copy())
                test_dates[0].set_time_zone_to_utc()
                self.assertEqual(test_dates[0].time_zone.hours, 0,
                                 test_dates[0])
                self.assertEqual(test_dates[0].time_zone.minutes, 0,
                                 test_dates[0])
                test_dates[1].set_time_zone_to_local()
                self.assertEqual(test_dates[1].time_zone.hours,
                                 utc_offset_hours, test_dates[1])

                self.assertEqual(test_dates[1].time_zone.minutes,
                                 utc_offset_minutes, test_dates[1])
                test_dates[2].set_time_zone(
                    data.TimeZone(hours=-13, minutes=-45))

                test_dates[3].set_time_zone(
                    data.TimeZone(hours=8, minutes=30))
                for i_test_date in list(test_dates):
                    i_test_date_str = str(i_test_date)
                    date_no_tz = i_test_date.copy()
                    date_no_tz.time_zone = data.TimeZone(hours=0, minutes=0)
                    if (i_test_date.time_zone.hours >= 0 or
                            i_test_date.time_zone.minutes >= 0):
                        utc_offset = date_no_tz - i_test_date
                    else:
                        utc_offset = (i_test_date - date_no_tz) * -1
                    self.assertEqual(utc_offset.hours,
                                     i_test_date.time_zone.hours,
                                     i_test_date_str + " utc offset (hrs)")
                    self.assertEqual(utc_offset.minutes,
                                     i_test_date.time_zone.minutes,
                                     i_test_date_str + " utc offset (mins)")
                    for j_test_date in list(test_dates):
                        j_test_date_str = str(j_test_date)
                        self.assertEqual(
                            i_test_date, j_test_date,
                            i_test_date_str + " == " + j_test_date_str)
                        duration = j_test_date - i_test_date
                        self.assertEqual(
                            duration, data.Duration(days=0),
                            i_test_date_str + " - " + j_test_date_str)

    def test_timepoint_dumper(self):
        """Test the dumping of TimePoint instances."""
        parser = parsers.TimePointParser(allow_truncated=True,
                                         default_to_unknown_time_zone=True)
        dumper = dumpers.TimePointDumper()
        for expression, timepoint_kwargs in get_timepointparser_tests(
                allow_truncated=True):
            ctrl_timepoint = data.TimePoint(**timepoint_kwargs)
            try:
                test_timepoint = parser.parse(str(ctrl_timepoint))
            except parsers.ISO8601SyntaxError as syn_exc:
                raise ValueError(
                    "Parsing failed for the dump of {0}: {1}".format(
                        expression, syn_exc))
            self.assertEqual(test_timepoint,
                             ctrl_timepoint, expression)
        for timepoint_kwargs, format_results in (
                get_timepoint_dumper_tests()):
            ctrl_timepoint = data.TimePoint(**timepoint_kwargs)
            for format_, ctrl_data in format_results:
                test_data = dumper.dump(ctrl_timepoint, format_)
                self.assertEqual(test_data, ctrl_data, format_)
        for timepoint_kwargs, format_exception_results in (
                get_timepointdumper_failure_tests()):
            ctrl_timepoint = data.TimePoint(**timepoint_kwargs)
            for format_, ctrl_exception, num_expanded_year_digits in (
                    format_exception_results):
                dumper = dumpers.TimePointDumper(
                    num_expanded_year_digits=num_expanded_year_digits)
                self.assertRaises(ctrl_exception, dumper.dump,
                                  ctrl_timepoint, format_)
        value_error_timepoint = data.TimePoint(minute_of_hour=10)
        value_error_timepoint.minute_of_hour = "1O"
        self.assertRaises(ValueError, dumper.dump, value_error_timepoint, "%M")

    def test_timepoint_dumper_bounds_error_message(self):
        """Test the exception text contains the information expected"""
        the_error = dumpers.TimePointDumperBoundsError("TimePoint1", "year",
                                                       10, 20)
        the_string = the_error.__str__()
        self.assertTrue("TimePoint1" in the_string,
                        "Failed to find TimePoint1 in {}".format(the_string))
        self.assertTrue("year" in the_string,
                        "Failed to find TimePoint1 in {}".format(the_string))
        self.assertTrue("10" in the_string,
                        "Failed to find TimePoint1 in {}".format(the_string))
        self.assertTrue("20" in the_string,
                        "Failed to find TimePoint1 in {}".format(the_string))

    get_test_timepoint_dumper_get_time_zone = [
        ["+250:00", None],
        ["+25:00", ('25', '00')],
        ["+12:00", ('12', '00')],
        ["+12:45", ('12', '45')],
        ["+01:00", ('01', '00')],
        ["Z", (0, 0)],
        ["-03:00", (-3, 0)],
        ["-03:30", (-3, -30)],
        ["-11:00", (-11, 0)],
        ["+00:00", ('00', '00')],
        ["-00:00", (0, 0)]
    ]

    def test_timepoint_dumper_get_time_zone(self):
        """Test the time zone returned by TimerPointDumper.get_time_zone"""
        dumper = dumpers.TimePointDumper(num_expanded_year_digits=2)
        for value, expected in self.get_test_timepoint_dumper_get_time_zone:
            tz = dumper.get_time_zone(value)
            self.assertEqual(expected, tz)

    def test_timepoint_dumper_after_copy(self):
        """Test that printing the TimePoint attributes works after it has
        been copied, see issue #102 for more information"""
        time_point = data.TimePoint(year=2000, truncated=True,
                                    truncated_dump_format='CCYY')
        the_copy = time_point.copy()
        self.assertEqual(str(time_point), str(the_copy))

    def test_timepoint_parser(self):
        """Test the parsing of date/time expressions."""

        # Test unknown time zone assumptions.
        parser = parsers.TimePointParser(
            allow_truncated=True,
            default_to_unknown_time_zone=True)
        for expression, timepoint_kwargs in get_timepointparser_tests(
                allow_truncated=True):
            timepoint_kwargs = copy.deepcopy(timepoint_kwargs)
            try:
                test_data = str(parser.parse(expression))
            except parsers.ISO8601SyntaxError as syn_exc:
                raise ValueError("Parsing failed for {0}: {1}".format(
                    expression, syn_exc))
            ctrl_data = str(data.TimePoint(**timepoint_kwargs))
            self.assertEqual(test_data, ctrl_data, expression)
            ctrl_data = expression
            test_data = str(parser.parse(expression, dump_as_parsed=True))
            self.assertEqual(test_data, ctrl_data, expression)

        # Test local time zone assumptions (the default).
        utc_offset_hours, utc_offset_minutes = (
            get_local_time_zone_hours_minutes()
        )
        parser = parsers.TimePointParser(allow_truncated=True)
        for expression, timepoint_kwargs in get_timepointparser_tests(
                allow_truncated=True, skip_time_zones=True):
            timepoint_kwargs = copy.deepcopy(timepoint_kwargs)
            try:
                test_timepoint = parser.parse(expression)
            except parsers.ISO8601SyntaxError as syn_exc:
                raise ValueError("Parsing failed for {0}: {1}".format(
                    expression, syn_exc))
            test_data = (test_timepoint.time_zone.hours,
                         test_timepoint.time_zone.minutes)
            ctrl_data = (utc_offset_hours, utc_offset_minutes)
            self.assertEqual(test_data, ctrl_data,
                             "Local time zone for " + expression)

        # Test given time zone assumptions.
        utc_offset_hours, utc_offset_minutes = (
            get_local_time_zone_hours_minutes()
        )
        given_utc_offset_hours = -2  # This is an arbitrary number!
        if given_utc_offset_hours == utc_offset_hours:
            # No point testing this twice, change it.
            given_utc_offset_hours = -3
        given_utc_offset_minutes = -15
        given_time_zone_hours_minutes = (
            given_utc_offset_hours, given_utc_offset_minutes)
        parser = parsers.TimePointParser(
            allow_truncated=True,
            assumed_time_zone=given_time_zone_hours_minutes
        )
        for expression, timepoint_kwargs in get_timepointparser_tests(
                allow_truncated=True, skip_time_zones=True):
            timepoint_kwargs = copy.deepcopy(timepoint_kwargs)
            try:
                test_timepoint = parser.parse(expression)
            except parsers.ISO8601SyntaxError as syn_exc:
                raise ValueError("Parsing failed for {0}: {1}".format(
                    expression, syn_exc))
            test_data = (test_timepoint.time_zone.hours,
                         test_timepoint.time_zone.minutes)
            ctrl_data = given_time_zone_hours_minutes
            self.assertEqual(test_data, ctrl_data,
                             "A given time zone for " + expression)

        # Test UTC time zone assumptions.
        parser = parsers.TimePointParser(
            allow_truncated=True,
            assumed_time_zone=(0, 0)
        )
        for expression, timepoint_kwargs in get_timepointparser_tests(
                allow_truncated=True, skip_time_zones=True):
            timepoint_kwargs = copy.deepcopy(timepoint_kwargs)
            try:
                test_timepoint = parser.parse(expression)
            except parsers.ISO8601SyntaxError as syn_exc:
                raise ValueError("Parsing failed for {0}: {1}".format(
                    expression, syn_exc))
            test_data = (test_timepoint.time_zone.hours,
                         test_timepoint.time_zone.minutes)
            ctrl_data = (0, 0)
            self.assertEqual(test_data, ctrl_data,
                             "UTC for " + expression)

    def test_timepoint_strftime_strptime(self):
        """Test the strftime/strptime for date/time expressions."""
        import datetime
        parser = parsers.TimePointParser()
        parse_tokens = parser_spec.STRFTIME_TRANSLATE_INFO.keys()
        parse_tokens.remove("%z")  # Don't test datetime's tz handling.
        format_string = ""
        for i, token in enumerate(parse_tokens):
            format_string += token
            if i % 2 == 0:
                format_string += " "
            if i % 3 == 0:
                format_string += ":"
            if i % 5 == 0:
                format_string += "?foobar"
            if i % 7 == 0:
                format_string += "++("
        strftime_string = format_string
        strptime_strings = [format_string]
        for key in parser_spec.STRPTIME_EXCLUSIVE_GROUP_INFO.keys():
            strptime_strings[-1] = strptime_strings[-1].replace(key, "")
        strptime_strings.append(format_string)
        for values in parser_spec.STRPTIME_EXCLUSIVE_GROUP_INFO.values():
            for value in values:
                strptime_strings[-1] = strptime_strings[-1].replace(value, "")
        ctrl_date = datetime.datetime(2002, 3, 1, 12, 30, 2)

        # Test %z dumping.
        for sign in [1, -1]:
            for hour in range(0, 24):
                for minute in range(0, 59):
                    if hour == 0 and minute == 0 and sign == -1:
                        # -0000, same as +0000, but invalid.
                        continue
                    test_date = data.TimePoint(
                        year=ctrl_date.year,
                        month_of_year=ctrl_date.month,
                        day_of_month=ctrl_date.day,
                        hour_of_day=ctrl_date.hour,
                        minute_of_hour=ctrl_date.minute,
                        second_of_minute=ctrl_date.second,
                        time_zone_hour=sign * hour,
                        time_zone_minute=sign * minute
                    )
                    ctrl_string = "-" if sign == -1 else "+"
                    ctrl_string += "%02d%02d" % (hour, minute)
                    self.assertEqual(test_date.strftime("%z"),
                                     ctrl_string,
                                     "%z for " + str(test_date))

        test_date = data.TimePoint(
            year=ctrl_date.year,
            month_of_year=ctrl_date.month,
            day_of_month=ctrl_date.day,
            hour_of_day=ctrl_date.hour,
            minute_of_hour=ctrl_date.minute,
            second_of_minute=ctrl_date.second
        )
        for test_date in [test_date, test_date.copy().to_week_date(),
                          test_date.copy().to_ordinal_date()]:
            ctrl_data = ctrl_date.strftime(strftime_string)
            test_data = test_date.strftime(strftime_string)
            self.assertEqual(test_data, ctrl_data, strftime_string)
            for strptime_string in strptime_strings:
                ctrl_dump = ctrl_date.strftime(strptime_string)
                test_dump = test_date.strftime(strptime_string)
                self.assertEqual(test_dump, ctrl_dump, strptime_string)
                if "%s" in strptime_string:
                    # The datetime library can't handle this for strptime!
                    ctrl_data = ctrl_date
                else:
                    ctrl_data = datetime.datetime.strptime(
                        ctrl_dump, strptime_string)
                test_data = parser.strptime(test_dump, strptime_string)

                ctrl_data = (
                    ctrl_data.year, ctrl_data.month, ctrl_data.day,
                    ctrl_data.hour, ctrl_data.minute, ctrl_data.second
                )
                test_data = tuple(list(test_data.get_calendar_date()) +
                                  list(test_data.get_hour_minute_second()))
                if "%y" in strptime_string:
                    # %y is the decadal year (00 to 99) within a century.
                    # The datetime library, for some reason, sets a default
                    # century of '2000' - so nuke this extra information.
                    ctrl_data = tuple([ctrl_data[0] % 100] +
                                      list(ctrl_data[1:]))
                self.assertEqual(test_data, ctrl_data, test_dump + "\n" +
                                 strptime_string)

    def test_timerecurrence_alt_calendars(self):
        """Test recurring date/time series for alternate calendars."""
        for calendar_mode in ["360", "365", "366"]:
            data.CALENDAR.set_mode(calendar_mode + "day")
            self.assertEqual(
                data.CALENDAR.mode,
                getattr(data.Calendar, "MODE_%s" % calendar_mode)
            )
            parser = parsers.TimeRecurrenceParser()
            tests = get_timerecurrence_expansion_tests_for_alt_calendar(
                calendar_mode)
            for expression, ctrl_results in tests:
                try:
                    test_recurrence = parser.parse(expression)
                except parsers.ISO8601SyntaxError:
                    raise ValueError(
                        "TimeRecurrenceParser test failed to parse '%s'" %
                        expression
                    )
                test_results = []
                for time_point in test_recurrence:
                    test_results.append(str(time_point))
                self.assertEqual(test_results, ctrl_results,
                                 expression + "(%s)" % calendar_mode)
            data.CALENDAR.set_mode()
            self.assertEqual(data.CALENDAR.mode,
                             data.Calendar.MODE_GREGORIAN)

    def test_timerecurrence(self):
        """Test the recurring date/time series data model."""
        parser = parsers.TimeRecurrenceParser()
        for expression, ctrl_results in get_timerecurrence_expansion_tests():
            try:
                test_recurrence = parser.parse(expression)
            except parsers.ISO8601SyntaxError:
                raise ValueError(
                    "TimeRecurrenceParser test failed to parse '%s'" %
                    expression
                )
            test_results = []
            for i, time_point in enumerate(test_recurrence):
                if i > 2:
                    break
                test_results.append(str(time_point))
            self.assertEqual(test_results, ctrl_results, expression)
            if test_recurrence.start_point is None:
                forward_method = test_recurrence.get_prev
                backward_method = test_recurrence.get_next
            else:
                forward_method = test_recurrence.get_next
                backward_method = test_recurrence.get_prev
            test_points = [test_recurrence[0]]
            test_points.append(forward_method(test_points[-1]))
            test_points.append(forward_method(test_points[-1]))
            test_results = [str(point) for point in test_points]
            self.assertEqual(test_results, ctrl_results, expression)
            if test_recurrence[2] is not None:
                test_points = [test_recurrence[2]]
                test_points.append(backward_method(test_points[-1]))
                test_points.append(backward_method(test_points[-1]))
                test_points.append(backward_method(test_points[-1]))
            self.assertEqual(test_points[3], None, expression)
            test_points.pop(3)
            test_points.reverse()
            test_results = [str(point) for point in test_points]
            self.assertEqual(test_results, ctrl_results, expression)

        for expression, results in get_timerecurrence_membership_tests():
            try:
                test_recurrence = parser.parse(expression)
            except parsers.ISO8601SyntaxError:
                raise ValueError(
                    "TimeRecurrenceParser test failed to parse '%s'" %
                    expression
                )
            for timepoint_expression, ctrl_is_member in results:
                timepoint = parsers.parse_timepoint_expression(
                    timepoint_expression)
                test_is_member = test_recurrence.get_is_valid(timepoint)
                self.assertEqual(test_is_member, ctrl_is_member,
                                 timepoint_expression + " in " + expression)

    def test_timerecurrence_parser(self):
        """Test the recurring date/time series parsing."""
        parser = parsers.TimeRecurrenceParser()
        for expression, test_info in get_timerecurrenceparser_tests():
            try:
                test_data = str(parser.parse(expression))
            except parsers.ISO8601SyntaxError:
                raise ValueError("Parsing failed for %s" % expression)
            ctrl_data = str(data.TimeRecurrence(**test_info))
            self.assertEqual(test_data, ctrl_data, expression)

    def test_util_cache(self):
        """Test the cache provided in the util file"""
        # here we change the cache size to simplify testing when cache is full
        util.MAX_CACHE_SIZE = 2

        class TempClass(object):
            times_called = 0

            @util.cache_results
            def sum(self, x, y):
                self.times_called += 1
                return x + y
        temp_class = TempClass()
        # call it twice, filling the cache
        self.assertEqual(3, temp_class.sum(1, 2))
        self.assertEqual(3, temp_class.sum(2, 1))
        # next two calls are cached
        self.assertEqual(3, temp_class.sum(1, 2))
        self.assertEqual(3, temp_class.sum(2, 1))
        # this call should remove element from cache
        self.assertEqual(2, temp_class.sum(1, 1))
        # in total, we have only three calls, as 2 were cached!
        self.assertEqual(3, temp_class.times_called)

    # data provider for the test test_get_local_time_zone_no_dst
    # the format for the parameters is
    # [tz_seconds, expected_hours, expected_minutes]]
    get_local_time_zone_no_dst = [
        [45900, 12, 45],  # pacific/chatham, +12:45
        [20700, 5, 45],  # asia/kathmandu, +05:45
        [3600, 1, 0],  # arctic/longyearbyen, +01:00
        [0, 0, 0],  # UTC
        [-10800, -3, 0],  # america/sao_paulo, -03:00
        [-12600, -3, 30]  # america/st_johns, -03:30
    ]

    @mock.patch('isodatetime.timezone.time')
    def test_get_local_time_zone_no_dst(self, mock_time):
        """Test that the hour/minute returned is correct.

        Parts of the time module are mocked so that we can specify scenarios
        without daylight saving time."""
        for tz_seconds, expected_hours, expected_minutes in \
                self.get_local_time_zone_no_dst:
            # for a pre-defined timezone
            mock_time.timezone.__neg__.return_value = tz_seconds
            # time without dst
            mock_time.daylight = False
            # and localtime also without dst
            mock_localtime = mock.Mock()
            mock_time.localtime.return_value = mock_localtime
            mock_localtime.tm_isdst = 0
            hours, minutes = timezone.get_local_time_zone()
            self.assertEqual(expected_hours, hours)
            self.assertEqual(expected_minutes, minutes)

    # data provider for the test test_get_local_time_zone_with_dst
    # the format for the parameters is
    # [tz_seconds, tz_alt_seconds, expected_hours, expected_minutes]
    get_local_time_zone_with_dst = [
        [45900, 49500, 13, 45],  # pacific/chatham, +12:45 and +13:45
        [43200, 46800, 13, 0],  # pacific/auckland, +12:00 and +13:00
        [3600, 7200, 2, 0],  # arctic/longyearbyen, +01:00
        [0, 0, 0, 0],  # UTC
        [-10800, -7200, -2, 0],  # america/sao_paulo, -03:00 and -02:00,
        [-12600, -9000, -2, 30]  # america/st_johns, -03:30 and -02:30
    ]

    @mock.patch('isodatetime.timezone.time')
    def test_get_local_time_zone_with_dst(self, mock_time):
        """Test that the hour/minute returned is correct

        Parts of the time module are mocked so that we can specify scenarios
        with daylight saving time."""
        for tz_seconds, tz_alt_seconds, expected_hours, expected_minutes in \
                self.get_local_time_zone_with_dst:
            # for a pre-defined timezone
            mock_time.timezone.__neg__.return_value = tz_seconds
            # time without dst
            mock_time.daylight = True
            # and localtime also without dst
            mock_localtime = mock.MagicMock()
            mock_time.localtime.return_value = mock_localtime
            mock_localtime.tm_isdst = 1
            # and with the following alternative time for when dst is set
            mock_time.altzone.__neg__.return_value = tz_alt_seconds
            hours, minutes = timezone.get_local_time_zone()
            self.assertEqual(expected_hours, hours)
            self.assertEqual(expected_minutes, minutes)

    # data provider for the test test_get_local_time_zone_format
    # the format for the parameters is
    # [tz_seconds, tz_format_mode, expected_format]
    get_test_get_local_time_zone_format = [
        # UTC
        # UTC, returns Z, flags are never used for UTC
        [0, timezone.TimeZoneFormatMode.normal, "Z"],
        # Positive values, some with minutes != 0
        # asia/macao, +08:00
        [28800, timezone.TimeZoneFormatMode.normal, "+0800"],
        # asia/macao, +08:00
        [28800, timezone.TimeZoneFormatMode.extended, "+08:00"],
        # asia/macao, +08:00
        [28800, timezone.TimeZoneFormatMode.reduced, "+08"],
        # pacific/chatham, +12:45
        [45900, timezone.TimeZoneFormatMode.normal, "+1245"],
        # pacific/chatham, +12:45
        [45900, timezone.TimeZoneFormatMode.extended, "+12:45"],
        # pacific/chatham, +12:45
        [45900, timezone.TimeZoneFormatMode.reduced, "+1245"],
        # Negative values, some with minutes != 0
        # america/buenos_aires, -03:00
        [-10800, timezone.TimeZoneFormatMode.normal, "-0300"],
        # america/buenos_aires, -03:00
        [-10800, timezone.TimeZoneFormatMode.extended, "-03:00"],
        # america/buenos_aires, -03:00
        [-10800, timezone.TimeZoneFormatMode.reduced, "-03"],
        # america/st_johns, -03:30
        [-12600, timezone.TimeZoneFormatMode.normal, "-0330"],
        # america/st_johns, -03:30
        [-12600, timezone.TimeZoneFormatMode.extended, "-03:30"],
        # america/st_johns, -03:30
        [-12600, timezone.TimeZoneFormatMode.reduced, "-0330"]
    ]

    @mock.patch('isodatetime.timezone.time')
    def test_get_local_time_zone_format(self, mock_time):
        """Test that the UTC offset string format is correct

        Parts of the time module are mocked so that we can specify scenarios
        with certain timezone seconds offsets. DST is not really
        important for this test case"""
        for tz_seconds, tz_format_mode, expected_format in \
                self.get_test_get_local_time_zone_format:
            # for a pre-defined timezone
            mock_time.timezone.__neg__.return_value = tz_seconds
            # time without dst
            mock_time.daylight = False
            # and localtime also without dst
            mock_localtime = mock.Mock()
            mock_time.localtime.return_value = mock_localtime
            mock_localtime.tm_isdst = 0
            tz_format = timezone.get_local_time_zone_format(tz_format_mode)
            self.assertEqual(expected_format, tz_format)

    def test_duration_to_weeks(self):
        """Test that the duration does not lose precision when converted
        from days"""
        duration_in_days = data.Duration(days=365)
        duration_in_days.to_weeks()
        duration_in_weeks = data.Duration(weeks=52)
        self.assertEqual(duration_in_days.weeks, duration_in_weeks.weeks)

    def test_duration_floordiv(self):
        """Test the existing dunder floordir, which will be removed when we
        move to Python 3"""
        duration = data.Duration(years=4, months=4, days=4, hours=4,
                                 minutes=4, seconds=4)
        duration //= 2
        self.assertEqual(2, duration.years)
        self.assertEqual(2, duration.months)
        self.assertEqual(2, duration.days)
        self.assertEqual(2, duration.hours)
        self.assertEqual(2, duration.minutes)
        self.assertEqual(2, duration.seconds)

    def test_duration_in_weeks_floordiv(self):
        """Test the existing dunder floordir, which will be removed when we
        move to Python 3"""
        duration = data.Duration(weeks=4)
        duration //= 2
        self.assertEqual(2, duration.weeks)

    def test_timepoint_add_duration(self):
        """Test adding a duration to a timepoint"""
        seconds_added = 5
        timepoint = data.TimePoint(year=1900, month_of_year=1, day_of_month=1,
                                   hour_of_day=1, minute_of_hour=1)
        duration = data.Duration(seconds=seconds_added)
        t = timepoint + duration
        self.assertEqual(seconds_added, t.second_of_minute)

    def test_timepoint_add_duration_without_minute(self):
        """Test adding a duration to a timepoint"""
        seconds_added = 5
        timepoint = data.TimePoint(year=1900, month_of_year=1, day_of_month=1,
                                   hour_of_day=1)
        duration = data.Duration(seconds=seconds_added)
        t = timepoint + duration
        self.assertEqual(seconds_added, t.second_of_minute)


def assert_equal(data1, data2):
    """A function-level equivalent of the unittest method."""
    if data1 != data2:
        raise AssertionError()


def test_timepoint_at_year(test_year):
    """Test the TimePoint and Calendar data model over a given year."""
    import datetime
    import random
    my_date = datetime.datetime(test_year, 1, 1)
    stop_date = datetime.datetime(test_year + 1, 1, 1)
    test_duration_attributes = [
        ("weeks", 110),
        ("days", 770),
        ("hours", 770 * 24),
        ("minutes", 770 * 24 * 60),
        ("seconds", 770 * 24 * 60 * 60)
    ]
    while my_date <= stop_date:
        ctrl_data = my_date.isocalendar()
        test_date = data.TimePoint(
            year=my_date.year,
            month_of_year=my_date.month,
            day_of_month=my_date.day
        )
        test_week_date = test_date.to_week_date()
        test_data = test_week_date.get_week_date()
        assert_equal(test_data, ctrl_data)
        ctrl_data = (my_date.year, my_date.month, my_date.day)
        test_data = test_week_date.get_calendar_date()
        assert_equal(test_data, ctrl_data)
        ctrl_data = my_date.toordinal()
        year, day_of_year = test_date.get_ordinal_date()
        test_data = day_of_year
        test_data += data.get_days_since_1_ad(year - 1)
        assert_equal(test_data, ctrl_data)
        for attribute, attr_max in test_duration_attributes:
            kwargs = {attribute: random.randrange(0, attr_max)}
            ctrl_data = my_date + datetime.timedelta(**kwargs)
            ctrl_data = (ctrl_data.year, ctrl_data.month, ctrl_data.day)
            test_data = (
                test_date + data.Duration(
                    **kwargs)).get_calendar_date()
            assert_equal(test_data, ctrl_data)
            ctrl_data = (my_date - datetime.timedelta(**kwargs))
            ctrl_data = (ctrl_data.year, ctrl_data.month, ctrl_data.day)
            test_data = (
                test_date - data.Duration(
                    **kwargs)).get_calendar_date()
            assert_equal(test_data, ctrl_data)
        kwargs = {}
        for attribute, attr_max in test_duration_attributes:
            kwargs[attribute] = random.randrange(0, attr_max)
        test_date_minus = (
            test_date - data.Duration(**kwargs))
        test_data = test_date - test_date_minus
        ctrl_data = data.Duration(**kwargs)
        assert_equal(test_data, ctrl_data)
        test_data = (test_date_minus + (test_date - test_date_minus))
        ctrl_data = test_date
        assert_equal(test_data, ctrl_data)
        test_data = (test_date_minus + data.Duration(**kwargs))
        ctrl_data = test_date
        assert_equal(test_data, ctrl_data)
        ctrl_data = (my_date + datetime.timedelta(minutes=450) +
                     datetime.timedelta(hours=5) -
                     datetime.timedelta(seconds=500, weeks=5))
        ctrl_data = [(ctrl_data.year, ctrl_data.month, ctrl_data.day),
                     (ctrl_data.hour, ctrl_data.minute, ctrl_data.second)]
        test_data = (
            test_date + data.Duration(minutes=450) +
            data.Duration(hours=5) -
            data.Duration(weeks=5, seconds=500)
        )
        test_data = [test_data.get_calendar_date(),
                     test_data.get_hour_minute_second()]
        assert_equal(test_data, ctrl_data)
        timedelta = datetime.timedelta(days=1)
        my_date += timedelta


if __name__ == "__main__":
    unittest.TextTestRunner(verbosity=2).run(
        unittest.TestLoader().loadTestsFromTestCase(TestSuite))
