Fix day filter
[cds-indico.git] / indico / util / date_time.py
blobef80d8aad44e294439d8f75a2782caa5ee578799
1 # This file is part of Indico.
2 # Copyright (C) 2002 - 2015 European Organization for Nuclear Research (CERN).
4 # Indico is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License as
6 # published by the Free Software Foundation; either version 3 of the
7 # License, or (at your option) any later version.
9 # Indico is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with Indico; if not, see <http://www.gnu.org/licenses/>.
17 import calendar
18 import time
19 from collections import OrderedDict
20 from datetime import timedelta, datetime
21 from datetime import time as dt_time
23 import pytz
24 from flask import request
25 from babel.dates import format_datetime as _format_datetime
26 from babel.dates import format_time as _format_time
27 from babel.dates import format_date as _format_date
28 from babel.dates import format_timedelta as _format_timedelta
29 from babel.dates import get_timezone
30 from babel.numbers import format_number as _format_number
31 from dateutil.rrule import rrule, DAILY, MO, TU, WE, TH, FR, SA, SU
32 from dateutil.relativedelta import relativedelta
34 from MaKaC.common.timezoneUtils import nowutc, DisplayTZ
35 from indico.core.config import Config
36 from indico.util.i18n import get_current_locale, _, ngettext
39 now_utc = nowutc
42 def utc_timestamp(datetimeVal):
43 return int(calendar.timegm(datetimeVal.utctimetuple()))
46 def as_utc(dt):
47 """Returns the given datetime with tzinfo=UTC.
49 The given datetime object **MUST** be naive but already contain UTC!
50 """
51 return pytz.utc.localize(dt)
54 def server_to_utc(dt):
55 """Converts the given datetime in the server's TZ to UTC.
57 The given datetime **MUST** be naive but already contain the correct time in the server's TZ.
58 """
59 server_tz = get_timezone(Config.getInstance().getDefaultTimezone())
60 return server_tz.localize(dt).astimezone(pytz.utc)
63 def utc_to_server(dt):
64 """Converts the given UTC datetime to the server's TZ."""
65 server_tz = get_timezone(Config.getInstance().getDefaultTimezone())
66 return dt.astimezone(server_tz)
69 def format_datetime(dt, format='medium', locale=None, timezone=None, server_tz=False, keep_tz=False):
70 """
71 Basically a wrapper around Babel's own format_datetime
72 """
73 if not locale:
74 locale = get_current_locale()
75 if keep_tz:
76 assert timezone is None
77 timezone = dt.tzinfo
78 elif not timezone and dt.tzinfo:
79 timezone = DisplayTZ().getDisplayTZ()
80 elif server_tz:
81 timezone = Config.getInstance().getDefaultTimezone()
83 return _format_datetime(dt, format=format, locale=locale, tzinfo=timezone).encode('utf-8')
86 def format_date(d, format='medium', locale=None):
87 """
88 Basically a wrapper around Babel's own format_date
89 """
90 if not locale:
91 locale = get_current_locale()
93 return _format_date(d, format=format, locale=locale).encode('utf-8')
96 def format_time(t, format='short', locale=None, timezone=None, server_tz=False):
97 """
98 Basically a wrapper around Babel's own format_time
99 """
100 if not locale:
101 locale = get_current_locale()
102 if not timezone and t.tzinfo:
103 timezone = DisplayTZ().getDisplayTZ()
104 elif server_tz:
105 timezone = Config.getInstance().getDefaultTimezone()
106 if timezone:
107 timezone = get_timezone(timezone)
109 return _format_time(t, format=format, locale=locale, tzinfo=timezone).encode('utf-8')
112 def format_timedelta(td, format='short', locale=None):
114 Basically a wrapper around Babel's own format_timedelta
116 if not locale:
117 locale = get_current_locale()
119 return _format_timedelta(td, format=format, locale=locale).encode('utf-8')
122 def format_human_timedelta(delta, granularity='seconds'):
123 """Formats a timedelta in a human-readable way
125 :param delta: the timedelta to format
126 :param granularity: the granularity, i.e. the lowest unit that is
127 still displayed. when set e.g. to 'minutes',
128 the output will never contain seconds unless
129 the whole timedelta spans less than a minute.
130 Accepted values are 'seconds', 'minutes',
131 'hours' and 'days'.
133 field_order = ('days', 'hours', 'minutes', 'seconds')
134 long_names = {
135 'seconds': lambda n: ngettext(u'{0} second', u'{0} seconds', n).format(n),
136 'minutes': lambda n: ngettext(u'{0} minute', u'{0} minutes', n).format(n),
137 'hours': lambda n: ngettext(u'{0} hour', u'{0} hours', n).format(n),
138 'days': lambda n: ngettext(u'{0} day', u'{0} days', n).format(n),
140 short_names = {
141 'seconds': lambda n: ngettext(u'{0}s', u'{0}s', n).format(n),
142 'minutes': lambda n: ngettext(u'{0}m', u'{0}m', n).format(n),
143 'hours': lambda n: ngettext(u'{0}h', u'{0}h', n).format(n),
144 'days': lambda n: ngettext(u'{0}d', u'{0}d', n).format(n),
147 values = OrderedDict((key, 0) for key in field_order)
148 values['seconds'] = delta.total_seconds()
149 values['days'], values['seconds'] = divmod(values['seconds'], 86400)
150 values['hours'], values['seconds'] = divmod(values['seconds'], 3600)
151 values['minutes'], values['seconds'] = divmod(values['seconds'], 60)
152 for key, value in values.iteritems():
153 values[key] = int(value)
154 # keep all fields covered by the granularity, and if that results in
155 # no non-zero fields, include otherwise excluded ones
156 used_fields = set(field_order[:field_order.index(granularity) + 1])
157 available_fields = [x for x in field_order if x not in used_fields]
158 used_fields -= {k for k, v in values.iteritems() if not v}
159 while not sum(values[x] for x in used_fields) and available_fields:
160 used_fields.add(available_fields.pop(0))
161 for key in available_fields:
162 values[key] = 0
163 nonzero = OrderedDict((k, v) for k, v in values.iteritems() if v)
164 if not nonzero:
165 return long_names[granularity](0)
166 elif len(nonzero) == 1:
167 key, value = nonzero.items()[0]
168 return long_names[key](value)
169 else:
170 parts = [short_names[key](value) for key, value in nonzero.iteritems()]
171 return u' '.join(parts)
174 def format_human_date(dt, format='medium', locale=None):
176 Return the date in a human-like format for yesterday, today and tomorrow.
177 Format the date otherwise.
179 today = nowutc().date()
180 oneday = timedelta(days=1)
182 if not locale:
183 locale = get_current_locale()
185 if dt == today - oneday:
186 return _("yesterday")
187 elif dt == today:
188 return _("today")
189 elif dt == today + oneday:
190 return _("tomorrow")
191 else:
192 return format_date(dt, format, locale=locale)
195 def format_number(number, locale=None):
196 if not locale:
197 locale = get_current_locale()
198 return _format_number(number, locale=locale).encode('utf-8')
201 def is_same_month(date_1, date_2):
203 This method ensures that is the same month of the same year
205 return date_1.month == date_2.month and date_1.year == date_2.year
208 def timedelta_split(delta):
210 Decomposes a timedelta into hours, minutes and seconds
211 (timedelta only stores days and seconds)n
213 sec = delta.seconds + delta.days * 24 * 3600
214 hours, remainder = divmod(sec, 3600)
215 minutes, seconds = divmod(remainder, 60)
216 return hours, minutes, seconds
219 ## ATTENTION: Do not use this one for new developments ##
220 # It is flawed, as even though the returned value is DST-safe,
221 # it is in the _local timezone_, meaning that the number of seconds
222 # returned is the one for the hour with the same "value" for the
223 # local timezone.
224 def int_timestamp(datetimeVal, tz=pytz.timezone('UTC')):
226 Returns the number of seconds from the local epoch to the UTC time
228 return int(time.mktime(datetimeVal.astimezone(tz).timetuple()))
231 def overlaps(range1, range2, inclusive=False):
232 start1, end1 = range1
233 start2, end2 = range2
235 if inclusive:
236 return start1 <= end2 and start2 <= end1
237 else:
238 return start1 < end2 and start2 < end1
241 def get_overlap(range1, range2):
242 if not overlaps(range1, range2):
243 return None, None
245 start1, end1 = range1
246 start2, end2 = range2
248 latest_start = max(start1, start2)
249 earliest_end = min(end1, end2)
251 return latest_start, earliest_end
254 def iterdays(start, end, skip_weekends=False):
255 weekdays = (MO, TU, WE, TH, FR) if skip_weekends else None
256 start = get_day_start(start) if isinstance(start, datetime) else start
257 end = get_day_end(end) if isinstance(end, datetime) else end
258 return rrule(DAILY, dtstart=start, until=end, byweekday=weekdays)
261 def is_weekend(d):
262 return d.weekday() in [e.weekday for e in (SA, SU)]
265 def get_datetime_from_request(prefix='', default=None, source=None):
266 """Retrieves date and time from request data."""
267 if source is None:
268 source = request.values
270 if default is None:
271 default = datetime.now()
273 date_str = source.get('{}date'.format(prefix), '')
274 time_str = source.get('{}time'.format(prefix), '')
276 try:
277 parsed_date = datetime.strptime(date_str, '%Y-%m-%d').date()
278 except ValueError:
279 parsed_date = default.date()
281 try:
282 parsed_time = datetime.strptime(time_str, '%H:%M').time()
283 except ValueError:
284 parsed_time = default.time()
286 return datetime.combine(parsed_date, parsed_time)
289 def get_day_start(day):
290 tzinfo = None
291 if isinstance(day, datetime):
292 tzinfo = day.tzinfo
293 day = day.date()
294 return datetime.combine(day, dt_time(0, tzinfo=tzinfo))
297 def get_day_end(day):
298 tzinfo = None
299 if isinstance(day, datetime):
300 tzinfo = day.tzinfo
301 day = day.date()
302 return datetime.combine(day, dt_time(23, 59, tzinfo=tzinfo))
305 def round_up_to_minutes(dt, precision=15):
307 Rounds up a date time object to the given precision in minutes.
309 :param dt: datetime -- the time to round up
310 :param precision: int -- the precision to round up by in minutes. Negative
311 values for the precision are allowed but will round down instead of up.
312 :returns: datetime -- the time rounded up by the given precision in minutes.
314 increment = precision * 60
315 secs_in_current_hour = (dt.minute * 60) + dt.second + (dt.microsecond * 1e-6)
316 delta = (secs_in_current_hour // increment) * increment + increment - secs_in_current_hour
317 return dt + timedelta(seconds=delta)
320 def get_month_start(date):
321 return date + relativedelta(day=1)
324 def get_month_end(date):
325 return date + relativedelta(day=1, months=+1, days=-1)
328 def round_up_month(date, from_day=1):
329 """Rounds up a date to the next month unless its day is before *from_day*.
331 :param date: date object
332 :param from_day: day from which one to round *date* up
334 if date.day >= from_day:
335 return date + relativedelta(day=1, months=+1)
336 else:
337 return date