Package s3 :: Module s3datetime
[frames] | no frames]

Source Code for Module s3.s3datetime

   1  # -*- coding: utf-8 -*- 
   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  # Constants 
  63  # 
  64  ISOFORMAT = "%Y-%m-%dT%H:%M:%S" #: ISO 8601 Combined Date+Time format 
  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} 
68 69 # ============================================================================= 70 -class S3DateTime(object):
71 """ 72 Toolkit for date+time parsing/representation 73 """ 74 75 # ------------------------------------------------------------------------- 76 @classmethod
77 - def date_represent(cls, dt, format=None, utc=False, calendar=None):
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 # Compute the break point 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
114 - def datetime_represent(cls, dt, format=None, utc=False, calendar=None):
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
149 - def time_represent(cls, time, format=None, utc=False):
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 # Make sure to use datetime.datetime (to support timedelta) 165 if not isinstance(time, datetime.datetime): 166 today = datetime.datetime.utcnow().date() 167 time = datetime.datetime.combine(today, time) 168 # Add UTC offset 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 # Prevent error with dates<1900: convert into datetime.time 174 time = time.time() 175 if time: 176 try: 177 return time.strftime(str(format)) 178 except AttributeError: 179 # Invalid argument type 180 raise TypeError("Invalid argument type: %s" % type(time)) 181 else: 182 return current.messages["NONE"]
183 184 # ----------------------------------------------------------------------------- 185 @staticmethod
186 - def get_offset_value(string):
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
233 # ============================================================================= 234 -class S3Calendar(object):
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 # Constants to be implemented by subclasses 246 # ------------------------------------------------------------------------- 247 248 JDEPOCH = 1721425.5 # first day of this calendar as Julian Day number 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 # Monday 263 264 # ------------------------------------------------------------------------- 265 # Methods to be implemented by subclasses 266 # ------------------------------------------------------------------------- 267 @classmethod
268 - def from_jd(cls, jd):
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 # Gregorian calendar uses default method 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 # Gregorian calendar uses default method 292 return cls._gregorian_to_jd(year, month, day)
293 294 # ------------------------------------------------------------------------- 295 # Common Interface Methods (must not be implemented by subclasses): 296 # ------------------------------------------------------------------------- 297 @property
298 - def name(self):
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
310 - def calendar(self):
311 """ Get the current calendar """ 312 313 calendar = self._calendar 314 if calendar is None: 315 calendar = self._set_calendar(self.name) 316 return calendar
317 318 # ------------------------------------------------------------------------- 319 @property
320 - def first_dow(self):
321 """ Get the first day of the week for this calendar """ 322 323 calendar = self.calendar 324 325 first_dow = calendar._first_dow 326 if first_dow is None: 327 # Deployment setting? 328 first_dow = current.deployment_settings.get_L10n_firstDOW() 329 if first_dow is None: 330 # Calendar-specific default 331 first_dow = calendar.FIRST_DOW 332 calendar._first_dow = first_dow 333 334 return first_dow
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 # Default format 352 if dtfmt is None: 353 if local: 354 dtfmt = current.deployment_settings.get_L10n_date_format() 355 else: 356 dtfmt = "%Y-%m-%d" # ISO Date Format 357 358 # Use the current calendar 359 calendar = self.calendar 360 361 # Parse the dtstr 362 try: 363 timetuple = calendar._parse(dtstr, dtfmt) 364 except (ValueError, TypeError): 365 return None 366 367 # Convert timetuple to Gregorian calendar 368 timetuple = calendar._gdate(timetuple) 369 370 # Convert into datetime 371 dt = datetime.datetime(*timetuple) 372 return dt.date()
373 374 # -------------------------------------------------------------------------
375 - def parse_datetime(self, dtstr, dtfmt=None, local=False):
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 # Default format 390 if dtfmt is None: 391 if local: 392 dtfmt = current.deployment_settings.get_L10n_datetime_format() 393 else: 394 dtfmt = ISOFORMAT # ISO Date/Time Format 395 396 # Use the current calendar 397 calendar = self.calendar 398 399 # Parse the dtstr 400 try: 401 timetuple = calendar._parse(dtstr, dtfmt) 402 except (ValueError, TypeError): 403 return None 404 405 # Convert timetuple to Gregorian calendar 406 timetuple = calendar._gdate(timetuple) 407 408 # Convert into datetime 409 dt = datetime.datetime(*timetuple) 410 return dt
411 412 # -------------------------------------------------------------------------
413 - def format_date(self, dt, dtfmt=None, local=False):
414 """ 415 Format a date according to this calendar 416 417 @param dt: the date (datetime.date or datetime.datetime) 418 @return: the date as string 419 """ 420 421 if dt is None: 422 return current.messages["NONE"] 423 424 # Default format 425 if dtfmt is None: 426 if local: 427 dtfmt = current.deployment_settings.get_L10n_date_format() 428 else: 429 dtfmt = "%Y-%m-%d" # ISO Date Format 430 431 # Deal with T's 432 from s3utils import s3_str 433 dtfmt = s3_str(dtfmt) 434 435 return self.calendar._format(dt, dtfmt)
436 437 # -------------------------------------------------------------------------
438 - def format_datetime(self, dt, dtfmt=None, local=False):
439 """ 440 Format a datetime according to this calendar 441 442 @param dt: the datetime (datetime.datetime) 443 @return: the datetime as string 444 """ 445 446 if dt is None: 447 return current.messages["NONE"] 448 449 # Default format 450 if dtfmt is None: 451 if local: 452 dtfmt = current.deployment_settings.get_L10n_datetime_format() 453 else: 454 dtfmt = ISOFORMAT # ISO Date/Time Format 455 456 # Deal with T's 457 from s3utils import s3_str 458 dtfmt = s3_str(dtfmt) 459 460 # Remove microseconds 461 # - for the case that the calendar falls back to .isoformat 462 if isinstance(dt, datetime.datetime): 463 dt = dt.replace(microsecond=0) 464 465 return self.calendar._format(dt, dtfmt)
466 467 # ------------------------------------------------------------------------- 468 # Base class methods (must not be implemented by subclasses): 469 # -------------------------------------------------------------------------
470 - def __init__(self, name=None):
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 # Supported calendars 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 # -------------------------------------------------------------------------
501 - def _set_calendar(self, name=None):
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 # Fallback 511 if name not in calendars: 512 name = self.CALENDAR 513 514 # Instantiate the Calendar 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 # -------------------------------------------------------------------------
526 - def _get_parser(self, dtfmt):
527 528 # Gregorian calendar does not use a parser 529 if self.name == "Gregorian": 530 return None 531 532 # Configure the parser 533 parser = self._parser 534 if parser is None: 535 parser = S3DateTimeParser(self, dtfmt) 536 else: 537 parser.set_format(dtfmt) 538 self._parser = parser 539 540 return parser
541 542 # -------------------------------------------------------------------------
543 - def _parse(self, dtstr, dtfmt):
544 545 # Get the parser 546 parser = self._get_parser(dtfmt) 547 548 if not parser: 549 # Gregorian calendar - use strptime 550 try: 551 timetuple = time.strptime(dtstr, dtfmt) 552 except ValueError, e: 553 # Seconds missing? 554 try: 555 timetuple = time.strptime(dtstr + ":00", dtfmt) 556 except ValueError: 557 raise e 558 return timetuple[:6] 559 560 # Use calendar-specific parser 561 return parser.parse(dtstr)
562 563 # -------------------------------------------------------------------------
564 - def _format(self, dt, dtfmt):
565 """ 566 Get a string representation for a datetime.datetime according 567 to this calendar and dtfmt, to be implemented by subclass 568 569 @param dt: the datetime.datetime 570 @param dtfmt: the datetime format (strftime) 571 572 @return: the string representation (str) 573 574 @raises TypeError: for invalid argument types 575 """ 576 577 if self.name == "Gregorian": 578 # Gregorian Calendar uses strftime 579 fmt = str(dtfmt) 580 try: 581 dtstr = dt.strftime(fmt) 582 except ValueError: 583 # Dates < 1900 not supported by strftime 584 year = "%04i" % dt.year 585 fmt = fmt.replace("%Y", year).replace("%y", year[-2:]) 586 dtstr = dt.replace(year=1900).strftime(fmt) 587 except AttributeError: 588 # Invalid argument type 589 raise TypeError("Invalid argument type: %s" % type(dt)) 590 591 else: 592 if not isinstance(dt, datetime.datetime): 593 try: 594 timetuple = (dt.year, dt.month, dt.day, 0, 0, 0) 595 except AttributeError: 596 # Invalid argument type 597 raise TypeError("Invalid argument type: %s" % type(dt)) 598 else: 599 timetuple = (dt.year, dt.month, dt.day, 600 dt.hour, dt.minute, dt.second, 601 ) 602 603 formatter = S3DateTimeFormatter(self) 604 dtstr = formatter.render(self._cdate(timetuple), dtfmt) 605 606 return dtstr
607 608 # -------------------------------------------------------------------------
609 - def _cdate(self, timetuple):
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 # Gregorian Calendar does nothing here 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 # -------------------------------------------------------------------------
628 - def _gdate(self, timetuple):
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 # Gregorian Calendar does nothing here 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
648 - def _gregorian_to_jd(year, month, day):
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
673 - def _jd_to_gregorian(jd):
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
707 # ============================================================================= 708 -class S3PersianCalendar(S3Calendar):
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 # first day of this calendar as Julian Day number 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 # Shambe 738 739 # ------------------------------------------------------------------------- 740 # Methods to be implemented by subclasses 741 # ------------------------------------------------------------------------- 742 @classmethod
743 - def from_jd(cls, jd):
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
808 # ============================================================================= 809 -class S3AfghanCalendar(S3PersianCalendar):
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 # Shambe
836
837 # ============================================================================= 838 -class S3NepaliCalendar(S3Calendar):
839 """ 840 S3Calendar subclass implementing the Nepali calendar (Bikram Samvat) 841 """ 842 843 # ------------------------------------------------------------------------- 844 # Constants to be implemented by subclasses 845 # ------------------------------------------------------------------------- 846 847 CALENDAR = "Nepali" 848 849 JDEPOCH = 1700709.5 # first day of this calendar as Julian Day number 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 # Sombaar (=Monday) 870 871 # There is no algorithm to predict the days in the individual months 872 # of the Bikram Samvat calendar for a particular year, so we have to 873 # hardcode this information as a mapping dict (taken from jquery.calendars 874 # in order to match the front-end widget's calculations). 875 # Outside of the year range of this dict (years A.B.S.), we have to 876 # fall back to an approximation formula which may though give a day 877 # ahead or behind the actual date 878 NEPALI_CALENDAR_DATA = { 879 # These data are from http://www.ashesh.com.np 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 # These data are from http://nepalicalendar.rat32.com/index.php 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 # These data are from http://www.ashesh.com.np/nepali-calendar/ 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 # Methods to be implemented by subclasses 1017 # ------------------------------------------------------------------------- 1018 @classmethod
1019 - def from_jd(cls, jd):
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 # Get the Gregorian year 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 # Calculate days since January 1st in Gregorian year 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 # Convert January 1st of the Gregorian year to JD and 1096 # add the days that went since then 1097 return cls._gregorian_to_jd(gyear, 1, 1) + gdoy
1098 1099 # ------------------------------------------------------------------------- 1100 @classmethod
1101 - def _get_calendar_data(cls, year):
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
1111 # ============================================================================= 1112 -class S3DateTimeParser(object):
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 # Get the effective calendar 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 # -------------------------------------------------------------------------
1152 - def set_format(self, dtfmt):
1153 """ 1154 Update the date/time format for this parser, and generate 1155 the corresponding pyparsing grammar 1156 1157 @param dtfmt: the date/time format 1158 """ 1159 1160 if not isinstance(dtfmt, basestring): 1161 raise TypeError("Invalid date/time format: %s (%s)" % (dtfmt, type(dtfmt))) 1162 1163 import pyparsing as pp 1164 self.ParseException = pp.ParseException 1165 1166 from s3utils import s3_unicode 1167 1168 # Get the rules 1169 rules = self.rules 1170 if rules is None: 1171 rules = self.rules = self._get_rules() 1172 1173 # Interpret the format 1174 result = [] 1175 sequence = [] 1176 1177 def close(s): 1178 s = "".join(s).strip() 1179 if s: 1180 result.append(pp.Suppress(pp.Literal(s)))
1181 1182 rule = False 1183 for c in s3_unicode(dtfmt): 1184 if rule and c in rules: 1185 # Close previous sequence 1186 sequence.pop() 1187 close(sequence) 1188 # Append control rule 1189 result.append(rules[c]) 1190 # Start new sequence 1191 sequence = [] 1192 # Close rule 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 # Default = ignore everything 1210 grammar = pp.Suppress(pp.Regex(".*")) 1211 1212 self.grammar = grammar 1213 return grammar
1214 1215 # -------------------------------------------------------------------------
1216 - def _validate(self, parse_result):
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 # Get the current date 1227 now = current.request.utcnow 1228 today = (now.year, now.month, now.day, 0, 0, 0) 1229 1230 # Convert today into current calendar 1231 cyear, cmonth = calendar._cdate(today)[:2] 1232 1233 # Year 1234 year = parse_result.get("year4") 1235 if year is None: 1236 year = parse_result.get("year2") 1237 if year is None: 1238 # Fall back to current year of the calendar 1239 year = cyear 1240 else: 1241 # Add the current century of the calendar 1242 current_century = int(cyear / 100) * 100 1243 year = current_century + year 1244 1245 # Month 1246 month = parse_result.get("month") or cmonth 1247 1248 # Day of Month 1249 day = parse_result.get("day") or 1 1250 1251 # Correct the date by converting to JD and back 1252 year, month, day = calendar.from_jd(calendar.to_jd(year, month, day)) 1253 1254 # Hours 1255 hour = parse_result.get("hour24") 1256 if hour is None: 1257 # 12 hours? 1258 hour = parse_result.get("hour12") 1259 if hour is None: 1260 hour = 0 1261 else: 1262 # Do we have am or pm? 1263 if hour == 12: 1264 hour = 0 1265 if parse_result.get("ampm", "AM") == "PM": 1266 hour += 12 1267 1268 # Minute 1269 minute = parse_result.get("minute") or 0 1270 1271 # Second 1272 second = parse_result.get("second") or 0 1273 1274 return (year, month, day, hour, minute, second)
1275 1276 # ------------------------------------------------------------------------- 1277 @staticmethod
1278 - def _parse_int(s, l, tokens):
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 # -------------------------------------------------------------------------
1287 - def _get_rules(self):
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 # Day 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 # Month 1331 CaselessLiteral = pp.CaselessLiteral 1332 replaceWith = pp.replaceWith 1333 # ...numeric 1334 num_months = len(calendar.MONTH_NAME) 1335 month = numeric(1, num_months).setResultsName("month") 1336 # ...name 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 # ...abbreviation 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 # Year 1360 Word = pp.Word 1361 nums = pp.nums 1362 # ...without century 1363 year2 = Word(nums, min=1, max=2) 1364 year2 = year2.setParseAction(parse_int).setResultsName("year2") 1365 # ...with century 1366 year4 = Word(nums, min=1, max=4) 1367 year4 = year4.setParseAction(parse_int).setResultsName("year4") 1368 1369 # Hour 1370 hour24 = numeric(0, 23).setResultsName("hour24") 1371 hour12 = numeric(0, 12).setResultsName("hour12") 1372 1373 # Minute 1374 minute = numeric(0, 59).setResultsName("minute") 1375 1376 # Second 1377 second = numeric(0, 59).setResultsName("second") 1378 1379 # AM/PM 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
1401 # ============================================================================= 1402 -class S3DateTimeFormatter(object):
1403 """ Date/Time Formatter for non-Gregorian calendars """ 1404
1405 - def __init__(self, calendar):
1406 """ 1407 Constructor 1408 1409 @param calendar: the calendar 1410 """ 1411 1412 # Get the effective calendar 1413 if not calendar: 1414 raise TypeError("Invalid calendar: %s (%s)" % (calendar, type(calendar))) 1415 self.calendar = calendar.calendar
1416 1417 # -------------------------------------------------------------------------
1418 - def render(self, timetuple, dtfmt):
1419 """ 1420 Render a timetuple as string according to the given format 1421 1422 @param timetuple: the timetuple (y, m, d, hh, mm, ss) 1423 @param dtfmt: the date/time format (string) 1424 1425 @todo: support day-of-week options 1426 """ 1427 1428 y, m, d, hh, mm, ss = timetuple 1429 1430 T = current.T 1431 calendar = self.calendar 1432 1433 from s3utils import s3_unicode 1434 1435 rules = {"d": "%02d" % d, 1436 "b": T(calendar.MONTH_ABBR[m - 1]), 1437 "B": T(calendar.MONTH_NAME[m - 1]), 1438 "m": "%02d" % m, 1439 "y": "%02d" % (y % 100), 1440 "Y": "%04d" % y, 1441 "H": "%02d" % hh, 1442 "I": "%02d" % ((hh % 12) or 12), 1443 "p": T("AM") if hh < 12 else T("PM"), 1444 "M": "%02d" % mm, 1445 "S": "%02d" % ss, 1446 } 1447 1448 # Interpret the format 1449 result = [] 1450 sequence = [] 1451 1452 def close(s): 1453 s = "".join(s) 1454 if s: 1455 result.append(s)
1456 1457 rule = False 1458 for c in s3_unicode(dtfmt): 1459 if rule and c in rules: 1460 # Close previous sequence 1461 sequence.pop() 1462 close(sequence) 1463 # Append control rule 1464 result.append(s3_unicode(rules[c])) 1465 # Start new sequence 1466 sequence = [] 1467 # Close rule 1468 rule = False 1469 continue 1470 1471 if c == "%" and not rule: 1472 rule = True 1473 else: 1474 rule = False 1475 sequence.append(c) 1476 if sequence: 1477 close(sequence) 1478 1479 return "".join(result)
1480
1481 # ============================================================================= 1482 # Date/Time Parser and Formatter (@todo: integrate with S3Calendar) 1483 # 1484 -def s3_parse_datetime(string, dtfmt=None):
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
1505 #-------------------------------------------------------------------------- 1506 -def s3_format_datetime(dt=None, dtfmt=None):
1507 """ 1508 Format a datetime object according to the given format. 1509 1510 @param dt: the datetime object, defaults to datetime.datetime.utcnow() 1511 @param dtfmt: the string format (defaults to ISOFORMAT) 1512 1513 @return: a string 1514 """ 1515 1516 if not dt: 1517 dt = datetime.datetime.utcnow() 1518 if dtfmt is None: 1519 dtfmt = ISOFORMAT 1520 return dt.strftime(dtfmt)
1521
1522 # ============================================================================= 1523 # ISO-8601 Format Date/Time 1524 # 1525 -def s3_decode_iso_datetime(dtstr):
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 # Default seconds/microseconds=zero 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
1556 #-------------------------------------------------------------------------- 1557 -def s3_encode_iso_datetime(dt):
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 # Time Zone Handling 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
1589 #-------------------------------------------------------------------------- 1590 -def s3_get_utc_offset():
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 # 1st choice is the personal preference (useful for GETs if user 1600 # wishes to see times in their local timezone) 1601 offset = session.auth.user.utc_offset 1602 if offset: 1603 offset = offset.strip() 1604 1605 if not offset: 1606 # 2nd choice is what the client provides in the hidden form 1607 # field (for form POSTs) 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 # Make this the preferred value during this session 1616 if logged_in: 1617 session.auth.user.utc_offset = offset 1618 1619 if not offset: 1620 # 3rd choice is the server default (what most clients should see 1621 # the timezone as) 1622 offset = current.deployment_settings.L10n.utc_offset 1623 1624 session.s3.utc_offset = offset 1625 return offset
1626
1627 # ============================================================================= 1628 # Utilities 1629 # 1630 -def s3_relative_datetime(dtexpr):
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 # END ========================================================================= 1683