From 0bb18c5529a03a20268ea334f64b9571f4867e45 Mon Sep 17 00:00:00 2001 From: Nis Martensen Date: Sat, 27 Feb 2021 18:49:43 +0100 Subject: [PATCH 1/6] add heuristic for reading timedelta Since dates, times, and elapsed time are all usually stored as simple numbers in XLSX, openpyxl uses a heuristic to recognize the values as date/time representation and returns datetime.datetime or datetime.time objects for them. The heuristic is based on inspecting the number format style string (is this a date format?) and the stored value itself (to distinguish times from datetimes). This commit refines the heuristic by adding support for recognizing and returning time interval durations as datetime.timedelta. --HG-- branch : 3.0 --- openpyxl/styles/numbers.py | 7 ++++++ openpyxl/styles/stylesheet.py | 7 ++++++ openpyxl/styles/tests/test_number_style.py | 29 ++++++++++++++++++++++ openpyxl/utils/datetime.py | 9 ++++++- openpyxl/utils/tests/test_datetime.py | 20 +++++++++++++++ openpyxl/workbook/workbook.py | 1 + openpyxl/worksheet/_reader.py | 13 +++++++--- openpyxl/worksheet/tests/test_reader.py | 19 +++++++++++++- 8 files changed, 100 insertions(+), 5 deletions(-) diff --git a/openpyxl/styles/numbers.py b/openpyxl/styles/numbers.py index a4c5d81e..5bf714fb 100644 --- a/openpyxl/styles/numbers.py +++ b/openpyxl/styles/numbers.py @@ -95,6 +95,7 @@ COLORS = r"\[(BLACK|BLUE|CYAN|GREEN|MAGENTA|RED|WHITE|YELLOW)\]" LITERAL_GROUP = r'".*?"' # anything in quotes LOCALE_GROUP = r'\[.+\]' # anything in square brackets, including colours STRIP_RE = re.compile(f"{LITERAL_GROUP}|{LOCALE_GROUP}") +TIMEDELTA_RE = re.compile(r'\[hh?\](:mm(:ss(\.0*)?)?)?|\[mm?\]:ss(\.0*)?|\[ss?\](\.0*)?', re.I) # Spec 18.8.31 numFmts @@ -108,6 +109,12 @@ def is_date_format(fmt): return re.search(r"[^\\][dmhysDMHYS]", fmt) is not None +def is_timedelta_format(fmt): + if fmt is None: + return False + return TIMEDELTA_RE.match(fmt) is not None + + def is_datetime(fmt): """ Return date, time or datetime diff --git a/openpyxl/styles/stylesheet.py b/openpyxl/styles/stylesheet.py index 3cde3f2e..85f0008e 100644 --- a/openpyxl/styles/stylesheet.py +++ b/openpyxl/styles/stylesheet.py @@ -25,6 +25,7 @@ from .numbers import ( BUILTIN_FORMATS_MAX_SIZE, BUILTIN_FORMATS_REVERSE, is_date_format, + is_timedelta_format, builtin_format_code ) from .named_styles import ( @@ -156,6 +157,7 @@ class Stylesheet(Serialisable): And index datetime formats """ date_formats = set() + timedelta_formats = set() custom = self.custom_formats formats = self.number_formats for idx, style in enumerate(self.cell_styles): @@ -170,7 +172,11 @@ class Stylesheet(Serialisable): if is_date_format(fmt): # Create an index of which styles refer to datetimes date_formats.add(idx) + if is_timedelta_format(fmt): + # Create an index of which styles refer to timedeltas + timedelta_formats.add(idx) self.date_formats = date_formats + self.timedelta_formats = timedelta_formats def to_tree(self, tagname=None, idx=None, namespace=None): @@ -204,6 +210,7 @@ def apply_stylesheet(archive, wb): wb._cell_styles = stylesheet.cell_styles wb._named_styles = stylesheet.named_styles wb._date_formats = stylesheet.date_formats + wb._timedelta_formats = stylesheet.timedelta_formats for ns in wb._named_styles: ns.bind(wb) diff --git a/openpyxl/styles/tests/test_number_style.py b/openpyxl/styles/tests/test_number_style.py index 39d1d024..150a08c7 100644 --- a/openpyxl/styles/tests/test_number_style.py +++ b/openpyxl/styles/tests/test_number_style.py @@ -84,6 +84,35 @@ def test_is_date_format(format, result): assert is_date_format(format) is result +@pytest.mark.parametrize("format, result", + [ + ('m:ss', False), + ('[h]', True), + ('[hh]', True), + ('[h]:mm:ss', True), + ('[hh]:mm:ss', True), + ('[h]:mm:ss.000', True), + ('[hh]:mm:ss.0', True), + ('[h]:mm', True), + ('[hh]:mm', True), + ('[m]:ss', True), + ('[mm]:ss', True), + ('[m]:ss.000', True), + ('[mm]:ss.0', True), + ('[s]', True), + ('[ss]', True), + ('[s].000', True), + ('[ss].0', True), + ('[m]', False), + ('[mm]', False), + ('h:mm', False), + ] + ) +def test_is_timedelta_format(format, result): + from ..numbers import is_timedelta_format + assert is_timedelta_format(format) is result + + @pytest.mark.parametrize("fmt, typ", [ (FORMAT_DATE_DATETIME, "datetime"), diff --git a/openpyxl/utils/datetime.py b/openpyxl/utils/datetime.py index 0b03a984..e7656a22 100644 --- a/openpyxl/utils/datetime.py +++ b/openpyxl/utils/datetime.py @@ -88,10 +88,17 @@ def to_excel(dt, epoch=WINDOWS_EPOCH): return days -def from_excel(value, epoch=WINDOWS_EPOCH): +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(microseconds=-td.microseconds + + round(td.microseconds/1000)*1000) + 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: diff --git a/openpyxl/utils/tests/test_datetime.py b/openpyxl/utils/tests/test_datetime.py index 8aba0bf7..176d17e0 100644 --- a/openpyxl/utils/tests/test_datetime.py +++ b/openpyxl/utils/tests/test_datetime.py @@ -123,6 +123,26 @@ def test_from_excel(value, expected): assert FUT(value) == expected +@pytest.mark.parametrize("value, expected", + [ + (0, timedelta(hours=0)), + (0.5, timedelta(hours=12)), + (-0.5, timedelta(hours=-12)), + (1.25, timedelta(hours=30)), + (-1.25, timedelta(hours=-30)), + (59.5, timedelta(days=59, hours=12)), + (60.5, timedelta(days=60, hours=12)), + (61.5, timedelta(days=61, hours=12)), + (0.9999999995, timedelta(days=1)), + (1.0000000005, timedelta(days=1)), + (None, None), + ]) +def test_from_excel_timedelta(value, expected): + from ..datetime import from_excel + FUT = from_excel + assert FUT(value, timedelta=True) == expected + + @pytest.mark.parametrize("value, expected", [ (39385, datetime(2011, 10, 31)), diff --git a/openpyxl/workbook/workbook.py b/openpyxl/workbook/workbook.py index a87afe2c..451c6468 100644 --- a/openpyxl/workbook/workbook.py +++ b/openpyxl/workbook/workbook.py @@ -104,6 +104,7 @@ class Workbook(object): self._number_formats = IndexedList() self._date_formats = {} + self._timedelta_formats = {} self._protections = IndexedList([Protection()]) diff --git a/openpyxl/worksheet/_reader.py b/openpyxl/worksheet/_reader.py index 46e7c092..22343aa7 100644 --- a/openpyxl/worksheet/_reader.py +++ b/openpyxl/worksheet/_reader.py @@ -85,7 +85,8 @@ def _cast_number(value): class WorkSheetParser(object): def __init__(self, src, shared_strings, data_only=False, - epoch=WINDOWS_EPOCH, date_formats=set()): + epoch=WINDOWS_EPOCH, date_formats=set(), + timedelta_formats=set()): self.min_row = self.min_col = None self.epoch = epoch self.source = src @@ -96,6 +97,7 @@ class WorkSheetParser(object): self.row_counter = self.col_counter = 0 self.tables = TablePartList() self.date_formats = date_formats + self.timedelta_formats = timedelta_formats self.row_dimensions = {} self.column_dimensions = {} self.number_formats = [] @@ -198,7 +200,10 @@ class WorkSheetParser(object): elif value is not None: if data_type == 'n': value = _cast_number(value) - if style_id in self.date_formats: + if style_id in self.timedelta_formats: + data_type = 'd' + value = from_excel(value, self.epoch, timedelta=True) + elif style_id in self.date_formats: data_type = 'd' try: value = from_excel(value, self.epoch) @@ -337,7 +342,9 @@ class WorksheetReader(object): def __init__(self, ws, xml_source, shared_strings, data_only): self.ws = ws - self.parser = WorkSheetParser(xml_source, shared_strings, data_only, ws.parent.epoch, ws.parent._date_formats) + self.parser = WorkSheetParser(xml_source, shared_strings, + data_only, ws.parent.epoch, ws.parent._date_formats, + ws.parent._timedelta_formats) self.tables = [] diff --git a/openpyxl/worksheet/tests/test_reader.py b/openpyxl/worksheet/tests/test_reader.py index f784cd17..5fc9c855 100644 --- a/openpyxl/worksheet/tests/test_reader.py +++ b/openpyxl/worksheet/tests/test_reader.py @@ -63,6 +63,7 @@ def Workbook(): self._cell_styles.add(StyleArray([0,4,6,0,0,1,0,0,0])) #fillId=4, borderId=6, alignmentId=1)) self.sheetnames = [] self._date_formats = set() + self._timedelta_formats = set() def create_sheet(self, title): return Worksheet(self) @@ -80,7 +81,8 @@ def WorkSheetParser(): styles.add((StyleArray([i]*9))) styles.add(StyleArray([0,4,6,14,0,1,0,0,0])) #fillId=4, borderId=6, number_format=14 alignmentId=1)) date_formats = set([1, 29]) - return WorkSheetParser(None, {0:'a'}, date_formats=date_formats) + timedelta_formats = set([30]) + return WorkSheetParser(None, {0:'a'}, date_formats=date_formats, timedelta_formats=timedelta_formats) from warnings import simplefilter simplefilter("always") @@ -288,6 +290,21 @@ class TestWorksheetParser: 'style_id':0, 'value': datetime.datetime(2011, 12, 25, 14, 23, 55)} + def test_timedelta(self, WorkSheetParser): + parser = WorkSheetParser + + src = """ + + 1.25 + + """ + element = fromstring(src) + + cell = parser.parse_cell(element) + assert cell == {'column': 1, 'data_type': 'd', 'row': 1, + 'style_id':30, 'value':datetime.timedelta(days=1, hours=6)} + + def test_mac_date(self, WorkSheetParser): parser = WorkSheetParser parser.epoch = CALENDAR_MAC_1904 -- GitLab From 03899006fc8d54a2e1891e622d9c041366ed23cb Mon Sep 17 00:00:00 2001 From: Nis Martensen Date: Sat, 27 Feb 2021 22:46:49 +0100 Subject: [PATCH 2/6] update documentation on datetime and timedelta handling --HG-- branch : 3.0 --- doc/datetime.rst | 78 +++++++++++------------------------------------- 1 file changed, 17 insertions(+), 61 deletions(-) diff --git a/doc/datetime.rst b/doc/datetime.rst index fef6c89c..4d2356fe 100644 --- a/doc/datetime.rst +++ b/doc/datetime.rst @@ -8,6 +8,13 @@ module representations when reading from and writing to files. In either representation, the maximum date and time precision in XLSX files is millisecond precision. +XLSX files are not suitable for storing historic dates (before 1900 or +1904), due to bugs in Excel that cannot be fixed without causing +backward compatibility problems. To discourage users from trying anyway, +Excel deliberately refuses to recognize and display such dates. You +should not try using `openpyxl` either, even if it does not throw errors +when you do so. + Using the ISO 8601 format ------------------------- @@ -71,70 +78,19 @@ and set it like this: -Reading timedelta values ------------------------- - -Excel users can use custom number formats resembling ``[h]:mm:ss`` or -``[mm]:ss`` to store and accurately display time interval durations. -(The brackets in the format tell Excel to not wrap around at 24 hours or -60 minutes.) - -If you need to retrieve such time interval durations from an XLSX file -using `openpyxl`, there is no way to get them directly as -`datetime.timedelta` objects. `openpyxl` will only see the single number -representation of the values, and returns the corresponding -`datetime.time` or `datetime.datetime` object for each cell. To -translate these to timedelta objects with correct length, you can pass -them through a helper function. - -Here is a helper for files using the 1904 date system: - -.. code:: - - def helper_1904(dt): - if isinstance(dt, datetime.time): - return datetime.timedelta( - hours=dt.hour, - minutes=dt.minute, - seconds=dt.second, - microseconds=dt.microsecond - ) - # else we have a datetime - return dt - datetime.datetime(1904, 1, 1) - - -If your files use the 1900 date system, you can use this: - -.. code:: - - def helper_1900(dt): - if isinstance(dt, datetime.time): - return datetime.timedelta( - hours=dt.hour, - minutes=dt.minute, - seconds=dt.second, - microseconds=dt.microsecond - ) - # else we have a datetime - if dt < datetime.datetime(1899, 12, 31) or dt >= datetime.datetime(1900, 3, 1): - return dt - datetime.datetime(1899, 12, 30) - return dt - datetime.datetime(1899, 12, 31) - - -.. warning:: +Handling timedelta values +------------------------- - Unfortunately, due to the 1900 leap year compatibility issue - mentioned above, it is impossible to create a helper function that - always returns 100% correct timedelta values from workbooks using the - 1900 date system. Returned values for data in the interval [60,61) - days will be returned 24 hours too small, with no way to detect this - in the helper. Therefore, if you need to read timedelta values that - can reach around 60 days (1440 hours), you MUST make sure your files - use the 1904 date system to get reliable results! +Excel users can use number formats resembling ``[h]:mm:ss`` or +``[mm]:ss`` to display time interval durations. +The brackets in the format tell Excel to not wrap around at 24 hours or +60 minutes. +`openpyxl` recognizes these number formats when reading XLSX files and +returns datetime.timedelta values for the corresponding cells. +When writing timedelta values from worksheet cells to file, `openpyxl` +uses the ``[h]:mm:ss`` number format for these cells. -Writing timedelta values ------------------------- Due to the issues with storing and retrieving timedelta values described above, the best option is to not use datetime representations for -- GitLab From 7a4166a20285bf64d1554cecad2de24f2885ac59 Mon Sep 17 00:00:00 2001 From: Nis Martensen Date: Mon, 1 Mar 2021 18:24:51 +0100 Subject: [PATCH 3/6] update timedelta regular expression, function and tests --HG-- branch : 3.0 --- openpyxl/styles/numbers.py | 5 +++-- openpyxl/styles/tests/test_number_style.py | 7 +++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/openpyxl/styles/numbers.py b/openpyxl/styles/numbers.py index 5bf714fb..f7dadf2d 100644 --- a/openpyxl/styles/numbers.py +++ b/openpyxl/styles/numbers.py @@ -95,7 +95,7 @@ COLORS = r"\[(BLACK|BLUE|CYAN|GREEN|MAGENTA|RED|WHITE|YELLOW)\]" LITERAL_GROUP = r'".*?"' # anything in quotes LOCALE_GROUP = r'\[.+\]' # anything in square brackets, including colours STRIP_RE = re.compile(f"{LITERAL_GROUP}|{LOCALE_GROUP}") -TIMEDELTA_RE = re.compile(r'\[hh?\](:mm(:ss(\.0*)?)?)?|\[mm?\]:ss(\.0*)?|\[ss?\](\.0*)?', re.I) +TIMEDELTA_RE = re.compile(r'\[hh?\](:mm(:ss(\.0*)?)?)?|\[mm?\](:ss(\.0*)?)?|\[ss?\](\.0*)?', re.I) # Spec 18.8.31 numFmts @@ -112,7 +112,8 @@ def is_date_format(fmt): def is_timedelta_format(fmt): if fmt is None: return False - return TIMEDELTA_RE.match(fmt) is not None + fmt = fmt.split(";")[0] # only look at the first format + return TIMEDELTA_RE.search(fmt) is not None def is_datetime(fmt): diff --git a/openpyxl/styles/tests/test_number_style.py b/openpyxl/styles/tests/test_number_style.py index 150a08c7..6f665bc6 100644 --- a/openpyxl/styles/tests/test_number_style.py +++ b/openpyxl/styles/tests/test_number_style.py @@ -103,9 +103,12 @@ def test_is_date_format(format, result): ('[ss]', True), ('[s].000', True), ('[ss].0', True), - ('[m]', False), - ('[mm]', False), + ('[m]', True), + ('[mm]', True), ('h:mm', False), + ('[Blue]\+[h]:mm;[Red]\-[h]:mm;[h]:mm', True), + ('[Blue]\+[h]:mm;[Red]\-[h]:mm;[Green][h]:mm', True), + ('[h]:mm;[=0]\-', True), ] ) def test_is_timedelta_format(format, result): -- GitLab From f91e9f0fdf41f1a90f584b23e124b263ae1f0395 Mon Sep 17 00:00:00 2001 From: Nis Martensen Date: Mon, 1 Mar 2021 21:32:15 +0100 Subject: [PATCH 4/6] code simplification --HG-- branch : 3.0 --- openpyxl/worksheet/_reader.py | 9 ++++----- openpyxl/worksheet/tests/test_reader.py | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/openpyxl/worksheet/_reader.py b/openpyxl/worksheet/_reader.py index 22343aa7..b684dd47 100644 --- a/openpyxl/worksheet/_reader.py +++ b/openpyxl/worksheet/_reader.py @@ -200,13 +200,12 @@ class WorkSheetParser(object): elif value is not None: if data_type == 'n': value = _cast_number(value) - if style_id in self.timedelta_formats: - data_type = 'd' - value = from_excel(value, self.epoch, timedelta=True) - elif style_id in self.date_formats: + if style_id in self.date_formats: data_type = 'd' try: - value = from_excel(value, self.epoch) + value = from_excel( + value, self.epoch, timedelta=style_id in self.timedelta_formats + ) except ValueError: msg = """Cell {0} is marked as a date but the serial value {1} is outside the limits for dates. The cell will be treated as an error.""".format(coordinate, value) warn(msg) diff --git a/openpyxl/worksheet/tests/test_reader.py b/openpyxl/worksheet/tests/test_reader.py index 5fc9c855..3cd0c9f3 100644 --- a/openpyxl/worksheet/tests/test_reader.py +++ b/openpyxl/worksheet/tests/test_reader.py @@ -80,7 +80,7 @@ def WorkSheetParser(): for i in range(29): styles.add((StyleArray([i]*9))) styles.add(StyleArray([0,4,6,14,0,1,0,0,0])) #fillId=4, borderId=6, number_format=14 alignmentId=1)) - date_formats = set([1, 29]) + date_formats = set([1, 29, 30]) timedelta_formats = set([30]) return WorkSheetParser(None, {0:'a'}, date_formats=date_formats, timedelta_formats=timedelta_formats) -- GitLab From 8e386d779956318cdeb12562b6b8ad0aeec28930 Mon Sep 17 00:00:00 2001 From: Nis Martensen Date: Tue, 2 Mar 2021 19:32:06 +0100 Subject: [PATCH 5/6] improve datetime format detection --HG-- branch : 3.0 --- openpyxl/styles/numbers.py | 2 +- openpyxl/styles/tests/test_number_style.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/openpyxl/styles/numbers.py b/openpyxl/styles/numbers.py index f7dadf2d..09c43364 100644 --- a/openpyxl/styles/numbers.py +++ b/openpyxl/styles/numbers.py @@ -93,7 +93,7 @@ FORMAT_CURRENCY_EUR_SIMPLE = '[$EUR ]#,##0.00_-' COLORS = r"\[(BLACK|BLUE|CYAN|GREEN|MAGENTA|RED|WHITE|YELLOW)\]" LITERAL_GROUP = r'".*?"' # anything in quotes -LOCALE_GROUP = r'\[.+\]' # anything in square brackets, including colours +LOCALE_GROUP = r'\[(?!hh?\]|mm?\]|ss?\])[^\]]*\]' # anything in square brackets, except hours or minutes or seconds STRIP_RE = re.compile(f"{LITERAL_GROUP}|{LOCALE_GROUP}") TIMEDELTA_RE = re.compile(r'\[hh?\](:mm(:ss(\.0*)?)?)?|\[mm?\](:ss(\.0*)?)?|\[ss?\](\.0*)?', re.I) diff --git a/openpyxl/styles/tests/test_number_style.py b/openpyxl/styles/tests/test_number_style.py index 6f665bc6..02e55bcb 100644 --- a/openpyxl/styles/tests/test_number_style.py +++ b/openpyxl/styles/tests/test_number_style.py @@ -77,6 +77,16 @@ def test_strip_quotes(fmt, stripped): (r"0_ ;[Red]\-0\ ", False), (r"\Y000000", False), (r'#,##0.0####" YMD"', False), + ('[h]', True), + ('[ss]', True), + ('[s].000', True), + ('[m]', True), + ('[mm]', True), + ('[Blue]\+[h]:mm;[Red]\-[h]:mm;[Green][h]:mm', True), + ('[>=100][Magenta][s].00', True), + ('[h]:mm;[=0]\-', True), + ('[>=100][Magenta].00', False), + ('[>=100][Magenta]General', False), ] ) def test_is_date_format(format, result): @@ -108,7 +118,10 @@ def test_is_date_format(format, result): ('h:mm', False), ('[Blue]\+[h]:mm;[Red]\-[h]:mm;[h]:mm', True), ('[Blue]\+[h]:mm;[Red]\-[h]:mm;[Green][h]:mm', True), + ('[>=100][Magenta][s].00', True), ('[h]:mm;[=0]\-', True), + ('[>=100][Magenta].00', False), + ('[>=100][Magenta]General', False), ] ) def test_is_timedelta_format(format, result): -- GitLab From 16de600ee7e7693616dd0155db09e0d2f4e6b050 Mon Sep 17 00:00:00 2001 From: Nis Martensen Date: Tue, 2 Mar 2021 20:07:32 +0100 Subject: [PATCH 6/6] datetime documentation updates --HG-- branch : 3.0 --- doc/datetime.rst | 51 +++++++++++++++++++++--------------------------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/doc/datetime.rst b/doc/datetime.rst index 4d2356fe..8e544dc4 100644 --- a/doc/datetime.rst +++ b/doc/datetime.rst @@ -3,17 +3,21 @@ Dates and Times Dates and times can be stored in two distinct ways in XLSX files: as an ISO 8601 formatted string or as a single number. `openpyxl` supports -both representations and translates between them and python's datetime +both representations and translates between them and Python's datetime module representations when reading from and writing to files. In either representation, the maximum date and time precision in XLSX files is millisecond precision. -XLSX files are not suitable for storing historic dates (before 1900 or -1904), due to bugs in Excel that cannot be fixed without causing -backward compatibility problems. To discourage users from trying anyway, -Excel deliberately refuses to recognize and display such dates. You -should not try using `openpyxl` either, even if it does not throw errors -when you do so. +XLSX files are not suitable for storing historic dates (before 1900), +due to bugs in Excel that cannot be fixed without causing backward +compatibility problems. To discourage users from trying anyway, Excel +deliberately refuses to recognize and display such dates. Consequently, +it is not advised to use `openpyxl` for such purposes either, especially +when exchanging files with others. + +The date and time representations in Excel do not support timezones. +Timezone information attached to Python datetimes is therefore lost when +datetimes are stored in XLSX files. Using the ISO 8601 format @@ -28,11 +32,11 @@ writing your file, set the workbook's ``iso_dates`` flag to ``True``: The benefit of using this format is that the meaning of the stored information is not subject to interpretation, as it is with the single -number format. +number format [#f1]_. The ISO 8601 format has no concept of timedeltas (time interval -durations). Do not expect to be able to store and retrieve timedelta -values directly with this, more on that below. +durations). `openpyxl` therefore always uses the single number format +for timedelta values when writing them to file. The 1900 and 1904 date systems @@ -58,8 +62,8 @@ More information on this issue is available from Microsoft: In workbooks using the 1900 date system, `openpyxl` behaves the same as Excel when translating between the worksheets' date/time numbers and -python datetimes in January and February 1900. The only exception is 29 -February 1900, which cannot be represented as a python datetime object +Python datetimes in January and February 1900. The only exception is 29 +February 1900, which cannot be represented as a Python datetime object since it is not a valid date. You can get the date system of a workbook like this: @@ -82,27 +86,16 @@ Handling timedelta values ------------------------- Excel users can use number formats resembling ``[h]:mm:ss`` or -``[mm]:ss`` to display time interval durations. -The brackets in the format tell Excel to not wrap around at 24 hours or -60 minutes. +``[mm]:ss`` to display time interval durations, which `openpyxl` +considers to be equivalent to timedeltas in Python. `openpyxl` recognizes these number formats when reading XLSX files and returns datetime.timedelta values for the corresponding cells. When writing timedelta values from worksheet cells to file, `openpyxl` uses the ``[h]:mm:ss`` number format for these cells. +.. rubric:: Footnotes -Due to the issues with storing and retrieving timedelta values described -above, the best option is to not use datetime representations for -timedelta in XLSX at all, and store the days or hours as regular -numbers: - - >>> import openpyxl - >>> import datetime - >>> duration = datetime.timedelta(hours=42, minutes=3, seconds=14) - >>> wb = openpyxl.Workbook() - >>> ws = wb.active - >>> days = duration / datetime.timedelta(days=1) - >>> ws["A1"] = days - >>> print(days) - 1.7522453703703704 +.. [#f1] For example, the serial 1 in an Excel worksheet can be + interpreted as 00:00, as 24:00, as 1900-01-01, as 1440 + (minutes), etc., depending solely on the formatting applied. -- GitLab