diff --git a/doc/source/whatsnew/v0.23.0.txt b/doc/source/whatsnew/v0.23.0.txt index 246eab386b2ab..52d8cf5f0a66d 100644 --- a/doc/source/whatsnew/v0.23.0.txt +++ b/doc/source/whatsnew/v0.23.0.txt @@ -489,6 +489,8 @@ I/O - Bug in :func:`DataFrame.to_latex()` where pairs of braces meant to serve as invisible placeholders were escaped (:issue:`18667`) - 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`) - Plotting diff --git a/pandas/io/excel.py b/pandas/io/excel.py index 92b29c8da7e3f..b03987e933bff 100644 --- a/pandas/io/excel.py +++ b/pandas/io/excel.py @@ -4,7 +4,7 @@ # --------------------------------------------------------------------- # ExcelFile class -from datetime import datetime, date, time, MINYEAR +from datetime import datetime, date, time, MINYEAR, timedelta import os import abc @@ -21,7 +21,6 @@ from pandas.io.common import (_is_url, _urlopen, _validate_header_arg, get_filepath_or_buffer, _NA_VALUES, _stringify_path) -from pandas.core.indexes.period import Period import pandas._libs.json as json from pandas.compat import (map, zip, reduce, range, lrange, u, add_metaclass, string_types, OrderedDict) @@ -777,17 +776,30 @@ def _pop_header_name(row, index_col): def _conv_value(val): - # Convert numpy types to Python types for the Excel writers. + """ 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, Period): - val = "{val}".format(val=val) - elif is_list_like(val): - val = str(val) + elif isinstance(val, (datetime, date, timedelta)): + pass + else: + val = compat.to_str(val) return val @@ -1460,6 +1472,9 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0, 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) stylekey = json.dumps(cell.style) if num_format_str: diff --git a/pandas/tests/io/test_excel.py b/pandas/tests/io/test_excel.py index 3263f71dea3c3..efbabcfd8fc4c 100644 --- a/pandas/tests/io/test_excel.py +++ b/pandas/tests/io/test_excel.py @@ -2,7 +2,7 @@ import os import sys import warnings -from datetime import datetime, date, time +from datetime import datetime, date, time, timedelta from distutils.version import LooseVersion from functools import partial from warnings import catch_warnings @@ -1440,6 +1440,56 @@ def test_excel_date_datetime_format(self): # to use df_expected to check the result tm.assert_frame_equal(rs2, df_expected) + def test_to_excel_interval_no_labels(self): + # GH19242 - test writing Interval without labels + _skip_if_no_xlrd() + + with ensure_clean(self.ext) as path: + frame = DataFrame(np.random.randint(-10, 10, size=(20, 1)), + dtype=np.int64) + expected = frame.copy() + frame['new'] = pd.cut(frame[0], 10) + expected['new'] = pd.cut(expected[0], 10).astype(str) + frame.to_excel(path, 'test1') + reader = ExcelFile(path) + recons = read_excel(reader, 'test1') + tm.assert_frame_equal(expected, recons) + + def test_to_excel_interval_labels(self): + # GH19242 - test writing Interval with labels + _skip_if_no_xlrd() + + with ensure_clean(self.ext) as path: + frame = DataFrame(np.random.randint(-10, 10, size=(20, 1)), + dtype=np.int64) + expected = frame.copy() + intervals = pd.cut(frame[0], 10, labels=['A', 'B', 'C', 'D', 'E', + 'F', 'G', 'H', 'I', 'J']) + frame['new'] = intervals + expected['new'] = pd.Series(list(intervals)) + frame.to_excel(path, 'test1') + reader = ExcelFile(path) + recons = read_excel(reader, 'test1') + tm.assert_frame_equal(expected, recons) + + def test_to_excel_timedelta(self): + # GH 19242, GH9155 - test writing timedelta to xls + _skip_if_no_xlrd() + + with ensure_clean('.xls') as path: + frame = DataFrame(np.random.randint(-10, 10, size=(20, 1)), + columns=['A'], + dtype=np.int64 + ) + expected = frame.copy() + frame['new'] = frame['A'].apply(lambda x: timedelta(seconds=x)) + expected['new'] = expected['A'].apply( + lambda x: timedelta(seconds=x).total_seconds() / float(86400)) + frame.to_excel(path, 'test1') + reader = ExcelFile(path) + recons = read_excel(reader, 'test1') + tm.assert_frame_equal(expected, recons) + def test_to_excel_periodindex(self): _skip_if_no_xlrd()