| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140 |
- # Copyright (c) 2010-2024 openpyxl
- """Manage Excel date weirdness."""
- # Python stdlib imports
- import datetime
- from math import isnan
- import re
- # constants
- MAC_EPOCH = datetime.datetime(1904, 1, 1)
- WINDOWS_EPOCH = datetime.datetime(1899, 12, 30)
- CALENDAR_WINDOWS_1900 = 2415018.5 # Julian date of WINDOWS_EPOCH
- CALENDAR_MAC_1904 = 2416480.5 # Julian date of MAC_EPOCH
- CALENDAR_WINDOWS_1900 = WINDOWS_EPOCH
- CALENDAR_MAC_1904 = MAC_EPOCH
- SECS_PER_DAY = 86400
- ISO_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
- ISO_REGEX = re.compile(r'''
- (?P<date>(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2}))?T?
- (?P<time>(?P<hour>\d{2}):(?P<minute>\d{2})(:(?P<second>\d{2})(?P<microsecond>\.\d{1,3})?)?)?Z?''',
- re.VERBOSE)
- ISO_DURATION = re.compile(r'PT((?P<hours>\d+)H)?((?P<minutes>\d+)M)?((?P<seconds>\d+(\.\d{1,3})?)S)?')
- def to_ISO8601(dt):
- """Convert from a datetime to a timestamp string."""
- if hasattr(dt, "microsecond") and dt.microsecond:
- return dt.isoformat(timespec="milliseconds")
- return dt.isoformat()
- def from_ISO8601(formatted_string):
- """Convert from a timestamp string to a datetime object. According to
- 18.17.4 in the specification the following ISO 8601 formats are
- supported.
- Dates B.1.1 and B.2.1
- Times B.1.2 and B.2.2
- Datetimes B.1.3 and B.2.3
- There is no concept of timedeltas in the specification, but Excel
- writes them (in strict OOXML mode), so these are also understood.
- """
- if not formatted_string:
- return None
- match = ISO_REGEX.match(formatted_string)
- if match and any(match.groups()):
- parts = match.groupdict(0)
- for key in ["year", "month", "day", "hour", "minute", "second"]:
- if parts[key]:
- parts[key] = int(parts[key])
- if parts["microsecond"]:
- parts["microsecond"] = int(float(parts['microsecond']) * 1_000_000)
- if not parts["date"]:
- dt = datetime.time(parts['hour'], parts['minute'], parts['second'], parts["microsecond"])
- elif not parts["time"]:
- dt = datetime.date(parts['year'], parts['month'], parts['day'])
- else:
- del parts["time"]
- del parts["date"]
- dt = datetime.datetime(**parts)
- return dt
- match = ISO_DURATION.match(formatted_string)
- if match and any(match.groups()):
- parts = match.groupdict(0)
- for key, val in parts.items():
- if val:
- parts[key] = float(val)
- return datetime.timedelta(**parts)
- raise ValueError("Invalid datetime value {}".format(formatted_string))
- def to_excel(dt, epoch=WINDOWS_EPOCH):
- """Convert Python datetime to Excel serial"""
- if isinstance(dt, datetime.time):
- return time_to_days(dt)
- if isinstance(dt, datetime.timedelta):
- return timedelta_to_days(dt)
- if isnan(dt.year): # Pandas supports Not a Date
- return
- if not hasattr(dt, "date"):
- dt = datetime.datetime.combine(dt, datetime.time())
- # rebase on epoch and adjust for < 1900-03-01
- days = (dt - epoch).days
- if 0 < days <= 60 and epoch == WINDOWS_EPOCH:
- days -= 1
- return days + time_to_days(dt)
- def from_excel(value, epoch=WINDOWS_EPOCH, timedelta=False):
- """Convert Excel serial to Python datetime"""
- if value is None:
- return
- if timedelta:
- td = datetime.timedelta(days=value)
- if td.microseconds:
- # round to millisecond precision
- td = datetime.timedelta(seconds=td.total_seconds() // 1,
- microseconds=round(td.microseconds, -3))
- return td
- day, fraction = divmod(value, 1)
- diff = datetime.timedelta(milliseconds=round(fraction * SECS_PER_DAY * 1000))
- if 0 <= value < 1 and diff.days == 0:
- return days_to_time(diff)
- if 0 < value < 60 and epoch == WINDOWS_EPOCH:
- day += 1
- return epoch + datetime.timedelta(days=day) + diff
- def time_to_days(value):
- """Convert a time value to fractions of day"""
- return (
- (value.hour * 3600)
- + (value.minute * 60)
- + value.second
- + value.microsecond / 10**6
- ) / SECS_PER_DAY
- def timedelta_to_days(value):
- """Convert a timedelta value to fractions of a day"""
- return value.total_seconds() / SECS_PER_DAY
- def days_to_time(value):
- mins, seconds = divmod(value.seconds, 60)
- hours, mins = divmod(mins, 60)
- return datetime.time(hours, mins, seconds, value.microseconds)
|