# Copyright (c) 2019 Riverbed Technology, Inc.
#
# This software is licensed under the terms and conditions of the MIT License
# accompanying the software ("License").  This software is distributed "AS IS"
# as set forth in the License.

"""
This module contains a number of utilities for working with
dates and times, in conjunction with the python `datetime` module.
"""

import re
import time
import calendar
from datetime import datetime, timedelta, tzinfo
from decimal import Decimal
from dateutil.relativedelta import relativedelta

__all__ = ['ensure_timezone', 'force_to_utc', 'datetime_to_seconds',
           'datetime_to_microseconds', 'datetime_to_nanoseconds',
           'usec_string_to_datetime', 'nsec_string_to_datetime',
           'timedelta_total_seconds', 'timedelta_str',
           'TimeParser', 'parse_timedelta', 'parse_range']


#
# tzinfo objects for utc and the local timezone,
# https://launchpad.net/dateutil
#

ZERO = timedelta(0)
EPOCHORDINAL = datetime.utcfromtimestamp(0).toordinal()


class tzutc(tzinfo):

    def utcoffset(self, dt):
        return ZERO

    def dst(self, dt):
        return ZERO

    def tzname(self, dt):
        return "UTC"

    def __repr__(self):
        return "%s()" % self.__class__.__name__

    __reduce__ = object.__reduce__


class tzlocal(tzinfo):

    _std_offset = timedelta(seconds=-time.timezone)
    if time.daylight:
        _dst_offset = timedelta(seconds=-time.altzone)
    else:
        _dst_offset = _std_offset

    def utcoffset(self, dt):
        if self._isdst(dt):
            return self._dst_offset
        else:
            return self._std_offset

    def dst(self, dt):
        if self._isdst(dt):
            return self._dst_offset-self._std_offset
        else:
            return ZERO

    def tzname(self, dt):
        return time.tzname[self._isdst(dt)]

    def _isdst(self, dt):
        # We can't use mktime here. It is unstable when deciding if
        # the hour near to a change is DST or not.
        #
        # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour,
        #                         dt.minute, dt.second, dt.weekday(), 0, -1))
        # return time.localtime(timestamp).tm_isdst
        #
        # The code above yields the following result:
        #
        # >>> import tz, datetime
        # >>> t = tz.tzlocal()
        # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
        # 'BRDT'
        # >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname()
        # 'BRST'
        # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
        # 'BRST'
        # >>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname()
        # 'BRDT'
        # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
        # 'BRDT'
        #
        # Here is a more stable implementation:
        #
        timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400
                     + dt.hour * 3600
                     + dt.minute * 60
                     + dt.second)
        return time.localtime(timestamp+time.timezone).tm_isdst

    def __eq__(self, other):
        if not isinstance(other, tzlocal):
            return False
        return (self._std_offset == other._std_offset and
                self._dst_offset == other._dst_offset)

    def __ne__(self, other):
        return not self.__eq__(other)

    def __repr__(self):
        return "%s()" % self.__class__.__name__

    __reduce__ = object.__reduce__


def ensure_timezone(dt):
    """Return a datetime object that corresponds to `dt` but that always has
    timezone info.

    If `dt` already has timezone info, then it is simply returned.
    If `dt` does not have timezone info, then the local time zone is assumed.
    """

    if dt.tzinfo is not None:
        return dt

    return dt.replace(tzinfo=tzlocal())


def force_to_utc(dt):
    """Return a datetime object that corresponds to `dt` but in UTC rather than
    local time.

    If `dt` includes timezone info, then this routine simply converts from the
    given timezone to UTC.
    If `dt` does not include timezone info, then it is assumed to be in local
    time, which is then converted to UTC.
    """

    return ensure_timezone(dt).astimezone(tzutc())


def datetime_to_seconds(dt):
    """Return the number of seconds since the Unix epoch for
    the datetime object `dt`.
    """
    dt = ensure_timezone(dt)

    sec = int(calendar.timegm(dt.utctimetuple()))
    return sec


def datetime_to_microseconds(dt):
    """Return the number of microseconds since the Unix epoch for
    the datetime object `dt`.
    """
    dt = ensure_timezone(dt)

    sec = int(calendar.timegm(dt.utctimetuple()))
    return sec * 1000000 + dt.microsecond


def datetime_to_nanoseconds(dt):
    """Return the number of nanoseconds since the Unix epoch for
    the datetime object `dt`.
    """
    dt = ensure_timezone(dt)

    if not hasattr(dt, 'nanosecond'):
        return 1000 * datetime_to_microseconds(dt)

    return int(calendar.timegm(dt.utctimetuple()))*1000000000+dt.nanosecond


def sec_string_to_datetime(s):
    """Convert the string `s` which represents a time in seconds
    since the Unix epoch to a datetime object.
    """
    return datetime.fromtimestamp(s, tzutc())


def msec_string_to_datetime(s):
    """Convert the string `s` which represents a time in milliseconds
    since the Unix epoch to a datetime object.
    """
    sec = Decimal(s) / Decimal(1000)
    return datetime.fromtimestamp(sec, tzutc())


def usec_string_to_datetime(s):
    """Convert the string `s` which represents a time in microseconds
    since the Unix epoch to a datetime object.
    """
    sec = Decimal(s) / Decimal(1000000)
    return datetime.fromtimestamp(sec, tzutc())


def nsec_to_datetime(ns):
    """Convert the value `ns` which represents a time in nanoseconds
    since the Unix epoch (either as an integer or a string) to a
    datetime object.
    """
    if isinstance(ns, str):
        ns = int(ns)
    if ns == 0:
        return None

    # we want full nanosecond precision if we're using datetimeng but
    # float can't represent absolute timestamps with enough precision.
    # Decimal solves that problem but "regular" datetime objects can't
    # be constructed from instances of Decimal.  so, we create an
    # appropriate argument to fromtimestamp() by checking which
    # implementation of datetime we're using...
    if hasattr(datetime, 'nanosecond'):
        sec = Decimal(ns) / 1000000000
    else:
        sec = float(ns) / 1000000000

    return datetime.fromtimestamp(sec, tzutc())


def string_to_datetime(s):
    """Determine level of precision by number of digits and return
    a datetime object.

    Note: this method is only valid for datetimes between year 2001 and
    year 2286 since it assumes second level precision has 10 digits.
    """
    t = int(s)
    digits = len(str(t))
    if digits == 10:
        return sec_string_to_datetime(s)
    elif digits == 13:
        return msec_string_to_datetime(s)
    elif digits == 16:
        return usec_string_to_datetime(s)
    elif digits == 19:
        return nsec_string_to_datetime(s)
    else:
        raise TypeError('Unable to determine units of s: %s' % s)


nsec_string_to_datetime = nsec_to_datetime


def usec_string_to_timedelta(s):
    """Convert the string `s` which represents a number of microseconds
    to a timedelta object.
    """
    sec = float(s) / 1000000
    return timedelta(seconds=sec)


def timedelta_total_seconds(td):
    """Handle backwards compatibility for timedelta.total_seconds."""
    if hasattr(td, 'total_seconds'):
        return td.total_seconds()
    return float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6


# move out of TimeParser scope since Python3 won't allow it
# ref: https://stackoverflow.com/a/13913933/2157429
class _informat(object):
    def __init__(self, pattern, has_date, has_year):
        self.pattern = pattern
        self.has_date = has_date
        self.has_year = has_year
        if has_year:
            assert has_date

    def match(self, s):
        tm = datetime.strptime(s, self.pattern)

        if self.has_year:
            return tm

        now = datetime.now()
        if self.has_date:
            return tm.replace(year=now.year)

        return tm.replace(month=now.month, day=now.day, year=now.year)


# all the different time of day formats we can parse.
# refer to the python time module documentation for details
# on the format strings
_time_formats = (
    '%H:%M:%S.%f',
    '%H:%M:%S',
    '%H:%M',
    '%I:%M:%S %p',
    '%I:%M:%S%p',
    '%I:%M %p',
    '%I:%M%p',
    '%I %p',
    '%I%p'
)

# all the different date formats we can parse.
# note that dates are "normalized" a bit, see parse() below.
_date_formats = (
    '%m/%d',
    '%B %d',
    '%b %d'
)

_date_year_formats = (
    '%Y/%m/%d',
    '%m/%d/%Y',
    '%m/%d/%y',
    '%B/%d/%Y',
    '%b/%d/%Y',
)


class TimeParser(object):
    """Instances of this class parse strings representing dates and/or
    times into python `datetime.datetime` objects.

    This class is capable of parsing a variety of different formats.
    On the first call, the method `parse()` may take some time, as it
    tries a series of pre-defined formats one after another.  After
    successfully parsing a string, the parser object remembers the format
    that was used so subsequent calls with identically formatted strings
    are as efficient as the underlying method `datetime.strptime`.
    """
    def __init__(self):
        """ Construct a new TimeParser object """
        self._fmt = None

    @classmethod
    def _parse_no_hint(cls, s):
        """Parse string s as a date/time without any hint about the format.

        If it can be parsed, returns a tuple of the datetime object
        and the format object that was used.  If the string cannot be
        parsed, raises ValueError.
        """
        for fmt in cls._formats:
            try:
                dt = fmt.match(s)
                return dt, fmt
            except ValueError:
                pass

        raise ValueError("Could not parse datetime string: %s" % s)

    @classmethod
    def parse_one(cls, s):
        """Do a "one-shot" parsing of string s as a date/time.

        Doesn't remember anything about the format that was used.
        """
        dt, ignored = cls._parse_no_hint(s)
        return dt

    def parse(self, s):
        """Parse the string `s` as a date and time.

        Returns a `datetime.datetime` object on success or raises `ValueError`
        if the string cannot be parsed.
        """
        # begin with some normalization, strip whitespace and convert
        # dashes to slashes so that 05-10-2011 is equivalent to 05/10/2011
        s = s.strip().replace('-', '/')

        if self._fmt is not None:
            try:
                return self._fmt.match(s)
            except ValueError:
                pass

        dt, self._fmt = self._parse_no_hint(s)
        return dt


    _formats = (
        (
            [_informat(_tf, False, False) for _tf in _time_formats]
            + [_informat('%s %s' % (_tf, _df), True, False)
               for _tf in _time_formats for _df in _date_formats]
            + [_informat('%s %s' % (_df, _tf), True, False)
               for _tf in _time_formats for _df in _date_formats]
            + [_informat('%s %s' % (_tf, _df), True, True)
               for _tf in _time_formats for _df in _date_year_formats]
            + [_informat('%s %s' % (_df, _tf), True, True)
               for _tf in _time_formats for _df in _date_year_formats]
        )
     )


_timedelta_units = {
    'us': 0.000001, 'usec': 0.000001, 'microsecond': 0.000001, 'microseconds': 0.000001,
    'ms': 0.001, 'msec': 0.001, 'millisecond': 0.001, 'milliseconds': 0.001,
    's': 1, 'sec': 1, 'second': 1, 'seconds': 1,
    'm': 60, 'min': 60, 'minute': 60, 'minutes': 60,
    'h': 60*60, 'hr': 60*60, 'hour': 60*60, 'hours': 60*60,
    'd': 24*60*60, 'day': 24*60*60, 'days': 24*60*60,
    'w': 7*24*60*60, 'week': 7*24*60*60, 'weeks': 7*24*60*60,
    'month': 30*24*60*60, 'months': 30*24*60*60,
    'q': 91*24*60*60, 'quarter': 91*24*60*60, 'quarters': 91*24*60*60,
    'y': 365*24*60*60, 'year': 365*24*60*60, 'years': 365*24*60*60
}

_timedelta_re = re.compile("([0-9]*\\.*[0-9]*) *([a-zA-Z]*)")

# Build map from casual units to standard units recognizable by datetime
units_map = {}
for k, v in _timedelta_units.items():
    if v == 1:
        units_map[k] = 'second'
    elif v == 60:
        units_map[k] = 'minute'
    elif v == 60*60:
        units_map[k] = 'hour'
    elif v == 60*60*24:
        units_map[k] = 'day'
    elif v == 60*60*24*7:
        units_map[k] = 'week'
    elif v == 60*60*24*30:
        units_map[k] = 'month'
    elif v == 60*60*24*91:
        units_map[k] = 'quarter'
    elif v == 60*60*24*365:
        units_map[k] = 'year'


def timedelta_str(td):
    def pluralize(val, base, plural):
        if val > 1:
            return "%d %s" % (val, plural)
        else:
            return "%d %s" % (val, base)

    if td.days > 0:
        if td.seconds != 0 or td.microseconds != 0:
            msg = ("Timedelta has too many components for pretty string: %s"
                   % str(td))
            raise ValueError(msg)
        if td.days % 7 == 0:
            return pluralize(td.days / 7, "week", "weeks")
        else:
            return pluralize(td.days, "day", "days")
    elif td.seconds > 0:
        if td.microseconds != 0:
            msg = ("Timedelta has too many components for pretty string: %s"
                   % str(td))
            raise ValueError(msg)
        if td.seconds % (60*60) == 0:
            return pluralize(td.seconds / (60*60), "hour", "hours")
        elif td.seconds % 60 == 0:
            return pluralize(td.seconds / 60, "minute", "minutes")
        else:
            return pluralize(td.seconds, "second", "seconds")
    else:
        if td.microseconds % 1000 == 0:
            return pluralize(
                td.microseconds / 1000, "millisecond", "milliseconds"
            )
        else:
            return pluralize(td.microseconds, "microsecond", "microseconds")


def round_time(dt=None, round_to=60, round_up=False, trim=False):
    """Round a datetime object to any time laps in seconds.

    `dt`: datetime.datetime object, default now.
    `round_to`: Closest number of seconds to round to, default 1 minute.
    `round_up`: Default rounds down to nearest `round_to` interval,
               True here will instead round up.
    `trim`: Trim to nearest round_to value rather than rounding.
    """
    # ref http://stackoverflow.com/a/10854034/2157429
    if dt is None:
        dt = datetime.now()
    dt = ensure_timezone(dt)

    if trim:
        rounded = (round_time(dt, round_to, False),
                   round_time(dt, round_to, True))
        return max(rounded) if max(rounded) <= dt else min(rounded)

    seconds = (dt - dt.min.replace(tzinfo=dt.tzinfo)).seconds
    if round_up:
        rounding = (seconds + round_to / 2) // round_to * round_to
    else:
        rounding = (seconds - round_to / 2) // round_to * round_to
    return dt + timedelta(0, rounding - seconds, -dt.microsecond)


def parse_timedelta(s):
    """Parse the string `s` representing some duration of time
    (e.g., `"3 seconds"` or `"1 week"`) and return a `datetime.timedelta`
    object representing that length of time.

    If the string cannot be parsed, raises `ValueError`.
    """

    m = _timedelta_re.match(s)
    if not m:
        raise ValueError("Could not parse string as timedelta: %s" % s)

    if m.group(1) == "":
        val = 1
    else:
        val = float(m.group(1))

    if m.group(2) == "":
        units = 1
    else:
        try:
            units = _timedelta_units[m.group(2)]
        except KeyError:
            raise ValueError("Invalid timedelta units: %s" % m.group(2))

    return timedelta(seconds=units * float(val))


def floor_dt(dt, unit, begin_monday=False):
    """Derive the most recent start datetime of the current duration unit.
    i.e., for datetime(2016, 12, 14, 10, 14) and hour, should return
    datetime(2016, 12, 14, 10, 0)

    :param dt: datetime value
    :param unit: string, duration unit: year, quarter, month, week, day,
        hour, minute, second.
    :return : datetime.
    """

    if unit == 'week':
        offset = 0 if begin_monday else 1
        dt = dt - timedelta(days=dt.weekday() + offset)
        return floor_dt(dt, 'day')

    if unit == 'quarter':
        # Update the month to be first of current quarter
        dt = dt.replace(month=(dt.month - 1)//3 * 3 + 1)
        return floor_dt(dt, 'month')

    start_month, start_day, start_hour, start_minute, start_second = \
        1, 1, 0, 0, 0

    kwargs, smaller_unit = {}, False

    for u in ['year', 'month', 'day', 'hour', 'minute', 'second']:
        if smaller_unit:
            kwargs[u] = eval('start_' + u)
        else:
            kwargs[u] = getattr(dt, u)

        if not smaller_unit and u == unit:
            smaller_unit = True

    return datetime(**kwargs)


def parse_range(s, begin_monday=False):
    """Parse the string `s` representing a range of times
    (e.g., `"12:00 PM to 1:00 PM"` or `"last 2 weeks"`).

    Upon success returns a pair of `datetime.datetime` objects
    representing the beginning and end of the time range.
    If the string cannot be parsed, raises `ValueError`.
    """

    s = s.strip()

    if s == 'today':
        s = 'this day'
    elif s == 'yesterday':
        s = 'previous day'

    # first try something of the form "time1 to time2"
    i = s.split('to')
    if len(i) == 2:
        try:
            p = TimeParser()
            start = p.parse(i[0])
            end = p.parse(i[1])
            return start, end
        except ValueError:
            pass

    # try something of the form "last time"
    if s.startswith('last'):
        try:
            duration = parse_timedelta(s[4:].strip())
            end = datetime.now()
            start = end - duration
            return start, end
        except ValueError:
            pass

    # try to convert the datetime to
    elif s.startswith('previous'):
        try:
            duration = s[8:].strip()
            unit = units_map[_timedelta_re.match(duration).group(2)]
            now = datetime.now()

            how_many = _timedelta_re.match(duration).group(1)
            how_many = 1 if how_many == '' else int(how_many)

            if unit == 'quarter':
                delta = relativedelta(months=how_many*3)
            else:
                delta = relativedelta(**{unit+'s': how_many})

            start = floor_dt(now - delta, unit, begin_monday)
            end = floor_dt(now, unit, begin_monday)

            return start, end

        except ValueError:
            pass

    elif s.startswith('this'):
        try:
            duration = s[4:].strip()
            unit = units_map[_timedelta_re.match(duration).group(2)]
            now = datetime.now()

            start = floor_dt(now, unit, begin_monday)

            return start, now

        except ValueError:
            pass

    # if it still doesn't work, try the pilot time filter syntax
    # which looks like "time1,time2"
    i = s.split(',')

    # XXX normally, Pilot formats filters with the
    # following syntax:
    # 11/20/2007 12:28:01.719742,11/20/2007 12:35:19.719742, GMT -8
    # We will need to handle the third piece if we want to avoid problems
    # with daylight savings
    if len(i) >= 2:
        try:
            p = TimeParser()
            start = p.parse(i[0])
            end = p.parse(i[1])
            return start, end
        except ValueError:
            pass

    raise ValueError('cannot parse time range "%s"' % s)


# XXX probably incorrect in some locales?
_fmt_widths = {
    'a': 3, 'A': 9, 'b': 3, 'B': 9, 'c': 24,
    'd': 2, 'f': 6, 'H': 2, 'I': 2, 'j': 3,
    'm': 2, 'M': 2, 'p': 2, 'S': 2, 'U': 2,
    'w': 1, 'W': 2, 'x': 8, 'X': 8, 'y': 2,
    'Y': 4, 'z': 5, 'Z': 3, '%': 1
}


def max_width(fmt):
    """Given a time formatting string (i.e., a string that can be
    used as the `fmt` option to `datetime.strftime`, compute the
    maximum length of a formatted date string.
    """
    m = 0
    i = 0
    while i < len(fmt):
        if fmt[i] == '%':
            try:
                i += 1
                m += _fmt_widths[fmt[i]]
            except IndexError:
                raise ValueError(
                    'do not understand %%%s in format string' % fmt[i]
                )
        else:
            m += 1

        i += 1

    return m

# Copyright (c) 2019 Riverbed Technology, Inc.
#
# This software is licensed under the terms and conditions of the MIT License
# accompanying the software ("License").  This software is distributed "AS IS"
# as set forth in the License.

"""
This module contains a number of utilities for working with
dates and times, in conjunction with the python `datetime` module.
"""

import re
import time
import calendar
from datetime import datetime, timedelta, tzinfo
from decimal import Decimal
from dateutil.relativedelta import relativedelta

__all__ = ['ensure_timezone', 'force_to_utc', 'datetime_to_seconds',
           'datetime_to_microseconds', 'datetime_to_nanoseconds',
           'usec_string_to_datetime', 'nsec_string_to_datetime',
           'timedelta_total_seconds', 'timedelta_str',
           'TimeParser', 'parse_timedelta', 'parse_range']


#
# tzinfo objects for utc and the local timezone,
# https://launchpad.net/dateutil
#

ZERO = timedelta(0)
EPOCHORDINAL = datetime.utcfromtimestamp(0).toordinal()


class tzutc(tzinfo):

    def utcoffset(self, dt):
        return ZERO

    def dst(self, dt):
        return ZERO

    def tzname(self, dt):
        return "UTC"

    def __repr__(self):
        return "%s()" % self.__class__.__name__

    __reduce__ = object.__reduce__


class tzlocal(tzinfo):

    _std_offset = timedelta(seconds=-time.timezone)
    if time.daylight:
        _dst_offset = timedelta(seconds=-time.altzone)
    else:
        _dst_offset = _std_offset

    def utcoffset(self, dt):
        if self._isdst(dt):
            return self._dst_offset
        else:
            return self._std_offset

    def dst(self, dt):
        if self._isdst(dt):
            return self._dst_offset-self._std_offset
        else:
            return ZERO

    def tzname(self, dt):
        return time.tzname[self._isdst(dt)]

    def _isdst(self, dt):
        # We can't use mktime here. It is unstable when deciding if
        # the hour near to a change is DST or not.
        #
        # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour,
        #                         dt.minute, dt.second, dt.weekday(), 0, -1))
        # return time.localtime(timestamp).tm_isdst
        #
        # The code above yields the following result:
        #
        # >>> import tz, datetime
        # >>> t = tz.tzlocal()
        # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
        # 'BRDT'
        # >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname()
        # 'BRST'
        # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
        # 'BRST'
        # >>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname()
        # 'BRDT'
        # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
        # 'BRDT'
        #
        # Here is a more stable implementation:
        #
        timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400
                     + dt.hour * 3600
                     + dt.minute * 60
                     + dt.second)
        return time.localtime(timestamp+time.timezone).tm_isdst

    def __eq__(self, other):
        if not isinstance(other, tzlocal):
            return False
        return (self._std_offset == other._std_offset and
                self._dst_offset == other._dst_offset)

    def __ne__(self, other):
        return not self.__eq__(other)

    def __repr__(self):
        return "%s()" % self.__class__.__name__

    __reduce__ = object.__reduce__


def ensure_timezone(dt):
    """Return a datetime object that corresponds to `dt` but that always has
    timezone info.

    If `dt` already has timezone info, then it is simply returned.
    If `dt` does not have timezone info, then the local time zone is assumed.
    """

    if dt.tzinfo is not None:
        return dt

    return dt.replace(tzinfo=tzlocal())


def force_to_utc(dt):
    """Return a datetime object that corresponds to `dt` but in UTC rather than
    local time.

    If `dt` includes timezone info, then this routine simply converts from the
    given timezone to UTC.
    If `dt` does not include timezone info, then it is assumed to be in local
    time, which is then converted to UTC.
    """

    return ensure_timezone(dt).astimezone(tzutc())


def datetime_to_seconds(dt):
    """Return the number of seconds since the Unix epoch for
    the datetime object `dt`.
    """
    dt = ensure_timezone(dt)

    sec = int(calendar.timegm(dt.utctimetuple()))
    return sec


def datetime_to_microseconds(dt):
    """Return the number of microseconds since the Unix epoch for
    the datetime object `dt`.
    """
    dt = ensure_timezone(dt)

    sec = int(calendar.timegm(dt.utctimetuple()))
    return sec * 1000000 + dt.microsecond


def datetime_to_nanoseconds(dt):
    """Return the number of nanoseconds since the Unix epoch for
    the datetime object `dt`.
    """
    dt = ensure_timezone(dt)

    if not hasattr(dt, 'nanosecond'):
        return 1000 * datetime_to_microseconds(dt)

    return int(calendar.timegm(dt.utctimetuple()))*1000000000+dt.nanosecond


def sec_string_to_datetime(s):
    """Convert the string `s` which represents a time in seconds
    since the Unix epoch to a datetime object.
    """
    return datetime.fromtimestamp(s, tzutc())


def msec_string_to_datetime(s):
    """Convert the string `s` which represents a time in milliseconds
    since the Unix epoch to a datetime object.
    """
    sec = Decimal(s) / Decimal(1000)
    return datetime.fromtimestamp(sec, tzutc())


def usec_string_to_datetime(s):
    """Convert the string `s` which represents a time in microseconds
    since the Unix epoch to a datetime object.
    """
    sec = Decimal(s) / Decimal(1000000)
    return datetime.fromtimestamp(sec, tzutc())


def nsec_to_datetime(ns):
    """Convert the value `ns` which represents a time in nanoseconds
    since the Unix epoch (either as an integer or a string) to a
    datetime object.
    """
    if isinstance(ns, str):
        ns = int(ns)
    if ns == 0:
        return None

    # we want full nanosecond precision if we're using datetimeng but
    # float can't represent absolute timestamps with enough precision.
    # Decimal solves that problem but "regular" datetime objects can't
    # be constructed from instances of Decimal.  so, we create an
    # appropriate argument to fromtimestamp() by checking which
    # implementation of datetime we're using...
    if hasattr(datetime, 'nanosecond'):
        sec = Decimal(ns) / 1000000000
    else:
        sec = float(ns) / 1000000000

    return datetime.fromtimestamp(sec, tzutc())


def string_to_datetime(s):
    """Determine level of precision by number of digits and return
    a datetime object.

    Note: this method is only valid for datetimes between year 2001 and
    year 2286 since it assumes second level precision has 10 digits.
    """
    t = int(s)
    digits = len(str(t))
    if digits == 10:
        return sec_string_to_datetime(s)
    elif digits == 13:
        return msec_string_to_datetime(s)
    elif digits == 16:
        return usec_string_to_datetime(s)
    elif digits == 19:
        return nsec_string_to_datetime(s)
    else:
        raise TypeError('Unable to determine units of s: %s' % s)


nsec_string_to_datetime = nsec_to_datetime


def usec_string_to_timedelta(s):
    """Convert the string `s` which represents a number of microseconds
    to a timedelta object.
    """
    sec = float(s) / 1000000
    return timedelta(seconds=sec)


def timedelta_total_seconds(td):
    """Handle backwards compatibility for timedelta.total_seconds."""
    if hasattr(td, 'total_seconds'):
        return td.total_seconds()
    return float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6


# move out of TimeParser scope since Python3 won't allow it
# ref: https://stackoverflow.com/a/13913933/2157429
class _informat(object):
    def __init__(self, pattern, has_date, has_year):
        self.pattern = pattern
        self.has_date = has_date
        self.has_year = has_year
        if has_year:
            assert has_date

    def match(self, s):
        tm = datetime.strptime(s, self.pattern)

        if self.has_year:
            return tm

        now = datetime.now()
        if self.has_date:
            return tm.replace(year=now.year)

        return tm.replace(month=now.month, day=now.day, year=now.year)


# all the different time of day formats we can parse.
# refer to the python time module documentation for details
# on the format strings
_time_formats = (
    '%H:%M:%S.%f',
    '%H:%M:%S',
    '%H:%M',
    '%I:%M:%S %p',
    '%I:%M:%S%p',
    '%I:%M %p',
    '%I:%M%p',
    '%I %p',
    '%I%p'
)

# all the different date formats we can parse.
# note that dates are "normalized" a bit, see parse() below.
_date_formats = (
    '%m/%d',
    '%B %d',
    '%b %d'
)

_date_year_formats = (
    '%Y/%m/%d',
    '%m/%d/%Y',
    '%m/%d/%y',
    '%B/%d/%Y',
    '%b/%d/%Y',
)


class TimeParser(object):
    """Instances of this class parse strings representing dates and/or
    times into python `datetime.datetime` objects.

    This class is capable of parsing a variety of different formats.
    On the first call, the method `parse()` may take some time, as it
    tries a series of pre-defined formats one after another.  After
    successfully parsing a string, the parser object remembers the format
    that was used so subsequent calls with identically formatted strings
    are as efficient as the underlying method `datetime.strptime`.
    """
    def __init__(self):
        """ Construct a new TimeParser object """
        self._fmt = None

    @classmethod
    def _parse_no_hint(cls, s):
        """Parse string s as a date/time without any hint about the format.

        If it can be parsed, returns a tuple of the datetime object
        and the format object that was used.  If the string cannot be
        parsed, raises ValueError.
        """
        for fmt in cls._formats:
            try:
                dt = fmt.match(s)
                return dt, fmt
            except ValueError:
                pass

        raise ValueError("Could not parse datetime string: %s" % s)

    @classmethod
    def parse_one(cls, s):
        """Do a "one-shot" parsing of string s as a date/time.

        Doesn't remember anything about the format that was used.
        """
        dt, ignored = cls._parse_no_hint(s)
        return dt

    def parse(self, s):
        """Parse the string `s` as a date and time.

        Returns a `datetime.datetime` object on success or raises `ValueError`
        if the string cannot be parsed.
        """
        # begin with some normalization, strip whitespace and convert
        # dashes to slashes so that 05-10-2011 is equivalent to 05/10/2011
        s = s.strip().replace('-', '/')

        if self._fmt is not None:
            try:
                return self._fmt.match(s)
            except ValueError:
                pass

        dt, self._fmt = self._parse_no_hint(s)
        return dt


    _formats = (
        (
            [_informat(_tf, False, False) for _tf in _time_formats]
            + [_informat('%s %s' % (_tf, _df), True, False)
               for _tf in _time_formats for _df in _date_formats]
            + [_informat('%s %s' % (_df, _tf), True, False)
               for _tf in _time_formats for _df in _date_formats]
            + [_informat('%s %s' % (_tf, _df), True, True)
               for _tf in _time_formats for _df in _date_year_formats]
            + [_informat('%s %s' % (_df, _tf), True, True)
               for _tf in _time_formats for _df in _date_year_formats]
        )
     )


_timedelta_units = {
    'us': 0.000001, 'usec': 0.000001, 'microsecond': 0.000001, 'microseconds': 0.000001,
    'ms': 0.001, 'msec': 0.001, 'millisecond': 0.001, 'milliseconds': 0.001,
    's': 1, 'sec': 1, 'second': 1, 'seconds': 1,
    'm': 60, 'min': 60, 'minute': 60, 'minutes': 60,
    'h': 60*60, 'hr': 60*60, 'hour': 60*60, 'hours': 60*60,
    'd': 24*60*60, 'day': 24*60*60, 'days': 24*60*60,
    'w': 7*24*60*60, 'week': 7*24*60*60, 'weeks': 7*24*60*60,
    'month': 30*24*60*60, 'months': 30*24*60*60,
    'q': 91*24*60*60, 'quarter': 91*24*60*60, 'quarters': 91*24*60*60,
    'y': 365*24*60*60, 'year': 365*24*60*60, 'years': 365*24*60*60
}

_timedelta_re = re.compile("([0-9]*\\.*[0-9]*) *([a-zA-Z]*)")

# Build map from casual units to standard units recognizable by datetime
units_map = {}
for k, v in _timedelta_units.items():
    if v == 1:
        units_map[k] = 'second'
    elif v == 60:
        units_map[k] = 'minute'
    elif v == 60*60:
        units_map[k] = 'hour'
    elif v == 60*60*24:
        units_map[k] = 'day'
    elif v == 60*60*24*7:
        units_map[k] = 'week'
    elif v == 60*60*24*30:
        units_map[k] = 'month'
    elif v == 60*60*24*91:
        units_map[k] = 'quarter'
    elif v == 60*60*24*365:
        units_map[k] = 'year'


def timedelta_str(td):
    def pluralize(val, base, plural):
        if val > 1:
            return "%d %s" % (val, plural)
        else:
            return "%d %s" % (val, base)

    if td.days > 0:
        if td.seconds != 0 or td.microseconds != 0:
            msg = ("Timedelta has too many components for pretty string: %s"
                   % str(td))
            raise ValueError(msg)
        if td.days % 7 == 0:
            return pluralize(td.days / 7, "week", "weeks")
        else:
            return pluralize(td.days, "day", "days")
    elif td.seconds > 0:
        if td.microseconds != 0:
            msg = ("Timedelta has too many components for pretty string: %s"
                   % str(td))
            raise ValueError(msg)
        if td.seconds % (60*60) == 0:
            return pluralize(td.seconds / (60*60), "hour", "hours")
        elif td.seconds % 60 == 0:
            return pluralize(td.seconds / 60, "minute", "minutes")
        else:
            return pluralize(td.seconds, "second", "seconds")
    else:
        if td.microseconds % 1000 == 0:
            return pluralize(
                td.microseconds / 1000, "millisecond", "milliseconds"
            )
        else:
            return pluralize(td.microseconds, "microsecond", "microseconds")


def round_time(dt=None, round_to=60, round_up=False, trim=False):
    """Round a datetime object to any time laps in seconds.

    `dt`: datetime.datetime object, default now.
    `round_to`: Closest number of seconds to round to, default 1 minute.
    `round_up`: Default rounds down to nearest `round_to` interval,
               True here will instead round up.
    `trim`: Trim to nearest round_to value rather than rounding.
    """
    # ref http://stackoverflow.com/a/10854034/2157429
    if dt is None:
        dt = datetime.now()
    dt = ensure_timezone(dt)

    if trim:
        rounded = (round_time(dt, round_to, False),
                   round_time(dt, round_to, True))
        return max(rounded) if max(rounded) <= dt else min(rounded)

    seconds = (dt - dt.min.replace(tzinfo=dt.tzinfo)).seconds
    if round_up:
        rounding = (seconds + round_to / 2) // round_to * round_to
    else:
        rounding = (seconds - round_to / 2) // round_to * round_to
    return dt + timedelta(0, rounding - seconds, -dt.microsecond)


def parse_timedelta(s):
    """Parse the string `s` representing some duration of time
    (e.g., `"3 seconds"` or `"1 week"`) and return a `datetime.timedelta`
    object representing that length of time.

    If the string cannot be parsed, raises `ValueError`.
    """

    m = _timedelta_re.match(s)
    if not m:
        raise ValueError("Could not parse string as timedelta: %s" % s)

    if m.group(1) == "":
        val = 1
    else:
        val = float(m.group(1))

    if m.group(2) == "":
        units = 1
    else:
        try:
            units = _timedelta_units[m.group(2)]
        except KeyError:
            raise ValueError("Invalid timedelta units: %s" % m.group(2))

    return timedelta(seconds=units * float(val))


def floor_dt(dt, unit, begin_monday=False):
    """Derive the most recent start datetime of the current duration unit.
    i.e., for datetime(2016, 12, 14, 10, 14) and hour, should return
    datetime(2016, 12, 14, 10, 0)

    :param dt: datetime value
    :param unit: string, duration unit: year, quarter, month, week, day,
        hour, minute, second.
    :return : datetime.
    """

    if unit == 'week':
        offset = 0 if begin_monday else 1
        dt = dt - timedelta(days=dt.weekday() + offset)
        return floor_dt(dt, 'day')

    if unit == 'quarter':
        # Update the month to be first of current quarter
        dt = dt.replace(month=(dt.month - 1)//3 * 3 + 1)
        return floor_dt(dt, 'month')

    start_month, start_day, start_hour, start_minute, start_second = \
        1, 1, 0, 0, 0

    kwargs, smaller_unit = {}, False

    for u in ['year', 'month', 'day', 'hour', 'minute', 'second']:
        if smaller_unit:
            kwargs[u] = eval('start_' + u)
        else:
            kwargs[u] = getattr(dt, u)

        if not smaller_unit and u == unit:
            smaller_unit = True

    return datetime(**kwargs)


def parse_range(s, begin_monday=False):
    """Parse the string `s` representing a range of times
    (e.g., `"12:00 PM to 1:00 PM"` or `"last 2 weeks"`).

    Upon success returns a pair of `datetime.datetime` objects
    representing the beginning and end of the time range.
    If the string cannot be parsed, raises `ValueError`.
    """

    s = s.strip()

    if s == 'today':
        s = 'this day'
    elif s == 'yesterday':
        s = 'previous day'

    # first try something of the form "time1 to time2"
    i = s.split('to')
    if len(i) == 2:
        try:
            p = TimeParser()
            start = p.parse(i[0])
            end = p.parse(i[1])
            return start, end
        except ValueError:
            pass

    # try something of the form "last time"
    if s.startswith('last'):
        try:
            duration = parse_timedelta(s[4:].strip())
            end = datetime.now()
            start = end - duration
            return start, end
        except ValueError:
            pass

    # try to convert the datetime to
    elif s.startswith('previous'):
        try:
            duration = s[8:].strip()
            unit = units_map[_timedelta_re.match(duration).group(2)]
            now = datetime.now()

            how_many = _timedelta_re.match(duration).group(1)
            how_many = 1 if how_many == '' else int(how_many)

            if unit == 'quarter':
                delta = relativedelta(months=how_many*3)
            else:
                delta = relativedelta(**{unit+'s': how_many})

            start = floor_dt(now - delta, unit, begin_monday)
            end = floor_dt(now, unit, begin_monday)

            return start, end

        except ValueError:
            pass

    elif s.startswith('this'):
        try:
            duration = s[4:].strip()
            unit = units_map[_timedelta_re.match(duration).group(2)]
            now = datetime.now()

            start = floor_dt(now, unit, begin_monday)

            return start, now

        except ValueError:
            pass

    # if it still doesn't work, try the pilot time filter syntax
    # which looks like "time1,time2"
    i = s.split(',')

    # XXX normally, Pilot formats filters with the
    # following syntax:
    # 11/20/2007 12:28:01.719742,11/20/2007 12:35:19.719742, GMT -8
    # We will need to handle the third piece if we want to avoid problems
    # with daylight savings
    if len(i) >= 2:
        try:
            p = TimeParser()
            start = p.parse(i[0])
            end = p.parse(i[1])
            return start, end
        except ValueError:
            pass

    raise ValueError('cannot parse time range "%s"' % s)


# XXX probably incorrect in some locales?
_fmt_widths = {
    'a': 3, 'A': 9, 'b': 3, 'B': 9, 'c': 24,
    'd': 2, 'f': 6, 'H': 2, 'I': 2, 'j': 3,
    'm': 2, 'M': 2, 'p': 2, 'S': 2, 'U': 2,
    'w': 1, 'W': 2, 'x': 8, 'X': 8, 'y': 2,
    'Y': 4, 'z': 5, 'Z': 3, '%': 1
}


def max_width(fmt):
    """Given a time formatting string (i.e., a string that can be
    used as the `fmt` option to `datetime.strftime`, compute the
    maximum length of a formatted date string.
    """
    m = 0
    i = 0
    while i < len(fmt):
        if fmt[i] == '%':
            try:
                i += 1
                m += _fmt_widths[fmt[i]]
            except IndexError:
                raise ValueError(
                    'do not understand %%%s in format string' % fmt[i]
                )
        else:
            m += 1

        i += 1

    return m

# Copyright (c) 2019 Riverbed Technology, Inc. # # This software is licensed under the terms and conditions of the MIT License # accompanying the software ("License").  This software is distributed "AS IS" # as set forth in the License. This module contains a number of utilities for working with dates and times, in conjunction with the python `datetime` module. # # tzinfo objects for utc and the local timezone, # https://launchpad.net/dateutil # # We can't use mktime here. It is unstable when deciding if # the hour near to a change is DST or not. # # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour, #                         dt.minute, dt.second, dt.weekday(), 0, -1)) # return time.localtime(timestamp).tm_isdst # # The code above yields the following result: # # >>> import tz, datetime # >>> t = tz.tzlocal() # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() # 'BRDT' # >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname() # 'BRST' # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() # 'BRST' # >>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname() # 'BRDT' # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() # 'BRDT' # # Here is a more stable implementation: # Return a datetime object that corresponds to `dt` but that always has     timezone info.      If `dt` already has timezone info, then it is simply returned.     If `dt` does not have timezone info, then the local time zone is assumed. Return a datetime object that corresponds to `dt` but in UTC rather than     local time.      If `dt` includes timezone info, then this routine simply converts from the     given timezone to UTC.     If `dt` does not include timezone info, then it is assumed to be in local     time, which is then converted to UTC. Return the number of seconds since the Unix epoch for     the datetime object `dt`. Return the number of microseconds since the Unix epoch for     the datetime object `dt`. Return the number of nanoseconds since the Unix epoch for     the datetime object `dt`. Convert the string `s` which represents a time in seconds     since the Unix epoch to a datetime object. Convert the string `s` which represents a time in milliseconds     since the Unix epoch to a datetime object. Convert the string `s` which represents a time in microseconds     since the Unix epoch to a datetime object. Convert the value `ns` which represents a time in nanoseconds     since the Unix epoch (either as an integer or a string) to a     datetime object. # we want full nanosecond precision if we're using datetimeng but # float can't represent absolute timestamps with enough precision. # Decimal solves that problem but "regular" datetime objects can't # be constructed from instances of Decimal.  so, we create an # appropriate argument to fromtimestamp() by checking which # implementation of datetime we're using... Determine level of precision by number of digits and return     a datetime object.      Note: this method is only valid for datetimes between year 2001 and     year 2286 since it assumes second level precision has 10 digits. Convert the string `s` which represents a number of microseconds     to a timedelta object. Handle backwards compatibility for timedelta.total_seconds. # move out of TimeParser scope since Python3 won't allow it # ref: https://stackoverflow.com/a/13913933/2157429 # all the different time of day formats we can parse. # refer to the python time module documentation for details # on the format strings # all the different date formats we can parse. # note that dates are "normalized" a bit, see parse() below. Instances of this class parse strings representing dates and/or     times into python `datetime.datetime` objects.      This class is capable of parsing a variety of different formats.     On the first call, the method `parse()` may take some time, as it     tries a series of pre-defined formats one after another.  After     successfully parsing a string, the parser object remembers the format     that was used so subsequent calls with identically formatted strings     are as efficient as the underlying method `datetime.strptime`. Construct a new TimeParser object Parse string s as a date/time without any hint about the format.          If it can be parsed, returns a tuple of the datetime object         and the format object that was used.  If the string cannot be         parsed, raises ValueError. Do a "one-shot" parsing of string s as a date/time.          Doesn't remember anything about the format that was used. Parse the string `s` as a date and time.          Returns a `datetime.datetime` object on success or raises `ValueError`         if the string cannot be parsed. # begin with some normalization, strip whitespace and convert # dashes to slashes so that 05-10-2011 is equivalent to 05/10/2011 # Build map from casual units to standard units recognizable by datetime Round a datetime object to any time laps in seconds.      `dt`: datetime.datetime object, default now.     `round_to`: Closest number of seconds to round to, default 1 minute.     `round_up`: Default rounds down to nearest `round_to` interval,                True here will instead round up.     `trim`: Trim to nearest round_to value rather than rounding. # ref http://stackoverflow.com/a/10854034/2157429 Parse the string `s` representing some duration of time     (e.g., `"3 seconds"` or `"1 week"`) and return a `datetime.timedelta`     object representing that length of time.      If the string cannot be parsed, raises `ValueError`. Derive the most recent start datetime of the current duration unit.     i.e., for datetime(2016, 12, 14, 10, 14) and hour, should return     datetime(2016, 12, 14, 10, 0)      :param dt: datetime value     :param unit: string, duration unit: year, quarter, month, week, day,         hour, minute, second.     :return : datetime. # Update the month to be first of current quarter Parse the string `s` representing a range of times     (e.g., `"12:00 PM to 1:00 PM"` or `"last 2 weeks"`).      Upon success returns a pair of `datetime.datetime` objects     representing the beginning and end of the time range.     If the string cannot be parsed, raises `ValueError`. # first try something of the form "time1 to time2" # try something of the form "last time" # try to convert the datetime to # if it still doesn't work, try the pilot time filter syntax # which looks like "time1,time2" # XXX normally, Pilot formats filters with the # following syntax: # 11/20/2007 12:28:01.719742,11/20/2007 12:35:19.719742, GMT -8 # We will need to handle the third piece if we want to avoid problems # with daylight savings # XXX probably incorrect in some locales? Given a time formatting string (i.e., a string that can be     used as the `fmt` option to `datetime.strftime`, compute the     maximum length of a formatted date string.