64

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?

1
  • 1
    You can use SSF.format(fmt, val, opts). And the doc is in here Commented Aug 20, 2019 at 9:06

14 Answers 14

99

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 :)

Sign up to request clarification or add additional context in comments.

15 Comments

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.
Tested several solutions and this is first/only one which gives me expected values
@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.
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.
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.
|
68

I made a one-liner for you:

function ExcelDateToJSDate(date) {
  return new Date(Math.round((date - 25569)*86400*1000));
}

7 Comments

@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.
It didn't work for me with the Math.round. It worked removing it though.
The time is not accurate with this one-liner function. But the date is correct.
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.
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!
|
31

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];
 }

4 Comments

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.
I don't mind at all. I'm always open to constructive input.
The "short answer" doesn't work for dates between 1 Jan and 28 Feb 1900.
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.
16

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);
}

2 Comments

Oh dear. The plot thickens. support.office.com/en-us/article/…
While answer stackoverflow.com/a/56449509/3187415 looks neat your answer is working perfectly across dates ranging from day 1 (1900-01-01) to nowadays.
14

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));
}

3 Comments

This worked for me.. You need to offset UTC otherwise u will get wrong dates.
This solution worked for UTC-5 to UTC+5:30. I have tested only those range timezone only
This answer is very close, but not completely correct. William's answer here is actually more accurate. You don't actually need to adjust for the timezone if you stick to the UTC functions.
6

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.

1 Comment

I like this solution as it's elegant.
6

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;
}

Comments

2

Similar answer to @william-denman but i think timezones are the problem for most people.

// For methods like `.getFullYear()` `.getDate()`
const date = new Date(0, 0, excelSerialDate - 1);

// For methods like `.getTime()` or `.toJSON()`
const date = new Date(Date.UTC(0, 0, excelSerialDate - 1));

DATES

Let's say Frank types "6/16/1982" into Excel. Frank is not implying any time of day nor any timezone. Excel stores this as 30118.

But JavaScript's Date requires a time, so we choose midnight. But wait... do we choose midnight UTC? Or midnight in the local timezone? Well... that depends on how you intend to use the date object.

// some methods use your local timezone
date.getDate()
date.getMonth()
date.getFullYear()

// some methods use UTC
date.getTime()
date.toJSON()
date.getUTCDate()

The Date object has conveniences but it forces you to think about timezones which should not be relevant for pure dates. I might even say this is the safest option because it gives you the exact spreadsheet value without any interpretation:

const parseExcelDate = (excelDate) => {
  const d = new Date(0, 0, excelDate - 1);
  return { year: d.getFullYear(), month:1 + d.getMonth() , day: d.getDate() };
}
const { year, month, day } = parseExcelDate(30118);

DATETIMES

An excel date with a decimal means it has a time of day. But again - what timezone? 8am Frank's time? 8am UTC? 8am his browsers time? 8am your servers time?

Another problem - the Date constructor doesn't support decimals. So the only easy option is to parse it as UTC and only use UTC methods:

const utc = new Date((excelDate - 25569) * 8.64e7);
utc.getTime(); // good
utc.getDate(); // bad

Or you could shift it to your local timezone (there be dragons) or use a third-party library.

Bulletproof

Ultimately, the bulletproof answer is:

const parseExcelDate = (excelDate) => {
  if (excelDate < 61) excelDate += 1; // lotus123 bugfix
  const utc = new Date((excelDate - 25569) * 8.64e7);
  return {
    year: utc.getUTCFullYear(),
    month: utc.getUTCMonth() + 1,
    day: utc.getUTCDate(),
    hours: utc.getUTCHours(),
    minutes: utc.getUTCMinutes(),
    seconds: utc.getUTCSeconds(),
    milliseconds: utc.getUTCMilliseconds()
  };
}

// unhelpful, pedantic answer
const { year, month, day, ... } = parseExcelDate(30118);

Have fun! /s

Comments

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

Comments

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);
}

1 Comment

This is the only answer that worked for my time zone (EST).
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);
};

Comments

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);
  }

Comments

0

This method resolve my problem:

  const excelDateToJSDate = (excelDateValue) => {
    new Date(Math.round(((excelDateValue - 25569) * 86400000) + (new Date().getTimezoneOffset() * 60000)));
  };

Normal date start with 01/01/1970 in Java, Javascript and other languages.

new Date(0);
Thu Jan 01 1970 00:00:00 GMT+0000

Date samples with excel date format:

1.01.1900 00:00:00 => 1
1.01.1900 00:00:01 => 1,00001157407407
1.01.1900 23:59:59 => 1,99998842592593
2.01.1900 00:00:00 => 2
1.01.1970 00:00:00 => 25569

This means, 1 second is '0,00001157407407' and 1 day is '1'.

Step 1: Subtract 70 years from the Excel date.

excelDateValue - 25569

Step 2: Convert time to milliseconds by multiplying by 86400000.

24 hours * 60 minutes * 60 seconds * 1000 milliseconds = 86400000

Step 3: The Excel date formatter converts dates without timezone information. To convert dates with timezone information, you must add the timezone offset.

new Date().getTimezoneOffset() * 60000

Comments

-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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.