# Source code for logilab.common.date

    # copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
    # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
    #
    # This file is part of logilab-common.
    #
    # logilab-common is free software: you can redistribute it and/or modify it under
    # the terms of the GNU Lesser General Public License as published by the Free
    # Software Foundation, either version 2.1 of the License, or (at your option) any
    # later version.
    #
    # logilab-common is distributed in the hope that it will be useful, but WITHOUT
    # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
    # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
    # details.
    #
    # You should have received a copy of the GNU Lesser General Public License along
    # with logilab-common.  If not, see <http://www.gnu.org/licenses/>.
    """Date manipulation helper functions."""

    __docformat__ = "restructuredtext en"

    import math
    import re
    import sys
    from locale import getlocale, LC_TIME
    from datetime import date, time, datetime, timedelta
    from time import strptime as time_strptime
    from calendar import monthrange, timegm
    from typing import Union, List, Any, Optional, Generator

    from logilab.common.deprecation import callable_deprecated

    try:
        from mx.DateTime import RelativeDateTime, Date, DateTimeType
    except ImportError:
        endOfMonth = None
        DateTimeType = datetime
    else:
        endOfMonth = RelativeDateTime(months=1, day=-1)

    # NOTE: should we implement a compatibility layer between date representations
    #       as we have in lgc.db ?

    FRENCH_FIXED_HOLIDAYS = {
        "jour_an": "%s-01-01",
        "fete_travail": "%s-05-01",
        "armistice1945": "%s-05-08",
        "fete_nat": "%s-07-14",
        "assomption": "%s-08-15",
        "toussaint": "%s-11-01",
        "armistice1918": "%s-11-11",
        "noel": "%s-12-25",
    }

    FRENCH_MOBILE_HOLIDAYS = {
        "paques2004": "2004-04-12",
        "ascension2004": "2004-05-20",
        "pentecote2004": "2004-05-31",
        "paques2005": "2005-03-28",
        "ascension2005": "2005-05-05",
        "pentecote2005": "2005-05-16",
        "paques2006": "2006-04-17",
        "ascension2006": "2006-05-25",
        "pentecote2006": "2006-06-05",
        "paques2007": "2007-04-09",
        "ascension2007": "2007-05-17",
        "pentecote2007": "2007-05-28",
        "paques2008": "2008-03-24",
        "ascension2008": "2008-05-01",
        "pentecote2008": "2008-05-12",
        "paques2009": "2009-04-13",
        "ascension2009": "2009-05-21",
        "pentecote2009": "2009-06-01",
        "paques2010": "2010-04-05",
        "ascension2010": "2010-05-13",
        "pentecote2010": "2010-05-24",
        "paques2011": "2011-04-25",
        "ascension2011": "2011-06-02",
        "pentecote2011": "2011-06-13",
        "paques2012": "2012-04-09",
        "ascension2012": "2012-05-17",
        "pentecote2012": "2012-05-28",
    }

    # XXX this implementation cries for multimethod dispatching

    def get_step(dateobj: Union[date, datetime], nbdays: int = 1) -> timedelta:
        # assume date is either a python datetime or a mx.DateTime object
        if isinstance(dateobj, date):
            return ONEDAY * nbdays
        return nbdays  # mx.DateTime is ok with integers

    def datefactory(
        year: int, month: int, day: int, sampledate: Union[date, datetime]
    ) -> Union[date, datetime]:
        # assume date is either a python datetime or a mx.DateTime object
        if isinstance(sampledate, datetime):
            return datetime(year, month, day)
        if isinstance(sampledate, date):
            return date(year, month, day)
        return Date(year, month, day)

    def weekday(dateobj: Union[date, datetime]) -> int:
        # assume date is either a python datetime or a mx.DateTime object
        if isinstance(dateobj, date):
            return dateobj.weekday()
        return dateobj.day_of_week

    def str2date(datestr: str, sampledate: Union[date, datetime]) -> Union[date, datetime]:
        # NOTE: datetime.strptime is not an option until we drop py2.4 compat
        year, month, day = [int(chunk) for chunk in datestr.split("-")]
        return datefactory(year, month, day, sampledate)

    def days_between(start: Union[date, datetime], end: Union[date, datetime]) -> int:
        if isinstance(start, date):
            # mypy: No overload variant of "__sub__" of "datetime" matches argument type "date"
            # we ensure that end is a date
            assert isinstance(end, date)
            delta = end - start  # type: ignore
            # datetime.timedelta.days is always an integer (floored)
            if delta.seconds:
                return delta.days + 1
            return delta.days
        else:
            return int(math.ceil((end - start).days))

    def get_national_holidays(
        begin: Union[date, datetime], end: Union[date, datetime]
    ) -> Union[List[date], List[datetime]]:
        """return french national days off between begin and end"""
        begin = datefactory(begin.year, begin.month, begin.day, begin)
        end = datefactory(end.year, end.month, end.day, end)
        holidays = [str2date(datestr, begin) for datestr in FRENCH_MOBILE_HOLIDAYS.values()]
        for year in range(begin.year, end.year + 1):
            for datestr in FRENCH_FIXED_HOLIDAYS.values():
                date = str2date(datestr % year, begin)
                if date not in holidays:
                    holidays.append(date)
        return [day for day in holidays if begin <= day < end]

    def add_days_worked(start: date, days: int) -> date:
        """adds date but try to only take days worked into account"""
        step = get_step(start)
        weeks, plus = divmod(days, 5)
        end = start + ((weeks * 7) + plus) * step
        if weekday(end) >= 5:  # saturday or sunday
            end += 2 * step
        end += len([x for x in get_national_holidays(start, end + step) if weekday(x) < 5]) * step
        if weekday(end) >= 5:  # saturday or sunday
            end += 2 * step
        return end

    def nb_open_days(start: Union[date, datetime], end: Union[date, datetime]) -> int:
        assert start <= end
        step = get_step(start)
        days = days_between(start, end)
        weeks, plus = divmod(days, 7)
        if weekday(start) > weekday(end):
            plus -= 2
        elif weekday(end) == 6:
            plus -= 1
        open_days = weeks * 5 + plus
        nb_week_holidays = len(
            [x for x in get_national_holidays(start, end + step) if weekday(x) < 5 and x < end]
        )
        open_days -= nb_week_holidays
        if open_days < 0:
            return 0
        return open_days

    def date_range(
        begin: date, end: date, incday: Optional[Any] = None, incmonth: Optional[bool] = None
    ) -> Generator[date, Any, None]:
        """yields each date between begin and end

        :param begin: the start date
        :param end: the end date
        :param incr: the step to use to iterate over dates. Default is
                     one day.
        :param include: None (means no exclusion) or a function taking a
                        date as parameter, and returning True if the date
                        should be included.

        When using mx datetime, you should *NOT* use incmonth argument, use instead
        oneDay, oneHour, oneMinute, oneSecond, oneWeek or endOfMonth (to enumerate
        months) as `incday` argument
        """
        assert not (incday and incmonth)
        begin = todate(begin)
        end = todate(end)
        if incmonth:
            while begin < end:
                yield begin
                begin = next_month(begin, incmonth)
        else:
            incr = get_step(begin, incday or 1)
            while begin < end:
                yield begin
                begin += incr

    # makes py datetime usable #####################################################

    ONEDAY: timedelta = timedelta(days=1)
    ONEWEEK: timedelta = timedelta(days=7)

    strptime = callable_deprecated("Use strptime from datetime.datetime instead")(datetime.strptime)

    def strptime_time(value, format="%H:%M"):
        return time(*time_strptime(value, format)[3:6])

    def todate(somedate: date) -> date:
        """return a date from a date (leaving unchanged) or a datetime"""
        if isinstance(somedate, datetime):
            return date(somedate.year, somedate.month, somedate.day)
        assert isinstance(somedate, (date, DateTimeType)), repr(somedate)
        return somedate

    def totime(somedate):
        """return a time from a time (leaving unchanged), date or datetime"""
        # XXX mx compat
        if not isinstance(somedate, time):
            return time(somedate.hour, somedate.minute, somedate.second)
        assert isinstance(somedate, (time)), repr(somedate)
        return somedate

    def todatetime(somedate):
        """return a date from a date (leaving unchanged) or a datetime"""
        # take care, datetime is a subclass of date
        if isinstance(somedate, datetime):
            return somedate
        assert isinstance(somedate, (date, DateTimeType)), repr(somedate)
        return datetime(somedate.year, somedate.month, somedate.day)

    def datetime2ticks(somedate: Union[date, datetime]) -> int:
        return timegm(somedate.timetuple()) * 1000 + int(getattr(somedate, "microsecond", 0) / 1000)

    def ticks2datetime(ticks: int) -> datetime:
        miliseconds, microseconds = divmod(ticks, 1000)
        try:
            return datetime.fromtimestamp(miliseconds)
        except (ValueError, OverflowError):
            epoch = datetime.fromtimestamp(0)
            nb_days, seconds = divmod(int(miliseconds), 86400)
            delta = timedelta(nb_days, seconds=seconds, microseconds=microseconds)
            try:
                return epoch + delta
            except (ValueError, OverflowError):
                raise

    def days_in_month(somedate: date) -> int:
        return monthrange(somedate.year, somedate.month)[1]

    def days_in_year(somedate):
        feb = date(somedate.year, 2, 1)
        if days_in_month(feb) == 29:
            return 366
        else:
            return 365

    def previous_month(somedate, nbmonth=1):
        while nbmonth:
            somedate = first_day(somedate) - ONEDAY
            nbmonth -= 1
        return somedate

    def next_month(somedate: date, nbmonth: int = 1) -> date:
        while nbmonth:
            somedate = last_day(somedate) + ONEDAY
            nbmonth -= 1
        return somedate

    def first_day(somedate):
        return date(somedate.year, somedate.month, 1)

    def last_day(somedate: date) -> date:
        return date(somedate.year, somedate.month, days_in_month(somedate))

    def ustrftime(somedate: datetime, fmt: str = "%Y-%m-%d") -> str:
        """like strftime, but returns a unicode string instead of an encoded
        string which may be problematic with localized date.
        """
        if sys.version_info >= (3, 3):
            # datetime.date.strftime() supports dates since year 1 in Python >=3.3.
            return somedate.strftime(fmt)
        else:
            try:
                if sys.version_info < (3, 0):
                    encoding = getlocale(LC_TIME)[1] or "ascii"
                    return str(somedate.strftime(str(fmt)), encoding)
                else:
                    return somedate.strftime(fmt)
            except ValueError:
                if somedate.year >= 1900:
                    raise
                # datetime is not happy with dates before 1900
                # we try to work around this, assuming a simple
                # format string
                fields = {
                    "Y": somedate.year,
                    "m": somedate.month,
                    "d": somedate.day,
                }
                if isinstance(somedate, datetime):
                    fields.update({"H": somedate.hour, "M": somedate.minute, "S": somedate.second})
                fmt = re.sub("%([YmdHMS])", r"%(\1)02d", fmt)
                return str(fmt) % fields

    def utcdatetime(dt: datetime) -> datetime:
        if dt.tzinfo is None:
            return dt
        # mypy: No overload variant of "__sub__" of "datetime" matches argument type "None"
        return dt.replace(tzinfo=None) - dt.utcoffset()  # type: ignore

    def utctime(dt):
        if dt.tzinfo is None:
            return dt
        return (dt + dt.utcoffset() + dt.dst()).replace(tzinfo=None)

    def datetime_to_seconds(date):
        """return the number of seconds since the begining of the day for that date"""
        return date.second + 60 * date.minute + 3600 * date.hour

    def timedelta_to_days(delta):
        """return the time delta as a number of seconds"""
        return delta.days + delta.seconds / (3600 * 24)

    def timedelta_to_seconds(delta):
        """return the time delta as a fraction of days"""
        return delta.days * (3600 * 24) + delta.seconds