From 9bdc5c8d48e506d00326702b997babe70f941a26 Mon Sep 17 00:00:00 2001 From: William Ayd Date: Tue, 27 Feb 2018 16:16:48 -0800 Subject: [PATCH] Consistent Timedelta Writing for all Excel Engines (#19921) --- doc/source/whatsnew/v0.23.0.txt | 2 +- pandas/io/excel.py | 97 +++++++++++++++------------------ pandas/tests/io/test_excel.py | 5 -- 3 files changed, 45 insertions(+), 59 deletions(-) diff --git a/doc/source/whatsnew/v0.23.0.txt b/doc/source/whatsnew/v0.23.0.txt index 5330f7e7e998b..6865428c352c1 100644 --- a/doc/source/whatsnew/v0.23.0.txt +++ b/doc/source/whatsnew/v0.23.0.txt @@ -867,7 +867,7 @@ I/O - Bug in :func:`read_json` where large numeric values were causing an ``OverflowError`` (:issue:`18842`) - Bug in :func:`DataFrame.to_parquet` where an exception was raised if the write destination is S3 (:issue:`19134`) - :class:`Interval` now supported in :func:`DataFrame.to_excel` for all Excel file types (:issue:`19242`) -- :class:`Timedelta` now supported in :func:`DataFrame.to_excel` for xls file type (:issue:`19242`, :issue:`9155`) +- :class:`Timedelta` now supported in :func:`DataFrame.to_excel` for all Excel file types (:issue:`19242`, :issue:`9155`, :issue:`19900`) - 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`) Plotting diff --git a/pandas/io/excel.py b/pandas/io/excel.py index 78af86cc00f7f..0f9df845117db 100644 --- a/pandas/io/excel.py +++ b/pandas/io/excel.py @@ -779,35 +779,6 @@ def _pop_header_name(row, index_col): return none_fill(row[i]), row[:i] + [''] + row[i + 1:] -def _conv_value(val): - """ Convert numpy types to Python types for the Excel writers. - - Parameters - ---------- - val : object - Value to be written into cells - - Returns - ------- - If val is a numpy int, float, or bool, then the equivalent Python - types are returned. :obj:`datetime`, :obj:`date`, and :obj:`timedelta` - are passed and formatting must be handled in the writer. :obj:`str` - representation is returned for all other types. - """ - if is_integer(val): - val = int(val) - elif is_float(val): - val = float(val) - elif is_bool(val): - val = bool(val) - elif isinstance(val, (datetime, date, timedelta)): - pass - else: - val = compat.to_str(val) - - return val - - @add_metaclass(abc.ABCMeta) class ExcelWriter(object): """ @@ -953,6 +924,39 @@ def _get_sheet_name(self, sheet_name): 'cur_sheet property') return sheet_name + def _value_with_fmt(self, val): + """Convert numpy types to Python types for the Excel writers. + + Parameters + ---------- + val : object + Value to be written into cells + + Returns + ------- + Tuple with the first element being the converted value and the second + being an optional format + """ + fmt = None + + if is_integer(val): + val = int(val) + elif is_float(val): + val = float(val) + elif is_bool(val): + val = bool(val) + elif isinstance(val, datetime): + fmt = self.datetime_format + elif isinstance(val, date): + fmt = self.date_format + elif isinstance(val, timedelta): + val = val.total_seconds() / float(86400) + fmt = '0' + else: + val = compat.to_str(val) + + return val, fmt + @classmethod def check_extension(cls, ext): """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, row=startrow + cell.row + 1, column=startcol + cell.col + 1 ) - xcell.value = _conv_value(cell.val) + xcell.value, fmt = self._value_with_fmt(cell.val) + if fmt: + xcell.number_format = fmt style_kwargs = {} if cell.style: @@ -1469,25 +1475,16 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0, style_dict = {} for cell in cells: - val = _conv_value(cell.val) - - num_format_str = None - if isinstance(cell.val, datetime): - num_format_str = self.datetime_format - elif isinstance(cell.val, date): - num_format_str = self.date_format - elif isinstance(cell.val, timedelta): - delta = cell.val - val = delta.total_seconds() / float(86400) + val, fmt = self._value_with_fmt(cell.val) stylekey = json.dumps(cell.style) - if num_format_str: - stylekey += num_format_str + if fmt: + stylekey += fmt if stylekey in style_dict: style = style_dict[stylekey] else: - style = self._convert_to_style(cell.style, num_format_str) + style = self._convert_to_style(cell.style, fmt) style_dict[stylekey] = style 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, wks.freeze_panes(*(freeze_panes)) for cell in cells: - val = _conv_value(cell.val) - - num_format_str = None - if isinstance(cell.val, datetime): - num_format_str = self.datetime_format - elif isinstance(cell.val, date): - num_format_str = self.date_format + val, fmt = self._value_with_fmt(cell.val) stylekey = json.dumps(cell.style) - if num_format_str: - stylekey += num_format_str + if fmt: + stylekey += fmt if stylekey in style_dict: style = style_dict[stylekey] else: style = self.book.add_format( - _XlsxStyler.convert(cell.style, num_format_str)) + _XlsxStyler.convert(cell.style, fmt)) style_dict[stylekey] = style if cell.mergestart is not None and cell.mergeend is not None: diff --git a/pandas/tests/io/test_excel.py b/pandas/tests/io/test_excel.py index fdf9954285db8..15d3062394d6e 100644 --- a/pandas/tests/io/test_excel.py +++ b/pandas/tests/io/test_excel.py @@ -1373,11 +1373,6 @@ def test_to_excel_interval_labels(self, merge_cells, engine, ext): def test_to_excel_timedelta(self, merge_cells, engine, ext): # GH 19242, GH9155 - test writing timedelta to xls - if engine == 'openpyxl': - pytest.xfail('Timedelta roundtrip broken with openpyxl') - if engine == 'xlsxwriter' and (sys.version_info[0] == 2 and - sys.platform.startswith('linux')): - pytest.xfail('Not working on linux with Py2 and xlsxwriter') frame = DataFrame(np.random.randint(-10, 10, size=(20, 1)), columns=['A'], dtype=np.int64