Source code for shuup.utils.dates

# -*- coding: utf-8 -*-
# This file is part of Shuup.
#
# Copyright (c) 2012-2021, Shuup Commerce Inc. All rights reserved.
#
# This source code is licensed under the OSL-3.0 license found in the
# LICENSE file in the root directory of this source tree.
from __future__ import unicode_literals

import datetime
import django
import itertools
import six
import time
from django.utils import timezone
from django.utils.translation import ugettext as _, ungettext

__all__ = ("parse_date", "parse_time", "try_parse_date", "try_parse_time", "try_parse_datetime")

_date_formats = (
    "%Y-%m-%d",
    "%Y%m%d",
    "%Y/%m/%d",
    "%d.%m.%y",
    "%d.%m.%Y",
    "%Y %m %d",
    "%m/%d/%Y",
)

_time_formats = (
    "%H:%M:%S",
    "%H:%M",
)

_datetime_formats = list(
    itertools.chain.from_iterable(
        [["{} %H:%M:%S".format(fmt) for fmt in _date_formats], ["{} %H:%M".format(fmt) for fmt in _date_formats]]
    )
)

locale_year_and_month_formats = {
    # Sourced from the Unicode CLDR, version 27.1.
    # All locales not listed here use "MMM y".
    "be": "LLL y",
    "bg": "MM.y 'г'.",
    "bs": "MMM y.",
    "ca": "LLL y",
    "cs": "LLLL y",
    "dz": "y སྤྱི་ཟླ་MMM",
    "eu": "y MMM",
    "fi": "LLL y",
    "fo": "y MMM",
    "hr": "LLL y.",
    "hu": "y. MMM",
    "hy": "yթ. LLL",
    "ja": "y年M月",
    "ka": "MMM, y",
    "kea": "MMM 'di' y",
    "ko": "y년 MMM",
    "ky": "y-'ж'. MMM",
    "lt": "y-MM",
    "lv": "y. 'g'. MMM",
    "mk": "MMM y 'г'.",
    "ml": "y MMM",
    "mn": "y MMM",
    "ne": "y MMM",
    "os": "LLL y",
    "pl": "MM.y",
    "pt": "MM/y",
    "ru": "LLL y",
    "seh": "MMM 'de' y",
    "si": "y MMM",
    "sk": "LLLL y",
    "sr": "MMM y.",
    "uk": "LLL y",
    "uz": "y MMM",
}


def _parse_date_str(value):
    value = value.strip()
    for fmt in _date_formats:
        try:
            return datetime.datetime.strptime(value, fmt).date()
        except Exception:
            pass
    try:
        return datetime.datetime.strptime(value, "%Y-%m-%d %H:%M:%S.%f").date()
    except Exception:
        pass


def _parse_datetime_str(value):
    value = value.strip()
    for fmt in itertools.chain.from_iterable((_datetime_formats, _date_formats)):
        try:
            return datetime.datetime.strptime(value, fmt)
        except Exception:
            pass
    try:
        return datetime.datetime.strptime(value, "%Y-%m-%d %H:%M:%S.%f")
    except Exception:
        pass


def _parse_time_str(value):
    value = value.strip()
    for fmt in _time_formats:
        try:
            return datetime.datetime.strptime(value, fmt).time()
        except Exception:
            pass
    return None


[docs]def parse_date(value): """ Tries to make a date out of the value. If impossible, it raises an exception. :param value: A value of some kind. :return: Date :rtype: datetime.date :raise ValueError: """ # This should be first since `datetime.datetime` is a subclass of `datetime.date` if isinstance(value, datetime.datetime): return value.date() if isinstance(value, datetime.date): return value elif isinstance(value, six.string_types): date = _parse_date_str(value) if not date: raise ValueError("Error! Unable to parse `%s` as date." % value) return date raise ValueError("Error! Unable to parse `%s` as date (unknown type)." % value)
def parse_datetime(value): """ Tries to make a datetime out of the value. If impossible, it raises an exception. :param value: A value of some kind. :return: DateTime. :rtype: datetime.datetime :raise ValueError: """ # This should be first since `datetime.datetime` is a subclass of `datetime.date` if isinstance(value, datetime.datetime): return value if isinstance(value, datetime.date): return datetime.datetime.combine(value, datetime.datetime.min.time()) elif isinstance(value, six.string_types): date = _parse_datetime_str(value) if not date: raise ValueError("Error! Unable to parse `%s` as datetime." % value) return date raise ValueError("Error! Unable to parse `%s` as datetime (unknown type)." % value)
[docs]def parse_time(value): """ Tries to make a time out of the value. If impossible, it raises an exception. :param value: A value of some kind. :return: Time. :rtype: datetime.time :raise ValueError: """ if isinstance(value, datetime.time): return value if isinstance(value, datetime.datetime): return value.time() if isinstance(value, six.string_types): parsed_time = _parse_time_str(value) if not parsed_time: raise ValueError("Error! Unable to parse `%s` as date." % value) return parsed_time raise ValueError("Error! Unable to parse `%s` as date (unknown type)." % value)
[docs]def try_parse_datetime(value): """ Tries to make a datetime out of the value. If impossible, returns None. :param value: A value of some kind. :return: Datetime. :rtype: datetime.datetime """ if value is None: return None try: return parse_datetime(value) except ValueError: return None
[docs]def try_parse_date(value): """ Tries to make a time out of the value. If impossible, returns None. :param value: A value of some kind. :return: Date. :rtype: datetime.date """ if value is None: return None try: return parse_date(value) except ValueError: return None
[docs]def try_parse_time(value): """ Tries to make a time out of the value. If impossible, returns None. :param value: A value of some kind. :return: Time. :rtype: datetime.time """ if value is None: return None try: return parse_time(value) except ValueError: return None
def get_year_and_month_format(locale): """ Get the Babel/Unicode format string for a "year and month" format for the given locale. Only the "language" part of the locale is taken into account here. :param locale: Babel locale. :type locale: babel.Locale :return: format string. :rtype: str """ return locale_year_and_month_formats.get(locale.language.lower(), "MMM y") def to_aware(date, time=datetime.time.min, tz=None): """ Convert date or datetime to aware datetime. :type date: datetime.date|datetime.datetime :param date: Date or datetime object to convert. :type time: datetime.time :param time: Time value for supplementing dates to datetimes, default 0:00:00. :type tz: datetime.tzinfo|None Timezone to use, default ``timezone.get_current_timezone()``. :rtype: datetime.datetime :return: Converted aware datetime object. """ if isinstance(date, datetime.datetime): if timezone.is_aware(date): return date return dst_safe_timezone_aware(date, tz) assert isinstance(date, datetime.date), "%r should be date" % (date,) combined = datetime.datetime.combine(date, time) return dst_safe_timezone_aware(combined, tz) def dst_safe_timezone_aware(dt, tz=None): """ Safely make datetime aware considering Daylight Saving Time (DST) cases. If `timezone.make_aware` raises `pytz.exceptions.NonExistentTimeError`, it means the time doesn't exist on that timezone it is probably DST. :type dt: datetime.datetime :param dt: Datetime object to make aware. :type tz: datetime.tzinfo|None Timezone to use, default ``timezone.get_current_timezone()``. :rtype: datetime.datetime :return: Converted aware datetime object. """ from pytz.exceptions import NonExistentTimeError try: return timezone.make_aware(dt, timezone=tz) except NonExistentTimeError: if django.VERSION < (1, 9): # is_dst parameter is not available for Django 1.8 if tz is None: tz = timezone.get_current_timezone() return tz.localize(dt, is_dst=True) return timezone.make_aware(dt, timezone=tz, is_dst=True) def local_now(tz=None): """ Get current time as aware datetime in local timezone. :type tz: datetime.tzinfo|None Timezone to use, default ``timezone.get_current_timezone()`` :rtype: datetime.datetime """ return timezone.localtime(to_aware(timezone.now()), timezone=tz) def to_timestamp(date): """ Get a UNIX timestamp from a date or datetime. :param datetime.date|datetime.datetime date: the datetime to convert to unix timestamp. :rtype float """ return time.mktime(date.timetuple()) def to_datetime_range(start, end): for value in [start, end]: if not isinstance(value, datetime.date): raise TypeError("Error! Provided value `{!r}` is neither date nor datetime.".format(value)) start_is_datetime = isinstance(start, datetime.datetime) end_is_datetime = isinstance(end, datetime.datetime) if start_is_datetime != end_is_datetime: raise TypeError("Error! Start and end must be of the same type: `{!r}` - `{!r}`.".format(start, end)) # Add +1 day to end if it's a date to make the range inclusive end_delta = datetime.timedelta(days=(1 if not end_is_datetime else 0)) return (to_aware(start), to_aware(end) + end_delta) class DurationRange(object): """ Present duration range, min days to max days. """ def __init__(self, min_duration, max_duration=None): assert isinstance(min_duration, datetime.timedelta) assert max_duration is None or (isinstance(max_duration, datetime.timedelta)) assert max_duration is None or max_duration >= min_duration self.min_duration = min_duration self.max_duration = max_duration if max_duration is not None else min_duration @classmethod def from_days(cls, min_days, max_days=None): return cls( datetime.timedelta(days=min_days), (datetime.timedelta(days=max_days) if max_days is not None else None) ) def __str__(self): if self.min_duration == self.max_duration: days = self.max_duration.days return ungettext("%s day", "%s days", days) % (days,) return _("%(min)s--%(max)s days") % {"min": self.min_duration.days, "max": self.max_duration.days}