The Short Answer (for dates > 1900-02-28)
new Date(Date.UTC(0, 0, excelSerialDate - 1));
The Short Answer (for dates <= 1900-02-28)
new Date(Date.UTC(0, 0, excelSerialDate));
Why This Works
I really liked the answers by @leggett and @SteveR, and while they mostly work, I wanted to dig a bit deeper to understand how Date.UTC()
worked.
Note: There could be issues with timezone offsets, especially for older dates (pre-1970). See Browsers, time zones, Chrome 67 Error (historic timezone changes) so I'd like to stay in UTC and not rely on any shifting of hours if at all possible.
Excel dates are integers based on Jan 1st, 1900 (on PC. on MAC it is based from Jan 1st, 1904). Let's assume we are on a PC.
1900-01-01 is 1.0
1901-01-01 is 367.0, +366 days (Excel incorrectly treats 1900 as a leap year)
1902-01-01 is 732.0, +365 days (as expected)
Dates in JS are based on Jan 1st 1970 UTC
. If we use Date.UTC(year, month, ?day, ?hour, ?minutes, ?seconds)
it will return the number of milliseconds since that base time, in UTC. It has some interesting functionality which we can use to our benefit.
All normal ranges of the parameters of Date.UTC()
are 0 based except day
. It does accept numbers outside those ranges and converts the input to over or underflow the other parameters.
Date.UTC(1970, 0, 1, 0, 0, 0, 0) is 0ms
Date.UTC(1970, 0, 1, 0, 0, 0, 1) is 1ms
Date.UTC(1970, 0, 1, 0, 0, 1, 0) is 1000ms
It can do dates earlier than 1970-01-01 too. Here, we decrement the day from 0 to 1, and increase the hours, minutes, seconds and milliseconds.
Date.UTC(1970, 0, 0, 23, 59, 59, 999) is -1ms
It's even smart enough to convert years in the range 0-99 to 1900-1999
Date.UTC(70, 0, 0, 23, 59, 59, 999) is -1ms
Now, how do we represent 1900-01-01? To easier view the output in terms of a date I like to do
new Date(Date.UTC(1970, 0, 1, 0, 0, 0, 0)).toISOString() gives "1970-01-01T00:00:00.000Z"
new Date(Date.UTC(0, 0, 1, 0, 0, 0, 0)).toISOString() gives "1900-01-01T00:00:00.000Z"
Now we have to deal with timezones. Excel doesn't have a concept of a timezone in its date representation, but JS does. The easiest way to work this out, IMHO, is to consider all Excel dates entered as UTC (if you can).
Start with an Excel date of 732.0
new Date(Date.UTC(0, 0, 732, 0, 0, 0, 0)).toISOString() gives "1902-01-02T00:00:00.000Z"
which we know is off by 1 day because of the leap year issue mentioned above. We must decrement the day parameter by 1.
new Date(Date.UTC(0, 0, 732 - 1, 0, 0, 0, 0)) gives "1902-01-01T00:00:00.000Z"
It is important to note that if we construct a date using the new Date(year, month, day) constructor, the parameters use your local timezone. I am in the PT (UTC-7/UTC-8) timezone and I get
new Date(1902, 0, 1).toISOString() gives me "1902-01-01T08:00:00.000Z"
For my unit tests, I use
new Date(Date.UTC(1902, 0, 1)).toISOString() gives "1902-01-01T00:00:00.000Z"
A Typescript function to convert an excel serial date to a js date is
public static SerialDateToJSDate(excelSerialDate: number): Date {
return new Date(Date.UTC(0, 0, excelSerialDate - 1));
}
And to extract the UTC date to use
public static SerialDateToISODateString(excelSerialDate: number): string {
return this.SerialDateToJSDate(excelSerialDate).toISOString().split('T')[0];
}