# HG changeset patch # User Goffi # Date 1543657219 -3600 # Node ID 3ba53b1cd1e6b86714c859212c9d500c7cbd68e3 # Parent 6555e9835ff8fff79bafbdc8004a6d085d63f22f tools (common/date_utils): date_parse_ext + timezone handling: - new date_parse_ext method parse a date where relative informations can be added. e.g.: "1 week ago", "now - 2 days", "01/01/01 01:01 + 3 hours". - new TZ_UTC and TZ_LOCAL constants can be used to use UTC time or local time zone. - date_parse* can now use a default_tz, which will be used if timezone if not specified in date format (default UTC). - tz_info can now be used in date_format to format the time in given timezone (default: UTC). diff -r 6555e9835ff8 -r 3ba53b1cd1e6 sat/tools/common/date_utils.py --- a/sat/tools/common/date_utils.py Sat Dec 01 10:34:42 2018 +0100 +++ b/sat/tools/common/date_utils.py Sat Dec 01 10:40:19 2018 +0100 @@ -20,34 +20,78 @@ """tools to help manipulating time and dates""" from sat.core.constants import Const as C +from sat.core.i18n import _ import datetime -from dateutil import parser as dateutil_parser +import dateutil +from dateutil import tz +from dateutil.relativedelta import relativedelta +from dateutil.utils import default_tzinfo from babel import dates import calendar import time +import re + +RELATIVE_RE = re.compile(ur"(?P.*?)(?P[-+]?) *(?P\d+) *" + ur"(?P(second|minute|hour|day|week|month|year))s?" + ur"(?P +ago)?", re.I) + +TZ_UTC = tz.tzutc() +TZ_LOCAL = tz.gettz() -def date_parse(value): +def date_parse(value, default_tz=TZ_UTC): """Parse a date and return corresponding unix timestamp @param value(unicode): date to parse, in any format supported by dateutil.parser + @param default_tz(datetime.tzinfo): default timezone + @return (int): timestamp + """ + dt = default_tzinfo(dateutil.parser.parse(unicode(value), dayfirst=True), default_tz) + return calendar.timegm(dt.utctimetuple()) + +def date_parse_ext(value, default_tz=TZ_UTC): + """Extended date parse which accept relative date + + @param value(unicode): date to parse, in any format supported by dateutil.parser + and with the hability to specify X days/weeks/months/years in the past or future. + Relative date are specified either with something like `[main_date] +1 week` + or with something like `3 days ago`, and it is case insensitive. [main_date] is + a date parsable by dateutil.parser, or empty to specify current date/time. + "now" can also be used to specify current date/time. + @param default_tz(datetime.tzinfo): same as for date_parse @return (int): timestamp """ - return calendar.timegm(dateutil_parser.parse(unicode(value)).utctimetuple()) + m = RELATIVE_RE.match(value) + if m is None: + return date_parse(value, default_tz=default_tz) + + if m.group(u"direction") and m.group(u"ago"): + raise ValueError( + _(u"You can't use a direction (+ or -) and \"ago\" at the same time")) + + if m.group(u"direction") == u'-' or m.group(u"ago"): + direction = -1 + else: + direction = 1 + + date = m.group(u"date").strip().lower() + if not date or date == u"now": + dt = datetime.datetime.now(tz.tzutc()) + else: + dt = default_tzinfo(dateutil.parser.parse(date, dayfirst=True)) + + quantity = int(m.group(u"quantity")) + key = m.group(u"unit").lower() + u"s" + delta_kw = {key: direction * quantity} + dt = dt + relativedelta(**delta_kw) + return calendar.timegm(dt.utctimetuple()) -def date_fmt( - timestamp, - fmt="short", - date_only=False, - auto_limit=7, - auto_old_fmt="short", - auto_new_fmt="relative", - locale_str=C.DEFAULT_LOCALE, -): +def date_fmt(timestamp, fmt="short", date_only=False, auto_limit=7, auto_old_fmt="short", + auto_new_fmt="relative", locale_str=C.DEFAULT_LOCALE, tz_info=TZ_UTC): """format date according to locale - @param timestamp(basestring, int): unix time + @param timestamp(basestring, float): unix time @param fmt(str): one of: - short: e.g. u'31/12/17' - medium: e.g. u'Apr 1, 2007' @@ -63,43 +107,56 @@ - auto_day: shorcut to set auto format with change on day old format will be short, and new format will be time only or a free value which is passed to babel.dates.format_datetime + (see http://babel.pocoo.org/en/latest/dates.html?highlight=pattern#pattern-syntax) @param date_only(bool): if True, only display date (not datetime) @param auto_limit (int): limit in days before using auto_old_fmt use 0 to have a limit at last midnight (day change) @param auto_old_fmt(unicode): format to use when date is older than limit @param auto_new_fmt(unicode): format to use when date is equal to or more recent than limit + @param locale_str(unicode): locale to use (as understood by babel) + @param tz_info(datetime.tzinfo): time zone to use """ + timestamp = float(timestamp) if fmt == "auto_day": fmt, auto_limit, auto_old_fmt, auto_new_fmt = "auto", 0, "short", "HH:mm" if fmt == "auto": if auto_limit == 0: - today = time.mktime(datetime.date.today().timetuple()) - if int(timestamp) < today: + now = datetime.datetime.now(tz_info) + # we want to use given tz_info, so we don't use date() or today() + today = datetime.datetime(year=now.year, month=now.month, day=now.day, + tzinfo=now.tzinfo) + today = calendar.timegm(today.utctimetuple()) + if timestamp < today: fmt = auto_old_fmt else: fmt = auto_new_fmt else: - days_delta = (time.time() - int(timestamp)) / 3600 + days_delta = (time.time() - timestamp) / 3600 if days_delta > (auto_limit or 7): fmt = auto_old_fmt else: fmt = auto_new_fmt if fmt == "relative": - delta = int(timestamp) - time.time() + delta = timestamp - time.time() return dates.format_timedelta( delta, granularity="minute", add_direction=True, locale=locale_str ) elif fmt in ("short", "long"): - formatter = dates.format_date if date_only else dates.format_datetime - return formatter(int(timestamp), format=fmt, locale=locale_str) + if date_only: + dt = datetime.fromtimestamp(timestamp, tz_info) + return dates.format_date(dt, format=fmt, locale=locale_str) + else: + return dates.format_datetime(timestamp, format=fmt, locale=locale_str, + tzinfo=tz_info) elif fmt == "iso": if date_only: fmt = "yyyy-MM-dd" else: fmt = "yyyy-MM-ddTHH:mm:ss'Z'" - return dates.format_datetime(int(timestamp), format=fmt) + return dates.format_datetime(timestamp, format=fmt) else: - return dates.format_datetime(int(timestamp), format=fmt, locale=locale_str) + return dates.format_datetime(timestamp, format=fmt, locale=locale_str, + tzinfo=tz_info)