# -*- coding: utf-8 -*-
"""
Module containing a UTC-based datetime class.

:copyright:
    The ObsPy Development Team (devs@obspy.org)
:license:
    GNU Lesser General Public License, Version 3
    (https://www.gnu.org/copyleft/lesser.html)
"""
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
from future.builtins import *  # NOQA @UnusedWildImport
from future.utils import native_str

import datetime
import math
import re
import sys
import time

import numpy as np


# Regular expression used in the init function of the UTCDateTime objects which
# is called a lot. Thus pre-compile it.
_YEAR0REGEX = re.compile(r"^(\d{1,3}[-/,])(.*)$")

TIMESTAMP0 = datetime.datetime(1970, 1, 1, 0, 0)
# XXX the strftime problem seems to be specific to Python < 3.2
# XXX so this can be removed after dropping Python 2 support
STRFTIME_MAPPING = (
    ('%Y', '04d', 'year', None),
    ('%m', '02d', 'month', None),
    ('%d', '02d', 'day', None),
    ('%H', '02d', 'hour', None),
    ('%M', '02d', 'minute', None),
    ('%S', '02d', 'second', None),
    ('%f', '06d', 'microsecond', None),
    ('%y', '02d', 'year', lambda x: x % 100),
    )


class UTCDateTime(object):
    """
    A UTC-based datetime object.

    This datetime class is based on the POSIX time, a system for describing
    instants in time, defined as the number of seconds elapsed since midnight
    Coordinated Universal Time (UTC) of Thursday, January 1, 1970. Using a
    single float timestamp allows higher precision as the default Python
    :class:`datetime.datetime` class. It features the full `ISO8601:2004`_
    specification and some additional string patterns during object
    initialization.

    :type args: int, float, str, :class:`datetime.datetime`, optional
    :param args: The creation of a new `UTCDateTime` object depends from the
        given input parameters. All possible options are summarized in the
        `Examples`_ section below.
    :type iso8601: bool, optional
    :param iso8601: Enforce `ISO8601:2004`_ detection. Works only with a string
        as first input argument.
    :type precision: int, optional
    :param precision: Sets the precision used by the rich comparison operators.
        Defaults to ``6`` digits after the decimal point. See also `Precision`_
        section below.

    .. versionchanged:: 0.5.1
        UTCDateTime is no longer based on Python's datetime.datetime class
        instead uses timestamp as a single floating point value which allows
        higher precision.

    .. rubric:: Supported Operations

    ``UTCDateTime = UTCDateTime + delta``
        Adds/removes ``delta`` seconds (given as int or float) to/from the
        current ``UTCDateTime`` object and returns a new ``UTCDateTime``
        object.
        See also: :meth:`~obspy.core.utcdatetime.UTCDateTime.__add__`.

    ``delta = UTCDateTime - UTCDateTime``
        Calculates the time difference in seconds between two ``UTCDateTime``
        objects. The time difference is given as float data type and may also
        contain a negative number.
        See also: :meth:`~obspy.core.utcdatetime.UTCDateTime.__sub__`.

    .. rubric:: _`Examples`

    (1) Using a timestamp.

        >>> UTCDateTime(0)
        UTCDateTime(1970, 1, 1, 0, 0)

        >>> UTCDateTime(1240561632)
        UTCDateTime(2009, 4, 24, 8, 27, 12)

        >>> UTCDateTime(1240561632.5)
        UTCDateTime(2009, 4, 24, 8, 27, 12, 500000)

    (2) Using a `ISO8601:2004`_ string. The detection may be enforced by
        setting the ``iso8601`` parameter to True.

        * Calendar date representation.

            >>> UTCDateTime("2009-12-31T12:23:34.5")
            UTCDateTime(2009, 12, 31, 12, 23, 34, 500000)

            >>> UTCDateTime("20091231T122334.5")           # compact
            UTCDateTime(2009, 12, 31, 12, 23, 34, 500000)

            >>> UTCDateTime("2009-12-31T12:23:34.5Z")      # w/o time zone
            UTCDateTime(2009, 12, 31, 12, 23, 34, 500000)

            >>> UTCDateTime("2009-12-31T12:23:34+01:15")   # w/ time zone
            UTCDateTime(2009, 12, 31, 11, 8, 34)

        * Ordinal date representation.

            >>> UTCDateTime("2009-365T12:23:34.5")
            UTCDateTime(2009, 12, 31, 12, 23, 34, 500000)

            >>> UTCDateTime("2009365T122334.5")            # compact
            UTCDateTime(2009, 12, 31, 12, 23, 34, 500000)

            >>> UTCDateTime("2009001", iso8601=True)       # enforce ISO8601
            UTCDateTime(2009, 1, 1, 0, 0)

        * Week date representation.

            >>> UTCDateTime("2009-W53-7T12:23:34.5")
            UTCDateTime(2010, 1, 3, 12, 23, 34, 500000)

            >>> UTCDateTime("2009W537T122334.5")           # compact
            UTCDateTime(2010, 1, 3, 12, 23, 34, 500000)

            >>> UTCDateTime("2009W011", iso8601=True)      # enforce ISO8601
            UTCDateTime(2008, 12, 29, 0, 0)

    (3) Using not ISO8601 compatible strings.

        >>> UTCDateTime("1970-01-01 12:23:34")
        UTCDateTime(1970, 1, 1, 12, 23, 34)

        >>> UTCDateTime("1970,01,01,12:23:34")
        UTCDateTime(1970, 1, 1, 12, 23, 34)

        >>> UTCDateTime("1970,001,12:23:34")
        UTCDateTime(1970, 1, 1, 12, 23, 34)

        >>> UTCDateTime("20090701121212")
        UTCDateTime(2009, 7, 1, 12, 12, 12)

        >>> UTCDateTime("19700101")
        UTCDateTime(1970, 1, 1, 0, 0)

        >>> UTCDateTime("20110818_03:00:00")
        UTCDateTime(2011, 8, 18, 3, 0)

        >>> UTCDateTime("1970/01/17 12:23:34")
        UTCDateTime(1970, 1, 17, 12, 23, 34)

    (4) Using multiple arguments in the following order: `year, month,
        day[, hour[, minute[, second[, microsecond]]]`. The year, month and day
        arguments are required.

        >>> UTCDateTime(1970, 1, 1)
        UTCDateTime(1970, 1, 1, 0, 0)

        >>> UTCDateTime(1970, 1, 1, 12, 23, 34, 123456)
        UTCDateTime(1970, 1, 1, 12, 23, 34, 123456)

    (5) Using the following keyword arguments: `year, month, day, julday, hour,
        minute, second, microsecond`. Either the combination of year, month and
        day, or year and Julian day are required.

        >>> UTCDateTime(year=1970, month=1, day=1, minute=15, microsecond=20)
        UTCDateTime(1970, 1, 1, 0, 15, 0, 20)

        >>> UTCDateTime(year=2009, julday=234, hour=14, minute=13)
        UTCDateTime(2009, 8, 22, 14, 13)

    (6) Using a Python :class:`datetime.datetime` object.

        >>> dt = datetime.datetime(2009, 5, 24, 8, 28, 12, 5001)
        >>> UTCDateTime(dt)
        UTCDateTime(2009, 5, 24, 8, 28, 12, 5001)

    .. rubric:: _`Precision`

    The :class:`UTCDateTime` class works with a default precision of ``6``
    digits which effects the comparison of date/time values, e.g.:

    >>> dt = UTCDateTime(0)
    >>> dt2 = UTCDateTime(0.00001)
    >>> dt3 = UTCDateTime(0.0000001)
    >>> print(dt.precision)
    6
    >>> dt == dt2  # 5th digit is within current precision
    False
    >>> dt == dt3  # 7th digit will be neglected
    True

    You may change that behavior either by,

    (1) using the ``precision`` keyword during object initialization:

        >>> dt = UTCDateTime(0, precision=4)
        >>> dt2 = UTCDateTime(0.00001, precision=4)
        >>> print(dt.precision)
        4
        >>> dt == dt2
        True

    (2) or set it the class attribute ``DEFAULT_PRECISION`` for all new
        :class:`UTCDateTime` objects using a monkey patch:

        >>> UTCDateTime.DEFAULT_PRECISION = 4
        >>> dt = UTCDateTime(0)
        >>> dt2 = UTCDateTime(0.00001)
        >>> print(dt.precision)
        4
        >>> dt == dt2
        True

        Don't forget to reset ``DEFAULT_PRECISION`` if not needed anymore!

        >>> UTCDateTime.DEFAULT_PRECISION = 6

    .. _ISO8601:2004: https://en.wikipedia.org/wiki/ISO_8601
    """
    DEFAULT_PRECISION = 6

    def __init__(self, *args, **kwargs):
        """
        Creates a new UTCDateTime object.
        """
        # set default precision
        self.precision = kwargs.pop('precision', self.DEFAULT_PRECISION)
        # set directly to nanoseconds if given
        ns = kwargs.pop('ns', None)
        if ns is not None:
            self._ns = ns
            return
        # iso8601 flag
        iso8601 = kwargs.pop('iso8601', False) is True
        # check parameter
        if len(args) == 0 and len(kwargs) == 0:
            # use current date/time if no argument is given
            self._from_timestamp(time.time())
            return
        elif len(args) == 1 and len(kwargs) == 0:
            value = args[0]
            if isinstance(value, UTCDateTime):
                # ugly workaround to be able to unpickle UTCDateTime objects
                # that were pickled on ObsPy <1.1
                try:
                    self._ns = value._ns
                except AttributeError:
                    # work around floating point accuracy/rounding issue on
                    # Py3.3, see
                    # https://travis-ci.org/obspy/obspy/jobs/208941376#L751
                    # timestamp is 1251073203.0399999618 so when converting to
                    # integer nanosecond based UTCDateTime this should be
                    # rounded to 1251073203040000 nanoseconds.. but on Py3.3 it
                    # ends up as 1251073203039999, so we manually set
                    # microseconds with correct rounding without artifacts from
                    # floating point precision. see #1664
                    timestamp_seconds = int(value.__dict__['timestamp'])
                    timestamp_microseconds = round(
                        (value.__dict__['timestamp'] % 1.0) * 1e6)
                    dt_ = datetime.datetime.utcfromtimestamp(timestamp_seconds)
                    dt_ = dt_.replace(microsecond=timestamp_microseconds)
                    self._from_datetime(dt_)
                return
            # check types
            # The string instance check is mainly needed to not convert
            # numpy strings as these can be converted to floats on
            # numpy >= 1.14.
            if not isinstance(value, (str, bytes)):
                try:
                    # got a timestamp
                    self._from_timestamp(value.__float__())
                    return
                except Exception:
                    pass
            if isinstance(value, datetime.datetime):
                # got a Python datetime.datetime object
                self._from_datetime(value)
                return
            elif isinstance(value, datetime.date):
                # got a Python datetime.date object
                dt = datetime.datetime(value.year, value.month, value.day)
                self._from_datetime(dt)
                return
            elif isinstance(value, (bytes, str)):
                if not isinstance(value, (str, native_str)):
                    value = value.decode()
                # got a string instance
                value = value.strip()

                # Raising in the case where the leading string is less than 4
                # chars; linked to #2167
                if re.match(_YEAR0REGEX, value):
                    raise ValueError(
                        "'%s' does not start with a 4 digit year" % value)

                # check for ISO8601 date string
                if value.count("T") == 1 or iso8601:
                    try:
                        self._from_iso8601_string(value)
                        return
                    except Exception:
                        if iso8601:
                            raise
                # try to apply some standard patterns
                value = value.replace('T', ' ')
                value = value.replace('_', ' ')
                value = value.replace('-', ' ')
                value = value.replace(':', ' ')
                value = value.replace(',', ' ')
                value = value.replace('/', ' ')
                value = value.replace('Z', ' ')
                value = value.replace('W', ' ')
                # check for ordinal date (julian date)
                parts = value.split(' ')
                # check for patterns
                if len(parts) == 1 and len(value) == 7 and value.isdigit():
                    # looks like an compact ordinal date string
                    pattern = "%Y%j"
                elif len(parts) > 1 and len(parts[1]) == 3 and \
                        parts[1].isdigit():
                    # looks like an ordinal date string
                    value = ''.join(parts)
                    if len(parts) > 2:
                        pattern = "%Y%j%H%M%S"
                    else:
                        pattern = "%Y%j"
                else:
                    # some parts should have 2 digits
                    for i in range(1, min(len(parts), 6)):
                        if len(parts[i]) == 1:
                            parts[i] = '0' + parts[i]
                    value = ''.join(parts)
                    # fill missing elements with zeros
                    value += '0' * (14 - len(value))
                    pattern = "%Y%m%d%H%M%S"
                ms = 0
                if '.' in value:
                    parts = value.split('.')
                    value = parts[0].strip()
                    try:
                        ms = float('.' + parts[1].strip())
                    except Exception:
                        pass
                # all parts should be digits now - here we filter unknown
                # patterns and pass it directly to Python's  datetime.datetime
                if not ''.join(parts).isdigit():
                    dt = datetime.datetime(*args, **kwargs)
                    self._from_datetime(dt)
                    return
                dt = datetime.datetime.strptime(value, pattern)
                dt += datetime.timedelta(seconds=ms)
                self._from_datetime(dt)
                return
        # check for ordinal/julian date kwargs
        if 'julday' in kwargs:
            try:
                int(kwargs['julday'])
            except (ValueError, TypeError):
                msg = "Failed to convert 'julday' to int: {!s}".format(
                    kwargs['julday'])
                raise TypeError(msg)
            if not (1 <= int(kwargs['julday']) <= 366):
                msg = "'julday' out of bounds: {!s}".format(kwargs['julday'])
                raise ValueError(msg)
            if 'year' in kwargs:
                # year given as kwargs
                year = kwargs['year']
            elif len(args) == 1:
                # year is first (and only) argument
                year = args[0]
            try:
                temp = "%4d%03d" % (int(year),
                                    int(kwargs['julday']))
                dt = datetime.datetime.strptime(temp, '%Y%j')
            except Exception:
                pass
            else:
                kwargs['month'] = dt.month
                kwargs['day'] = dt.day
                kwargs.pop('julday')

        # check if seconds are given as float value
        if len(args) == 6 and isinstance(args[5], float):
            _frac, _sec = math.modf(round(args[5], 6))
            kwargs['second'] = int(_sec)
            kwargs['microsecond'] = int(round(_frac * 1e6))
            args = args[0:5]
        dt = datetime.datetime(*args, **kwargs)
        self._from_datetime(dt)

    def _set(self, **kwargs):
        """
        Sets current timestamp using kwargs.
        """
        year = kwargs.get('year', self.year)
        month = kwargs.get('month', self.month)
        day = kwargs.get('day', self.day)
        hour = kwargs.get('hour', self.hour)
        minute = kwargs.get('minute', self.minute)
        second = kwargs.get('second', self.second)
        microsecond = kwargs.get('microsecond', self.microsecond)
        julday = kwargs.get('julday', None)
        if julday:
            self._ns = UTCDateTime(year=year, julday=julday, hour=hour,
                                   minute=minute, second=second,
                                   microsecond=microsecond)._ns
        else:
            self._ns = UTCDateTime(year, month, day, hour, minute,
                                   second, microsecond)._ns

    def _get_ns(self):
        return self.__ns

    def _set_ns(self, value):
        # allow setting numpy integer types..
        if isinstance(value, np.integer):
            value_ = int(value)
            # ..and be paranoid and check that it's still the same value after
            # type casting
            if value_ != value:
                msg = ('Numpy integer value ({!s}) changed during casting to '
                       'Python builtin integer ({!s}).').format(value, value_)
                raise ValueError(msg)
            value = value_
        if not isinstance(value, int):
            raise TypeError('nanoseconds must be set as int/long type')
        self.__ns = value

    _ns = property(_get_ns, _set_ns)

    def _from_datetime(self, dt):
        """
        Use Python datetime object to set current time.

        :type dt: :class:`datetime.datetime`
        :param dt: Python datetime object.
        """
        # see datetime.timedelta.total_seconds
        try:
            td = (dt - TIMESTAMP0)
        except TypeError:
            td = (dt.replace(tzinfo=None) - dt.utcoffset()) - TIMESTAMP0
        self._ns = \
            (td.days * 86400 + td.seconds) * 10**9 + td.microseconds * 1000

    def _from_timestamp(self, value):
        """
        Use given timestamp to set current time.

        :type value: int, float
        :param value: Timestamp in seconds.
        """
        self._ns = int(round(value * 10**9))

    def _from_iso8601_string(self, value):
        """
        Parses an ISO8601:2004 date time string.
        """
        # remove trailing 'Z'
        value = value.replace('Z', '')
        # split between date and time
        try:
            (date, time) = value.split("T")
        except Exception:
            date = value
            time = ""
        # remove all hyphens in date
        date = date.replace('-', '')
        # remove colons in time
        time = time.replace(':', '')
        # guess date pattern
        length_date = len(date)
        if date.count('W') == 1 and length_date == 8:
            # we got a week date: YYYYWwwD
            # remove week indicator 'W'
            date = date.replace('W', '')
            date_pattern = "%Y%W%w"
            year = int(date[0:4])
            # [Www] is the week number prefixed by the letter 'W', from W01
            # through W53.
            # strpftime %W == Week number of the year (Monday as the first day
            # of the week) as a decimal number [00,53]. All days in a new year
            # preceding the first Monday are considered to be in week 0.
            week = int(date[4:6]) - 1
            # [D] is the weekday number, from 1 through 7, beginning with
            # Monday and ending with Sunday.
            # strpftime %w == Weekday as a decimal number [0(Sunday),6]
            day = int(date[6])
            if day == 7:
                day = 0
            date = "%04d%02d%1d" % (year, week, day)
        elif length_date == 7 and date.isdigit() and value.count('-') != 2:
            # we got a ordinal date: YYYYDDD
            date_pattern = "%Y%j"
        elif length_date == 8 and date.isdigit():
            # we got a calendar date: YYYYMMDD
            date_pattern = "%Y%m%d"
        else:
            raise ValueError("Wrong or incomplete ISO8601:2004 date format")
        # check for time zone information
        # note that the zone designator is the actual offset from UTC and
        # does not include any information on daylight saving time
        if time.count('+') == 1 and '+' in time[-6:]:
            (time, tz) = time.rsplit('+')
            delta = -1
        elif time.count('-') == 1 and '-' in time[-6:]:
            (time, tz) = time.rsplit('-')
            delta = 1
        else:
            delta = 0
        if delta:
            while len(tz) < 3:
                tz += '0'
            delta = delta * (int(tz[0:2]) * 60 * 60 + int(tz[2:]) * 60)
        # split microseconds
        ms = 0
        if '.' in time:
            (time, ms) = time.split(".")
            ms = float('0.' + ms.strip())
        # guess time pattern
        length_time = len(time)
        if length_time == 6 and time.isdigit():
            time_pattern = "%H%M%S"
        elif length_time == 4 and time.isdigit():
            time_pattern = "%H%M"
        elif length_time == 2 and time.isdigit():
            time_pattern = "%H"
        elif length_time == 0:
            time_pattern = ""
        else:
            raise ValueError("Wrong or incomplete ISO8601:2004 time format")
        # parse patterns
        dt = datetime.datetime.strptime(date + 'T' + time,
                                        date_pattern + 'T' + time_pattern)
        # add microseconds and eventually correct time zone
        dt += datetime.timedelta(seconds=float(delta) + ms)
        self._from_datetime(dt)

    def _get_timestamp(self):
        """
        Returns UTC timestamp in seconds.

        :rtype: float
        :return: Timestamp in seconds.

        .. rubric:: Example

        >>> dt = UTCDateTime(2008, 10, 1, 12, 30, 35, 123456)
        >>> dt.timestamp
        1222864235.123456
        """
        return self._ns / 1e9

    timestamp = property(_get_timestamp)

    def __float__(self):
        """
        Returns UTC timestamp in seconds.

        :rtype: float
        :return: Timestamp in seconds.

        .. rubric:: Example

        >>> dt = UTCDateTime(2008, 10, 1, 12, 30, 35, 123456)
        >>> float(dt)
        1222864235.123456
        """
        return self.timestamp

    def _get_datetime(self):
        """
        Returns a Python datetime object.

        :rtype: :class:`datetime.datetime`
        :return: Python datetime object.

        .. rubric:: Example

        >>> dt = UTCDateTime(2008, 10, 1, 12, 30, 35, 45020)
        >>> dt.datetime
        datetime.datetime(2008, 10, 1, 12, 30, 35, 45020)
        """
        # datetime.utcfromtimestamp will cut off but not round
        # avoid through adding timedelta - also avoids the year 2038 problem
        dt = datetime.timedelta(seconds=self._ns // 10**9,
                                microseconds=self._ns % 10**9 // 1000)
        try:
            return TIMESTAMP0 + dt
        except OverflowError:
            # for very large future / past dates
            return datetime.datetime.utcfromtimestamp(self.timestamp)

    datetime = property(_get_datetime)

    def _get_date(self):
        """
        Returns a Python date object..

        :rtype: :class:`datetime.date`
        :return: Python date object.

        .. rubric:: Example

        >>> dt = UTCDateTime(2008, 10, 1, 12, 30, 35, 45020)
        >>> dt.date
        datetime.date(2008, 10, 1)
        """
        return self.datetime.date()

    date = property(_get_date)

    def _get_year(self):
        """
        Returns year of the current UTCDateTime object.

        :rtype: int
        :return: Returns year as an integer.

        .. rubric:: Example

        >>> dt = UTCDateTime(2012, 2, 11)
        >>> dt.year
        2012
        """
        return self.datetime.year

    def _set_year(self, value):
        """
        Sets year of current UTCDateTime object.

        :param value: Year
        :type value: int

        .. rubric:: Example

        >>> dt = UTCDateTime(2012, 2, 11, 10, 11, 12)
        >>> dt.year = 2010
        >>> dt
        UTCDateTime(2010, 2, 11, 10, 11, 12)
        """
        self._set(year=value)

    year = property(_get_year, _set_year)

    def _get_month(self):
        """
        Returns month as an integer (January is 1, December is 12).

        :rtype: int
        :return: Returns month as an integer, where January is 1 and December
            is 12.

        .. rubric:: Example

        >>> dt = UTCDateTime(2012, 2, 11)
        >>> dt.month
        2
        """
        return self.datetime.month

    def _set_month(self, value):
        """
        Sets month of current UTCDateTime object.

        :param value: Month
        :type value: int

        .. rubric:: Example

        >>> dt = UTCDateTime(2012, 2, 11, 10, 11, 12)
        >>> dt.month = 10
        >>> dt
        UTCDateTime(2012, 10, 11, 10, 11, 12)
        """
        self._set(month=value)

    month = property(_get_month, _set_month)

    def _get_day(self):
        """
        Returns day as an integer.

        :rtype: int
        :return: Returns day as an integer.

        .. rubric:: Example

        >>> dt = UTCDateTime(2012, 2, 11)
        >>> dt.day
        11
        """
        return self.datetime.day

    def _set_day(self, value):
        """
        Sets day of current UTCDateTime object.

        :param value: Day
        :type value: int

        .. rubric:: Example

        >>> dt = UTCDateTime(2012, 2, 11, 10, 11, 12)
        >>> dt.day = 20
        >>> dt
        UTCDateTime(2012, 2, 20, 10, 11, 12)
        """
        self._set(day=value)

    day = property(_get_day, _set_day)

    def _get_weekday(self):
        """
        Return the day of the week as an integer (Monday is 0, Sunday is 6).

        :rtype: int
        :return: Returns day of the week as an integer, where Monday is 0 and
            Sunday is 6.

        .. rubric:: Example

        >>> dt = UTCDateTime(2008, 10, 1, 12, 30, 35, 45020)
        >>> dt.weekday
        2
        """
        return self.datetime.weekday()

    weekday = property(_get_weekday)

    def _get_time(self):
        """
        Returns a Python time object.

        :rtype: :class:`datetime.time`
        :return: Python time object.

        .. rubric:: Example

        >>> dt = UTCDateTime(2008, 10, 1, 12, 30, 35, 45020)
        >>> dt.time
        datetime.time(12, 30, 35, 45020)
        """
        return self.datetime.time()

    time = property(_get_time)

    def _get_hour(self):
        """
        Returns hour as an integer.

        :rtype: int
        :return: Returns hour as an integer.

        .. rubric:: Example

        >>> dt = UTCDateTime(2012, 2, 11, 10, 11, 12)
        >>> dt.hour
        10
        """
        return self.datetime.hour

    def _set_hour(self, value):
        """
        Sets hours of current UTCDateTime object.

        :param value: Hours
        :type value: int

        .. rubric:: Example

        >>> dt = UTCDateTime(2012, 2, 11, 10, 11, 12)
        >>> dt.hour = 20
        >>> dt
        UTCDateTime(2012, 2, 11, 20, 11, 12)
        """
        self._set(hour=value)

    hour = property(_get_hour, _set_hour)

    def _get_minute(self):
        """
        Returns minute as an integer.

        :rtype: int
        :return: Returns minute as an integer.

        .. rubric:: Example

        >>> dt = UTCDateTime(2012, 2, 11, 10, 11, 12)
        >>> dt.minute
        11
        """
        return self.datetime.minute

    def _set_minute(self, value):
        """
        Sets minutes of current UTCDateTime object.

        :param value: Minutes
        :type value: int

        .. rubric:: Example

        >>> dt = UTCDateTime(2012, 2, 11, 10, 11, 12)
        >>> dt.minute = 20
        >>> dt
        UTCDateTime(2012, 2, 11, 10, 20, 12)
        """
        self._set(minute=value)

    minute = property(_get_minute, _set_minute)

    def _get_second(self):
        """
        Returns seconds as an integer.

        :rtype: int
        :return: Returns seconds as an integer.

        .. rubric:: Example

        >>> dt = UTCDateTime(2012, 2, 11, 10, 11, 12)
        >>> dt.second
        12
        """
        return self.datetime.second

    def _set_second(self, value):
        """
        Sets seconds of current UTCDateTime object.

        :param value: Seconds
        :type value: int

        .. rubric:: Example

        >>> dt = UTCDateTime(2012, 2, 11, 10, 11, 12)
        >>> dt.second = 20
        >>> dt
        UTCDateTime(2012, 2, 11, 10, 11, 20)
        """
        self._set(second=value)

    second = property(_get_second, _set_second)

    def _get_microsecond(self):
        """
        Returns microseconds as an integer.

        :rtype: int
        :return: Returns microseconds as an integer.

        .. rubric:: Example

        >>> dt = UTCDateTime(2012, 2, 11, 10, 11, 12, 345234)
        >>> dt.microsecond
        345234
        """
        return int(self._ns % 10**9 // 1000)

    def _set_microsecond(self, value):
        """
        Sets microseconds of current UTCDateTime object.

        :param value: Microseconds
        :type value: int

        .. rubric:: Example

        >>> dt = UTCDateTime(2012, 2, 11, 10, 11, 12, 345234)
        >>> dt.microsecond = 999123
        >>> dt
        UTCDateTime(2012, 2, 11, 10, 11, 12, 999123)
        """
        self._set(microsecond=value)

    microsecond = property(_get_microsecond, _set_microsecond)

    def _get_julday(self):
        """
        Returns Julian day as an integer.

        :rtype: int
        :return: Julian day as an integer.

        .. rubric:: Example

        >>> dt = UTCDateTime(2008, 10, 1, 12, 30, 35, 45020)
        >>> dt.julday
        275
        """
        return self.utctimetuple().tm_yday

    def _set_julday(self, value):
        """
        Sets Julian day of current UTCDateTime object.

        :param value: Julian day
        :type value: int

        .. rubric:: Example

        >>> dt = UTCDateTime(2008, 12, 5, 12, 30, 35, 45020)
        >>> dt.julday = 275
        >>> dt
        UTCDateTime(2008, 10, 1, 12, 30, 35, 45020)
        """
        self._set(julday=value)

    julday = property(_get_julday, _set_julday)

    def timetuple(self):
        """
        Return a time.struct_time such as returned by time.localtime().

        :rtype: time.struct_time
        """
        return self.datetime.timetuple()

    def utctimetuple(self):
        """
        Return a time.struct_time of current UTCDateTime object.

        :rtype: time.struct_time
        """
        return self.datetime.utctimetuple()

    def __add__(self, value):
        """
        Adds seconds and microseconds to current UTCDateTime object.

        :type value: int, float
        :param value: Seconds to add
        :rtype: :class:`~obspy.core.utcdatetime.UTCDateTime`
        :return: New UTCDateTime object.

        .. rubric:: Example

        >>> dt = UTCDateTime(1970, 1, 1, 0, 0)
        >>> dt + 2
        UTCDateTime(1970, 1, 1, 0, 0, 2)

        >>> UTCDateTime(1970, 1, 1, 0, 0) + 1.123456
        UTCDateTime(1970, 1, 1, 0, 0, 1, 123456)
        """
        if isinstance(value, datetime.timedelta):
            # see datetime.timedelta.total_seconds
            value = (value.microseconds + (value.seconds + value.days *
                     86400) * 10**6) / 1e6
        elif isinstance(value, UTCDateTime):
            msg = ("unsupported operand type(s) for +: 'UTCDateTime' and "
                   "'UTCDateTime'")
            raise TypeError(msg)
        return UTCDateTime(ns=self._ns + int(round(value * 1e9)))

    def __sub__(self, value):
        """
        Subtracts seconds and microseconds from current UTCDateTime object.

        :type value: int, float or :class:`~obspy.core.utcdatetime.UTCDateTime`
        :param value: Seconds or UTCDateTime object to subtract. Subtracting an
            UTCDateTime objects results into a relative time span in seconds.
        :rtype: :class:`~obspy.core.utcdatetime.UTCDateTime` or float
        :return: New UTCDateTime object or relative time span in seconds.

        .. rubric:: Example

        >>> dt = UTCDateTime(1970, 1, 2, 0, 0)
        >>> dt - 2
        UTCDateTime(1970, 1, 1, 23, 59, 58)

        >>> UTCDateTime(1970, 1, 2, 0, 0) - 1.123456
        UTCDateTime(1970, 1, 1, 23, 59, 58, 876544)

        >>> UTCDateTime(1970, 1, 2, 0, 0) - UTCDateTime(1970, 1, 1, 0, 0)
        86400.0
        """
        if isinstance(value, UTCDateTime):
            return round((self._ns - value._ns) / 1e9, self.__precision)
        elif isinstance(value, datetime.timedelta):
            # see datetime.timedelta.total_seconds
            value = (value.microseconds + (value.seconds + value.days *
                     86400) * 10**6) / 1e6
        return UTCDateTime(ns=self._ns - int(round(value * 1e9)))

    def __str__(self):
        """
        Returns ISO8601 string representation from current UTCDateTime object.

        :return: string

        .. rubric:: Example

        >>> dt = UTCDateTime(2008, 10, 1, 12, 30, 35, 45020)
        >>> str(dt)
        '2008-10-01T12:30:35.045020Z'
        """
        dt = self.datetime
        pattern = "%%.%dlf" % (self.precision)
        ns = pattern % ((self._ns % 10**9) / 1e9)
        return "%04d-%02d-%02dT%02d:%02d:%02d.%sZ" % (
            dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second,
            ns[2:self.precision + 2])

    def _repr_pretty_(self, p, cycle):  # @UnusedVariable
        p.text(str(self))

    def __unicode__(self):
        """
        Returns ISO8601 unicode representation from current UTCDateTime object.

        :return: string

        .. rubric:: Example

        >>> dt = UTCDateTime(2008, 10, 1, 12, 30, 35, 45020)
        >>> dt.__unicode__()
        '2008-10-01T12:30:35.045020Z'
        """
        return str(self.__str__())

    def __eq__(self, other):
        """
        Rich comparison operator '=='.

        .. rubric: Example

        Comparing two UTCDateTime object will always compare timestamps rounded
        to a precision of 6 digits by default.

        >>> t1 = UTCDateTime(123.000000012)
        >>> t2 = UTCDateTime(123.000000099)
        >>> t1 == t2
        True

        But the actual timestamp differ

        >>> t1.timestamp == t2.timestamp
        False

        Resetting the precision changes the behavior of the operator

        >>> t1.precision = 11
        >>> t1 == t2
        False
        """
        if isinstance(other, UTCDateTime):
            return round((self._ns - other._ns) / 1e9, self.__precision) == 0
        elif isinstance(other, float) or isinstance(other, int):
            return round(self.timestamp - float(other), self.__precision) == 0
        elif isinstance(other, datetime.datetime):
            return self.datetime == other
        return False

    def __ne__(self, other):
        """
        Rich comparison operator '!='.

        .. rubric: Example

        Comparing two UTCDateTime object will always compare timestamps rounded
        to a precision of 6 digits by default.

        >>> t1 = UTCDateTime(123.000000012)
        >>> t2 = UTCDateTime(123.000000099)
        >>> t1 != t2
        False

        But the actual timestamp differ

        >>> t1.timestamp != t2.timestamp
        True

        Resetting the precision changes the behavior of the operator

        >>> t1.precision = 11
        >>> t1 != t2
        True
        """
        return not self.__eq__(other)

    def __lt__(self, other):
        """
        Rich comparison operator '<'.

        .. rubric: Example

        Comparing two UTCDateTime object will always compare timestamps rounded
        to a precision of 6 digits by default.

        >>> t1 = UTCDateTime(123.000000012)
        >>> t2 = UTCDateTime(123.000000099)
        >>> t1 < t2
        False

        But the actual timestamp differ

        >>> t1.timestamp < t2.timestamp
        True

        Resetting the precision changes the behavior of the operator

        >>> t1.precision = 11
        >>> t1 < t2
        True
        """
        if isinstance(other, UTCDateTime):
            return round((self._ns - other._ns) / 1e9, self.__precision) < 0
        elif isinstance(other, float) or isinstance(other, int):
            return round(self.timestamp - float(other), self.__precision) < 0
        elif isinstance(other, datetime.datetime):
            return self.datetime < other
        return False

    def __le__(self, other):
        """
        Rich comparison operator '<='.

        .. rubric: Example

        Comparing two UTCDateTime object will always compare timestamps rounded
        to a precision of 6 digits by default.

        >>> t1 = UTCDateTime(123.000000099)
        >>> t2 = UTCDateTime(123.000000012)
        >>> t1 <= t2
        True

        But the actual timestamp differ

        >>> t1.timestamp <= t2.timestamp
        False

        Resetting the precision changes the behavior of the operator

        >>> t1.precision = 11
        >>> t1 <= t2
        False
        """
        if isinstance(other, UTCDateTime):
            return round((self._ns - other._ns) / 1e9, self.__precision) <= 0
        elif isinstance(other, float) or isinstance(other, int):
            return round(self.timestamp - float(other), self.__precision) <= 0
        elif isinstance(other, datetime.datetime):
            return self.datetime <= other
        return False

    def __gt__(self, other):
        """
        Rich comparison operator '>'.

        .. rubric: Example

        Comparing two UTCDateTime object will always compare timestamps rounded
        to a precision of 6 digits by default.

        >>> t1 = UTCDateTime(123.000000099)
        >>> t2 = UTCDateTime(123.000000012)
        >>> t1 > t2
        False

        But the actual timestamp differ

        >>> t1.timestamp > t2.timestamp
        True

        Resetting the precision changes the behavior of the operator

        >>> t1.precision = 11
        >>> t1 > t2
        True
        """
        if isinstance(other, UTCDateTime):
            return round((self._ns - other._ns) / 1e9, self.__precision) > 0
        elif isinstance(other, float) or isinstance(other, int):
            return round(self.timestamp - float(other), self.__precision) > 0
        elif isinstance(other, datetime.datetime):
            return self.datetime > other
        return False

    def __ge__(self, other):
        """
        Rich comparison operator '>='.

        .. rubric: Example

        Comparing two UTCDateTime object will always compare timestamps rounded
        to a precision of 6 digits by default.

        >>> t1 = UTCDateTime(123.000000012)
        >>> t2 = UTCDateTime(123.000000099)
        >>> t1 >= t2
        True

        But the actual timestamp differ

        >>> t1.timestamp >= t2.timestamp
        False

        Resetting the precision changes the behavior of the operator

        >>> t1.precision = 11
        >>> t1 >= t2
        False
        """
        if isinstance(other, UTCDateTime):
            return round((self._ns - other._ns) / 1e9, self.__precision) >= 0
        elif isinstance(other, float) or isinstance(other, int):
            return round(self.timestamp - float(other), self.__precision) >= 0
        elif isinstance(other, datetime.datetime):
            return self.datetime >= other
        return False

    def __repr__(self):
        """
        Returns a representation of UTCDatetime object.
        """
        return 'UTCDateTime' + self.datetime.__repr__()[17:]

    def __abs__(self):
        """
        Returns absolute timestamp value of the current UTCDateTime object.
        """
        # needed for unittest.assertAlmostEqual tests on Linux
        return abs(self.timestamp)

    def __hash__(self):
        """
        An object is hashable if it has a hash value which never changes
        during its lifetime. As an UTCDateTime object may change over time,
        it's not hashable. Use the :meth:`~UTCDateTime.datetime()` method to
        generate a :class:`datetime.datetime` object for hashing. But be aware:
        once the UTCDateTime object changes, the hash is not valid anymore.
        """
        # explicitly flag it as unhashable
        return None

    def strftime(self, format):
        """
        Return a string representing the date and time, controlled by an
        explicit format string.

        :type format: str
        :param format: Format string.
        :return: Formatted string representing the date and time.

        Format codes referring to hours, minutes or seconds will see 0 values.
        See methods :meth:`~datetime.datetime.strftime()` and
        :meth:`~datetime.datetime.strptime()` for more information.
        """
        # See https://bugs.python.org/issue32195
        # This is an attempt to get consistent behavior across platforms.
        if sys.version_info.major > 2 and sys.platform.startswith("linux"):
            format = format.replace("%Y", "%04Y")

        try:
            ret = self.datetime.strftime(format)
        # this is trying to work around strftime refusing to work with years
        # <1900
        # XXX this problem seems to be specific to Python < 3.2
        # XXX so this can be removed after dropping Python 2 support
        except ValueError as e:
            # some other error? just raise it..
            if 'the datetime strftime() methods require year' not in str(e):
                raise
            # otherwise, try to do replace all strftime '%' commands with
            # simple string formatting
            format_ = self._strftime_replacement(format)
            # if there's still some strftime commands in there, we have a
            # problem still ('%%' is a %-sign literal)
            if '%' in format_.replace('%%', ''):
                raise
            ret = format_
        return ret

    def _strftime_replacement(self, strftime_string):
        """
        Replace all simple, year-independent strftime commands

        >>> t = UTCDateTime(1813, 10, 30, 12, 34, 56, 789012)
        >>> print(t._strftime_replacement('"%Y-%m-%dT%H:%M:%S.%f %y"'))
        "1813-10-30T12:34:56.789012 13"
        """
        for strftime_key, format_spec, property_name, func in STRFTIME_MAPPING:
            if strftime_key not in strftime_string:
                continue
            strftime_string = strftime_string.replace(
                    strftime_key, '{%s:%s}' % (property_name, format_spec))
            replacement = getattr(self, property_name)
            if func is not None:
                replacement = func(replacement)
            strftime_string = strftime_string.format(
                **{property_name: replacement})
        return strftime_string

    @staticmethod
    def strptime(date_string, format):
        """
        Return a UTCDateTime corresponding to date_string, parsed according to
        given format.

        :type date_string: str
        :param date_string: Date and time string.
        :type format: str
        :param format: Format string.
        :return: :class:`~obspy.core.utcdatetime.UTCDateTime`

        See methods :meth:`~datetime.datetime.strftime()` and
        :meth:`~datetime.datetime.strptime()` for more information.
        """
        return UTCDateTime(datetime.datetime.strptime(date_string, format))

    def timetz(self):
        """
        Return time object with same hour, minute, second, microsecond, and
        tzinfo attributes. See also method :meth:`datetime.datetime.time()`.

        .. rubric:: Example

        >>> dt = UTCDateTime(2008, 10, 1, 12, 30, 35, 45020)
        >>> dt.timetz()
        datetime.time(12, 30, 35, 45020)
        """
        return self.datetime.timetz()

    def utcoffset(self):
        """
        Returns None (to stay compatible with :class:`datetime.datetime`)

        .. rubric:: Example

        >>> dt = UTCDateTime(2008, 10, 1, 12, 30, 35, 45020)
        >>> dt.utcoffset()
        """
        return self.datetime.utcoffset()

    def dst(self):
        """
        Returns None (to stay compatible with :class:`datetime.datetime`)

        .. rubric:: Example

        >>> dt = UTCDateTime(2008, 10, 1, 12, 30, 35, 45020)
        >>> dt.dst()
        """
        return self.datetime.dst()

    def tzname(self):
        """
        Returns None (to stay compatible with :class:`datetime.datetime`)

        .. rubric:: Example

        >>> dt = UTCDateTime(2008, 10, 1, 12, 30, 35, 45020)
        >>> dt.tzname()
        """
        return self.datetime.tzname()

    def ctime(self):
        """
        Return a string representing the date and time.

        .. rubric:: Example

        >>> UTCDateTime(2002, 12, 4, 20, 30, 40).ctime()
        'Wed Dec  4 20:30:40 2002'
        """
        return self.datetime.ctime()

    def isoweekday(self):
        """
        Return the day of the week as an integer (Monday is 1, Sunday is 7).

        :rtype: int
        :return: Returns day of the week as an integer, where Monday is 1 and
            Sunday is 7.

        .. rubric:: Example

        >>> dt = UTCDateTime(2008, 10, 1, 12, 30, 35, 45020)
        >>> dt.isoweekday()
        3
        """
        return self.datetime.isoweekday()

    def isocalendar(self):
        """
        Returns a tuple containing (ISO year, ISO week number, ISO weekday).

        :rtype: tuple of ints
        :return: Returns a tuple containing ISO year, ISO week number and ISO
            weekday.

        .. rubric:: Example

        >>> dt = UTCDateTime(2008, 10, 1, 12, 30, 35, 45020)
        >>> dt.isocalendar()
        (2008, 40, 3)
        """
        return self.datetime.isocalendar()

    def isoformat(self, sep="T"):
        """
        Return a string representing the date and time in ISO 8601 format.

        :rtype: str
        :return: String representing the date and time in ISO 8601 format like
            YYYY-MM-DDTHH:MM:SS.mmmmmm or, if microsecond is 0,
            YYYY-MM-DDTHH:MM:SS.

        .. rubric:: Example

        >>> dt = UTCDateTime(2008, 10, 1, 12, 30, 35, 45020)
        >>> dt.isoformat()
        '2008-10-01T12:30:35.045020'

        >>> dt = UTCDateTime(2008, 10, 1)
        >>> dt.isoformat()
        '2008-10-01T00:00:00'
        """
        return self.datetime.isoformat(sep=native_str(sep))

    def format_fissures(self):
        """
        Returns string representation for the IRIS Fissures protocol.

        :return: string

        .. rubric:: Example

        >>> dt = UTCDateTime(2008, 10, 1, 12, 30, 35, 45020)
        >>> print(dt.format_fissures())
        2008275T123035.0450Z
        """
        return "%04d%03dT%02d%02d%02d.%04dZ" % \
            (self.year, self.julday, self.hour, self.minute, self.second,
             self.microsecond // 100)

    def format_arclink(self):
        """
        Returns string representation for the ArcLink protocol.

        :return: string

        .. rubric:: Example

        >>> dt = UTCDateTime(2008, 10, 1, 12, 30, 35, 45020)
        >>> print(dt.format_arclink())
        2008,10,1,12,30,35,45020
        """
        return "%d,%d,%d,%d,%d,%d,%d" % (self.year, self.month, self.day,
                                         self.hour, self.minute, self.second,
                                         self.microsecond)

    def format_seedlink(self):
        """
        Returns string representation for the SeedLink protocol.

        :return: string

        .. rubric:: Example

        >>> dt = UTCDateTime(2008, 10, 1, 12, 30, 35.45020)
        >>> print(dt.format_seedlink())
        2008,10,1,12,30,35
        """
        # round seconds down to integer
        seconds = int(float(self.second) + float(self.microsecond) / 1.0e6)
        return "%d,%d,%d,%d,%d,%g" % (self.year, self.month, self.day,
                                      self.hour, self.minute, seconds)

    def format_seed(self, compact=False):
        """
        Returns string representation for a SEED volume.

        :type compact: bool, optional
        :param compact: Delivers a compact SEED date string if enabled. Default
            value is set to False.
        :rtype: string
        :return: Datetime string in the SEED format.

        .. rubric:: Example

        >>> dt = UTCDateTime(2008, 10, 1, 12, 30, 35, 45020)
        >>> print(dt.format_seed())
        2008,275,12:30:35.0450

        >>> dt = UTCDateTime(2008, 10, 1, 0, 30, 0, 0)
        >>> print(dt.format_seed(compact=True))
        2008,275,00:30
        """
        if not compact:
            if self.time == datetime.time(0):
                return "%04d,%03d" % (self.year, self.julday)
            return "%04d,%03d,%02d:%02d:%02d.%04d" % (self.year, self.julday,
                                                      self.hour, self.minute,
                                                      self.second,
                                                      self.microsecond // 100)
        temp = "%04d,%03d" % (self.year, self.julday)
        if self.time == datetime.time(0):
            return temp
        temp += ",%02d" % (self.hour)
        if self.microsecond:
            return temp + ":%02d:%02d.%04d" % (self.minute, self.second,
                                               self.microsecond // 100)
        elif self.second:
            return temp + ":%02d:%02d" % (self.minute, self.second)
        elif self.minute:
            return temp + ":%02d" % (self.minute)
        return temp

    def format_iris_web_service(self):
        """
        Returns string representation usable for the IRIS Web services.

        :return: string

        .. rubric:: Example

        >>> dt = UTCDateTime(2008, 5, 27, 12, 30, 35, 45020)
        >>> print(dt.format_iris_web_service())
        2008-05-27T12:30:35.045
        """
        return "%04d-%02d-%02dT%02d:%02d:%02d.%03d" % \
            (self.year, self.month, self.day, self.hour, self.minute,
             self.second, self.microsecond // 1000)

    def _get_precision(self):
        """
        Returns precision of current UTCDateTime object.

        :return: int

        .. rubric:: Example

        >>> dt = UTCDateTime()
        >>> dt.precision
        6
        """
        return self.__precision

    def _set_precision(self, value=6):
        """
        Set precision of current UTCDateTime object.

        :type value: int, optional
        :param value: Precision value used by the rich comparison operators.
            Defaults to ``6``.

        .. rubric:: Example

        (1) Default precision

            >>> dt = UTCDateTime()
            >>> dt.precision
            6

        (2) Set precision during initialization of UTCDateTime object.

            >>> dt = UTCDateTime(precision=5)
            >>> dt.precision
            5

        (3) Set precision for an existing UTCDateTime object.

            >>> dt = UTCDateTime()
            >>> dt.precision = 12
            >>> dt.precision
            12
        """
        self.__precision = int(value)

    precision = property(_get_precision, _set_precision)

    def toordinal(self):
        """
        Return proleptic Gregorian ordinal. January 1 of year 1 is day 1.

        See :meth:`datetime.datetime.toordinal()`.

        :return: int

        .. rubric:: Example

        >>> dt = UTCDateTime(2012, 1, 1)
        >>> dt.toordinal()
        734503
        """
        return self.datetime.toordinal()

    @staticmethod
    def now():
        """
        Returns current UTC datetime.
        """
        return UTCDateTime()

    @staticmethod
    def utcnow():
        """
        Returns current UTC datetime.
        """
        return UTCDateTime()

    def _get_hours_after_midnight(self):
        """
        Calculate foating point hours after midnight.

        >>> t = UTCDateTime("2015-09-27T03:16:12.123456Z")
        >>> t._get_hours_after_midnight()
        3.270034293333333
        """
        timedelta = (
            self.datetime -
            self.datetime.replace(hour=0, minute=0, second=0, microsecond=0))
        return timedelta.total_seconds() / 3600.0

    @property
    def matplotlib_date(self):
        """
        Maplotlib date number representation.

        Useful for plotting on matplotlib time-based axes, like created by e.g.
        :meth:`obspy.core.stream.Stream.plot()`.

        >>> t = UTCDateTime("2009-08-24T00:20:07.700000Z")
        >>> t.matplotlib_date
        733643.0139780092

        :rtype: float
        """
        from matplotlib.dates import date2num
        return date2num(self.datetime)


if __name__ == '__main__':
    import doctest
    doctest.testmod(exclude_empty=True)
