datetime.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. # Copyright (c) 2010-2024 openpyxl
  2. """Manage Excel date weirdness."""
  3. # Python stdlib imports
  4. import datetime
  5. from math import isnan
  6. import re
  7. # constants
  8. MAC_EPOCH = datetime.datetime(1904, 1, 1)
  9. WINDOWS_EPOCH = datetime.datetime(1899, 12, 30)
  10. CALENDAR_WINDOWS_1900 = 2415018.5 # Julian date of WINDOWS_EPOCH
  11. CALENDAR_MAC_1904 = 2416480.5 # Julian date of MAC_EPOCH
  12. CALENDAR_WINDOWS_1900 = WINDOWS_EPOCH
  13. CALENDAR_MAC_1904 = MAC_EPOCH
  14. SECS_PER_DAY = 86400
  15. ISO_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
  16. ISO_REGEX = re.compile(r'''
  17. (?P<date>(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2}))?T?
  18. (?P<time>(?P<hour>\d{2}):(?P<minute>\d{2})(:(?P<second>\d{2})(?P<microsecond>\.\d{1,3})?)?)?Z?''',
  19. re.VERBOSE)
  20. ISO_DURATION = re.compile(r'PT((?P<hours>\d+)H)?((?P<minutes>\d+)M)?((?P<seconds>\d+(\.\d{1,3})?)S)?')
  21. def to_ISO8601(dt):
  22. """Convert from a datetime to a timestamp string."""
  23. if hasattr(dt, "microsecond") and dt.microsecond:
  24. return dt.isoformat(timespec="milliseconds")
  25. return dt.isoformat()
  26. def from_ISO8601(formatted_string):
  27. """Convert from a timestamp string to a datetime object. According to
  28. 18.17.4 in the specification the following ISO 8601 formats are
  29. supported.
  30. Dates B.1.1 and B.2.1
  31. Times B.1.2 and B.2.2
  32. Datetimes B.1.3 and B.2.3
  33. There is no concept of timedeltas in the specification, but Excel
  34. writes them (in strict OOXML mode), so these are also understood.
  35. """
  36. if not formatted_string:
  37. return None
  38. match = ISO_REGEX.match(formatted_string)
  39. if match and any(match.groups()):
  40. parts = match.groupdict(0)
  41. for key in ["year", "month", "day", "hour", "minute", "second"]:
  42. if parts[key]:
  43. parts[key] = int(parts[key])
  44. if parts["microsecond"]:
  45. parts["microsecond"] = int(float(parts['microsecond']) * 1_000_000)
  46. if not parts["date"]:
  47. dt = datetime.time(parts['hour'], parts['minute'], parts['second'], parts["microsecond"])
  48. elif not parts["time"]:
  49. dt = datetime.date(parts['year'], parts['month'], parts['day'])
  50. else:
  51. del parts["time"]
  52. del parts["date"]
  53. dt = datetime.datetime(**parts)
  54. return dt
  55. match = ISO_DURATION.match(formatted_string)
  56. if match and any(match.groups()):
  57. parts = match.groupdict(0)
  58. for key, val in parts.items():
  59. if val:
  60. parts[key] = float(val)
  61. return datetime.timedelta(**parts)
  62. raise ValueError("Invalid datetime value {}".format(formatted_string))
  63. def to_excel(dt, epoch=WINDOWS_EPOCH):
  64. """Convert Python datetime to Excel serial"""
  65. if isinstance(dt, datetime.time):
  66. return time_to_days(dt)
  67. if isinstance(dt, datetime.timedelta):
  68. return timedelta_to_days(dt)
  69. if isnan(dt.year): # Pandas supports Not a Date
  70. return
  71. if not hasattr(dt, "date"):
  72. dt = datetime.datetime.combine(dt, datetime.time())
  73. # rebase on epoch and adjust for < 1900-03-01
  74. days = (dt - epoch).days
  75. if 0 < days <= 60 and epoch == WINDOWS_EPOCH:
  76. days -= 1
  77. return days + time_to_days(dt)
  78. def from_excel(value, epoch=WINDOWS_EPOCH, timedelta=False):
  79. """Convert Excel serial to Python datetime"""
  80. if value is None:
  81. return
  82. if timedelta:
  83. td = datetime.timedelta(days=value)
  84. if td.microseconds:
  85. # round to millisecond precision
  86. td = datetime.timedelta(seconds=td.total_seconds() // 1,
  87. microseconds=round(td.microseconds, -3))
  88. return td
  89. day, fraction = divmod(value, 1)
  90. diff = datetime.timedelta(milliseconds=round(fraction * SECS_PER_DAY * 1000))
  91. if 0 <= value < 1 and diff.days == 0:
  92. return days_to_time(diff)
  93. if 0 < value < 60 and epoch == WINDOWS_EPOCH:
  94. day += 1
  95. return epoch + datetime.timedelta(days=day) + diff
  96. def time_to_days(value):
  97. """Convert a time value to fractions of day"""
  98. return (
  99. (value.hour * 3600)
  100. + (value.minute * 60)
  101. + value.second
  102. + value.microsecond / 10**6
  103. ) / SECS_PER_DAY
  104. def timedelta_to_days(value):
  105. """Convert a timedelta value to fractions of a day"""
  106. return value.total_seconds() / SECS_PER_DAY
  107. def days_to_time(value):
  108. mins, seconds = divmod(value.seconds, 60)
  109. hours, mins = divmod(mins, 60)
  110. return datetime.time(hours, mins, seconds, value.microseconds)