xldate.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. # -*- coding: cp1252 -*-
  2. # No part of the content of this file was derived from the works of David Giffin.
  3. ##
  4. # <p>Copyright © 2005-2008 Stephen John Machin, Lingfo Pty Ltd</p>
  5. # <p>This module is part of the xlrd package, which is released under a BSD-style licence.</p>
  6. #
  7. # <p>Provides function(s) for dealing with Microsoft Excel ™ dates.</p>
  8. ##
  9. # 2008-10-18 SJM Fix bug in xldate_from_date_tuple (affected some years after 2099)
  10. # The conversion from days to (year, month, day) starts with
  11. # an integral "julian day number" aka JDN.
  12. # FWIW, JDN 0 corresponds to noon on Monday November 24 in Gregorian year -4713.
  13. # More importantly:
  14. # Noon on Gregorian 1900-03-01 (day 61 in the 1900-based system) is JDN 2415080.0
  15. # Noon on Gregorian 1904-01-02 (day 1 in the 1904-based system) is JDN 2416482.0
  16. import datetime
  17. _JDN_delta = (2415080 - 61, 2416482 - 1)
  18. assert _JDN_delta[1] - _JDN_delta[0] == 1462
  19. # Pre-calculate the datetime epochs for efficiency.
  20. epoch_1904 = datetime.datetime(1904, 1, 1)
  21. epoch_1900 = datetime.datetime(1899, 12, 31)
  22. epoch_1900_minus_1 = datetime.datetime(1899, 12, 30)
  23. class XLDateError(ValueError): pass
  24. class XLDateNegative(XLDateError): pass
  25. class XLDateAmbiguous(XLDateError): pass
  26. class XLDateTooLarge(XLDateError): pass
  27. class XLDateBadDatemode(XLDateError): pass
  28. class XLDateBadTuple(XLDateError): pass
  29. _XLDAYS_TOO_LARGE = (2958466, 2958466 - 1462) # This is equivalent to 10000-01-01
  30. ##
  31. # Convert an Excel number (presumed to represent a date, a datetime or a time) into
  32. # a tuple suitable for feeding to datetime or mx.DateTime constructors.
  33. # @param xldate The Excel number
  34. # @param datemode 0: 1900-based, 1: 1904-based.
  35. # <br>WARNING: when using this function to
  36. # interpret the contents of a workbook, you should pass in the Book.datemode
  37. # attribute of that workbook. Whether
  38. # the workbook has ever been anywhere near a Macintosh is irrelevant.
  39. # @return Gregorian (year, month, day, hour, minute, nearest_second).
  40. # <br>Special case: if 0.0 <= xldate < 1.0, it is assumed to represent a time;
  41. # (0, 0, 0, hour, minute, second) will be returned.
  42. # <br>Note: 1904-01-01 is not regarded as a valid date in the datemode 1 system; its "serial number"
  43. # is zero.
  44. # @throws XLDateNegative xldate < 0.00
  45. # @throws XLDateAmbiguous The 1900 leap-year problem (datemode == 0 and 1.0 <= xldate < 61.0)
  46. # @throws XLDateTooLarge Gregorian year 10000 or later
  47. # @throws XLDateBadDatemode datemode arg is neither 0 nor 1
  48. # @throws XLDateError Covers the 4 specific errors
  49. def xldate_as_tuple(xldate, datemode):
  50. if datemode not in (0, 1):
  51. raise XLDateBadDatemode(datemode)
  52. if xldate == 0.00:
  53. return (0, 0, 0, 0, 0, 0)
  54. if xldate < 0.00:
  55. raise XLDateNegative(xldate)
  56. xldays = int(xldate)
  57. frac = xldate - xldays
  58. seconds = int(round(frac * 86400.0))
  59. assert 0 <= seconds <= 86400
  60. if seconds == 86400:
  61. hour = minute = second = 0
  62. xldays += 1
  63. else:
  64. # second = seconds % 60; minutes = seconds // 60
  65. minutes, second = divmod(seconds, 60)
  66. # minute = minutes % 60; hour = minutes // 60
  67. hour, minute = divmod(minutes, 60)
  68. if xldays >= _XLDAYS_TOO_LARGE[datemode]:
  69. raise XLDateTooLarge(xldate)
  70. if xldays == 0:
  71. return (0, 0, 0, hour, minute, second)
  72. if xldays < 61 and datemode == 0:
  73. raise XLDateAmbiguous(xldate)
  74. jdn = xldays + _JDN_delta[datemode]
  75. yreg = ((((jdn * 4 + 274277) // 146097) * 3 // 4) + jdn + 1363) * 4 + 3
  76. mp = ((yreg % 1461) // 4) * 535 + 333
  77. d = ((mp % 16384) // 535) + 1
  78. # mp /= 16384
  79. mp >>= 14
  80. if mp >= 10:
  81. return ((yreg // 1461) - 4715, mp - 9, d, hour, minute, second)
  82. else:
  83. return ((yreg // 1461) - 4716, mp + 3, d, hour, minute, second)
  84. ##
  85. # Convert an Excel date/time number into a datetime.datetime object.
  86. #
  87. # @param xldate The Excel number
  88. # @param datemode 0: 1900-based, 1: 1904-based.
  89. #
  90. # @return a datetime.datetime() object.
  91. #
  92. def xldate_as_datetime(xldate, datemode):
  93. """Convert an Excel date/time number into a datetime.datetime object."""
  94. # Set the epoch based on the 1900/1904 datemode.
  95. if datemode:
  96. epoch = epoch_1904
  97. else:
  98. if xldate < 60:
  99. epoch = epoch_1900
  100. else:
  101. # Workaround Excel 1900 leap year bug by adjusting the epoch.
  102. epoch = epoch_1900_minus_1
  103. # The integer part of the Excel date stores the number of days since
  104. # the epoch and the fractional part stores the percentage of the day.
  105. days = int(xldate)
  106. fraction = xldate - days
  107. # Get the the integer and decimal seconds in Excel's millisecond resolution.
  108. seconds = int(round(fraction * 86400000.0))
  109. seconds, milliseconds = divmod(seconds, 1000)
  110. return epoch + datetime.timedelta(days, seconds, 0, milliseconds)
  111. # === conversions from date/time to xl numbers
  112. def _leap(y):
  113. if y % 4: return 0
  114. if y % 100: return 1
  115. if y % 400: return 0
  116. return 1
  117. _days_in_month = (None, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
  118. ##
  119. # Convert a date tuple (year, month, day) to an Excel date.
  120. # @param year Gregorian year.
  121. # @param month 1 <= month <= 12
  122. # @param day 1 <= day <= last day of that (year, month)
  123. # @param datemode 0: 1900-based, 1: 1904-based.
  124. # @throws XLDateAmbiguous The 1900 leap-year problem (datemode == 0 and 1.0 <= xldate < 61.0)
  125. # @throws XLDateBadDatemode datemode arg is neither 0 nor 1
  126. # @throws XLDateBadTuple (year, month, day) is too early/late or has invalid component(s)
  127. # @throws XLDateError Covers the specific errors
  128. def xldate_from_date_tuple(date_tuple, datemode):
  129. """Create an excel date from a tuple of (year, month, day)"""
  130. year, month, day = date_tuple
  131. if datemode not in (0, 1):
  132. raise XLDateBadDatemode(datemode)
  133. if year == 0 and month == 0 and day == 0:
  134. return 0.00
  135. if not (1900 <= year <= 9999):
  136. raise XLDateBadTuple("Invalid year: %r" % ((year, month, day),))
  137. if not (1 <= month <= 12):
  138. raise XLDateBadTuple("Invalid month: %r" % ((year, month, day),))
  139. if day < 1 \
  140. or (day > _days_in_month[month] and not(day == 29 and month == 2 and _leap(year))):
  141. raise XLDateBadTuple("Invalid day: %r" % ((year, month, day),))
  142. Yp = year + 4716
  143. M = month
  144. if M <= 2:
  145. Yp = Yp - 1
  146. Mp = M + 9
  147. else:
  148. Mp = M - 3
  149. jdn = (1461 * Yp // 4) + ((979 * Mp + 16) // 32) + \
  150. day - 1364 - (((Yp + 184) // 100) * 3 // 4)
  151. xldays = jdn - _JDN_delta[datemode]
  152. if xldays <= 0:
  153. raise XLDateBadTuple("Invalid (year, month, day): %r" % ((year, month, day),))
  154. if xldays < 61 and datemode == 0:
  155. raise XLDateAmbiguous("Before 1900-03-01: %r" % ((year, month, day),))
  156. return float(xldays)
  157. ##
  158. # Convert a time tuple (hour, minute, second) to an Excel "date" value (fraction of a day).
  159. # @param hour 0 <= hour < 24
  160. # @param minute 0 <= minute < 60
  161. # @param second 0 <= second < 60
  162. # @throws XLDateBadTuple Out-of-range hour, minute, or second
  163. def xldate_from_time_tuple(time_tuple):
  164. """Create an excel date from a tuple of (hour, minute, second)"""
  165. hour, minute, second = time_tuple
  166. if 0 <= hour < 24 and 0 <= minute < 60 and 0 <= second < 60:
  167. return ((second / 60.0 + minute) / 60.0 + hour) / 24.0
  168. raise XLDateBadTuple("Invalid (hour, minute, second): %r" % ((hour, minute, second),))
  169. ##
  170. # Convert a datetime tuple (year, month, day, hour, minute, second) to an Excel date value.
  171. # For more details, refer to other xldate_from_*_tuple functions.
  172. # @param datetime_tuple (year, month, day, hour, minute, second)
  173. # @param datemode 0: 1900-based, 1: 1904-based.
  174. def xldate_from_datetime_tuple(datetime_tuple, datemode):
  175. return (
  176. xldate_from_date_tuple(datetime_tuple[:3], datemode)
  177. +
  178. xldate_from_time_tuple(datetime_tuple[3:])
  179. )