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

59

我有下面这段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中使用的日期序列号转换为日期字符串?)


1
你可以使用 SSF.format(fmt, val, opts)。文档在这里 - xianshenglu
12个回答

90

试一下这个:

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

为您量身定制 :)


3
为什么你要减去25569,即使1970年-1900年=25567天?并不是说这是我找到的第一段在线代码,正是因为这个原因它才能精确地工作。 - Marcin Ignac
5
你能否通过更改时区来测试你的解决方案?我在PST时区,我不得不减去25568,然后它就可以工作了,但在UTC+10:00时区中它不能工作,请在UTC+和UTC-时区中检查。 - shyam_
2
过于复杂的代码。那时间区呢,教授。 你知道 new Date() 创建的日期是在你当前的时区,而 Excel 序列号是在 UTC 时区。 - catamphetamine
1
我在东部标准时间,但这个方法让我少了一天。 - Coder0997
1
序列号为1时,数值不正确。 - Jhkcia
显示剩余10条评论

64
我为你写了一个单行代码:
function ExcelDateToJSDate(date) {
  return new Date(Math.round((date - 25569)*86400*1000));
}

1
@pappadog 我发现日期可能会偏差1毫秒,否则比silkfire的答案提供的0.0000001偏移更准确。 - Gil
3
使用Math.round对我没有起作用,但去掉它后就可以了。 - Jair Reina
2
这个一行函数的时间不准确,但日期是正确的。 - Darwin Gautalius
1
似乎没有考虑时区。例如,我在中国,使用UTC+8时间。使用43556.1265740741,我得到的是2019年4月1日上午11:02:16,比Excel值2019/4/1 3:02:16晚了8个小时。 - xianshenglu
1
我已经构建了一个测试,验证了14年的日期数字转换为与Excel相同的日期(忽略时区)。所以这个一行代码太棒了! - Christiaan Westerbeek
显示剩余2条评论

27

简短回答(针对日期 > 1900-02-28)

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

简短回答(对于小于等于1900年02月28日的日期)

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

为什么这个方案可行

我很喜欢@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中的日期基于1970年1月1日UTC。如果我们使用Date.UTC(year, month, ?day, ?hour, ?minutes, ?seconds),它将返回自那个基准时间以来的毫秒数,以UTC为基准。它具有一些有趣的功能,我们可以利用它们。 Date.UTC()参数的所有正常范围都是基于0,除了day。它可以接受超出这些范围的数字,并将输入转换为溢出或下溢其他参数。
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天。我们必须将日期参数减1。

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

需要注意的是,如果我们使用新的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];
 }

谢谢William。非常出色,调查周到! - Simon East
1
你的回答值得更多关注。我希望你不介意如果我编辑一下,在顶部包含简短的答案,但同时保留深入的解释。 - Simon East
我一点也不介意。我总是乐于接受建设性的意见。 - William Denman
1
“简短的答案”对于1900年1月1日至2月28日期间的日期无效。 - RobG
@RobG 很棒的发现。你是正确的。"简短回答"部分是另一个用户的编辑,因为我的原始答案确实让你通过步骤来找到问题,这样做很明显就会出现问题。我想普通人不会关注小范围的日期,所以他们不会遇到这个 bug。但为了完整起见,我已经更改了标题。 - William Denman

14
无需进行任何数学计算即可将其简化为一行代码。
// 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));
}

我在PST时区,该时区为UTC-0700,因此我使用offsetUTC = -17来获得00:00作为时间(24-7=17)。

如果您从Google表格中读取序列格式的日期,则此方法也很有用。 文档建议序列可以有小数部分来表示一天的一部分:

指示将日期、时间、日期时间和持续时间字段输出为“序列号”格式的双精度浮点数,这是由Lotus 1-2-3流行化的。值的整数部分(小数点左侧)计算自1899年12月30日以来的天数。小数部分(小数点右侧)计算时间为一天的一部分。例如,1900年1月1日中午将是2.5,其中2是它距离1899年12月30日已经过去的天数,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,否则日期会出错。 - Coder0997
这个解决方案适用于UTC-5到UTC+5:30。我只测试了这些时区范围。 - Murugan
1
这个答案非常接近,但并不完全正确。实际上,William在这里的回答更加准确。如果您坚持使用UTC函数,则实际上不需要调整时区。 - Simon East

12

规格:

  1. https://support.office.com/en-gb/article/date-function-e36c0c8c-4104-49da-ab83-82328b832349
Excel将日期保存为连续的串行号码,以便可以在计算中使用。1900年1月1日是序列号1,而2008年1月1日是序列号39448,因为它是自1900年1月1日以来的39447天。
但是:https://support.microsoft.com/en-us/help/214326/excel-incorrectly-assumes-that-the-year-1900-is-a-leap-year 当Microsoft Multiplan和Microsoft Excel发布时,它们也假定1900年是闰年。这种假设允许Microsoft Multiplan和Microsoft Excel使用与Lotus 1-2-3相同的序列日期系统,并提供更大的与Lotus 1-2-3的兼容性。将1900年视为闰年也使用户更轻松地从一个程序移动工作表到另一个程序。
  1. https://www.ecma-international.org/ecma-262/9.0/index.html#sec-time-values-and-time-range
时间在 ECMAScript 中以自 1970 年 1 月 1 日 UTC 起的毫秒数来衡量。在时间值中,闰秒被忽略。假设每天恰好有 86,400,000 毫秒。
  1. 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时间戳函数只精确到最近的一秒。

将它们组合起来:

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

作为一行代码:

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

哦,亲爱的。情节变得更加复杂了。未查到相关信息。 - Roland Bouman

5

我非常喜欢吉尔的答案,因为它很简单,但它缺少了时区偏移量。所以,这里是:

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

4

虽然我是在几年之后偶然发现了这个讨论,但我可能有一个更简单的解决方案来回答原始问题 - 以下是我最终采用的将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'

实际上,它只是建立一个新的日期对象,该对象通过添加Excel天数(从1开始),并将分钟调整为负本地时区偏移量而计算得出。


我喜欢这个解决方案,因为它很优雅。 - Non Plus Ultra

1

所以,我遇到了同样的问题,然后有一些解决方案出现了,但开始遇到了区域设置、时区等问题,但最终成功添加了所需的精度。

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

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

1

感谢@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);
}

这是唯一适用于我的时区(EST)的答案。 - Rick Hitchcock

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接