時間戳與時間字串的多時區轉換

bambi發表於2019-12-30

需求

  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進行了解和踩坑。

字串轉時間戳

    /**
	* @param1 {str} 時間字串 - '2015-01-01'、'2015-01-01 12'、 '2015-01-01 12:11'、'2015-01-01 12:12:12'格式
	* @param2 {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處理合成字串。

	/**
	* @param1 {number} 時間戳 - 1564704000000格式
	* @param2 {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小時偏差的影響。若一個時間控制元件使用此類多時區轉換的功能,當使用者選擇一個夏令時和非夏令時時,若對應的是本地時區,則時區具體數值將隨著是否夏令時而變化。

歡迎大家針對我的程式碼進行交流學習,提出更好的優化建議。

相關文章