1 #!/usr/bin/env python 2 3 """ 4 Date processing functions. 5 6 Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 16 details. 17 18 You should have received a copy of the GNU General Public License along with 19 this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 from datetime import date, datetime, timedelta 23 from pytz import timezone, UnknownTimeZoneError 24 import re 25 26 # iCalendar date and datetime parsing (from DateSupport in MoinSupport). 27 28 date_icalendar_regexp_str = ur'(?P<year>[0-9]{4})(?P<month>[0-9]{2})(?P<day>[0-9]{2})' 29 datetime_icalendar_regexp_str = date_icalendar_regexp_str + \ 30 ur'(?:' \ 31 ur'T(?P<hour>[0-2][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-6][0-9])' \ 32 ur'(?P<utc>Z)?' \ 33 ur')?' 34 35 match_date_icalendar = re.compile(date_icalendar_regexp_str, re.UNICODE).match 36 match_datetime_icalendar = re.compile(datetime_icalendar_regexp_str, re.UNICODE).match 37 38 def to_utc_datetime(dt): 39 if not dt: 40 return None 41 elif isinstance(dt, datetime): 42 return to_timezone(dt, "UTC") 43 else: 44 return dt 45 46 def to_timezone(dt, name): 47 try: 48 tz = name and timezone(name) or None 49 except UnknownTimeZoneError: 50 tz = None 51 return to_tz(dt, tz) 52 53 def to_tz(dt, tz): 54 if tz is not None and isinstance(dt, datetime): 55 if not dt.tzinfo: 56 return tz.localize(dt) 57 else: 58 return dt.astimezone(tz) 59 else: 60 return dt 61 62 def format_datetime(dt): 63 if not dt: 64 return None 65 elif isinstance(dt, datetime): 66 if dt.tzname() == "UTC": 67 return dt.strftime("%Y%m%dT%H%M%SZ") 68 else: 69 return dt.strftime("%Y%m%dT%H%M%S") 70 else: 71 return dt.strftime("%Y%m%d") 72 73 def get_datetime_item(dt): 74 if not dt: 75 return None, None 76 value = format_datetime(dt) 77 attr = isinstance(dt, datetime) and {"TZID" : dt.tzname(), "VALUE" : "DATE-TIME"} or {"VALUE" : "DATE"} 78 return value, attr 79 80 def get_datetime(value, attr=None): 81 82 """ 83 Return a datetime object from the given 'value' in iCalendar format, using 84 the 'attr' mapping (if specified) to control the conversion. 85 """ 86 87 if not attr or attr.get("VALUE") in (None, "DATE-TIME"): 88 m = match_datetime_icalendar(value) 89 if m: 90 year, month, day, hour, minute, second = map(m.group, [ 91 "year", "month", "day", "hour", "minute", "second" 92 ]) 93 94 if hour and minute and second: 95 dt = datetime( 96 int(year), int(month), int(day), int(hour), int(minute), int(second) 97 ) 98 99 # Impose the indicated timezone. 100 # NOTE: This needs an ambiguity policy for DST changes. 101 102 return to_timezone(dt, m.group("utc") and "UTC" or attr and attr.get("TZID") or None) 103 104 # Permit dates even if the VALUE is not set to DATE. 105 106 if not attr or attr.get("VALUE") in (None, "DATE"): 107 m = match_date_icalendar(value) 108 if m: 109 year, month, day = map(m.group, ["year", "month", "day"]) 110 return date(int(year), int(month), int(day)) 111 112 return None 113 114 def get_start_of_day(dt, tzid=None): 115 return datetime(dt.year, dt.month, dt.day, 0, 0, tzinfo=(tzid and timezone(tzid) or dt.tzinfo)) 116 117 def get_end_of_day(dt, tzid=None): 118 return get_start_of_day(dt + timedelta(1), tzid) 119 120 def get_start_of_next_day(dt, tzid=None): 121 if isinstance(dt, datetime): 122 return get_end_of_day(dt, tzid) 123 else: 124 return dt + timedelta(1) 125 126 def ends_on_same_day(dt, end): 127 return ( 128 dt.date() == end.date() or 129 end == get_end_of_day(dt) 130 ) 131 132 def get_timestamp(): 133 return format_datetime(to_timezone(datetime.utcnow(), "UTC")) 134 135 # vim: tabstop=4 expandtab shiftwidth=4