Skip to content

Commit 53d1dd0

Browse files
committed
Merge pull request #78 from jmcnamara/datetime
Added function to convert Excel date to datetime.
2 parents b782370 + 1a936de commit 53d1dd0

File tree

2 files changed

+204
-0
lines changed

2 files changed

+204
-0
lines changed

tests/test_xldate_to_datetime.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
###############################################################################
2+
#
3+
# Tests for the xlrd xldate.xldate_as_datetime() function.
4+
#
5+
6+
import unittest
7+
from datetime import datetime
8+
from xlrd import xldate
9+
10+
not_1904 = False
11+
is_1904 = True
12+
13+
14+
class TestConvertToDateTime(unittest.TestCase):
15+
"""
16+
Testcases to test the _xldate_to_datetime() function against dates
17+
extracted from Excel files, with 1900/1904 epochs.
18+
19+
"""
20+
21+
def test_dates_and_times_1900_epoch(self):
22+
"""
23+
Test the _xldate_to_datetime() function for dates and times in
24+
the Excel standard 1900 epoch.
25+
26+
"""
27+
# Test Excel dates strings and corresponding serial date numbers taken
28+
# from an Excel file.
29+
excel_dates = [
30+
# Excel's 0.0 date in the 1900 epoch is 1 day before 1900.
31+
('1899-12-31T00:00:00.000', 0),
32+
33+
# Date/time before the false Excel 1900 leapday.
34+
('1900-02-28T02:11:11.986', 59.09111094906),
35+
36+
# Date/time after the false Excel 1900 leapday.
37+
('1900-03-01T05:46:44.068', 61.24078782403),
38+
39+
# Random date/times in Excel's 0-9999.9999+ range.
40+
('1982-08-25T00:15:20.213', 30188.010650613425),
41+
('2065-04-19T00:16:48.290', 60376.011670023145),
42+
('3222-06-11T03:08:08.251', 483014.13065105322),
43+
('4379-08-03T06:14:48.580', 905652.26028449077),
44+
('5949-12-30T12:59:54.263', 1479232.5416002662),
45+
46+
# End of Excel's date range.
47+
('9999-12-31T23:59:59.000', 2958465.999988426),
48+
]
49+
50+
# Convert the Excel date strings to datetime objects and compare
51+
# against the dateitme return value of xldate.xldate_as_datetime().
52+
for excel_date in excel_dates:
53+
exp = datetime.strptime(excel_date[0], "%Y-%m-%dT%H:%M:%S.%f")
54+
got = xldate.xldate_as_datetime(excel_date[1], not_1904)
55+
56+
self.assertEqual(got, exp)
57+
58+
def test_dates_only_1900_epoch(self):
59+
"""
60+
Test the _xldate_to_datetime() function for dates in the Excel
61+
standard 1900 epoch.
62+
63+
"""
64+
# Test Excel dates strings and corresponding serial date numbers taken
65+
# from an Excel file.
66+
excel_dates = [
67+
# Excel's day 0 in the 1900 epoch is 1 day before 1900.
68+
('1899-12-31', 0),
69+
70+
# Excel's day 1 in the 1900 epoch.
71+
('1900-01-01', 1),
72+
73+
# Date/time before the false Excel 1900 leapday.
74+
('1900-02-28', 59),
75+
76+
# Date/time after the false Excel 1900 leapday.
77+
('1900-03-01', 61),
78+
79+
# Random date/times in Excel's 0-9999.9999+ range.
80+
('1902-09-27', 1001),
81+
('1999-12-31', 36525),
82+
('2000-01-01', 36526),
83+
('4000-12-31', 767376),
84+
('4321-01-01', 884254),
85+
('9999-01-01', 2958101),
86+
87+
# End of Excel's date range.
88+
('9999-12-31', 2958465),
89+
]
90+
91+
# Convert the Excel date strings to datetime objects and compare
92+
# against the dateitme return value of xldate.xldate_as_datetime().
93+
for excel_date in excel_dates:
94+
exp = datetime.strptime(excel_date[0], "%Y-%m-%d")
95+
got = xldate.xldate_as_datetime(excel_date[1], not_1904)
96+
97+
self.assertEqual(got, exp)
98+
99+
def test_dates_only_1904_epoch(self):
100+
"""
101+
Test the _xldate_to_datetime() function for dates in the Excel
102+
Mac/1904 epoch.
103+
104+
"""
105+
# Test Excel dates strings and corresponding serial date numbers taken
106+
# from an Excel file.
107+
excel_dates = [
108+
# Excel's day 0 in the 1904 epoch.
109+
('1904-01-01', 0),
110+
111+
# Random date/times in Excel's 0-9999.9999+ range.
112+
('1904-01-31', 30),
113+
('1904-08-31', 243),
114+
('1999-02-28', 34757),
115+
('1999-12-31', 35063),
116+
('2000-01-01', 35064),
117+
('2400-12-31', 181526),
118+
('4000-01-01', 765549),
119+
('9999-01-01', 2956639),
120+
121+
# End of Excel's date range.
122+
('9999-12-31', 2957003),
123+
]
124+
125+
# Convert the Excel date strings to datetime objects and compare
126+
# against the dateitme return value of xldate.xldate_as_datetime().
127+
for excel_date in excel_dates:
128+
exp = datetime.strptime(excel_date[0], "%Y-%m-%d")
129+
got = xldate.xldate_as_datetime(excel_date[1], is_1904)
130+
131+
self.assertEqual(got, exp)
132+
133+
def test_times_only(self):
134+
"""
135+
Test the _xldate_to_datetime() function for times only, i.e, the
136+
fractional part of the Excel date when the serial date is 0.
137+
138+
"""
139+
# Test Excel dates strings and corresponding serial date numbers taken
140+
# from an Excel file. The 1899-12-31 date is Excel's day 0.
141+
excel_dates = [
142+
# Random times in Excel's 0-0.9999+ range for 1 day.
143+
('1899-12-31T00:00:00.000', 0),
144+
('1899-12-31T00:15:20.213', 1.0650613425925924E-2),
145+
('1899-12-31T02:24:37.095', 0.10042934027777778),
146+
('1899-12-31T04:56:35.792', 0.2059698148148148),
147+
('1899-12-31T07:31:20.407', 0.31343063657407405),
148+
('1899-12-31T09:37:23.945', 0.40097158564814817),
149+
('1899-12-31T12:09:48.602', 0.50681252314814818),
150+
('1899-12-31T14:37:57.451', 0.60969271990740748),
151+
('1899-12-31T17:04:02.415', 0.71113906250000003),
152+
('1899-12-31T19:14:24.673', 0.80167445601851861),
153+
('1899-12-31T21:39:05.944', 0.90215212962962965),
154+
('1899-12-31T23:17:12.632', 0.97028509259259266),
155+
('1899-12-31T23:59:59.999', 0.99999998842592586),
156+
]
157+
158+
# Convert the Excel date strings to datetime objects and compare
159+
# against the dateitme return value of xldate.xldate_as_datetime().
160+
for excel_date in excel_dates:
161+
exp = datetime.strptime(excel_date[0], "%Y-%m-%dT%H:%M:%S.%f")
162+
got = xldate.xldate_as_datetime(excel_date[1], not_1904)
163+
164+
self.assertEqual(got, exp)

xlrd/xldate.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,16 @@
1717
# More importantly:
1818
# Noon on Gregorian 1900-03-01 (day 61 in the 1900-based system) is JDN 2415080.0
1919
# Noon on Gregorian 1904-01-02 (day 1 in the 1904-based system) is JDN 2416482.0
20+
import datetime
2021

2122
_JDN_delta = (2415080 - 61, 2416482 - 1)
2223
assert _JDN_delta[1] - _JDN_delta[0] == 1462
2324

25+
# Pre-calculate the datetime epochs for efficiency.
26+
epoch_1904 = datetime.datetime(1904, 1, 1)
27+
epoch_1900 = datetime.datetime(1899, 12, 31)
28+
epoch_1900_minus_1 = datetime.datetime(1899, 12, 30)
29+
2430
class XLDateError(ValueError): pass
2531

2632
class XLDateNegative(XLDateError): pass
@@ -90,6 +96,40 @@ def xldate_as_tuple(xldate, datemode):
9096
else:
9197
return ((yreg // 1461) - 4716, mp + 3, d, hour, minute, second)
9298

99+
100+
##
101+
# Convert an Excel date/time number into a datetime.datetime object.
102+
#
103+
# @param xldate The Excel number
104+
# @param datemode 0: 1900-based, 1: 1904-based.
105+
#
106+
# @return a datetime.datetime() object.
107+
#
108+
def xldate_as_datetime(xldate, datemode):
109+
"""Convert an Excel date/time number into a datetime.datetime object."""
110+
111+
# Set the epoch based on the 1900/1904 datemode.
112+
if datemode:
113+
epoch = epoch_1904
114+
else:
115+
if xldate < 60:
116+
epoch = epoch_1900
117+
else:
118+
# Workaround Excel 1900 leap year bug by adjusting the epoch.
119+
epoch = epoch_1900_minus_1
120+
121+
# The integer part of the Excel date stores the number of days since
122+
# the epoch and the fractional part stores the percentage of the day.
123+
days = int(xldate)
124+
fraction = xldate - days
125+
126+
# Get the the integer and decimal seconds in Excel's millisecond resolution.
127+
seconds = int(round(fraction * 86400000.0))
128+
seconds, milliseconds = divmod(seconds, 1000)
129+
130+
return epoch + datetime.timedelta(days, seconds, 0, milliseconds)
131+
132+
93133
# === conversions from date/time to xl numbers
94134

95135
def _leap(y):

0 commit comments

Comments
 (0)