57

I have the following javascript code that convert date (string) to the Date Serial Number used in Microsoft Excel:

function JSDateToExcelDate(inDate) {

    var returnDateTime = 25569.0 + ((inDate.getTime() - (inDate.getTimezoneOffset() * 60 * 1000)) / (1000 * 60 * 60 * 24));
    return returnDateTime.toString().substr(0,5);

}

So, how do I do the reverse? (Meaning that a Javascript code that convert the Date Serial Number used in Microsoft Excel to a date string?

Jack
  • 1,603
  • 5
  • 25
  • 36

12 Answers12

89

Try this:

function ExcelDateToJSDate(serial) {
   var utc_days  = Math.floor(serial - 25569);
   var utc_value = utc_days * 86400;                                        
   var date_info = new Date(utc_value * 1000);

   var fractional_day = serial - Math.floor(serial) + 0.0000001;

   var total_seconds = Math.floor(86400 * fractional_day);

   var seconds = total_seconds % 60;

   total_seconds -= seconds;

   var hours = Math.floor(total_seconds / (60 * 60));
   var minutes = Math.floor(total_seconds / 60) % 60;

   return new Date(date_info.getFullYear(), date_info.getMonth(), date_info.getDate(), hours, minutes, seconds);
}

Custom made for you :)

silkfire
  • 24,585
  • 15
  • 82
  • 105
  • +1 Thank you so much for your custom made by me. It really work. Marked as correct answer. – Jack Apr 26 '13 at 10:42
  • 3
    Why you are subtracting 25569 even though 1970 - 1900 = 25567 days? Not to say this is first code online I found that actually works precisely because of that. – Marcin Ignac May 02 '14 at 11:09
  • 1
    Tested several solutions and this is first/only one which gives me expected values – biesior Sep 17 '15 at 11:03
  • @biesior I'm curious, when do people actually parse Excel dates with JavaScript? – silkfire Sep 17 '15 at 11:29
  • 1
    @silkfire, hehe next one curious ;) Situation is simple, my colleague - remote service man, has an access to damaged log files, where excel dates wasn't converted to readable format, I just wrote fast JS so he can check the value online without installing Excel or any other programm on the target machine, just can open the site and write the value. fini. – biesior Sep 17 '15 at 11:36
  • 1
    @biesior Sounds neat! Nice job, glad the snippet I composed was helpful to you. – silkfire Sep 17 '15 at 11:53
  • found that this doent work for the month of feb, if i try to read value of date containing February, it dosnt give proper value. i had to minus 1 to make it work – shyam_ Jul 07 '16 at 22:14
  • @shyam_ It's working for me, what date in February are you trying to convert? – silkfire Jul 08 '16 at 08:17
  • 5
    can you test your solution by changing the timezone..i am in PST time zone i had to minus 25568, then it worked but in UTC+10:00 it doesnt work, please check in utc+ and utc- timezone. – shyam_ Jul 23 '16 at 21:20
  • 2
    Overly complicated piece of code. And what about time zone, professor. Did you know that `new Date()` creates a date IN YOUR CURRENT TIMEZONE while Excel serial is in UTC. – catamphetamine Sep 07 '17 at 12:29
  • 1
    This anwer is wrong for 2 reasons: 1) the final Date constructor in the return value will create the time as a local time - results will differ depending on the client. 2) the solution does not take the 1900 leap year bug into account. I believe my solutions solves both issues. https://stackoverflow.com/questions/16229494/converting-excel-date-serial-number-to-date-using-javascript/57184486#57184486 – Roland Bouman Jul 24 '19 at 13:51
  • 1
    I am in EST and this method gave me 1 day less. – Coder0997 Jul 15 '20 at 19:26
  • 1
    The value is not correct for serial =1 – Jhkcia Oct 26 '21 at 07:30
  • `new Date((serial - 25569) * 8.64e7)` will do if you don't care about dates before 1 Mar 1900. It deals with decimal days and treats *serial* as UTC so to see the date and time, use *UTC\** methods. – RobG Mar 14 '23 at 06:05
  • @MarcinIgnac—because 1 Jan 1900 is serial 1, so day 0 is 31 Dec 1899. Also, 1900 is treated as a leap year so all serials after 28 Feb are +1, so for them day 0 is 30 Dec 1899. To simplify things, use that as the epoch (hence 25569) and don't use serials before 1 Mar 1900 (i.e. less than 61). ;-) – RobG Mar 14 '23 at 06:07
62

I made a one-liner for you:

function ExcelDateToJSDate(date) {
  return new Date(Math.round((date - 25569)*86400*1000));
}
Gil
  • 3,529
  • 1
  • 19
  • 22
  • 1
    @pappadog I found that the date could be off by 1ms otherwise, and was more accurate than the 0.0000001 offset provided in silkfire's answer. – Gil Jun 02 '14 at 11:19
  • 3
    It didn't work for me with the Math.round. It worked removing it though. – Jair Reina Dec 30 '14 at 17:38
  • 2
    The time is not accurate with this one-liner function. But the date is correct. – Darwin Gautalius Feb 24 '16 at 04:25
  • 1
    It seems that the time-zone is not considered. For example, I am in China which is using UTC+8 time. With 43556.1265740741 I get `4/1/2019, 11:02:16 AM` which is 8 hours later than excel value `2019/4/1 3:02:16`. – xianshenglu May 23 '19 at 03:00
  • 1
    I have built a test verifying that 14 years of date numbers converted to the same dates (ignoring time zones) as excel would have. So this one-liner rocks! – Christiaan Westerbeek Oct 18 '19 at 18:04
  • https://learn.microsoft.com/en-us/office/dev/scripts/resources/samples/excel-samples#dates – meblum Jul 26 '22 at 16:22
  • Looks like there is a day of difference in my case, I used 44761 as number which I expect to be July 19 2022, but it returns July 18 2022 – Frankusky Aug 03 '22 at 20:17
25

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];
 }
William Denman
  • 3,046
  • 32
  • 34
  • Thanks William. Excellent, thorough investigation! – Simon East Nov 03 '21 at 10:22
  • 1
    Your answer deserves more attention. I hope you don't mind if I edit it to include the short answer at the top, but keep the deeper explanation too. – Simon East Nov 03 '21 at 11:06
  • I don't mind at all. I'm always open to constructive input. – William Denman Nov 03 '21 at 23:20
  • 1
    The "short answer" doesn't work for dates between 1 Jan and 28 Feb 1900. – RobG Mar 13 '23 at 12:13
  • Nice catch @RobG. You are correct. The "short answer" part was another user's edit, as my original answer really made you work through the steps and by doing so it would be pretty obvious that issue arises. I guess the average person would not be looking at the small range of dates, so they wouldn't be hit by the bug. But for completeness I've changed the heading. – William Denman Mar 13 '23 at 17:47
13

No need to do any math to get it down to one line.

// serialDate is whole number of days since Dec 30, 1899
// offsetUTC is -(24 - your timezone offset)
function SerialDateToJSDate(serialDate, offsetUTC) {
  return new Date(Date.UTC(0, 0, serialDate, offsetUTC));
}

I'm in PST which is UTC-0700 so I used offsetUTC = -17 to get 00:00 as the time (24 - 7 = 17).

This is also useful if you are reading dates out of Google Sheets in serial format. The documentation suggests that the serial can have a decimal to express part of a day:

Instructs date, time, datetime, and duration fields to be output as doubles in "serial number" format, as popularized by Lotus 1-2-3. The whole number portion of the value (left of the decimal) counts the days since December 30th 1899. The fractional portion (right of the decimal) counts the time as a fraction of the day. For example, January 1st 1900 at noon would be 2.5, 2 because it's 2 days after December 30st 1899, and .5 because noon is half a day. February 1st 1900 at 3pm would be 33.625. This correctly treats the year 1900 as not a leap year.

So, if you want to support a serial number with a decimal, you'd need to separate it out.

function SerialDateToJSDate(serialDate) {
  var days = Math.floor(serialDate);
  var hours = Math.floor((serialDate % 1) * 24);
  var minutes = Math.floor((((serialDate % 1) * 24) - hours) * 60)
  return new Date(Date.UTC(0, 0, serialDate, hours-17, minutes));
}
leggett
  • 304
  • 2
  • 6
  • This worked for me.. You need to offset UTC otherwise u will get wrong dates. – Coder0997 Jul 15 '20 at 19:27
  • This solution worked for UTC-5 to UTC+5:30. I have tested only those range timezone only – Murugan Jul 16 '20 at 12:46
  • 1
    This answer is very close, but not completely correct. [William's answer here](https://stackoverflow.com/a/67130235/195835) is actually more accurate. You don't actually need to adjust for the timezone if you stick to the UTC functions. – Simon East Nov 03 '21 at 11:11
12

Specs:

  1. https://support.office.com/en-gb/article/date-function-e36c0c8c-4104-49da-ab83-82328b832349

Excel stores dates as sequential serial numbers so that they can be used in calculations. January 1, 1900 is serial number 1, and January 1, 2008 is serial number 39448 because it is 39,447 days after January 1, 1900.

  1. But also: https://support.microsoft.com/en-us/help/214326/excel-incorrectly-assumes-that-the-year-1900-is-a-leap-year

When Microsoft Multiplan and Microsoft Excel were released, they also assumed that 1900 was a leap year. This assumption allowed Microsoft Multiplan and Microsoft Excel to use the same serial date system used by Lotus 1-2-3 and provide greater compatibility with Lotus 1-2-3. Treating 1900 as a leap year also made it easier for users to move worksheets from one program to the other.

  1. https://www.ecma-international.org/ecma-262/9.0/index.html#sec-time-values-and-time-range

Time is measured in ECMAScript in milliseconds since 01 January, 1970 UTC. In time values leap seconds are ignored. It is assumed that there are exactly 86,400,000 milliseconds per day.

  1. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#Unix_timestamp

new Date(value)

An integer value representing the number of milliseconds since January 1, 1970, 00:00:00 UTC (the Unix epoch), with leap seconds ignored. Keep in mind that most Unix Timestamp functions are only accurate to the nearest second.

Putting it together:

function xlSerialToJsDate(xlSerial){
  // milliseconds since 1899-12-31T00:00:00Z, corresponds to Excel serial 0.
  var xlSerialOffset = -2209075200000; 

  var elapsedDays;
  // each serial up to 60 corresponds to a valid calendar date.
  // serial 60 is 1900-02-29. This date does not exist on the calendar.
  // we choose to interpret serial 60 (as well as 61) both as 1900-03-01
  // so, if the serial is 61 or over, we have to subtract 1.
  if (xlSerial < 61) {
    elapsedDays = xlSerial;
  }
  else {
    elapsedDays = xlSerial - 1;
  }

  // javascript dates ignore leap seconds
  // each day corresponds to a fixed number of milliseconds:
  // 24 hrs * 60 mins * 60 s * 1000 ms
  var millisPerDay = 86400000;
    
  var jsTimestamp = xlSerialOffset + elapsedDays * millisPerDay;
  return new Date(jsTimestamp);
}

As one-liner:

function xlSerialToJsDate(xlSerial){
  return new Date(-2209075200000 + (xlSerial - (xlSerial < 61 ? 0 : 1)) * 86400000);
}
RobG
  • 142,382
  • 31
  • 172
  • 209
Roland Bouman
  • 31,125
  • 6
  • 66
  • 67
  • Oh dear. The plot thickens. https://support.office.com/en-us/article/date-systems-in-excel-e7fe7167-48a9-4b96-bb53-5612a800b487 – Roland Bouman Jul 25 '19 at 19:16
5

I really liked Gil's answer for it's simplicity, but it lacked the timezone offset. So, here it is:

function date2ms(d) {
  let date = new Date(Math.round((d - 25569) * 864e5));
  date.setMinutes(date.getMinutes() + date.getTimezoneOffset());
  return date;
}
Legalized
  • 161
  • 2
  • 5
4

Although I stumbled onto this discussion years after it began, I may have a simpler solution to the original question -- fwiw, here is the way I ended up doing the conversion from Excel "days since 1899-12-30" to the JS Date I needed:

var exdate = 33970; // represents Jan 1, 1993
var e0date = new Date(0); // epoch "zero" date
var offset = e0date.getTimezoneOffset(); // tz offset in min

// calculate Excel xxx days later, with local tz offset
var jsdate = new Date(0, 0, exdate-1, 0, -offset, 0);

jsdate.toJSON() => '1993-01-01T00:00:00.000Z'

Essentially, it just builds a new Date object that is calculated by adding the # of Excel days (1-based), and then adjusting the minutes by the negative local timezone offset.

SteveR
  • 1,015
  • 8
  • 12
1

So, there I was, having the same problem, then some solutions bumped up but started to have troubles with the Locale, Time Zones, etc, but in the end was able to add the precision needed

toDate(serialDate, time = false) {
    let locale = navigator.language;
    let offset = new Date(0).getTimezoneOffset();
    let date = new Date(0, 0, serialDate, 0, -offset, 0);
    if (time) {
        return serialDate.toLocaleTimeString(locale)
    }
    return serialDate.toLocaleDateString(locale)
}

The function's 'time' argument chooses between displaying the entire date or just the date's time

AdanCabanas
  • 167
  • 14
1

Thanks for @silkfire's solution!
After my verification. I found that when you're in the Eastern Hemisphere, @silkfire has the right answer; The western hemisphere is the opposite.
So, to deal with the time zone, see below:

function ExcelDateToJSDate(serial) {
   // Deal with time zone
   var step = new Date().getTimezoneOffset() <= 0 ? 25567 + 2 : 25567 + 1;
   var utc_days  = Math.floor(serial - step);
   var utc_value = utc_days * 86400;                                        
   var date_info = new Date(utc_value * 1000);

   var fractional_day = serial - Math.floor(serial) + 0.0000001;

   var total_seconds = Math.floor(86400 * fractional_day);

   var seconds = total_seconds % 60;

   total_seconds -= seconds;

   var hours = Math.floor(total_seconds / (60 * 60));
   var minutes = Math.floor(total_seconds / 60) % 60;

   return new Date(date_info.getFullYear(), date_info.getMonth(), date_info.getDate(), hours, minutes, seconds);
}
阳gege
  • 21
  • 3
0
// Parses an Excel Date ("serial") into a
// corresponding javascript Date in UTC+0 timezone.
//
// Doesn't account for leap seconds.
// Therefore is not 100% correct.
// But will do, I guess, since we're
// not doing rocket science here.
//
// https://www.pcworld.com/article/3063622/software/mastering-excel-date-time-serial-numbers-networkdays-datevalue-and-more.html
// "If you need to calculate dates in your spreadsheets,
//  Excel uses its own unique system, which it calls Serial Numbers".
//
lib.parseExcelDate = function (excelSerialDate) {
  // "Excel serial date" is just
  // the count of days since `01/01/1900`
  // (seems that it may be even fractional).
  //
  // The count of days elapsed
  // since `01/01/1900` (Excel epoch)
  // till `01/01/1970` (Unix epoch).
  // Accounts for leap years
  // (19 of them, yielding 19 extra days).
  const daysBeforeUnixEpoch = 70 * 365 + 19;

  // An hour, approximately, because a minute
  // may be longer than 60 seconds, see "leap seconds".
  const hour = 60 * 60 * 1000;

  // "In the 1900 system, the serial number 1 represents January 1, 1900, 12:00:00 a.m.
  //  while the number 0 represents the fictitious date January 0, 1900".
  // These extra 12 hours are a hack to make things
  // a little bit less weird when rendering parsed dates.
  // E.g. if a date `Jan 1st, 2017` gets parsed as
  // `Jan 1st, 2017, 00:00 UTC` then when displayed in the US
  // it would show up as `Dec 31st, 2016, 19:00 UTC-05` (Austin, Texas).
  // That would be weird for a website user.
  // Therefore this extra 12-hour padding is added
  // to compensate for the most weird cases like this
  // (doesn't solve all of them, but most of them).
  // And if you ask what about -12/+12 border then
  // the answer is people there are already accustomed
  // to the weird time behaviour when their neighbours
  // may have completely different date than they do.
  //
  // `Math.round()` rounds all time fractions
  // smaller than a millisecond (e.g. nanoseconds)
  // but it's unlikely that an Excel serial date
  // is gonna contain even seconds.
  //
  return new Date(Math.round((excelSerialDate - daysBeforeUnixEpoch) * 24 * hour) + 12 * hour);
};
catamphetamine
  • 4,489
  • 30
  • 25
0

dart implementation of @silkfire answer

DateTime getDateFromSerialDay(double serial) {
    final utc_days = (serial - 25569).floor();
    final utc_value = utc_days * 86400;
    final date_info = DateTime.fromMillisecondsSinceEpoch(utc_value * 1000);
    final fractional_day = serial - utc_days + 0.0000001;

    var total_seconds = (86400 * fractional_day).floor();

    var seconds = total_seconds % 60;

    total_seconds -= seconds;

    var hours = (total_seconds / (60 * 60) % 24).floor();
    var minutes = ((total_seconds / 60) % 60).floor();

    return DateTime(date_info.year, date_info.month, date_info.day, hours,
        minutes, seconds);
  }
nabil
  • 1
  • 1
-1

It's an old thread but hopefully I can save you the time I used readying around to write this npm package:

$ npm install js-excel-date-convert

Package Usage:

const toExcelDate = require('js-excel-date-convert').toExcelDate;
const fromExcelDate = require('js-excel-date-convert').fromExcelDate;
const jul = new Date('jul 5 1998');

toExcelDate(jul);  // 35981 (1900 date system)

fromExcelDate(35981); // "Sun, 05 Jul 1998 00:00:00 GMT"

You can verify these results with the example at https://learn.microsoft.com/en-us/office/troubleshoot/excel/1900-and-1904-date-system

The Code:

function fromExcelDate (excelDate, date1904) {
  const daysIn4Years = 1461;
  const daysIn70years = Math.round(25567.5 + 1); // +1 because of the leap-year bug
  const daysFrom1900 = excelDate + (date1904 ? daysIn4Years + 1 : 0);
  const daysFrom1970 = daysFrom1900 - daysIn70years;
  const secondsFrom1970 = daysFrom1970 * (3600 * 24);
  const utc = new Date(secondsFrom1970 * 1000);
  return !isNaN(utc) ? utc : null;
}

function toExcelDate (date, date1904) {
  if (isNaN(date)) return null;
  const daysIn4Years = 1461;
  const daysIn70years = Math.round(25567.5 + 1); // +1 because of the leap-year bug
  const daysFrom1970 = date.getTime() / 1000 / 3600 / 24;
  const daysFrom1900 = daysFrom1970 + daysIn70years;
  const daysFrom1904Jan2nd = daysFrom1900 - daysIn4Years - 1;
  return Math.round(date1904 ? daysFrom1904Jan2nd : daysFrom1900);
}

If you want to know how this works check: https://bettersolutions.com/excel/dates-times/1904-date-system.htm

Raschid JFR
  • 580
  • 8
  • 15