Skip to content

Commit

Permalink
Consistent Timedelta Writing for all Excel Engines (pandas-dev#19921)
Browse files Browse the repository at this point in the history
  • Loading branch information
WillAyd authored and jreback committed Feb 28, 2018
1 parent 61211a8 commit 9bdc5c8
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 59 deletions.
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 @@ -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):
"""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
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

0 comments on commit 9bdc5c8

Please sign in to comment.