Skip to content

ENH: Excel writer takes locale setting for date and datetime into account #5583

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

Closed
wants to merge 2 commits into from
Closed
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
3 changes: 3 additions & 0 deletions doc/source/release.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ pandas 0.13.1
New features
~~~~~~~~~~~~

- Added ``date_format`` and ``datetime_format`` attribute to ExcelWriter.
(:issue:`4133`)

API Changes
~~~~~~~~~~~

Expand Down
53 changes: 36 additions & 17 deletions pandas/io/excel.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,11 @@ class ExcelWriter(object):
Engine to use for writing. If None, defaults to
``io.excel.<extension>.writer``. NOTE: can only be passed as a keyword
argument.
date_format : string, default None
Format string for dates written into Excel files (e.g. 'YYYY-MM-DD')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • this needs to be added to io.rst (in the excel section) as well with a small example
  • pls add the same example in v0.13.1.txt as well

datetime_format : string, default None
Format string for datetime objects written into Excel files
(e.g. 'YYYY-MM-DD HH:MM:SS')
"""
# Defining an ExcelWriter implementation (see abstract methods for more...)

Expand Down Expand Up @@ -429,15 +434,25 @@ def save(self):
"""
pass

def __init__(self, path, engine=None, **engine_kwargs):
# validate that this engine can handle the extnesion
def __init__(self, path, engine=None,
date_format=None, datetime_format=None, **engine_kwargs):
# validate that this engine can handle the extension
ext = os.path.splitext(path)[-1]
self.check_extension(ext)

self.path = path
self.sheets = {}
self.cur_sheet = None

if date_format is None:
self.date_format = 'YYYY-MM-DD'
else:
self.date_format = date_format
if datetime_format is None:
self.datetime_format = 'YYYY-MM-DD HH:MM:SS'
else:
self.datetime_format = datetime_format

def _get_sheet_name(self, sheet_name):
if sheet_name is None:
sheet_name = self.cur_sheet
Expand Down Expand Up @@ -518,9 +533,9 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0):
style.__getattribute__(field))

if isinstance(cell.val, datetime.datetime):
xcell.style.number_format.format_code = "YYYY-MM-DD HH:MM:SS"
xcell.style.number_format.format_code = self.datetime_format
elif isinstance(cell.val, datetime.date):
xcell.style.number_format.format_code = "YYYY-MM-DD"
xcell.style.number_format.format_code = self.date_format

if cell.mergestart is not None and cell.mergeend is not None:
cletterstart = get_column_letter(startcol + cell.col + 1)
Expand Down Expand Up @@ -585,8 +600,8 @@ def __init__(self, path, engine=None, **engine_kwargs):
super(_XlwtWriter, self).__init__(path, **engine_kwargs)

self.book = xlwt.Workbook()
self.fm_datetime = xlwt.easyxf(num_format_str='YYYY-MM-DD HH:MM:SS')
self.fm_date = xlwt.easyxf(num_format_str='YYYY-MM-DD')
self.fm_datetime = xlwt.easyxf(num_format_str=self.datetime_format)
self.fm_date = xlwt.easyxf(num_format_str=self.date_format)

def save(self):
"""
Expand All @@ -612,9 +627,9 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0):

num_format_str = None
if isinstance(cell.val, datetime.datetime):
num_format_str = "YYYY-MM-DD HH:MM:SS"
num_format_str = self.datetime_format
if isinstance(cell.val, datetime.date):
num_format_str = "YYYY-MM-DD"
num_format_str = self.date_format

stylekey = json.dumps(cell.style)
if num_format_str:
Expand Down Expand Up @@ -699,11 +714,14 @@ class _XlsxWriter(ExcelWriter):
engine = 'xlsxwriter'
supported_extensions = ('.xlsx',)

def __init__(self, path, engine=None, **engine_kwargs):
def __init__(self, path, engine=None,
date_format=None, datetime_format=None, **engine_kwargs):
# Use the xlsxwriter module as the Excel writer.
import xlsxwriter

super(_XlsxWriter, self).__init__(path, engine=engine, **engine_kwargs)
super(_XlsxWriter, self).__init__(path, engine=engine,
date_format=date_format, datetime_format=datetime_format,
**engine_kwargs)

self.book = xlsxwriter.Workbook(path, **engine_kwargs)

Expand All @@ -729,9 +747,9 @@ def write_cells(self, cells, sheet_name=None, startrow=0, startcol=0):
for cell in cells:
num_format_str = None
if isinstance(cell.val, datetime.datetime):
num_format_str = "YYYY-MM-DD HH:MM:SS"
num_format_str = self.datetime_format
if isinstance(cell.val, datetime.date):
num_format_str = "YYYY-MM-DD"
num_format_str = self.date_format

stylekey = json.dumps(cell.style)
if num_format_str:
Expand Down Expand Up @@ -762,12 +780,16 @@ def _convert_to_style(self, style_dict, num_format_str=None):
style_dict: style dictionary to convert
num_format_str: optional number format string
"""
if style_dict is None:
return None

# Create a XlsxWriter format object.
xl_format = self.book.add_format()

if num_format_str is not None:
xl_format.set_num_format(num_format_str)

if style_dict is None:
return xl_format

# Map the cell font to XlsxWriter font properties.
if style_dict.get('font'):
font = style_dict['font']
Expand All @@ -788,9 +810,6 @@ def _convert_to_style(self, style_dict, num_format_str=None):
if style_dict.get('borders'):
xl_format.set_border()

if num_format_str is not None:
xl_format.set_num_format(num_format_str)

return xl_format

register_writer(_XlsxWriter)
40 changes: 39 additions & 1 deletion pandas/io/tests/test_excel.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# pylint: disable=E1101

from pandas.compat import u, range, map
from datetime import datetime
from datetime import datetime, date
import os

import nose
Expand Down Expand Up @@ -661,6 +661,44 @@ def test_excel_roundtrip_datetime(self):
recons = reader.parse('test1')
tm.assert_frame_equal(self.tsframe, recons)

# GH4133 - excel output format strings
def test_excel_date_datetime_format(self):
df = DataFrame([[date(2014, 1, 31),
date(1999, 9, 24)],
[datetime(1998, 5, 26, 23, 33, 4),
datetime(2014, 2, 28, 13, 5, 13)]],
index=['DATE', 'DATETIME'], columns=['X', 'Y'])
df_expected = DataFrame([[datetime(2014, 1, 31),
datetime(1999, 9, 24)],
[datetime(1998, 5, 26, 23, 33, 4),
datetime(2014, 2, 28, 13, 5, 13)]],
index=['DATE', 'DATETIME'], columns=['X', 'Y'])

with ensure_clean(self.ext) as filename1:
with ensure_clean(self.ext) as filename2:
writer1 = ExcelWriter(filename1)
writer2 = ExcelWriter(filename2,
date_format='DD.MM.YYYY',
datetime_format='DD.MM.YYYY HH-MM-SS')

df.to_excel(writer1, 'test1')
df.to_excel(writer2, 'test1')

writer1.close()
writer2.close()

reader1 = ExcelFile(filename1)
reader2 = ExcelFile(filename2)

rs1 = reader1.parse('test1', index_col=None)
rs2 = reader2.parse('test1', index_col=None)

tm.assert_frame_equal(rs1, rs2)

# since the reader returns a datetime object for dates, we need
# to use df_expected to check the result
tm.assert_frame_equal(rs2, df_expected)

def test_to_excel_periodindex(self):
_skip_if_no_xlrd()

Expand Down