背景:最近需要处理 .xlsx 文件, 好家伙,遇到了两个和日期相关的 Bug
结论:
- excel 日期的存储方式以及 对 1900 年错误处理成闰年 bug。
- 数据转换需要注意时区 bug
存储方式
参考 zhuanlan.zhihu.com/p/69101012
Excel 将日期值存储为序列号,1900-01-01 序列号是 1。 而 2023-10-01 序列号是 45200。
再来看一段代码
import dayjs from 'dayjs';
const d = dayjs('1900-01-01');
dayjs('1900-01-01').diff(d, 'day') + 1; // 1
dayjs('2023-10-01').diff(d, 'day') + 1; // 45199
???????这不对啊,不是 45200 么,代码的结果是 45199。
实际这是一个 bug,Excel 会把 1900 当做闰年。见 learn.microsoft.com/en-us/offic…
百度百科实际上 1900 不是闰年。baike.baidu.com/item/%E9%97…
库 xlsx 数值转 Date
在这篇文章(www.yuque.com/liyatang/he…)中日期内容为 2023-10-01 ,最终读取出来是 2023-09-30T15:59:17.000Z。注意,这个是 0 时区表达,+8 区表达是 Sat Sep 30 2023 23:59:17 GMT+0800 (中国标准时间)。
这有点奇怪了,找到了源码 github.com/SheetJS/she…。
经过测试确实是 2023-09-30T15:59:17.000Z。
var basedate = /*#__PURE__*/ new Date(1899, 11, 30, 0, 0, 0); // 2209161600000
function datenum(v /*:Date*/, date1904 /*:?boolean*/) /*:number*/ {
var epoch = /*#__PURE__*/ v.getTime();
if (date1904) epoch -= 1462 * 24 * 60 * 60 * 1000;
var dnthresh =
/*#__PURE__*/ basedate.getTime() +
/*#__PURE__*/ (v.getTimezoneOffset() -
/*#__PURE__*/ basedate.getTimezoneOffset()) *
60000;
return (epoch - dnthresh) / (24 * 60 * 60 * 1000);
}
var refdate = /*#__PURE__*/ new Date();
var dnthresh =
/*#__PURE__*/ basedate.getTime() +
/*#__PURE__*/ (refdate.getTimezoneOffset() -
/*#__PURE__*/ basedate.getTimezoneOffset()) *
60000;
var refoffset = /*#__PURE__*/ refdate.getTimezoneOffset();
function numdate(v /*:number*/) /*:Date*/ {
var out = new Date();
out.setTime(v * 24 * 60 * 60 * 1000 + dnthresh);
if (out.getTimezoneOffset() !== refoffset) {
out.setTime(out.getTime() + (out.getTimezoneOffset() - refoffset) * 60000);
}
return out;
}
numdate(45200); // 2023-09-30T15:59:17.000Z
关键逻辑如下
- basedate 是
new Date(1899, 11, 30, 0, 0, 0)
。因为 Excel 存在 1900 年 bug,所以退两天。 numdate = v => new Date().setTime(v * 24 * 60 * 60 * 1000 + dnthresh)
那问题出在哪里?看上面的第一行代码就存在不一致了。 这是因为时区也存在 bug。原因这篇文章讲解 www.yuque.com/liyatang/he…
var basedate = /*#__PURE__*/ new Date(1899, 11, 30, 0, 0, 0); // 2209161600000
// 实际结果是
+basedate; // 2209190743000
那怎么解决?
看到别人提供的方案是这样,通过 fixPrecisionLoss 再修正下
var basedate = /*#__PURE__*/ new Date(1899, 11, 30, 0, 0, 0); // 2209161600000
function datenum(v /*:Date*/, date1904 /*:?boolean*/) /*:number*/ {
var epoch = /*#__PURE__*/ v.getTime();
if (date1904) epoch -= 1462 * 24 * 60 * 60 * 1000;
var dnthresh =
/*#__PURE__*/ basedate.getTime() +
/*#__PURE__*/ (v.getTimezoneOffset() -
/*#__PURE__*/ basedate.getTimezoneOffset()) *
60000;
return (epoch - dnthresh) / (24 * 60 * 60 * 1000);
}
var refdate = /*#__PURE__*/ new Date();
var dnthresh =
/*#__PURE__*/ basedate.getTime() +
/*#__PURE__*/ (refdate.getTimezoneOffset() -
/*#__PURE__*/ basedate.getTimezoneOffset()) *
60000;
var refoffset = /*#__PURE__*/ refdate.getTimezoneOffset();
function numdate(v /*:number*/) /*:Date*/ {
var out = new Date();
out.setTime(v * 24 * 60 * 60 * 1000 + dnthresh);
if (out.getTimezoneOffset() !== refoffset) {
out.setTime(out.getTime() + (out.getTimezoneOffset() - refoffset) * 60000);
}
return out;
}
numdate(45200);
const importBugHotfixDiff = (function () {
function getTimezoneOffsetMS(date) {
const time = date.getTime();
const utcTime = Date.UTC(
date.getFullYear(),
date.getMonth(),
date.getDate(),
date.getHours(),
date.getMinutes(),
date.getSeconds(),
date.getMilliseconds()
);
return time - utcTime;
}
const baseDate = new Date(1899, 11, 30, 0, 0, 0);
const nD = new Date();
const dnthreshAsIs =
(nD.getTimezoneOffset() - baseDate.getTimezoneOffset()) * 60000;
const dnthreshToBe = getTimezoneOffsetMS(nD) - getTimezoneOffsetMS(baseDate);
return dnthreshAsIs - dnthreshToBe;
})();
function fixPrecisionLoss(date) {
return new Date(date.getTime() - importBugHotfixDiff);
}
fixPrecisionLoss(numdate(45200)); // 2023-09-30T16:00:00.000Z
搞定。
库 excel 数字转 Date
思路和上面类似,无非
- 解决 Excel 1990 闰年问题
- 解决 时区 bug。
function numberToDate(excelDate: number) {
if (!isNumber(excelDate)) {
return excelDate;
}
// let utc_days = Math.floor(excelDate - (25567 + 2))
// Excel 的 bug, 会把 1900 年当作闰年
// https://docs.microsoft.com/en-us/office/troubleshoot/excel/wrongly-assumes-1900-is-leap-year
// 东半球 & 0 时区 => 25567 + 2
// 西半球 => 25567 + 1
const step = new Date().getTimezoneOffset() <= 0 ? 25567 + 2 : 25567 + 1;
const utc_days = Math.floor(excelDate - step);
// 86400 => 24 * 60 * 60 => 一天的总秒数
const utc_value = utc_days * 86400;
// 一天的总毫秒数
const date_info = new Date(utc_value * 1000);
// 误差处理
const fractional_day = excelDate - Math.floor(excelDate) + 0.0000001;
// 自 1970 年至今的总秒数
let total_seconds = Math.floor(86400 * fractional_day);
const seconds = total_seconds % 60;
total_seconds -= seconds;
const hours = Math.floor(total_seconds / (60 * 60));
const minutes = Math.floor(total_seconds / 60) % 60;
return new Date(
date_info.getFullYear(),
date_info.getMonth(),
date_info.getDate(),
hours,
minutes,
seconds
);
}