Skip to content

Consistent Timedelta Writing for all Excel Engines #19921

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/source/whatsnew/v0.23.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
97 changes: 44 additions & 53 deletions pandas/io/excel.py
Original file line number Diff line number Diff line change
Expand Up @@ -775,35 +775,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):
"""
Expand Down Expand Up @@ -949,6 +920,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
Expand Down Expand Up @@ -1378,7 +1382,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:
Expand Down Expand Up @@ -1465,25 +1471,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:
Expand Down Expand Up @@ -1741,23 +1738,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:
Expand Down
5 changes: 0 additions & 5 deletions pandas/tests/io/test_excel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down