【问题标题】:Converting Excel Date Serial Number to Date using Javascript使用 Javascript 将 Excel 日期序列号转换为日期
【发布时间】: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

标签: javascript excel date


【解决方案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);
        }
        

        【讨论】:

        【解决方案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-时区。
            猜你喜欢
            • 1970-01-01
            • 2012-11-30
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-10-05
            • 2015-09-29
            相关资源
            最近更新 更多