Skip to content

Commit 1b88503

Browse files
Merge pull request #30 from RudolfCardinal/fix_spreadsheet_datetime_export_conversion
Fix spreadsheet datetime export conversion
2 parents 320d4b0 + a9dafd0 commit 1b88503

File tree

1 file changed

+61
-6
lines changed

1 file changed

+61
-6
lines changed

cardinal_pythonlib/excel.py

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
2727
"""
2828

29+
# =============================================================================
30+
# Imports
31+
# =============================================================================
32+
2933
import datetime
3034
import io
3135
from typing import Any
@@ -36,7 +40,18 @@
3640
from pendulum.datetime import DateTime
3741
from semantic_version import Version
3842

39-
from cardinal_pythonlib.datetimefunc import pendulum_to_datetime
43+
44+
# =============================================================================
45+
# Constants
46+
# =============================================================================
47+
48+
# ISO 8601, e.g. 2013-07-24T20:04:07+0100)
49+
ISO8601_STRFTIME_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
50+
51+
52+
# =============================================================================
53+
# Conversion functions
54+
# =============================================================================
4055

4156

4257
def excel_to_bytes(wb: Workbook) -> bytes:
@@ -63,9 +78,38 @@ def convert_for_openpyxl(x: Any) -> Any:
6378
6479
Returns:
6580
the same thing, or a more suitable value!
81+
82+
2025-03-06 update: We were doing this:
83+
84+
.. code-block:: python
85+
86+
if isinstance(x, DateTime):
87+
return pendulum_to_datetime(x)
88+
89+
However, conversion of pendulum.datetime.Datetime to datetime.datetime is
90+
insufficient, because with openpyxl==3.0.7 you can still end up with this
91+
error from openpyxl/utils/datetime.py, line 97, in to_excel:
92+
93+
.. code-block:: python
94+
95+
days = (dt - epoch).days
96+
# TypeError: can't subtract offset-naive and offset-aware datetimes
97+
98+
The "epoch" variable does NOT have a timezone attribute. So we need to
99+
ensure that what we produce here doesn't, either. In principle, there are
100+
three alternatives: (a) convert to a standard timezone (UTC), making things
101+
slightly and silently unhappier for those working outside UTC; (b) strip
102+
timezone information, causing errors if datetime values are subtracted; or
103+
(c) convert to a standard textual representation, including timezone
104+
information, preserving all data but letting the user sort out the meaning.
105+
Since ``convert_for_pyexcel_ods3`` was already converting
106+
pendulum.datetime.DateTime and datetime.datetime values to a standard
107+
string, via strftime, let's do that too. Note that this also anticipates
108+
the deprecation of timezone-aware dates from openpyxl==3.0.7
109+
(https://foss.heptapod.net/openpyxl/openpyxl/-/issues/1645).
66110
"""
67-
if isinstance(x, DateTime):
68-
return pendulum_to_datetime(x)
111+
if isinstance(x, (DateTime, datetime.datetime)):
112+
return x.strftime(ISO8601_STRFTIME_FORMAT)
69113
elif isinstance(x, (Version, uuid.UUID)):
70114
return str(x)
71115
else:
@@ -83,22 +127,33 @@ def convert_for_pyexcel_ods3(x: Any) -> Any:
83127
- ``None``
84128
- :class:`numpy.float64`
85129
- :class:`uuid.UUID`
130+
- subclasses of `str`
86131
87132
Args:
88133
x: a data value
89134
90135
Returns:
91136
the same thing, or a more suitable value!
92-
"""
93137
138+
2025-03-06 update: With pyexcel-ods3==0.6.0, we were getting a KeyError
139+
from pyexcel_ods3/odsw.py, in ODSSheetWriter.write_row. It does this:
140+
141+
.. code-block:: python
142+
143+
value_type = service.ODS_WRITE_FORMAT_COVERSION[type(cell)]
144+
145+
and we had a cell that looked like 'aq' but had the type <class
146+
'sqlalchemy.sql.elements.quoted_name'>, a subclass of str.
147+
"""
94148
if isinstance(x, (DateTime, datetime.datetime)):
95-
# ISO 8601, e.g. 2013-07-24T20:04:07+0100)
96-
return x.strftime("%Y-%m-%dT%H:%M:%S%z")
149+
return x.strftime(ISO8601_STRFTIME_FORMAT)
97150
elif x is None:
98151
return ""
99152
elif isinstance(x, (Version, uuid.UUID)):
100153
return str(x)
101154
elif isinstance(x, float64):
102155
return float(x)
156+
elif isinstance(x, str):
157+
return str(x)
103158
else:
104159
return x

0 commit comments

Comments
 (0)