1
2
3 """ S3 Date/Time Toolkit
4
5 @copyright: 2015-2019 (c) Sahana Software Foundation
6 @license: MIT
7
8 @requires: U{B{I{gluon}} <http://web2py.com>}
9
10 Permission is hereby granted, free of charge, to any person
11 obtaining a copy of this software and associated documentation
12 files (the "Software"), to deal in the Software without
13 restriction, including without limitation the rights to use,
14 copy, modify, merge, publish, distribute, sublicense, and/or sell
15 copies of the Software, and to permit persons to whom the
16 Software is furnished to do so, subject to the following
17 conditions:
18
19 The above copyright notice and this permission notice shall be
20 included in all copies or substantial portions of the Software.
21
22 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
24 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
26 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
27 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
28 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
29 OTHER DEALINGS IN THE SOFTWARE.
30 """
31
32 __all__ = ("ISOFORMAT",
33 "S3DateTime",
34 "S3Calendar",
35 "S3DateTimeParser",
36 "S3DateTimeFormatter",
37 "s3_parse_datetime",
38 "s3_format_datetime",
39 "s3_decode_iso_datetime",
40 "s3_encode_iso_datetime",
41 "s3_utc",
42 "s3_get_utc_offset",
43 "s3_relative_datetime",
44 )
45
46 import datetime
47 try:
48 import dateutil
49 import dateutil.parser
50 import dateutil.tz
51 except ImportError:
52 import sys
53 sys.stderr.write("ERROR: python-dateutil module needed for date handling\n")
54 raise
55 import math
56 import re
57 import time
58
59 from gluon import current
60
61
62
63
64 ISOFORMAT = "%Y-%m-%dT%H:%M:%S"
65 OFFSET = re.compile(r"([+|-]{0,1})(\d{1,2}):(\d\d)")
66 RELATIVE = re.compile(r"([+-]{0,1})([0-9]*)([YMDhms])")
67 SECONDS = {"D": 86400, "h": 3600, "m": 60, "s": 1}
71 """
72 Toolkit for date+time parsing/representation
73 """
74
75
76 @classmethod
78 """
79 Represent the date according to deployment settings &/or T()
80
81 @param dt: the date (datetime.date or datetime.datetime)
82 @param format: the format (overrides deployment setting)
83 @param utc: the date is given in UTC
84 @param calendar: the calendar to use (defaults to current.calendar)
85 """
86
87 if not format:
88 format = current.deployment_settings.get_L10n_date_format()
89
90 if calendar is None:
91 calendar = current.calendar
92 elif isinstance(calendar, basestring):
93 calendar = S3Calendar(calendar)
94
95 if dt:
96 if utc:
97 offset = cls.get_offset_value(current.session.s3.utc_offset)
98 if offset:
99 delta = datetime.timedelta(seconds=offset)
100 if not isinstance(dt, datetime.datetime):
101 combine = datetime.datetime.combine
102
103 bp = (combine(dt, datetime.time(8, 0, 0)) - delta).time()
104 dt = combine(dt, bp)
105 dt = dt + delta
106 dtstr = calendar.format_date(dt, dtfmt=format, local=True)
107 else:
108 dtstr = current.messages["NONE"]
109
110 return dtstr
111
112
113 @classmethod
115 """
116 Represent the datetime according to deployment settings &/or T()
117
118 @param dt: the datetime
119 @param utc: the datetime is given in UTC
120 @param calendar: the calendar to use (defaults to current.calendar)
121 """
122
123 if format is None:
124 format = current.deployment_settings.get_L10n_datetime_format()
125
126 if calendar is None:
127 calendar = current.calendar
128 elif isinstance(calendar, basestring):
129 calendar = S3Calendar(calendar)
130
131 if dt:
132 if utc:
133 offset = cls.get_offset_value(current.session.s3.utc_offset)
134 if offset:
135 delta = datetime.timedelta(seconds=offset)
136 if not isinstance(dt, datetime.datetime):
137 combine = datetime.datetime.combine
138 bp = (combine(dt, datetime.time(8, 0, 0)) - delta).time()
139 dt = combine(dt, bp)
140 dt = dt + datetime.timedelta(seconds=offset)
141 dtstr = calendar.format_datetime(dt, dtfmt=format, local=True)
142 else:
143 dtstr = current.messages["NONE"]
144
145 return dtstr
146
147
148 @classmethod
150 """
151 Represent the date according to deployment settings &/or T()
152
153 @param time: the time
154 @param format: the time format (overrides deployment setting)
155 @param utc: the time is given in UTC
156 """
157
158 settings = current.deployment_settings
159
160 if format is None:
161 format = settings.get_L10n_time_format()
162
163 if time and utc:
164
165 if not isinstance(time, datetime.datetime):
166 today = datetime.datetime.utcnow().date()
167 time = datetime.datetime.combine(today, time)
168
169 offset = cls.get_offset_value(current.session.s3.utc_offset)
170 if offset:
171 time = time + datetime.timedelta(seconds=offset)
172 if isinstance(time, datetime.datetime):
173
174 time = time.time()
175 if time:
176 try:
177 return time.strftime(str(format))
178 except AttributeError:
179
180 raise TypeError("Invalid argument type: %s" % type(time))
181 else:
182 return current.messages["NONE"]
183
184
185 @staticmethod
187 """
188 Convert an UTC offset string into a UTC offset value in seconds
189
190 @param string: the UTC offset in hours as string, valid formats
191 are: "+HH:MM", "+HHMM", "+HH" (positive sign can
192 be omitted), can also recognize decimal notation
193 with "." as mark
194 """
195
196 if not string:
197 return 0
198
199 sign = 1
200 offset_hrs = offset_min = 0
201
202 if isinstance(string, (int, long, float)):
203 offset_hrs = string
204 elif isinstance(string, basestring):
205 if string[:3] == "UTC":
206 string = string[3:]
207 string = string.strip()
208 match = OFFSET.match(string)
209 if match:
210 groups = match.groups()
211 if groups[0] == "-":
212 sign = -1
213 offset_hrs = int(groups[1])
214 offset_min = int(groups[2])
215 elif "." not in string:
216 try:
217 offset_hrs = int(string)
218 except ValueError:
219 return 0
220 if offset_hrs < -99 or offset_hrs > 99:
221 if offset_hrs < 0:
222 sign = -1
223 offset_hrs, offset_min = divmod(abs(offset_hrs), 100)
224 else:
225 try:
226 offset_hrs = float(string)
227 except ValueError:
228 return 0
229 else:
230 return 0
231 return sign * (3600 * offset_hrs + 60 * offset_min)
232
235 """
236 Calendar Base Class (implementing the Gregorian Calendar)
237
238 Subclasses define their own CALENDAR name, and are registered
239 with this name in the calendars dict in S3Calendar._set_calendar().
240 """
241
242 CALENDAR = "Gregorian"
243
244
245
246
247
248 JDEPOCH = 1721425.5
249
250 MONTH_NAME = ("January", "February", "March",
251 "April", "May", "June",
252 "July", "August", "September",
253 "October", "November", "December",
254 )
255
256 MONTH_ABBR = ("Jan", "Feb", "Mar", "Apr", "May", "Jun",
257 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
258 )
259
260 MONTH_DAYS = (31, (28, 29), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
261
262 FIRST_DOW = 1
263
264
265
266
267 @classmethod
269 """
270 Convert a Julian day number to a year/month/day tuple
271 of this calendar, to be implemented by subclass
272
273 @param jd: the Julian day number
274 """
275
276
277 return cls._jd_to_gregorian(jd)
278
279
280 @classmethod
281 - def to_jd(cls, year, month, day):
282 """
283 Convert a year/month/day tuple of this calendar into
284 a Julian day number, to be implemented by subclass
285
286 @param year: the year number
287 @param month: the month number
288 @param day: the day-of-month number
289 """
290
291
292 return cls._gregorian_to_jd(year, month, day)
293
294
295
296
297 @property
299 """ Get the name of the current """
300
301 name = self._name
302 if not name:
303 name = current.deployment_settings.get_L10n_calendar()
304 if not name:
305 name = self.CALENDAR
306 return name
307
308
309 @property
317
318
319 @property
335
336
337 - def parse_date(self, dtstr, dtfmt=None, local=False):
338 """
339 Parse a datetime string according to this calendar
340
341 @param dtstr: the datetime as string
342 @param dtfmt: the datetime format (strptime), overrides default
343 @param local: whether the default format is local (=deployment
344 setting) or ISO
345 @return: the datetime (datetime.datetime)
346 """
347
348 if dtstr is None:
349 return None
350
351
352 if dtfmt is None:
353 if local:
354 dtfmt = current.deployment_settings.get_L10n_date_format()
355 else:
356 dtfmt = "%Y-%m-%d"
357
358
359 calendar = self.calendar
360
361
362 try:
363 timetuple = calendar._parse(dtstr, dtfmt)
364 except (ValueError, TypeError):
365 return None
366
367
368 timetuple = calendar._gdate(timetuple)
369
370
371 dt = datetime.datetime(*timetuple)
372 return dt.date()
373
374
376 """
377 Parse a datetime string according to this calendar
378
379 @param dtstr: the datetime as string
380 @param dtfmt: the datetime format (strptime)
381 @param local: whether the default format is local (=deployment
382 setting) or ISO
383 @return: the datetime (datetime.datetime)
384 """
385
386 if dtstr is None:
387 return None
388
389
390 if dtfmt is None:
391 if local:
392 dtfmt = current.deployment_settings.get_L10n_datetime_format()
393 else:
394 dtfmt = ISOFORMAT
395
396
397 calendar = self.calendar
398
399
400 try:
401 timetuple = calendar._parse(dtstr, dtfmt)
402 except (ValueError, TypeError):
403 return None
404
405
406 timetuple = calendar._gdate(timetuple)
407
408
409 dt = datetime.datetime(*timetuple)
410 return dt
411
412
436
437
466
467
468
469
471 """
472 Constructor
473
474 @param name: the name of the calendar (see _set_calendar for
475 supported calendars). If constructed without name,
476 the L10.calendar deployment setting will be used
477 instead.
478 """
479
480
481 self._calendars = {"Gregorian": S3Calendar,
482 "Persian": S3PersianCalendar,
483 "Afghan": S3AfghanCalendar,
484 "Nepali": S3NepaliCalendar,
485 }
486
487 if name is None:
488 self._name = None
489 self._calendar = None
490 elif name == self.CALENDAR:
491 self._name = name
492 self._calendar = self
493 else:
494 self._set_calendar(name)
495
496 self._parser = None
497
498 self._first_dow = None
499
500
502 """
503 Set the current calendar
504
505 @param name: the name of the calendar (falls back to CALENDAR)
506 """
507
508 calendars = self._calendars
509
510
511 if name not in calendars:
512 name = self.CALENDAR
513
514
515 if name == self.CALENDAR:
516 calendar = self
517 else:
518 calendar = calendars[name](name)
519
520 self._name = name
521 self._calendar = calendar
522
523 return calendar
524
525
541
542
543 - def _parse(self, dtstr, dtfmt):
544
545
546 parser = self._get_parser(dtfmt)
547
548 if not parser:
549
550 try:
551 timetuple = time.strptime(dtstr, dtfmt)
552 except ValueError, e:
553
554 try:
555 timetuple = time.strptime(dtstr + ":00", dtfmt)
556 except ValueError:
557 raise e
558 return timetuple[:6]
559
560
561 return parser.parse(dtstr)
562
563
607
608
610 """
611 Convert a time tuple from Gregorian calendar to this calendar
612
613 @param timetuple: time tuple (y, m, d, hh, mm, ss)
614 @return: time tuple (this calendar)
615 """
616
617 if self.name == "Gregorian":
618
619 return timetuple
620
621 y, m, d, hh, mm, ss = timetuple
622 jd = self._gregorian_to_jd(y, m, d)
623 y, m, d = self.from_jd(jd)
624
625 return (y, m, d, hh, mm, ss)
626
627
629 """
630 Convert a time tuple from this calendar to Gregorian calendar
631
632 @param timetuple: time tuple (y, m, d, hh, mm, ss)
633 @return: time tuple (Gregorian)
634 """
635
636 if self.name == "Gregorian":
637
638 return timetuple
639
640 y, m, d, hh, mm, ss = timetuple
641 jd = self.to_jd(y, m, d)
642 y, m, d = self._jd_to_gregorian(jd)
643
644 return (y, m, d, hh, mm, ss)
645
646
647 @staticmethod
649 """
650 Convert a Gregorian date into a Julian day number (matching
651 jQuery calendars algorithm)
652
653 @param year: the year number
654 @param month: the month number
655 @param day: the day number
656 """
657
658 if year < 0:
659 year = year + 1
660
661 if month < 3:
662 month = month + 12
663 year = year - 1
664
665 a = math.floor(year/100)
666 b = 2 - a + math.floor(a / 4)
667
668 return math.floor(365.25 * (year + 4716)) + \
669 math.floor(30.6001 * (month + 1)) + day + b - 1524.5
670
671
672 @staticmethod
674 """
675 Convert a Julian day number to a Gregorian date (matching
676 jQuery calendars algorithm)
677
678 @param jd: the Julian day number
679 @return: tuple (year, month, day)
680 """
681
682 z = math.floor(jd + 0.5)
683 a = math.floor((z - 1867216.25) / 36524.25)
684
685 a = z + 1 + a - math.floor(a / 4)
686 b = a + 1524
687 c = math.floor((b - 122.1) / 365.25)
688 d = math.floor(365.25 * c)
689 e = math.floor((b - d) / 30.6001)
690
691 day = b - d - math.floor(e * 30.6001)
692 if e > 13.5:
693 month = e - 13
694 else:
695 month = e - 1
696
697 if month > 2.5:
698 year = c - 4716
699 else:
700 year = c - 4715
701
702 if year <= 0:
703 year = year - 1
704
705 return (int(year), int(month), int(day))
706
709 """
710 S3Calendar subclass implementing the Solar Hijri calendar
711
712 @note: this calendar is called "Persian" in jQuery calendars despite
713 it actually implements the modern Iranian (=algorithmic Solar
714 Hijri) rather than the traditional Persian (=observation-based
715 Jalali) variant. However, we use the name "Persian" to match
716 the jQuery calendars naming of calendars, in order to avoid
717 confusion about naming differences between these two components.
718 """
719
720 CALENDAR = "Persian"
721
722 JDEPOCH = 1948320.5
723
724 MONTH_NAME = ("Farvardin", "Ordibehesht", "Khordad",
725 "Tir", "Mordad", "Shahrivar",
726 "Mehr", "Aban", "Azar",
727 "Day", "Bahman", "Esfand",
728 )
729
730
731 MONTH_ABBR = ("Far", "Ord", "Kho", "Tir", "Mor", "Sha",
732 "Meh", "Aba", "Aza", "Day", "Bah", "Esf",
733 )
734
735 MONTH_DAYS = (31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, (29, 30))
736
737 FIRST_DOW = 6
738
739
740
741
742 @classmethod
744 """
745 Convert a Julian day number to a year/month/day tuple
746 of this calendar (matching jQuery calendars algorithm)
747
748 @param jd: the Julian day number
749 """
750
751 jd = math.floor(jd) + 0.5
752
753 depoch = jd - cls.to_jd(475, 1, 1)
754
755 cycle = math.floor(depoch / 1029983)
756 cyear = depoch % 1029983
757
758 if cyear != 1029982:
759 aux1 = math.floor(cyear / 366)
760 aux2 = cyear % 366
761 ycycle = math.floor(((2134 * aux1) + (2816 * aux2) + 2815) / 1028522) + aux1 + 1
762 else:
763 ycycle = 2820
764
765 year = ycycle + (2820 * cycle) + 474
766 if year <= 0:
767 year -= 1
768
769 yday = jd - cls.to_jd(year, 1, 1) + 1
770 if yday <= 186:
771 month = math.ceil(yday / 31)
772 else:
773 month = math.ceil((yday - 6) / 30)
774
775 day = jd - cls.to_jd(year, month, 1) + 1
776
777 return (int(year), int(month), int(day))
778
779
780 @classmethod
781 - def to_jd(cls, year, month, day):
782 """
783 Convert a year/month/day tuple of this calendar into
784 a Julian day number (matching jQuery calendars algorithm)
785
786 @param year: the year number
787 @param month: the month number
788 @param day: the day-of-month number
789 """
790
791 if year >= 0:
792 ep_base = year - 474
793 else:
794 ep_base = year - 473
795 ep_year = 474 + (ep_base % 2820)
796
797 if month <= 7:
798 mm = (month - 1) * 31
799 else:
800 mm = (month - 1) * 30 + 6
801
802 result = day + mm + math.floor((ep_year * 682 - 110) / 2816) + \
803 (ep_year - 1) * 365 + math.floor(ep_base / 2820) * 1029983 + \
804 cls.JDEPOCH - 1
805
806 return result
807
810 """
811 Afghan variant of the Solar Hijri calendar - this calendar uses
812 the same calendar rules as the "Persian" calendar, but with
813 different month names.
814
815 @note: this is using "romanized" Dari month names as translation
816 basis (rather than their actual English translation, which
817 would simply be the names of the signs of Zodiac the sun is
818 passing through in the respective months, e.g. Tawr (Sawr) = Taurus).
819 Transcriptions vary widely between sources, though - as do
820 the Dari and Pashto spellings :/
821 """
822
823 CALENDAR = "Afghan"
824
825 MONTH_NAME = ("Hamal", "Sawr", "Jawza",
826 "Saratan", "Asad", "Sonbola",
827 "Mizan", "Aqrab", "Qaws",
828 "Jadi", "Dalw", "Hut",
829 )
830
831 MONTH_ABBR = ("Ham", "Saw", "Jaw", "Sar", "Asa", "Son",
832 "Miz", "Aqr", "Qaw", "Jad", "Dal", "Hut",
833 )
834
835 FIRST_DOW = 6
836
839 """
840 S3Calendar subclass implementing the Nepali calendar (Bikram Samvat)
841 """
842
843
844
845
846
847 CALENDAR = "Nepali"
848
849 JDEPOCH = 1700709.5
850
851 MONTH_NAME = ("Baisakh", "Jestha", "Ashadh",
852 "Shrawan", "Bhadra", "Ashwin",
853 "Kartik", "Mangsir", "Paush",
854 "Mangh", "Falgun", "Chaitra",
855 )
856
857
858 MONTH_ABBR = ("Bai", "Je", "As",
859 "Shra", "Bha", "Ash",
860 "Kar", "Mang", "Pau",
861 "Ma", "Fal", "Chai",
862 )
863
864 MONTH_DAYS = ((30, 31), (31, 32), (31, 32),
865 (31, 32), (31, 32), (30, 31),
866 (29, 30), (29, 30), (29, 30),
867 (29, 30), (29, 30), (30, 31))
868
869 FIRST_DOW = 1
870
871
872
873
874
875
876
877
878 NEPALI_CALENDAR_DATA = {
879
880 1970: [18, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
881 1971: [18, 31, 31, 32, 31, 32, 30, 30, 29, 30, 29, 30, 30],
882 1972: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 30],
883 1973: [19, 30, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
884 1974: [19, 31, 31, 32, 30, 31, 31, 30, 29, 30, 29, 30, 30],
885 1975: [18, 31, 31, 32, 32, 30, 31, 30, 29, 30, 29, 30, 30],
886 1976: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
887 1977: [18, 31, 32, 31, 32, 31, 31, 29, 30, 29, 30, 29, 31],
888 1978: [18, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
889 1979: [18, 31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
890 1980: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
891 1981: [18, 31, 31, 31, 32, 31, 31, 29, 30, 30, 29, 30, 30],
892 1982: [18, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
893 1983: [18, 31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
894 1984: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
895 1985: [18, 31, 31, 31, 32, 31, 31, 29, 30, 30, 29, 30, 30],
896 1986: [18, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
897 1987: [18, 31, 32, 31, 32, 31, 30, 30, 29, 30, 29, 30, 30],
898 1988: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
899 1989: [18, 31, 31, 31, 32, 31, 31, 30, 29, 30, 29, 30, 30],
900 1990: [18, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
901 1991: [18, 31, 32, 31, 32, 31, 30, 30, 29, 30, 29, 30, 30],
902
903 1992: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
904 1993: [18, 31, 31, 31, 32, 31, 31, 30, 29, 30, 29, 30, 30],
905 1994: [18, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
906 1995: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 30],
907 1996: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
908 1997: [18, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
909 1998: [18, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
910 1999: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
911 2000: [17, 30, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
912 2001: [18, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
913 2002: [18, 31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
914 2003: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
915 2004: [17, 30, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
916 2005: [18, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
917 2006: [18, 31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
918 2007: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
919 2008: [17, 31, 31, 31, 32, 31, 31, 29, 30, 30, 29, 29, 31],
920 2009: [18, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
921 2010: [18, 31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
922 2011: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
923 2012: [17, 31, 31, 31, 32, 31, 31, 29, 30, 30, 29, 30, 30],
924 2013: [18, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
925 2014: [18, 31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
926 2015: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
927 2016: [17, 31, 31, 31, 32, 31, 31, 29, 30, 30, 29, 30, 30],
928 2017: [18, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
929 2018: [18, 31, 32, 31, 32, 31, 30, 30, 29, 30, 29, 30, 30],
930 2019: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
931 2020: [17, 31, 31, 31, 32, 31, 31, 30, 29, 30, 29, 30, 30],
932 2021: [18, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
933 2022: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 30],
934 2023: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
935 2024: [17, 31, 31, 31, 32, 31, 31, 30, 29, 30, 29, 30, 30],
936 2025: [18, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
937 2026: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
938 2027: [17, 30, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
939 2028: [17, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
940 2029: [18, 31, 31, 32, 31, 32, 30, 30, 29, 30, 29, 30, 30],
941 2030: [17, 31, 32, 31, 32, 31, 30, 30, 30, 30, 30, 30, 31],
942 2031: [17, 31, 32, 31, 32, 31, 31, 31, 31, 31, 31, 31, 31],
943 2032: [17, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32],
944 2033: [18, 31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
945 2034: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
946 2035: [17, 30, 32, 31, 32, 31, 31, 29, 30, 30, 29, 29, 31],
947 2036: [17, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
948 2037: [18, 31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
949 2038: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
950 2039: [17, 31, 31, 31, 32, 31, 31, 29, 30, 30, 29, 30, 30],
951 2040: [17, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
952 2041: [18, 31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
953 2042: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
954 2043: [17, 31, 31, 31, 32, 31, 31, 29, 30, 30, 29, 30, 30],
955 2044: [17, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
956 2045: [18, 31, 32, 31, 32, 31, 30, 30, 29, 30, 29, 30, 30],
957 2046: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
958 2047: [17, 31, 31, 31, 32, 31, 31, 30, 29, 30, 29, 30, 30],
959 2048: [17, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
960 2049: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 30],
961 2050: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
962 2051: [17, 31, 31, 31, 32, 31, 31, 30, 29, 30, 29, 30, 30],
963 2052: [17, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
964 2053: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 30],
965 2054: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
966 2055: [17, 31, 31, 32, 31, 31, 31, 30, 29, 30, 30, 29, 30],
967 2056: [17, 31, 31, 32, 31, 32, 30, 30, 29, 30, 29, 30, 30],
968 2057: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
969 2058: [17, 30, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
970 2059: [17, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
971 2060: [17, 31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
972 2061: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
973 2062: [17, 30, 32, 31, 32, 31, 31, 29, 30, 29, 30, 29, 31],
974 2063: [17, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
975 2064: [17, 31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
976 2065: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
977 2066: [17, 31, 31, 31, 32, 31, 31, 29, 30, 30, 29, 29, 31],
978 2067: [17, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
979 2068: [17, 31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
980 2069: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
981 2070: [17, 31, 31, 31, 32, 31, 31, 29, 30, 30, 29, 30, 30],
982 2071: [17, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
983 2072: [17, 31, 32, 31, 32, 31, 30, 30, 29, 30, 29, 30, 30],
984 2073: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 31],
985 2074: [17, 31, 31, 31, 32, 31, 31, 30, 29, 30, 29, 30, 30],
986 2075: [17, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
987 2076: [16, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 30],
988 2077: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 30, 29, 31],
989 2078: [17, 31, 31, 31, 32, 31, 31, 30, 29, 30, 29, 30, 30],
990 2079: [17, 31, 31, 32, 31, 31, 31, 30, 29, 30, 29, 30, 30],
991 2080: [16, 31, 32, 31, 32, 31, 30, 30, 30, 29, 29, 30, 30],
992
993 2081: [17, 31, 31, 32, 32, 31, 30, 30, 30, 29, 30, 30, 30],
994 2082: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 30, 30, 30],
995 2083: [17, 31, 31, 32, 31, 31, 30, 30, 30, 29, 30, 30, 30],
996 2084: [17, 31, 31, 32, 31, 31, 30, 30, 30, 29, 30, 30, 30],
997 2085: [17, 31, 32, 31, 32, 31, 31, 30, 30, 29, 30, 30, 30],
998 2086: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 30, 30, 30],
999 2087: [16, 31, 31, 32, 31, 31, 31, 30, 30, 29, 30, 30, 30],
1000 2088: [16, 30, 31, 32, 32, 30, 31, 30, 30, 29, 30, 30, 30],
1001 2089: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 30, 30, 30],
1002 2090: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 30, 30, 30],
1003 2091: [16, 31, 31, 32, 31, 31, 31, 30, 30, 29, 30, 30, 30],
1004 2092: [16, 31, 31, 32, 32, 31, 30, 30, 30, 29, 30, 30, 30],
1005 2093: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 30, 30, 30],
1006 2094: [17, 31, 31, 32, 31, 31, 30, 30, 30, 29, 30, 30, 30],
1007 2095: [17, 31, 31, 32, 31, 31, 31, 30, 29, 30, 30, 30, 30],
1008 2096: [17, 30, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30],
1009 2097: [17, 31, 32, 31, 32, 31, 30, 30, 30, 29, 30, 30, 30],
1010 2098: [17, 31, 31, 32, 31, 31, 31, 29, 30, 29, 30, 30, 31],
1011 2099: [17, 31, 31, 32, 31, 31, 31, 30, 29, 29, 30, 30, 30],
1012 2100: [17, 31, 32, 31, 32, 30, 31, 30, 29, 30, 29, 30, 30],
1013 }
1014
1015
1016
1017
1018 @classmethod
1020 """
1021 Convert a Julian day number to a year/month/day tuple
1022 of this calendar (matching jQuery calendars algorithm)
1023
1024 @param jd: the Julian day number
1025 """
1026
1027 gyear = cls._jd_to_gregorian(jd)[0]
1028
1029 gdoy = jd - cls._gregorian_to_jd(gyear, 1, 1) + 1
1030
1031 year = gyear + 56
1032 cdata = cls._get_calendar_data(year)
1033
1034 month = 9
1035 rdays = cdata[month] - cdata[0] + 1
1036
1037 while gdoy > rdays:
1038 month += 1
1039 if month > 12:
1040 month = 1
1041 year += 1
1042 cdata = cls._get_calendar_data(year)
1043 rdays += cdata[month]
1044
1045 day = cdata[month] - (rdays - gdoy)
1046
1047 return (int(year), int(month), int(day))
1048
1049
1050 @classmethod
1051 - def to_jd(cls, year, month, day):
1052 """
1053 Convert a year/month/day tuple of this calendar into
1054 a Julian day number (matching jQuery calendars algorithm)
1055
1056 @param year: the year number
1057 @param month: the month number
1058 @param day: the day-of-month number
1059 """
1060
1061 cmonth = month
1062 cyear = year
1063
1064
1065 if cmonth > 9 or cmonth == 9 and day > cls._get_calendar_data(cyear)[0]:
1066 gyear = year - 56
1067 else:
1068 gyear = year - 57
1069
1070
1071 gdoy = 0
1072 if month != 9:
1073 gdoy = day
1074 cmonth -= 1
1075
1076 cdata = cls._get_calendar_data(cyear)
1077 while cmonth != 9:
1078 if cmonth <= 0:
1079 cmonth = 12
1080 cyear -= 1
1081 cdata = cls._get_calendar_data(cyear)
1082 gdoy += cdata[cmonth]
1083 cmonth -= 1
1084
1085 if month == 9:
1086 gdoy += day - cdata[0]
1087 if gdoy <= 0:
1088 gyear_ = gyear + (1 if gyear < 0 else 0)
1089 gleapyear = gyear_ % 4 == 0 and \
1090 (gyear_ % 100 != 0 or gyear_ % 400 == 0)
1091 gdoy += 366 if gleapyear else 365
1092 else:
1093 gdoy += cdata[9] - cdata[0]
1094
1095
1096
1097 return cls._gregorian_to_jd(gyear, 1, 1) + gdoy
1098
1099
1100 @classmethod
1102 """
1103 Helper method to determine the days in the individual months
1104 of the BS calendar, as well as the start of the year
1105 """
1106
1107 default = [17, 31, 31, 32, 32, 31, 30, 30, 29, 30, 29, 30, 30]
1108
1109 return cls.NEPALI_CALENDAR_DATA.get(year, default)
1110
1113 """ Date/Time Parser for non-Gregorian calendars """
1114
1115 - def __init__(self, calendar, dtfmt=None):
1116 """
1117 Constructor
1118
1119 @param calendar: the calendar
1120 @param dtfmt: the date/time format
1121 """
1122
1123
1124 if not calendar:
1125 raise TypeError("Invalid calendar: %s (%s)" % (calendar, type(calendar)))
1126 self.calendar = calendar.calendar
1127
1128 self.grammar = None
1129 self.rules = None
1130
1131 self.set_format(dtfmt)
1132
1133
1134 - def parse(self, string):
1135 """
1136 Parse a date/time string
1137
1138 @param string: the date/time string
1139 @return: a timetuple (y, m, d, hh, mm, ss)
1140 """
1141
1142 if not isinstance(string, basestring):
1143 raise TypeError("Invalid argument type: expected str, got %s" % type(string))
1144 try:
1145 result = self.grammar.parseString(string)
1146 except self.ParseException:
1147 raise ValueError("Invalid date/time: %s" % string)
1148
1149 return self._validate(result)
1150
1151
1181
1182 rule = False
1183 for c in s3_unicode(dtfmt):
1184 if rule and c in rules:
1185
1186 sequence.pop()
1187 close(sequence)
1188
1189 result.append(rules[c])
1190
1191 sequence = []
1192
1193 rule = False
1194 continue
1195
1196 if c == "%" and not rule:
1197 rule = True
1198 else:
1199 rule = False
1200 sequence.append(c)
1201 if sequence:
1202 close(sequence)
1203
1204 if result:
1205 grammar = result[0]
1206 for item in result[1:]:
1207 grammar += item
1208 else:
1209
1210 grammar = pp.Suppress(pp.Regex(".*"))
1211
1212 self.grammar = grammar
1213 return grammar
1214
1215
1217 """
1218 Validate the parse result and convert it into a time tuple
1219
1220 @param parse_result: the parse result
1221 @return: a timetuple (y, m, d, hh, mm, ss)
1222 """
1223
1224 calendar = self.calendar
1225
1226
1227 now = current.request.utcnow
1228 today = (now.year, now.month, now.day, 0, 0, 0)
1229
1230
1231 cyear, cmonth = calendar._cdate(today)[:2]
1232
1233
1234 year = parse_result.get("year4")
1235 if year is None:
1236 year = parse_result.get("year2")
1237 if year is None:
1238
1239 year = cyear
1240 else:
1241
1242 current_century = int(cyear / 100) * 100
1243 year = current_century + year
1244
1245
1246 month = parse_result.get("month") or cmonth
1247
1248
1249 day = parse_result.get("day") or 1
1250
1251
1252 year, month, day = calendar.from_jd(calendar.to_jd(year, month, day))
1253
1254
1255 hour = parse_result.get("hour24")
1256 if hour is None:
1257
1258 hour = parse_result.get("hour12")
1259 if hour is None:
1260 hour = 0
1261 else:
1262
1263 if hour == 12:
1264 hour = 0
1265 if parse_result.get("ampm", "AM") == "PM":
1266 hour += 12
1267
1268
1269 minute = parse_result.get("minute") or 0
1270
1271
1272 second = parse_result.get("second") or 0
1273
1274 return (year, month, day, hour, minute, second)
1275
1276
1277 @staticmethod
1279 """ Parser helper to convert a token into an integer number """
1280
1281 try:
1282 return int(tokens[0])
1283 except (TypeError, ValueError):
1284 return None
1285
1286
1288 """
1289 Generate the general pyparsing rules for this calendar
1290
1291 @return: the rules dict
1292
1293 rules = {"d": Day of the month as a zero-padded decimal number
1294 "b": Month as locale’s abbreviated name
1295 "B": Month as locale’s full name
1296 "m": Month as a zero-padded decimal number
1297 "y": Year without century as a zero-padded decimal number
1298 "Y": Year with century as a decimal number
1299 "H": Hour (24-hour clock) as a zero-padded decimal number
1300 "I": Hour (12-hour clock) as a zero-padded decimal number
1301 "p": Locale’s equivalent of either AM or PM
1302 "M": Minute as a zero-padded decimal number
1303 "S": Second as a zero-padded decimal number
1304 }
1305
1306 @todo: support day-of-week options (recognize but suppress when parsing)
1307 """
1308
1309 import pyparsing as pp
1310
1311 T = current.T
1312 calendar = self.calendar
1313
1314 oneOf = pp.oneOf
1315 parse_int = self._parse_int
1316
1317 def numeric(minimum, maximum):
1318 """ Helper to define rules for zero-padded numeric values """
1319 zp = " ".join("%02d" % i \
1320 for i in xrange(minimum, min(10, maximum + 1)))
1321 np = " ".join("%d" % i \
1322 for i in xrange(minimum, maximum + 1))
1323 return (oneOf(zp) ^ oneOf(np)).setParseAction(parse_int)
1324
1325
1326 month_days = calendar.MONTH_DAYS
1327 days = [(max(d) if isinstance(d, tuple) else d) for d in month_days]
1328 day = numeric(1, max(days)).setResultsName("day")
1329
1330
1331 CaselessLiteral = pp.CaselessLiteral
1332 replaceWith = pp.replaceWith
1333
1334 num_months = len(calendar.MONTH_NAME)
1335 month = numeric(1, num_months).setResultsName("month")
1336
1337 expr = None
1338 for i, m in enumerate(calendar.MONTH_NAME):
1339 month_number = str(i+1)
1340 month_literal = CaselessLiteral(m)
1341 month_t = str(T(m))
1342 if month_t != m:
1343 month_literal |= CaselessLiteral(month_t)
1344 month_literal.setParseAction(replaceWith(month_number))
1345 expr = (expr | month_literal) if expr else month_literal
1346 month_name = expr.setParseAction(parse_int).setResultsName("month")
1347
1348 expr = None
1349 for i, m in enumerate(calendar.MONTH_ABBR):
1350 month_number = str(i+1)
1351 month_literal = CaselessLiteral(m)
1352 month_t = str(T(m))
1353 if month_t != m:
1354 month_literal |= CaselessLiteral(month_t)
1355 month_literal.setParseAction(replaceWith(month_number))
1356 expr = (expr | month_literal) if expr else month_literal
1357 month_abbr = expr.setParseAction(parse_int).setResultsName("month")
1358
1359
1360 Word = pp.Word
1361 nums = pp.nums
1362
1363 year2 = Word(nums, min=1, max=2)
1364 year2 = year2.setParseAction(parse_int).setResultsName("year2")
1365
1366 year4 = Word(nums, min=1, max=4)
1367 year4 = year4.setParseAction(parse_int).setResultsName("year4")
1368
1369
1370 hour24 = numeric(0, 23).setResultsName("hour24")
1371 hour12 = numeric(0, 12).setResultsName("hour12")
1372
1373
1374 minute = numeric(0, 59).setResultsName("minute")
1375
1376
1377 second = numeric(0, 59).setResultsName("second")
1378
1379
1380 am = ("AM", str(T("AM")), "am", str(T("am")))
1381 am = oneOf(" ".join(am)).setParseAction(pp.replaceWith("AM"))
1382 pm = ("PM", str(T("PM")), "pm", str(T("pm")))
1383 pm = oneOf(" ".join(pm)).setParseAction(pp.replaceWith("PM"))
1384 ampm = (am ^ pm).setResultsName("ampm")
1385
1386 rules = {"d": day,
1387 "b": month_abbr,
1388 "B": month_name,
1389 "m": month,
1390 "y": year2,
1391 "Y": year4,
1392 "H": hour24,
1393 "I": hour12,
1394 "p": ampm,
1395 "M": minute,
1396 "S": second,
1397 }
1398
1399 return rules
1400
1480
1485 """
1486 Parse a date/time string according to the given format.
1487
1488 @param string: the string
1489 @param dtfmt: the string format (defaults to ISOFORMAT)
1490
1491 @return: a datetime object, or None if the string is invalid
1492 """
1493
1494 if not string:
1495 return None
1496 if dtfmt is None:
1497 dtfmt = ISOFORMAT
1498 try:
1499 (y, m, d, hh, mm, ss) = time.strptime(string, dtfmt)[:6]
1500 dt = datetime.datetime(y, m, d, hh, mm, ss)
1501 except ValueError:
1502 dt = None
1503 return dt
1504
1521
1526 """
1527 Convert date/time string in ISO-8601 format into a datetime object
1528
1529 @note: this has "iso" in its name for consistency reasons,
1530 but can actually read a variety of formats
1531
1532 @param dtstr: the date/time string
1533
1534 @returns: a timezone-aware datetime.datetime object
1535
1536 @raises: ValueError if the string cannot be parsed
1537 """
1538
1539
1540 DEFAULT = datetime.datetime.utcnow().replace(hour = 8,
1541 minute = 0,
1542 second = 0,
1543 microsecond = 0,
1544 )
1545
1546 try:
1547 dt = dateutil.parser.parse(dtstr, default=DEFAULT)
1548 except (AttributeError, TypeError, ValueError):
1549 raise ValueError("Invalid date/time string: %s (%s)" % (dtstr, type(dtstr)))
1550
1551 if dt.tzinfo is None:
1552 dt = dt.replace(tzinfo=dateutil.tz.tzutc())
1553
1554 return dt
1555
1558 """
1559 Convert a datetime object into a ISO-8601 formatted
1560 string, omitting microseconds
1561
1562 @param dt: the datetime object
1563 """
1564
1565 if isinstance(dt, (datetime.datetime, datetime.time)):
1566 dx = dt.replace(microsecond=0)
1567 else:
1568 dx = dt
1569 return dx.isoformat()
1570
1571
1572
1573
1574 -def s3_utc(dt):
1575 """
1576 Get a datetime object for the same date/time as the
1577 datetime object, but in UTC
1578
1579 @param dt: the datetime object
1580 """
1581
1582 if dt:
1583 if dt.tzinfo is None:
1584 return dt.replace(tzinfo=dateutil.tz.tzutc())
1585 return dt.astimezone(dateutil.tz.tzutc())
1586 else:
1587 return None
1588
1591 """ Get the current UTC offset for the client """
1592
1593 offset = None
1594 session = current.session
1595 request = current.request
1596
1597 logged_in = current.auth.is_logged_in()
1598 if logged_in:
1599
1600
1601 offset = session.auth.user.utc_offset
1602 if offset:
1603 offset = offset.strip()
1604
1605 if not offset:
1606
1607
1608 offset = request.post_vars.get("_utc_offset", None)
1609 if offset:
1610 offset = int(offset)
1611 utcstr = offset < 0 and "+" or "-"
1612 hours = abs(int(offset/60))
1613 minutes = abs(int(offset % 60))
1614 offset = "%s%02d%02d" % (utcstr, hours, minutes)
1615
1616 if logged_in:
1617 session.auth.user.utc_offset = offset
1618
1619 if not offset:
1620
1621
1622 offset = current.deployment_settings.L10n.utc_offset
1623
1624 session.s3.utc_offset = offset
1625 return offset
1626
1631 """
1632 Return an absolute datetime for a relative date/time expression;
1633
1634 @param dtexpr: the relative date/time expression,
1635 syntax: "[+|-][numeric][Y|M|D|h|m|s]",
1636 e.g. "+12M" = twelve months from now,
1637 additionally recognizes the string "NOW"
1638
1639 @return: datetime.datetime (UTC), or None if dtexpr is invalid
1640 """
1641
1642 if dtexpr:
1643 dtexpr = dtexpr.strip()
1644 now = current.request.utcnow
1645 if dtexpr.lower() == "now":
1646 return now
1647 elif dtexpr[0] not in "+-":
1648 return None
1649 else:
1650 return None
1651
1652 from dateutil.relativedelta import relativedelta
1653 timedelta = datetime.timedelta
1654
1655 f = 1
1656 valid = False
1657 then = now
1658 for m in RELATIVE.finditer(dtexpr):
1659
1660 (sign, value, unit) = m.group(1,2,3)
1661
1662 try:
1663 value = int(value)
1664 except ValueError:
1665 continue
1666
1667 if sign == "-":
1668 f = -1
1669 elif sign == "+":
1670 f = 1
1671
1672 if unit == "Y":
1673 then += relativedelta(years = f * value)
1674 elif unit == "M":
1675 then += relativedelta(months = f * value)
1676 else:
1677 then += timedelta(seconds = f * value * SECONDS[unit])
1678 valid = True
1679
1680 return then if valid else None
1681
1682
1683