I was trying to do a basic date calculation as seen below. I want to find the remaining days in the month given the arguments below.
I thought I determined a solution, but there appears to be an extra 1/4 in the answer.
// returns last day of month
function lastDayOfMonth(date) {
return new Date(date.getFullYear(), date.getMonth() + 1, 0)
}
let day_start = new Date('2018-11-04');
let day_end = new Date('2019-01-10');
let day_last = lastDayOfMonth(day_start);
let total = 0;
if (day_end <= day_last) {
total = day_end - day_start;
} else {
total = day_last - day_start;
}
console.log(total / (1000 * 60 * 60 * 24)); // returns 26.25
The difference comes from the UTC offset, since as you have it written, day_start and day_end are midnight UTC, but day_last is midnight in your local timezone.
To fix, just do all calculations in UTC. Since day_last is the only one that is local, this is the only one you need to adjust. In order to produce this in UTC, you could use the setUTCFullYear and related methods (sadly I don't believe there is a way to do it with the Date constructor):
// returns last day of month
function lastDayOfMonth(date) {
// Set correct date in UTC:
let last_date = new Date();
last_date.setUTCFullYear(date.getFullYear());
last_date.setUTCMonth(date.getMonth() + 1);
last_date.setUTCDate(0);
// Zero out the time so that this will end up as midnight:
last_date.setUTCHours(0);
last_date.setUTCMinutes(0);
last_date.setUTCSeconds(0);
last_date.setUTCMilliseconds(0);
return last_date;
}
let day_start = new Date('2018-11-04');
let day_end = new Date('2019-01-10');
let day_last = lastDayOfMonth(day_start);
let total = 0;
if (day_end <= day_last) {
total = day_end - day_start;
} else {
total = day_last - day_start;
}
console.log(total / (1000 * 60 * 60 * 24)); // returns 26 flat
The Date constructor using the format, new Date(year, month, day) is evaluated using your local time zone. The Date constructor for the ISO 8601 string, YYYY-MM-DD is evaluated in UTC.
You can resolve this by printing your local date in ISO 8601 format and passing that into a newly constructed Date object.
function lastDayOfMonth(date) {
let localDate=new Date(date.getFullYear(), date.getMonth()+1, 0);
let m=localDate.getMonth();
let d=localDate.getDate();
return new Date(localDate.getFullYear()+'-'+(m<10?'0':'')+m+'-'+ (d<10?'0':'')+d);
}
I have some legacy webservices that sometimes does not localize dates. Sometimes they do, so I have to support both cases.
They shall always use Italy's locale (UTC+1 for standard time, and UTC+2 for daylight saving time), but sometimes they return dates omitting the timezone at the end of date ISO strings.
For example, the italian new year should be 2018-01-01T00:00:00+0100 and they instead return only 2018-01-01T00:00:00
This causes bad behaviour in Javascript, especially when dealing with deadlines and clients that are in other timezones.
I'd like to be able to write a piece of code that parses a date string in ISO format, assuming italian localization if there is no timezone specified.
My code is almost ok (it doesn't parse milliseconds, but I can live with that), unfortunately it miserably fails when executed by browsers in timezones that doesn't have daylight saving time. What should I do? Am I missing something?
Thanks in advance
/**
* Get the local timezone using standard time (no daylight saving time).
*/
Date.prototype.stdTimezoneOffset = function() {
var jan = new Date(this.getFullYear(), 0, 1);
var jul = new Date(this.getFullYear(), 6, 1);
return Math.max(jan.getTimezoneOffset(), jul.getTimezoneOffset());
}
/**
* Check whether current time is daylight saving time.
*/
Date.prototype.isDst = function() {
return this.getTimezoneOffset() < this.stdTimezoneOffset();
}
/**
* Check whether daylight saving time is observed in current timezone.
*/
Date.prototype.isDstObserved = function() {
var jan = new Date(this.getFullYear(), 0, 1);
var jul = new Date(this.getFullYear(), 6, 1);
return jan.getTimezoneOffset() != jul.getTimezoneOffset();
}
/**
* Cross-browser parse of a date using CET as default timezone.
*/
Date.parseFromCET = function(str) {
if (str == null) return null;
// split the input string into an array of integers
var a = str.split(/[^0-9]/)
.map(function(s) {
return parseInt(s, 10)
});
var b = new Date(
a[0], // yyyy
a[1] - 1 || 0, // MM
a[2] || 1, // dd
a[3] || 0, // hh
a[4] || 0, // mm
a[5] || 0 // ss
);
// if no timezone is present, force to CET
if (str.lastIndexOf('-') <= 7 && str.indexOf('+') == -1 && str.indexOf('Z') == -1) {
var CET_timezone_offset = b.isDst() ? '+0200' : '+0100'
var isoString = a[0] + '-' + a[1] + '-' + a[2] + 'T' +
a[3] + ':' + a[4] + ':' + a[5] + CET_timezone_offset;
return Date.parseFromCET(isoString);
}
// remove local timezone offset to go from UTC time to local time
b.setMinutes(b.getMinutes() - b.getTimezoneOffset());
// add/remove forced timezone offset to calculate exact local time
if (str.indexOf('+') > -1) {
let hours = Math.floor(a[a.length - 1] / 100);
let minutes = a[a.length - 1] % 100;
b.setMinutes(b.getMinutes() - minutes);
b.setHours(b.getHours() - hours);
}
if (str.lastIndexOf('-') > 7) {
let hours = Math.floor(a[a.length - 1] / 100);
let minutes = a[a.length - 1] % 100;
b.setMinutes(b.getMinutes() + minutes);
b.setHours(b.getHours() + hours);
}
return b;
}
The timestamp isn't consistent with the format in ECMA-262 as it's missing a colon in the offset. So parsing is implementation dependent and you may get an invalid date (e.g. in Safari).
The standard offset for Rome is +01:00. Daylight saving starts at 02:00 on the last Sunday in March (change to +02:00), and ends at 02:00 on the last Sunday in October (back to +01:00).
Note that there have been historical changes. Italy started using daylight saving in 1916, but there have been periods where it wasn't observed. It has been observed continuously since 1965, so as long as your dates are after that, you don't have to worry about past changes, only future ones.
The following is one way to go about it, it needs a lot more testing and should do validation of the input string and resulting Date object. You should probably also handle a timezone of "Z".
If you're going to do this a lot, a library that manages timezones will help greatly as it should also handle historical changes, past and future.
/** Get a Date for the last Sunday of the month
* #param {number|string} year - year for month
* #param {number|string} month - calendar month number, 1 = Jan, 2 = Feb, etc.
* #returns {Date} date for last Sunday for given year and month
*/
function getLastSunday(year, month) {
// Date for last day of month
var d = new Date(Date.UTC(year, month, 0));
// Adjust to previous Sunday
d.setUTCDate(d.getUTCDate() - d.getUTCDay());
return d;
}
/** Return a date set to the UTC start of Italian DST
* Starts at +0300 UTC on the last Sunday in March
*
* #param {number|string} year to get start of DST for
* #returns {Date} set to start date and +0300Z
*/
function getDSTStart(year) {
var d = getLastSunday(year, 3);
d.setUTCHours(3);
return d;
}
/** Return a date set to the UTC end of Italian DST
* Ends at +0400 UTC on the last Sunday in October
*
* #param {number|string} year to get start of DST for
* #returns {Date} set to start date and +0400Z
*/
function getDSTEnd(year) {
var d = getLastSunday(year, 10);
d.setUTCHours(4);
return d;
}
/** Given a year, month, day and hour, return
* true or false depending on whether DST is
* being observed in Italy.
* Use UTC to avoid local influences, assume standard time
*
* #param {number|string} year - subject year
* #param {number|string} month - subject calendar month
* #param {number|string} day - subject day
* #param {number|string} hour - subject hour
* #returns {number} offset for provided date and time
*/
function getItalianOffset(year, month, day, hour) {
var d = new Date(Date.UTC(year, month-1, day, +hour + 1));
return d >= getDSTStart(year) && d < getDSTEnd(year)? '+0200' : '+0100';
}
/** Convert offset in format +0000 to minutes
* EMCAScript offset has opposite sign
*
* #param {string} offset - in format +/-HHmm
* #reaturns {number} offset in minutes, + for west, - for east
*/
function offsetToMins(offset) {
sign = /^\+/.test(offset)? -1 : 1;
tz = 60 * offset.slice(-4, -2) + (1 * offset.slice(-2));
tz *= sign;
return tz;
}
/** Parse timestamp that may or may not have a timezone.
* If no timezone, assume Italian timezone (+0100), adjusting for
* daylight saving.
* DST starts at +0300Z on last Sunday in March
* DST ends at +0400Z on last Sunday in October
* 2018-01-01T00:00:00+0100 or 2018-01-01T00:00:00
*/
function parseItalianDate(s) {
var b = s.split(/\D/);
var hasTz = /[+-]\d{4}$/.test(s);
var d, sign, tz;
// If has offset, get from string
// Otherwise, calculate offset
if (hasTz) {
tz = s.slice(-5);
} else {
tz = getItalianOffset(b[0], b[1], b[2], b[3]);
}
// Create Date using correct offset
d = new Date(Date.UTC(b[0], b[1]-1, b[2], b[3], b[4], b[5]));
d.setUTCMinutes(d.getUTCMinutes() + offsetToMins(tz));
return d;
}
// Tests
['2018-01-01T00:00:00', // New year
'2018-03-25T01:00:00', // One hour before change over
'2018-03-25T03:00:00', // One hour after change over
'2018-03-25T01:00:00+0100',// As above but with timzone offset
'2018-03-25T03:00:00+0200',
'2018-10-27T03:00:00', // Still in DST
'2018-10-28T03:00:00', // After DST ended
'2018-10-28T03:00:00+0100'
].forEach(function(s) {
console.log(`${s} => ${formatDate(parseItalianDate(s))}`);
});
// Helper to format a date in Europe/Rome timezone
function formatDate(d) {
return d.toLocaleString('en-GB',{
year : 'numeric',
month : 'short',
day : '2-digit',
weekday: 'short',
hour : '2-digit',
minute : '2-digit',
second : '2-digit',
hour12 : 'false',
timeZone: 'Europe/Rome',
timeZoneName: 'short'
});
}
As RobG mentioned in his great answer, you'll have an easier time if you use a library for this. There are several to choose from. A good choice for modern applications is Luxon, which will support all of the scenarios you described. Here are some examples:
let dt = DateTime.fromISO('2018-01-01T00:00:00+0100', { setZone: true, zone: 'Europe/Rome'});
console.log(dt.toISO()); //=> "2018-01-01T00:00:00.000+01:00"
let dt = DateTime.fromISO('2018-01-01T00:00:00', { setZone: true, zone: 'Europe/Rome'});
console.log(dt.toISO()); //=> "2018-01-01T00:00:00.000+01:00"
let dt = DateTime.fromISO('2018-07-01T00:00:00', { setZone: true, zone: 'Europe/Rome'});
console.log(dt.toISO()); //=> "2018-07-01T00:00:00.000+02:00"
A few notes about the above:
fromISO will accept the offset missing, or with Z, or with an offset with or without a colon.
setZone: true tells it to keep the offset from the string if one is provided.
zone: 'Europe/Rome' tells it to use the time zone for Italy when no offset is provided.
You can, of course, use the Luxon DateTime object (dt here) in a variety of ways other than just emitting a string with toISO().
There are other libraries that can do such things, such as moment-timezone, and js-joda, but I prefer Luxon lately.
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?
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 :)
I made a one-liner for you:
function ExcelDateToJSDate(date) {
return new Date(Math.round((date - 25569)*86400*1000));
}
The Short Answer
new Date(Date.UTC(0, 0, excelSerialDate - 1));
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];
}
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.
2) 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.
3) 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.
4) 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-31-12T00:00:00Z, corresponds to xl 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);
}
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));
}
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;
}
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.
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
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);
}
// 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);
};
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);
}
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