不使用第三方库,实现时间戳与时间字符串互转

背景

最近工作中遇到关于时间戳与时间字符串的互相转换,期望不使用第三方库实现功能,在此做一下解决方案记录;

Q1: 为什么不是直接使用 new Date()

A1: 对于北京时间来说,我们都知道偏移量是 UTC+8,且没有冬令时夏令时问题,时间戳与字符串互转,new Date()即可解决问题,但是对于存在冬令时夏令时的时区来说,比如 America/Los_Angeles,每年 3 月 10 号 02:00 时之后,偏移量为 UTC-7,每年 11 月 03 2 点恢复为 UTC-8,不同时区,冬令时和夏令时的节点不同。收集所有时区的不同时间段的数据,可以应对任意时区,任意时间戳对应的偏移量;

第三方库可以快速解决此问题,参考 moment.js,此处分享的是不使用第三方库的思路与做法。

1
npm install moment-timezone --save 

夏令时和冬令时是什么

高纬度和中纬度的许多国家为了充分利用夏季的太阳光照,节约照明用电,而又不变动作息时间,实行夏令时。即在夏季到来前,把时针拨快一小时,到下半季秋季来临前,再把时针拨回一小时。实行夏令时的日期一般是:4-9月(北半球)10-3月(南半球)。

思路

数据

虽然不使用 moment-timezone,但是其整理的时区数据可以作为数据源来进行解析,获取对应时区的偏移量:data

下文用到的数据为 data

设置定时任务定时更新该时区数据

分析

通过阅读 moment-timezone.js,可知:

untils[lo] < timestamp < untils[hi] 时, i = hi

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

function closest (num, arr) {
var len = arr.length;
if (num < arr[0]) {
return 0;
} else if (len > 1 && arr[len - 1] === Infinity && num >= arr[len - 2]) {
return len - 1;
} else if (num >= arr[len - 1]) {
return -1;
}

var mid;
var lo = 0;
var hi = len - 1;
while (hi - lo > 1) {
mid = Math.floor((lo + hi) / 2);
if (arr[mid] <= num) {
lo = mid;
} else {
hi = mid;
}
}
return hi;
}

此时拿到的偏移量为 offset[hi]

通过时间戳转换为字符串

经过上面对数据的分析,我们知道了偏移量时是怎么获取的,因此

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* 一秒钟包含的毫秒数。
*/
export const MS_PER_SECOND = 1000;

/**
* 一分钟包含的秒数。
*/
export const SECONDS_PER_MINUTE = 60;

/**
* 一分钟包含的毫秒数。
*/
export const MS_PER_MINUTE = MS_PER_SECOND * SECONDS_PER_MINUTE;


function pad(n: number, length: number = 2): string {

return n.toString().padStart(length, '0');
}
/**
* ms: 时间戳
*
* timezone: 时区
*/
function timestamp2String(ms: number, timezone: string): string {

// 将数据转为 dict: { [key: string]: IZone } = {}
// pass

// 参考 moment 获取下标,pass

const offset = dict[timezone].offsets[n];


const d = new Date(ms - (offset * MS_PER_MINUTE));


return `${
d.yr
}-${
pad(d.getUTCMonth() + 1)
}-${
pad(d.getUTCDate())
} ${
pad(d.getUTCHours())
}:${
pad(d.getUTCMinutes)
}:${
pad(d.getUTCSeconds)
}`;

}

通过字符串转时间戳

首先,时间戳是没有时区概念的,由 UTC 时区 1970-01-01 00:00:00 作为基准时间,即为 0,每一秒 +1,时间戳在表示时间时是相对于起始点的;而对于日期来说,每一个时区的 2024-03-28 00:00:00 的时间戳都不一样,如果想要转为时间戳,那么需要知道当地的时区相对于 UTC 时间的偏移量;

e.g: 比如北京时间,相对 UTC 时间的偏移量为 +0800,因此北京时间 2024-03-28 00:00:00 的时间戳为 1711555200,该时间戳转为零时区的时间字符串为 2024-03-27 16:00:00

如果说偏移量是固定的,比如北京时间,那么直接获取偏移量即可计算出来,复杂的是存在冬令时和夏令时的时区,还是上面 时区的例子,存在 UTC-7 和 UTC-8 的可能,这时候要准确的获取字符串对应的时间戳就相对复杂。

容易出错的时间点,以America/Los_Angeles时区为例子,字符串 2024-03-10 03:00:00 转为时间戳为 1710064800,此时偏移量为 UTC-7,字符串 2024-03-09 18:00:00 转为时间戳为 1710036000,此时偏移量为 UTC-8;America/Los_Angeles

timestamp

如上图,a 到 b 的距离就是我们要求的偏移量。

我的思路是猜,首先冬令时夏令时的时区变换一般都有规律,我们先对这一个时间,先转换为 0 时区的时间戳,此时距离正确的时间戳差了 x 偏移量。针对这个时区猜测一个偏移量出来,然后通过getOffsetByts()验证这个偏移量是否正确,否则则表示该偏移量为错误的,尝试用新的偏移量和时间戳再计算出一个偏移量进行验算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

function getOffsetByts(ts: number, timezone: string): number {

// 参考 moment 获取下标,pass
const offset = dict[timezone].offsets[n];

return 0;
}


function string2Ts(time: string, timezone: string): number {

// 此处同样省略将数据转为字典
// 省略对 time 的格式验证,请实现验证 time 是合法时间字符串

const d = new Date(time);

const iosTs = new Date(
`${
d.yr
}-${
pad(d.getUTCMonth() + 1)
}-${
pad(d.getUTCDate())
}T${
pad(d.getUTCHours())
}:${
pad(d.getUTCMinutes)
}:${
pad(d.getUTCSeconds)
}.${
pad(dt.ms ?? 0, 3)
}+0000`
).getTime();

let count: number = 0;


let offset = getOffsetByts(iosTs, name);
while(true) {

const newOffset = getOffsetByts(iosTs- (offset * MS_PER_MINUTE), name);


// 根据猜测的偏移量计算得出的时间戳验算偏移量相等,猜测正确
if (offset === newOffset) {
break;
}

// 根据猜测的偏移量计算得出的时间戳验算偏移量不等,则猜测错误,设置新的偏移量,继续猜测
offset = newOffset;


count ++;

// 已经猜测了 10 次还未成功,不猜测了,避免死循环,10 次可按具体情况进行调节
if (count > 10) {
break;
}
}

return iosTs - (offset * MS_PER_MINUTE);
}