comparison sat/tools/common/date_utils.py @ 2703:3ba53b1cd1e6

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).
author Goffi <goffi@goffi.org>
date Sat, 01 Dec 2018 10:40:19 +0100
parents 56f94936df1e
children 003b8b4b56a7
comparison
equal deleted inserted replaced
2702:6555e9835ff8 2703:3ba53b1cd1e6
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. 18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19 19
20 """tools to help manipulating time and dates""" 20 """tools to help manipulating time and dates"""
21 21
22 from sat.core.constants import Const as C 22 from sat.core.constants import Const as C
23 from sat.core.i18n import _
23 import datetime 24 import datetime
24 from dateutil import parser as dateutil_parser 25 import dateutil
26 from dateutil import tz
27 from dateutil.relativedelta import relativedelta
28 from dateutil.utils import default_tzinfo
25 from babel import dates 29 from babel import dates
26 import calendar 30 import calendar
27 import time 31 import time
32 import re
33
34 RELATIVE_RE = re.compile(ur"(?P<date>.*?)(?P<direction>[-+]?) *(?P<quantity>\d+) *"
35 ur"(?P<unit>(second|minute|hour|day|week|month|year))s?"
36 ur"(?P<ago> +ago)?", re.I)
37
38 TZ_UTC = tz.tzutc()
39 TZ_LOCAL = tz.gettz()
28 40
29 41
30 def date_parse(value): 42 def date_parse(value, default_tz=TZ_UTC):
31 """Parse a date and return corresponding unix timestamp 43 """Parse a date and return corresponding unix timestamp
32 44
33 @param value(unicode): date to parse, in any format supported by dateutil.parser 45 @param value(unicode): date to parse, in any format supported by dateutil.parser
46 @param default_tz(datetime.tzinfo): default timezone
34 @return (int): timestamp 47 @return (int): timestamp
35 """ 48 """
36 return calendar.timegm(dateutil_parser.parse(unicode(value)).utctimetuple()) 49 dt = default_tzinfo(dateutil.parser.parse(unicode(value), dayfirst=True), default_tz)
50 return calendar.timegm(dt.utctimetuple())
51
52 def date_parse_ext(value, default_tz=TZ_UTC):
53 """Extended date parse which accept relative date
54
55 @param value(unicode): date to parse, in any format supported by dateutil.parser
56 and with the hability to specify X days/weeks/months/years in the past or future.
57 Relative date are specified either with something like `[main_date] +1 week`
58 or with something like `3 days ago`, and it is case insensitive. [main_date] is
59 a date parsable by dateutil.parser, or empty to specify current date/time.
60 "now" can also be used to specify current date/time.
61 @param default_tz(datetime.tzinfo): same as for date_parse
62 @return (int): timestamp
63 """
64 m = RELATIVE_RE.match(value)
65 if m is None:
66 return date_parse(value, default_tz=default_tz)
67
68 if m.group(u"direction") and m.group(u"ago"):
69 raise ValueError(
70 _(u"You can't use a direction (+ or -) and \"ago\" at the same time"))
71
72 if m.group(u"direction") == u'-' or m.group(u"ago"):
73 direction = -1
74 else:
75 direction = 1
76
77 date = m.group(u"date").strip().lower()
78 if not date or date == u"now":
79 dt = datetime.datetime.now(tz.tzutc())
80 else:
81 dt = default_tzinfo(dateutil.parser.parse(date, dayfirst=True))
82
83 quantity = int(m.group(u"quantity"))
84 key = m.group(u"unit").lower() + u"s"
85 delta_kw = {key: direction * quantity}
86 dt = dt + relativedelta(**delta_kw)
87 return calendar.timegm(dt.utctimetuple())
37 88
38 89
39 def date_fmt( 90 def date_fmt(timestamp, fmt="short", date_only=False, auto_limit=7, auto_old_fmt="short",
40 timestamp, 91 auto_new_fmt="relative", locale_str=C.DEFAULT_LOCALE, tz_info=TZ_UTC):
41 fmt="short",
42 date_only=False,
43 auto_limit=7,
44 auto_old_fmt="short",
45 auto_new_fmt="relative",
46 locale_str=C.DEFAULT_LOCALE,
47 ):
48 """format date according to locale 92 """format date according to locale
49 93
50 @param timestamp(basestring, int): unix time 94 @param timestamp(basestring, float): unix time
51 @param fmt(str): one of: 95 @param fmt(str): one of:
52 - short: e.g. u'31/12/17' 96 - short: e.g. u'31/12/17'
53 - medium: e.g. u'Apr 1, 2007' 97 - medium: e.g. u'Apr 1, 2007'
54 - long: e.g. u'April 1, 2007' 98 - long: e.g. u'April 1, 2007'
55 - full: e.g. u'Sunday, April 1, 2007' 99 - full: e.g. u'Sunday, April 1, 2007'
61 - auto: use auto_old_fmt if date is older than auto_limit 105 - auto: use auto_old_fmt if date is older than auto_limit
62 else use auto_new_fmt 106 else use auto_new_fmt
63 - auto_day: shorcut to set auto format with change on day 107 - auto_day: shorcut to set auto format with change on day
64 old format will be short, and new format will be time only 108 old format will be short, and new format will be time only
65 or a free value which is passed to babel.dates.format_datetime 109 or a free value which is passed to babel.dates.format_datetime
110 (see http://babel.pocoo.org/en/latest/dates.html?highlight=pattern#pattern-syntax)
66 @param date_only(bool): if True, only display date (not datetime) 111 @param date_only(bool): if True, only display date (not datetime)
67 @param auto_limit (int): limit in days before using auto_old_fmt 112 @param auto_limit (int): limit in days before using auto_old_fmt
68 use 0 to have a limit at last midnight (day change) 113 use 0 to have a limit at last midnight (day change)
69 @param auto_old_fmt(unicode): format to use when date is older than limit 114 @param auto_old_fmt(unicode): format to use when date is older than limit
70 @param auto_new_fmt(unicode): format to use when date is equal to or more recent 115 @param auto_new_fmt(unicode): format to use when date is equal to or more recent
71 than limit 116 than limit
117 @param locale_str(unicode): locale to use (as understood by babel)
118 @param tz_info(datetime.tzinfo): time zone to use
72 119
73 """ 120 """
121 timestamp = float(timestamp)
74 if fmt == "auto_day": 122 if fmt == "auto_day":
75 fmt, auto_limit, auto_old_fmt, auto_new_fmt = "auto", 0, "short", "HH:mm" 123 fmt, auto_limit, auto_old_fmt, auto_new_fmt = "auto", 0, "short", "HH:mm"
76 if fmt == "auto": 124 if fmt == "auto":
77 if auto_limit == 0: 125 if auto_limit == 0:
78 today = time.mktime(datetime.date.today().timetuple()) 126 now = datetime.datetime.now(tz_info)
79 if int(timestamp) < today: 127 # we want to use given tz_info, so we don't use date() or today()
128 today = datetime.datetime(year=now.year, month=now.month, day=now.day,
129 tzinfo=now.tzinfo)
130 today = calendar.timegm(today.utctimetuple())
131 if timestamp < today:
80 fmt = auto_old_fmt 132 fmt = auto_old_fmt
81 else: 133 else:
82 fmt = auto_new_fmt 134 fmt = auto_new_fmt
83 else: 135 else:
84 days_delta = (time.time() - int(timestamp)) / 3600 136 days_delta = (time.time() - timestamp) / 3600
85 if days_delta > (auto_limit or 7): 137 if days_delta > (auto_limit or 7):
86 fmt = auto_old_fmt 138 fmt = auto_old_fmt
87 else: 139 else:
88 fmt = auto_new_fmt 140 fmt = auto_new_fmt
89 141
90 if fmt == "relative": 142 if fmt == "relative":
91 delta = int(timestamp) - time.time() 143 delta = timestamp - time.time()
92 return dates.format_timedelta( 144 return dates.format_timedelta(
93 delta, granularity="minute", add_direction=True, locale=locale_str 145 delta, granularity="minute", add_direction=True, locale=locale_str
94 ) 146 )
95 elif fmt in ("short", "long"): 147 elif fmt in ("short", "long"):
96 formatter = dates.format_date if date_only else dates.format_datetime 148 if date_only:
97 return formatter(int(timestamp), format=fmt, locale=locale_str) 149 dt = datetime.fromtimestamp(timestamp, tz_info)
150 return dates.format_date(dt, format=fmt, locale=locale_str)
151 else:
152 return dates.format_datetime(timestamp, format=fmt, locale=locale_str,
153 tzinfo=tz_info)
98 elif fmt == "iso": 154 elif fmt == "iso":
99 if date_only: 155 if date_only:
100 fmt = "yyyy-MM-dd" 156 fmt = "yyyy-MM-dd"
101 else: 157 else:
102 fmt = "yyyy-MM-ddTHH:mm:ss'Z'" 158 fmt = "yyyy-MM-ddTHH:mm:ss'Z'"
103 return dates.format_datetime(int(timestamp), format=fmt) 159 return dates.format_datetime(timestamp, format=fmt)
104 else: 160 else:
105 return dates.format_datetime(int(timestamp), format=fmt, locale=locale_str) 161 return dates.format_datetime(timestamp, format=fmt, locale=locale_str,
162 tzinfo=tz_info)