背景 最近工作中遇到关于时间戳与时间字符串的互相转换,期望不使用第三方库实现功能,在此做一下解决方案记录;
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' ); } function timestamp2String (ms: number , timezone: string ): string { 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
如上图,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 { const offset = dict[timezone].offsets [n]; return 0 ; } function string2Ts (time: string , timezone: string ): number { 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 ++; if (count > 10 ) { break ; } } return iosTs - (offset * MS_PER_MINUTE ); }