From a93318dc68b41794c0adb1033d472886f594759d Mon Sep 17 00:00:00 2001 From: Nis Martensen Date: Tue, 9 Feb 2021 21:47:00 +0100 Subject: [PATCH 1/4] Add documentation on datetime handling --HG-- branch : 3.0 --- doc/datetime.rst | 107 +++++++++++++++++++++++++++++++++++++++++++++++ doc/index.rst | 5 +++ 2 files changed, 112 insertions(+) create mode 100644 doc/datetime.rst diff --git a/doc/datetime.rst b/doc/datetime.rst new file mode 100644 index 00000000..8efd442a --- /dev/null +++ b/doc/datetime.rst @@ -0,0 +1,107 @@ +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 +module representations when reading from and writing to files. + + +Using the ISO 8601 format +------------------------- + +To make `openpyxl` store dates and times in the ISO 8601 format on +writing your file, set the workbook's ``iso_dates`` flag to ``True``: + + >>> import openpyxl + >>> wb = openpyxl.Workbook() + >>> wb.iso_dates = 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. + +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. + + +The 1900 and 1904 date systems +------------------------------ + +The 'date system' of an XLSX file determines how dates and times in the +single number representation are interpreted. XLSX files always use one +of two possible date systems: + + * In the 1900 date system (the default), the reference date (with number 1) is 1900-01-01. + * In the 1904 date system, the reference date (with number 0) is 1904-01-01. + +Complications arise not only from the different start numbers of the +reference dates, but also from the fact that the 1900 date system has a +built-in (but wrong) assumption that the year 1900 had been a leap year. +Excel deliberately refuses to recognize and display dates before the +reference date correctly, in order to discourage people from storing +historical data. + +More information on this issue is available from Microsoft: + * https://docs.microsoft.com/en-us/office/troubleshoot/excel/1900-and-1904-date-system + * https://docs.microsoft.com/en-us/office/troubleshoot/excel/wrongly-assumes-1900-is-leap-year + +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 +since it is not a valid date. + + +Reading timedelta values +------------------------ + +If you need to retrieve time interval durations (rather than dates or +times) from an XLSX file, there is no way to get them directly. You will +need to translate the time and datetime values returned by `openpyxl` to +timedelta values using a helper function. + + +.. warning:: + + 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. Therefore, if your files use the single number time + representation, and reliable timedelta values are important for your + use case, you MUST make sure your files use the 1904 date system! + + +You can get the date system of a workbook like this: + + >>> import openpyxl + >>> wb = openpyxl.Workbook() + >>> if wb.epoch == openpyxl.utils.datetime.CALENDAR_WINDOWS_1900: + ... print("This workbook is using the 1900 date system.") + ... + This workbook is using the 1900 date system. + + +and set it like this: + + >>> wb.epoch = openpyxl.utils.datetime.CALENDAR_MAC_1904 + + +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 +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 diff --git a/doc/index.rst b/doc/index.rst index 4fe4be23..738ae53d 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -237,6 +237,11 @@ Other topics formula + .. toctree:: + :maxdepth: 1 + + datetime + .. toctree:: :maxdepth: 1 -- GitLab From fe3c15b6b98857c123d94f3aef86e4cc00789572 Mon Sep 17 00:00:00 2001 From: Nis Martensen Date: Sun, 14 Feb 2021 20:24:16 +0100 Subject: [PATCH 2/4] add timedelta conversion helper functions --HG-- branch : 3.0 --- doc/datetime.rst | 67 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/doc/datetime.rst b/doc/datetime.rst index 8efd442a..3ff65b35 100644 --- a/doc/datetime.rst +++ b/doc/datetime.rst @@ -53,6 +53,21 @@ 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: + + >>> import openpyxl + >>> wb = openpyxl.Workbook() + >>> if wb.epoch == openpyxl.utils.datetime.CALENDAR_WINDOWS_1900: + ... print("This workbook is using the 1900 date system.") + ... + This workbook is using the 1900 date system. + + +and set it like this: + + >>> wb.epoch = openpyxl.utils.datetime.CALENDAR_MAC_1904 + + Reading timedelta values ------------------------ @@ -62,30 +77,50 @@ times) from an XLSX file, there is no way to get them directly. You will need to translate the time and datetime values returned by `openpyxl` to timedelta values using a helper function. +Here is a helper for files using the 1904 date system: -.. warning:: +.. code:: - 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. Therefore, if your files use the single number time - representation, and reliable timedelta values are important for your - use case, you MUST make sure your files use the 1904 date system! + 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) -You can get the date system of a workbook like this: +If your files use the 1900 date system, you can use this: - >>> import openpyxl - >>> wb = openpyxl.Workbook() - >>> if wb.epoch == openpyxl.utils.datetime.CALENDAR_WINDOWS_1900: - ... print("This workbook is using the 1900 date system.") - ... - This workbook is using the 1900 date system. +.. 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) and dt < datetime.datetime(1900, 3, 1): + return dt - datetime.datetime(1899, 12, 31) + return dt - datetime.datetime(1899, 12, 30) -and set it like this: - >>> wb.epoch = openpyxl.utils.datetime.CALENDAR_MAC_1904 +.. warning:: + + 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! Writing timedelta values -- GitLab From a415905a25b73a14989a83070cc3aac162560bd7 Mon Sep 17 00:00:00 2001 From: Nis Martensen Date: Wed, 17 Feb 2021 19:11:06 +0100 Subject: [PATCH 3/4] adapt to "datetime one" having been merged --HG-- branch : 3.0 --- doc/datetime.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/datetime.rst b/doc/datetime.rst index 3ff65b35..74932669 100644 --- a/doc/datetime.rst +++ b/doc/datetime.rst @@ -106,7 +106,7 @@ If your files use the 1900 date system, you can use this: microseconds=dt.microsecond ) # else we have a datetime - if dt > datetime.datetime(1899, 12, 31) and dt < datetime.datetime(1900, 3, 1): + if dt > datetime.datetime(1899, 12, 30) and dt < datetime.datetime(1900, 3, 1): return dt - datetime.datetime(1899, 12, 31) return dt - datetime.datetime(1899, 12, 30) -- GitLab From 94d3dfa6a14b91cae53cf7db20d7779d438b0d27 Mon Sep 17 00:00:00 2001 From: Nis Martensen Date: Fri, 19 Feb 2021 19:30:21 +0100 Subject: [PATCH 4/4] try to be clearer about the use of the helper function --HG-- branch : 3.0 --- doc/datetime.rst | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/doc/datetime.rst b/doc/datetime.rst index 74932669..fef6c89c 100644 --- a/doc/datetime.rst +++ b/doc/datetime.rst @@ -4,7 +4,9 @@ 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 -module representations when reading from and writing to files. +module representations when reading from and writing to files. In either +representation, the maximum date and time precision in XLSX files is +millisecond precision. Using the ISO 8601 format @@ -72,10 +74,18 @@ and set it like this: Reading timedelta values ------------------------ -If you need to retrieve time interval durations (rather than dates or -times) from an XLSX file, there is no way to get them directly. You will -need to translate the time and datetime values returned by `openpyxl` to -timedelta values using a helper function. +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: @@ -106,9 +116,9 @@ If your files use the 1900 date system, you can use this: microseconds=dt.microsecond ) # else we have a datetime - if dt > datetime.datetime(1899, 12, 30) and dt < datetime.datetime(1900, 3, 1): - return dt - datetime.datetime(1899, 12, 31) - return dt - datetime.datetime(1899, 12, 30) + 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:: -- GitLab