Skip to content

Commit 9bdc5c8

Browse files
WillAydjreback
authored andcommitted
Consistent Timedelta Writing for all Excel Engines (pandas-dev#19921)
1 parent 61211a8 commit 9bdc5c8

File tree

3 files changed

+45
-59
lines changed

3 files changed

+45
-59
lines changed

doc/source/whatsnew/v0.23.0.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -867,7 +867,7 @@ I/O
867867
- Bug in :func:`read_json` where large numeric values were causing an ``OverflowError`` (:issue:`18842`)
868868
- Bug in :func:`DataFrame.to_parquet` where an exception was raised if the write destination is S3 (:issue:`19134`)
869869
- :class:`Interval` now supported in :func:`DataFrame.to_excel` for all Excel file types (:issue:`19242`)
870-
- :class:`Timedelta` now supported in :func:`DataFrame.to_excel` for xls file type (:issue:`19242`, :issue:`9155`)
870+
- :class:`Timedelta` now supported in :func:`DataFrame.to_excel` for all Excel file types (:issue:`19242`, :issue:`9155`, :issue:`19900`)
871871
- Bug in :meth:`pandas.io.stata.StataReader.value_labels` raising an ``AttributeError`` when called on very old files. Now returns an empty dict (:issue:`19417`)
872872

873873
Plotting

pandas/io/excel.py

+44-53
Original file line numberDiff line numberDiff line change
@@ -779,35 +779,6 @@ def _pop_header_name(row, index_col):
779779
return none_fill(row[i]), row[:i] + [''] + row[i + 1:]
780780

781781

782-
def _conv_value(val):
783-
""" Convert numpy types to Python types for the Excel writers.
784-
785-
Parameters
786-
----------
787-
val : object
788-
Value to be written into cells
789-
790-
Returns
791-
-------
792-
If val is a numpy int, float, or bool, then the equivalent Python
793-
types are returned. :obj:`datetime`, :obj:`date`, and :obj:`timedelta`
794-
are passed and formatting must be handled in the writer. :obj:`str`
795-
representation is returned for all other types.
796-
"""
797-
if is_integer(val):
798-
val = int(val)
799-
elif is_float(val):
800-
val = float(val)
801-
elif is_bool(val):
802-
val = bool(val)
803-
elif isinstance(val, (datetime, date, timedelta)):
804-
pass
805-
else:
806-
val = compat.to_str(val)
807-
808-
return val
809-
810-
811782
@add_metaclass(abc.ABCMeta)
812783
class ExcelWriter(object):
813784
"""
@@ -953,6 +924,39 @@ def _get_sheet_name(self, sheet_name):
953924
'cur_sheet property')
954925
return sheet_name
955926

927+
def _value_with_fmt(self, val):
928+
"""Convert numpy types to Python types for the Excel writers.
929+
930+
Parameters
931+
----------
932+
val : object
933+
Value to be written into cells
934+
935+
Returns
936+
-------
937+
Tuple with the first element being the converted value and the second
938+
being an optional format
939+
"""
940+
fmt = None
941+
942+
if is_integer(val):
943+
val = int(val)
944+
elif is_float(val):
945+
val = float(val)
946+
elif is_bool(val):
947+
val = bool(val)
948+
elif isinstance(val, datetime):
949+
fmt = self.datetime_format
950+
elif isinstance(val, date):
951+
fmt = self.date_format
952+
elif isinstance(val, timedelta):
953+
val = val.total_seconds() / float(86400)
954+
fmt = '0'
955+
else:
956+
val = compat.to_str(val)
957+
958+
return val, fmt
959+
956960
@classmethod
957961
def check_extension(cls, ext):
958962
"""checks that path's extension against the Writer's supported
@@ -1382,7 +1386,9 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0,
13821386
row=startrow + cell.row + 1,
13831387
column=startcol + cell.col + 1
13841388
)
1385-
xcell.value = _conv_value(cell.val)
1389+
xcell.value, fmt = self._value_with_fmt(cell.val)
1390+
if fmt:
1391+
xcell.number_format = fmt
13861392

13871393
style_kwargs = {}
13881394
if cell.style:
@@ -1469,25 +1475,16 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0,
14691475
style_dict = {}
14701476

14711477
for cell in cells:
1472-
val = _conv_value(cell.val)
1473-
1474-
num_format_str = None
1475-
if isinstance(cell.val, datetime):
1476-
num_format_str = self.datetime_format
1477-
elif isinstance(cell.val, date):
1478-
num_format_str = self.date_format
1479-
elif isinstance(cell.val, timedelta):
1480-
delta = cell.val
1481-
val = delta.total_seconds() / float(86400)
1478+
val, fmt = self._value_with_fmt(cell.val)
14821479

14831480
stylekey = json.dumps(cell.style)
1484-
if num_format_str:
1485-
stylekey += num_format_str
1481+
if fmt:
1482+
stylekey += fmt
14861483

14871484
if stylekey in style_dict:
14881485
style = style_dict[stylekey]
14891486
else:
1490-
style = self._convert_to_style(cell.style, num_format_str)
1487+
style = self._convert_to_style(cell.style, fmt)
14911488
style_dict[stylekey] = style
14921489

14931490
if cell.mergestart is not None and cell.mergeend is not None:
@@ -1745,23 +1742,17 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0,
17451742
wks.freeze_panes(*(freeze_panes))
17461743

17471744
for cell in cells:
1748-
val = _conv_value(cell.val)
1749-
1750-
num_format_str = None
1751-
if isinstance(cell.val, datetime):
1752-
num_format_str = self.datetime_format
1753-
elif isinstance(cell.val, date):
1754-
num_format_str = self.date_format
1745+
val, fmt = self._value_with_fmt(cell.val)
17551746

17561747
stylekey = json.dumps(cell.style)
1757-
if num_format_str:
1758-
stylekey += num_format_str
1748+
if fmt:
1749+
stylekey += fmt
17591750

17601751
if stylekey in style_dict:
17611752
style = style_dict[stylekey]
17621753
else:
17631754
style = self.book.add_format(
1764-
_XlsxStyler.convert(cell.style, num_format_str))
1755+
_XlsxStyler.convert(cell.style, fmt))
17651756
style_dict[stylekey] = style
17661757

17671758
if cell.mergestart is not None and cell.mergeend is not None:

pandas/tests/io/test_excel.py

-5
Original file line numberDiff line numberDiff line change
@@ -1373,11 +1373,6 @@ def test_to_excel_interval_labels(self, merge_cells, engine, ext):
13731373

13741374
def test_to_excel_timedelta(self, merge_cells, engine, ext):
13751375
# GH 19242, GH9155 - test writing timedelta to xls
1376-
if engine == 'openpyxl':
1377-
pytest.xfail('Timedelta roundtrip broken with openpyxl')
1378-
if engine == 'xlsxwriter' and (sys.version_info[0] == 2 and
1379-
sys.platform.startswith('linux')):
1380-
pytest.xfail('Not working on linux with Py2 and xlsxwriter')
13811376
frame = DataFrame(np.random.randint(-10, 10, size=(20, 1)),
13821377
columns=['A'],
13831378
dtype=np.int64

0 commit comments

Comments
 (0)