使用 Javascript 将 Excel 日期序列号转换为日期

Posted

技术标签:

【中文标题】使用 Javascript 将 Excel 日期序列号转换为日期【英文标题】:Converting Excel Date Serial Number to Date using Javascript 【发布时间】:2013-04-20 05:13:47 【问题描述】:

我有以下 javascript 代码将日期(字符串)转换为 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);


那么,我该如何做相反的事情呢? (意思是 Javascript 代码将 Microsoft Excel 中使用的日期序列号转换为日期字符串?

【问题讨论】:

您可以使用SSF.format(fmt, val, opts)。文档在here 【参考方案1】:

简短的回答

new Date(Date.UTC(0, 0, excelSerialDate - 1));

为什么会这样

我真的很喜欢@leggett 和@SteveR 的答案,虽然它们大部分都有效,但我想更深入地了解Date.UTC() 的工作原理。

注意:时区偏移可能存在问题,尤其是对于较早的日期(1970 年之前)。请参阅Browsers, time zones, Chrome 67 Error (historic timezone changes),所以我想留在 UTC,尽可能不依赖任何时间变化。

Excel 日期是基于 1900 年 1 月 1 日的整数(在 PC 上。在 MAC 上它基于 1904 年 1 月 1 日)。假设我们在 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)

JS 中的日期基于Jan 1st 1970 UTC。如果我们使用Date.UTC(year, month, ?day, ?hour, ?minutes, ?seconds),它将返回自基准时间以来的毫秒数,以UTC 表示。它有一些有趣的功能,我们可以利用这些功能为我们带来好处。

day外,Date.UTC()的所有参数的正常范围都是从0开始的。它确实接受这些范围之外的数字,并将输入转换为溢出或下溢其他参数。

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

它也可以做早于 1970-01-01 的日期。在这里,我们将天从 0 递减到 1,并增加小时、分钟、秒和毫秒。

Date.UTC(1970, 0, 0, 23, 59, 59, 999) is -1ms

它甚至足够聪明,可以将 0-99 范围内的年份转换为 1900-1999

Date.UTC(70, 0, 0, 23, 59, 59, 999) is -1ms

现在,我们如何表示 1900-01-01?为了更容易根据我喜欢的日期查看输出

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"

现在我们必须处理时区。 Excel 在其日期表示中没有时区的概念,但 JS 有。恕我直言,最简单的解决方法是将所有 Excel 日期输入为 UTC(如果可以的话)。

从 Excel 日期 732.0 开始

new Date(Date.UTC(0, 0, 732, 0, 0, 0, 0)).toISOString() gives "1902-01-02T00:00:00.000Z"

由于上面提到的闰年问题,我们知道它会提前 1 天。我们必须将 day 参数减 1。

new Date(Date.UTC(0, 0, 732 - 1, 0, 0, 0, 0)) gives "1902-01-01T00:00:00.000Z"

需要注意的是,如果我们使用 new Date(year, month, day) 构造函数构造日期,则参数使用您当地的时区。我在 PT (UTC-7/UTC-8) 时区,我得到了

new Date(1902, 0, 1).toISOString() gives me "1902-01-01T08:00:00.000Z"

对于我的单元测试,我使用

new Date(Date.UTC(1902, 0, 1)).toISOString() gives "1902-01-01T00:00:00.000Z"

一个将excel序列日期转换为js日期的Typescript函数是

public static SerialDateToJSDate(excelSerialDate: number): Date 
    return new Date(Date.UTC(0, 0, excelSerialDate - 1));
  

并提取要使用的 UTC 日期

public static SerialDateToISODateString(excelSerialDate: number): string 
   return this.SerialDateToJSDate(excelSerialDate).toISOString().split('T')[0];
 

【讨论】:

谢谢威廉。出色的彻底调查! 您的回答值得更多关注。我希望您不介意我对其进行编辑以在顶部包含简短答案,但也要保留更深入的解释。 我一点也不介意。我总是乐于接受建设性意见。【参考方案2】:

感谢@silkfire 的解决方案! 经过我的验证。我发现当你在东半球时,@silkfire 有正确的答案;西半球则相反。 因此,要处理时区,请参见下文:

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

【讨论】:

【参考方案3】:

我真的很喜欢 Gil 的简单回答,但它缺少时区偏移。所以,这里是:

function date2ms(d) 
  let date = new Date(Math.round((d - 25569) * 864e5));
  date.setMinutes(date.getMinutes() + date.getTimezoneOffset());
  return date;

【讨论】:

【参考方案4】:

这是一个旧线程,但希望我可以节省你准备编写这个 npm 包的时间:

$ npm installjs-excel-date-convert

包使用:

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"

您可以使用https://docs.microsoft.com/en-us/office/troubleshoot/excel/1900-and-1904-date-system 的示例验证这些结果

代码:

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

如果您想知道这是如何工作的,请查看:https://bettersolutions.com/excel/dates-times/1904-date-system.htm

【讨论】:

【参考方案5】:

规格:

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

Excel 将日期存储为连续的序列号,以便它们可以 用于计算。 1900 年 1 月 1 日是序列号 1,而 1 月 1, 2008 是序列号 39448,因为它是一月之后的 39,447 天 1900 年 1 月。

2) 还有:https://support.microsoft.com/en-us/help/214326/excel-incorrectly-assumes-that-the-year-1900-is-a-leap-year

当 Microsoft Multiplan 和 Microsoft Excel 发布时,它们还 假设 1900 年是闰年。这一假设允许微软 Multiplan 和 Microsoft Excel 使用相同的序列日期系统 由 Lotus 1-2-3 提供,并提供与 Lotus 1-2-3 的更大兼容性。 将 1900 年视为闰年也让用户更容易移动 从一个程序到另一个程序的工作表。

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

自 1970 年 1 月 1 日以来,时间在 ECMAScript 中以毫秒为单位测量 世界标准时间。在时间值中,闰秒被忽略。假设有 正好是每天 86,400,000 毫秒。

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

new Date(value)

一个整数值,表示自以来的毫秒数 1970 年 1 月 1 日 00:00:00 UTC(Unix 纪元),闰秒 忽略。请记住,大多数 Unix Timestamp 函数仅 精确到秒。

把它放在一起:

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

作为单行:

function xlSerialToJsDate(xlSerial)
  return new Date(-2209075200000 + (xlSerial - (xlSerial < 61 ? 0 : 1)) * 86400000);

【讨论】:

天哪。情节变厚了。 support.office.com/en-us/article/…【参考方案6】:

所以,我遇到了同样的问题,然后出现了一些解决方案,但开始在语言环境、时区等方面遇到问题,但最终能够增加所需的精度

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)

函数的“时间”参数选择显示整个日期还是只显示日期的时间

【讨论】:

【参考方案7】:

无需进行任何数学运算即可将其简化为一行。

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

我在太平洋标准时间,即 UTC-0700,所以我使用 offsetUTC = -17 将 00:00 作为时间 (24 - 7 = 17)。

如果您以串行格式从 Google 表格中读取日期,这也很有用。 The documentation 建议序列号可以用小数表示一天的一部分:

指示日期、时间、日期时间和持续时间字段以“序列号”格式作为双精度输出,正如 Lotus 1-2-3 所推广的那样。值的整数部分(小数点左侧)计算自 1899 年 12 月 30 日以来的天数。小数部分(小数点右侧)将时间计算为一天的小数部分。例如, 1900 年 1 月 1 日中午将是 2.5,2 因为它是 1899 年 12 月 30 日之后的 2 天,而 0.5 因为中午是半天。 1900 年 2 月 1 日下午 3 点将是 33.625。这正确地将 1900 年视为非闰年。

因此,如果您想支持带小数的序列号,则需要将其分开。

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

【讨论】:

这对我有用.. 你需要抵消 UTC 否则你会得到错误的日期。 此解决方案适用于 UTC-5 到 UTC+5:30。我只测试了那些范围时区 这个答案非常接近,但并不完全正确。 William's answer here 实际上更准确。如果您坚持使用 UTC 功能,则实际上不需要调整时区。【参考方案8】:

虽然我在讨论开始多年后偶然发现了这个讨论,但我可能对最初的问题有一个更简单的解决方案——fwiw,这是我最终将 Excel“自 1899 年 12 月 30 日以来的天数”转换为的方式我需要的 JS 日期:

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'

本质上,它只是构建一个新的 Date 对象,该对象通过添加 Excel 天数(从 1 开始),然后通过负本地时区偏移量调整分钟来计算。

【讨论】:

我喜欢这个解决方案,因为它很优雅。【参考方案9】:
// 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);
;

【讨论】:

【参考方案10】:

我为你做了一个单线:

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

【讨论】:

@pappadog 我发现日期可能会相差 1 毫秒,并且比 Silkfire 答案中提供的 0.0000001 偏移更准确。 Math.round 对我不起作用。不过,它可以删除它。 单行函数时间不准确。但日期是正确的。 似乎没有考虑时区。例如,我在中国使用 UTC+8 时间。使用 43556.1265740741 我得到 4/1/2019, 11:02:16 AM 比 excel 值 2019/4/1 3:02:16 晚 8 小时。 我建立了一个测试来验证 14 年的日期数字是否转换为与 Excel 相同的日期(忽略时区)。所以这个单线摇滚!【参考方案11】:

试试这个:

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

为您定制:)

【讨论】:

即使 1970 - 1900 = 25567 天,为什么还要减去 25569?并不是说这是我发现的第一个在线代码,因此它实际上可以正常工作。 测试了几种解决方案,这是第一个/唯一一个给我预期值的解决方案 @silkfire,呵呵,下一个好奇;)情况很简单,我的同事 - 远程服务人员,可以访问损坏的日志文件,其中 excel 日期未转换为可读格式,我只是写了快速JS,所以他可以在线查看值,而无需在目标机器上安装Excel或任何其他程序,只需打开站点并写入值即可。最后。 @biesior 听起来不错!干得好,很高兴我写的 sn-p 对你有帮助。 你能通过改变时区来测试你的解决方案吗?我在 PST 时区我不得不负 25568,然后它工作但在 UTC+10:00 它不起作用,请检查 UTC+ 和utc-时区。

以上是关于使用 Javascript 将 Excel 日期序列号转换为日期的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript按降序排序日期后跟时间[重复]

无法使用 Excel JavaScript API 设置 NumberFormat

X轴的Google Live图形值按降序排列

使用 jasper 将日期导出到 Excel

数组列表按布尔值排序,然后按日期 JavaScript / TypeScript

excel表格自动排序如何设置