1"""Copyright 2008 Orbitz WorldWide
2
3Licensed under the Apache License, Version 2.0 (the "License");
4you may not use this file except in compliance with the License.
5You may obtain a copy of the License at
6
7   http://www.apache.org/licenses/LICENSE-2.0
8
9Unless required by applicable law or agreed to in writing, software
10distributed under the License is distributed on an "AS IS" BASIS,
11WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12See the License for the specific language governing permissions and
13limitations under the License."""
14
15import pytz
16from datetime import datetime, timedelta, datetime as datetimetype
17from django.conf import settings
18
19months = ['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec']
20weekdays = ['sun','mon','tue','wed','thu','fri','sat']
21SECONDS_STRING = 'seconds'
22MINUTES_STRING = 'minutes'
23HOURS_STRING = 'hours'
24DAYS_STRING = 'days'
25WEEKS_STRING = 'weeks'
26MONTHS_STRING = 'months'
27YEARS_STRING = 'years'
28
29
30def parseATTime(s, tzinfo=None, now=None):
31    if tzinfo is None:
32        tzinfo = pytz.timezone(settings.TIME_ZONE)
33    if isinstance(s, datetimetype):
34        if s.tzinfo:
35            return s.astimezone(tzinfo)
36        return tzinfo.localize(s)
37
38    s = s.strip().lower().replace('_','').replace(',','').replace(' ','')
39    if s.isdigit():
40        if len(s) == 8 and int(s[:4]) > 1900 and int(s[4:6]) < 13 and int(s[6:]) < 32:
41            pass  # Fall back because its not a timestamp, its YYYYMMDD form
42        else:
43            return datetime.fromtimestamp(int(s),tzinfo)
44    if '+' in s:
45        ref,offset = s.split('+',1)
46        offset = '+' + offset
47    elif '-' in s:
48        ref,offset = s.split('-',1)
49        offset = '-' + offset
50    else:
51        ref,offset = s,''
52
53    return tzinfo.normalize(parseTimeReference(ref, tzinfo, now) + parseTimeOffset(offset))
54
55
56def parseTimeReference(ref, tzinfo=None, now=None):
57    if tzinfo is None:
58        tzinfo = pytz.timezone(settings.TIME_ZONE)
59    if isinstance(ref, datetimetype):
60        if ref.tzinfo:
61            return ref.astimezone(tzinfo)
62        return tzinfo.localize(ref)
63
64    if now is None:
65        now = datetime.now(tzinfo)
66    else:
67        now = parseATTime(now, tzinfo)
68
69    if not ref or ref == 'now':
70        return now
71
72    rawRef = ref
73
74    # Time-of-day reference
75    i = ref.find(':')
76    hour,minute = 0,0
77    if 0 < i < 3:
78        hour = int( ref[:i] )
79        minute = int( ref[i+1:i+3] )
80        ref = ref[i+3:]
81        if ref[:2] == 'am':
82            ref = ref[2:]
83        elif ref[:2] == 'pm':
84            hour = (hour + 12) % 24
85            ref = ref[2:]
86
87    # Xam or XXam
88    i = ref.find('am')
89    if 0 < i < 3:
90        hour = int( ref[:i] )
91        ref = ref[i+2:]
92
93    # Xpm or XXpm
94    i = ref.find('pm')
95    if 0 < i < 3:
96        hour = (int( ref[:i] ) + 12) % 24
97        ref = ref[i+2:]
98
99    if ref.startswith('noon'):
100        hour,minute = 12,0
101        ref = ref[4:]
102    elif ref.startswith('midnight'):
103        hour,minute = 0,0
104        ref = ref[8:]
105    elif ref.startswith('teatime'):
106        hour,minute = 16,0
107        ref = ref[7:]
108
109    refDate = now.replace(hour=hour,minute=minute,second=0,microsecond=0,tzinfo=None)
110
111    # Day reference
112    if ref in ('yesterday','today','tomorrow'):  # yesterday, today, tomorrow
113        if ref == 'yesterday':
114            refDate -= timedelta(days=1)
115        elif ref == 'tomorrow':
116            refDate += timedelta(days=1)
117
118    elif ref.count('/') == 2:  # MM/DD/YY[YY]
119        m,d,y = map(int,ref.split('/'))
120        if y < 1900:
121            y += 1900
122        if y < 1970:
123            y += 100
124        refDate = datetime(year=y,month=m,day=d,hour=hour,minute=minute)
125
126    elif len(ref) == 8 and ref.isdigit():  # YYYYMMDD
127        refDate = datetime(year=int(ref[:4]), month=int(ref[4:6]), day=int(ref[6:8]), hour=hour, minute=minute)
128
129    elif ref[:3] in months:  # MonthName DayOfMonth
130        d = None
131        if ref[-2:].isdigit():
132            d = int(ref[-2:])
133        elif ref[-1:].isdigit():
134            d = int(ref[-1:])
135        else:
136            raise Exception("Day of month required after month name")
137        refDate = datetime(year=refDate.year, month=months.index(ref[:3]) + 1, day=d, hour=hour, minute=minute)
138
139    elif ref[:3] in weekdays:  # DayOfWeek (Monday, etc)
140        todayDayName = refDate.strftime("%a").lower()[:3]
141        today = weekdays.index( todayDayName )
142        twoWeeks = weekdays * 2
143        dayOffset = today - twoWeeks.index(ref[:3])
144        if dayOffset < 0:
145            dayOffset += 7
146        refDate -= timedelta(days=dayOffset)
147
148    elif ref:
149        raise ValueError("Unknown day reference: %s" % rawRef)
150
151    return tzinfo.localize(refDate)
152
153
154def parseTimeOffset(offset):
155    if not offset:
156        return timedelta()
157
158    t = timedelta()
159
160    if offset[0].isdigit():
161        sign = 1
162    else:
163        try:
164            sign = { '+' : 1, '-' : -1 }[offset[0]]
165        except KeyError:
166            raise KeyError('Invalid offset: %s' % offset)
167        offset = offset[1:]
168
169    while offset:
170        i = 1
171        while offset[:i].isdigit() and i <= len(offset):
172            i += 1
173        num = int(offset[:i-1])
174        offset = offset[i-1:]
175        i = 1
176        while offset[:i].isalpha() and i <= len(offset):
177            i += 1
178        unit = offset[:i-1]
179        offset = offset[i-1:]
180        unitString = getUnitString(unit)
181        if unitString == MONTHS_STRING:
182            unitString = DAYS_STRING
183            num = num * 30
184        if unitString == YEARS_STRING:
185            unitString = DAYS_STRING
186            num = num * 365
187        t += timedelta(**{ unitString : sign * num})
188
189    return t
190
191
192def getUnitString(s):
193    if s.startswith('s'): return SECONDS_STRING
194    if s.startswith('min'): return MINUTES_STRING
195    if s.startswith('h'): return HOURS_STRING
196    if s.startswith('d'): return DAYS_STRING
197    if s.startswith('w'): return WEEKS_STRING
198    if s.startswith('mon'): return MONTHS_STRING
199    if s.startswith('y'): return YEARS_STRING
200    raise ValueError("Invalid offset unit '%s'" % s)
201