多时区 | 转换时间戳与时间字符串

7,203 阅读6分钟

需求

  1. 给出时间戳,根据指定时区,获取对应的时间字符串;
  2. 给出字符串,根据指定时区,获取对应的时间戳;
  3. 以上两种都需要展示出:时间字符串 + 时区,如:2019-12-02 02:00:01 +0800

说明

  1. 这里需要的时间格式字符串指的是:XXXX-XX-XX XX:XX:XX;
  2. 指定时区为两种:一种是直接指定一个时区字符串,如:-08:00、+00:00,另一种是指定本地时区;

难点分析

  1. 由于Date构造函数对参数格式的要求,以及产品需求对数值展示时的格式要求,需将所计算的数值再次进行格式化处理;
  2. 由于需要展示时区,以及获取本地时区与0时区的差用来做时区对应时间戳和事件字符串的换算,需要对本地时区与本地时区0时区差值进行计算。
  3. 本地时区需要动态计算,而计算本地时区需要依靠原生方法,需要对原生方法生成时区相关的各种api进行了解和踩坑。

字符串转时间戳

    /**
	* @date {str} 时间字符串 - '2015-01-01'、'2015-01-01 12'、 '2015-01-01 12:11'、'2015-01-01 12:12:12'格式
	* @timeZone {str} 时区字符串 - '+04:00'、'+00:00'格式
	* @returns {number} 按时区转换的时间戳
	*/
	getAnyTimespan(date, timeZone) {
		if(!/^\d{5,}$/.test(date)) {
			date = String(date)
			.replace(/^([0-9]{4})$/, '$1-01-01 00:00:00')
			.replace(/^([0-9]{4}-[0-9]{2})$/, '$1-01 00:00:00')
			.replace(/^([0-9]{4}-[0-9]{2}-[0-9]{2})$/, '$1 00:00:00')
			.replace(/^([0-9]{4}-[0-9]{2}-[0-9]{2}\s[0-9]{2})$/, '$1:00:00')
			date = new Date(date).getTime()
			
			return date - new Date(date).getTimezoneOffset() * 60 * 1000 - this.getOffsetMinute(timeZone) * 60 * 1000;
		}
		return date;
	},
	// 计算以分钟为单位的时区gap,例如 '+04:00'被转换为240
	getOffsetMinute(timeZone) {
		if (!/^[+-][0-9]{2}:((0[0-9])|([1-5][0-9]))$/.test(timeZone)) { // 与产品沟通,暂时使用较为严格的格式验证限制,缺少首位0和正负号视为非法格式
			return 0;
		}
		let time = timeZone.split(':');

		if (timeZone[0] === '-') {
			return parseInt(time[0]) * 60 - parseInt(time[1].slice(0,2)); //time[0]本身带正负号不需要加,time[1]不带需要加。
		} else {
			return parseInt(time[0]) * 60 + parseInt(time[1].slice(0,2));
		}
	},
  • getAnyTimespan输入字符串,返回时间戳,步骤如下:
    • 判断是否是时间戳格式,已经是时间戳格式则自动原封不动返回传入时间戳;
    • 若传入参数不是时间戳,则视为时间字符串处理,步骤如下:
      • 将字符串处理为本地时间的时间戳(js没有提供直接将字符串转为指定时区时间戳的方法,由于每个月天数不一样,也较难直接通过字符串计算时间戳);
        • 格式化为字符串格式,防止报错阻断代码执行;
        • 利用repalce进行正则判断,补0为Date构造函数标准的参数格式;
          • 关于不标准的参数格式:
            • '2019''2019-01''2019-01-02'仅有年月日的时间格式,会被按0时区转换为时间戳,而其他格式会按本地时区转为时间戳,并且其他有时分秒的格式无法直接获得0时区时间戳,所以必须转为'2019-01-02 00:00:00',才可以获得统一的值;
            • '2019-01-02 00'作为参数是非法时间格式;
            • 综上所述,将利用repalce统一补0;
      • 将本地时间的时间戳换算为目标时区的时间戳,分为三步:
        • 算出本地时间与0时区的时间差(本案例使用Date构造函数上的原生方法getTimezoneOffset进行换算);
          • 本地与0时区时间差的换算踩坑
            • new Date('2015-08-01 12:02:30').getTimezoneOffset()可得出0时区与浏览器本地时区的时间差分钟数;
            • 请注意,用此方法获取分钟数的时候一定要像我这样传入要转换的具体时间,如:'2015-08-01 12:02:30',因为根据不同地区的约定俗成,有些地区的时区会受夏令时影响。例如英国在冬季使用格林威治标准时间+00:00,但在夏令时期间,使用+01:00作为本地时区。而浏览器生成本地时间和计算本地时间的各个数值都是以这个本地时区为依据。
        • 算出0时区与目标时区的时间差(本案例使用getOffsetMinute方法换算时区字符串为具体的时间差分钟数);
        • 用换算出的本地时区时间戳减去这两个时间差,得出给定时间字符串在目标时区的时间戳;

时间戳转字符串

由于无法直接根据时间戳获取目标时区的时间字符串,每个月的天数不一样,直接计算出对应日期较为困难。我的思路是:根据本地时区和目标时区的时间差,计算出本地时区在目标时区当前时间的时间戳,进而通过new Date(默认转本地时间)获得目标时区当前时间。获取本地时间戳以获取所对应的年月日时分秒,并进行补0处理合成字符串。

	/**
	* @date {number} 时间戳 - 1564704000000格式
	* @timeZone {str} 时区字符串 - '+04:00'、'+00:00'格式
	* @returns {str} - '2015-01-01 12:12:12'格式
	*/
	getAnyTimeString(date, timeZone) {
		let sameTimelocalTimeStamp = date - 0 + new Date(date).getTimezoneOffset() * 60 * 1000 + this.getOffsetMinute(timeZone) * 60 * 1000;
		let targetTime = new Date(sameTimelocalTimeStamp)
		let targetTimeString = 
			String(targetTime.getFullYear()) +
			'-' +
			String(targetTime.getMonth() + 1).replace(/^(\d)$/,'0$1') +
			'-' +
			String(targetTime.getDate()).replace(/^(\d)$/,'0$1') +
			' ' +
			String(targetTime.getHours()).replace(/^(\d)$/,'0$1') +
			':' +
			String(targetTime.getMinutes()).replace(/^(\d)$/,'0$1') +
			':' +
			String(targetTime.getSeconds()).replace(/^(\d)$/,'0$1')
		return targetTimeString;
	},

时区的计算

由于时区需要被单独展示出来,所以这里提供了时区的计算方法。代码中case1、case2都是给出指定的时区字符串,这里不做分析。仅在获取默认值本地时区时,需要注意一些细节。

    timeZone() {
        switch (this.timeZoneType) { // 时间控件无论是否支持多时区(发送时间戳)都默认本地时区,可配置其他
            case 1:
                return '+00:00';
            case 2:
                return this.activeTimeZone;
            default:
                let timegapHour = String(0 - Math.floor(new Date(date).getTimezoneOffset()/60)) // 获取小时
                    .replace(/^([+-]?)(\d)$/, '$1' + 0 + '$2') // 单数小时补0
                    .replace(/^(\d+)$/, '+' + '$1'); // 整数小时补+号
                let timegapMinute = String(Math.abs(new Date(date).getTimezoneOffset()%60)) // 获取分钟
                    .replace(/^(\d)$/, 0 + '$1'); // 单数分钟补0
                return `${timegapHour}:${timegapMinute}`;
        }
    },

  • 通过new Date(date).getTimezoneOffset()获取时区差值时,仍需要注意传入要转换的时间。同一地区,展示的时间不同时,若时间的夏令时不同,则时区不同。
  • 对于算出的小时数值进行补0补+号处理;

总结

本次功能的实现,在格式化部分,均使用正则方法对字符串和数字格式进行处理。在时区对应值的计算上,均使用0时区作为媒介,获取时间差进行计算。需要注意的是夏令时时区与时区差的获取,必须传入具体时间作为参数,才可以避免由于夏令时产生的1小时偏差的影响。若一个时间控件使用此类多时区转换的功能,当用户选择一个夏令时和非夏令时时,若对应的是本地时区,则时区具体数值将随着是否夏令时而变化。

欢迎大家针对我的代码进行交流学习,提出更好的优化建议。