一天24小時?談DateTime處理
概述
DateTime的處理,在開發應用程式時很常見,但身為開發者一陣子,總有那麼幾次會栽在DateTime的處理問題上。就日期來說,年要潤與不潤,月是大月小月,這雖然都有一定規則可循,但其中的複雜度在開發應用程式時有時程壓力下,要能夠處理得好也不是太容易的事。
本文會以Web Application為範例,簡述幾個DateTime處理上可能遇到的問題,及處理方式。
問題
一天24小時
再簡單不過不是?但是在現在電腦系統中廣泛使用的格里曆,處理小時與日期轉換時是否可以將一天當作24小時來處理?
舉個例子來說,開發一個部落格系統,在「最新文章」的區塊要在畫面上顯示過去一天新發表的文章。大概會像這樣
- 由前端向後端發起請求
- 後端收到請求時以server的時間為準取得當下時間Now作為lte(less than or equal)
- 拿lte減去24hr作為gte (greater than or equal)
以Javascript前後端,接上MongoDB寫起來大概是這樣
fetch('http://backend.com/latest-posts')
.then(function(response) {
return response.json();
})
.then(function(data) {
$('target-pre').html(data);
});
async function (req, res) {
const endTime = new Date()
const starTime = new Date(endTime.getTime() - 24 * 60 * 60 * 1000)
const posts = await Post.find({
createdAt: {
$gte: `${startTime.getFullYear()}-${startTime.getMonth()}-${startTime.getDate()} 00:00:00`
$lte: `${endTime.getFullYear()}-${endTime.getMonth()}-${endTime.getDate()} 00:00:00`
}
})
res.json(posts)
}
寫過一陣子Web Application的工程師大多能做得出來。過程中也做了些安全上的保護:
endTime
是由Server決定的,所以不會 受到前端使用者本地時間的影響,所以這隻程式不需擔心使用者電腦校時有沒有問題- 接著拿
endTime
減去24hr取得gte,去跟資料庫拿出這個區間的資料回給前端顯示在畫面上。所有往資料庫的輸入都是同一台機器,也就是後端決定的,也沒有被XSS或SQL inject等安全疑慮
但其實這隻程式在很多國家,一年會壞個兩次
在有實行日光節約的國家(e.g. 美國),每年有個兩天並不是24小時。
例如以2021年來說,在美國東部標準時間(EST)為例子
- Mar 14, 2021 01:59:59 的下一秒鐘是 03:00:00,日光節約結束
- Nov 7, 2021 02:59:59 的下一秒鐘是 02:00:00,日光節約開始
於是在2021年的EST Timezone,Mar 14只有23小時,而Nov 7卻有25個小時。只要你的程式被執行的當下,往前算24小時會經過這個被切換的小時的話,被從資料庫抓出來的資料就會多或少一個小時的資料。
DMY與YMD
不同國家對於日期格式的使用有不同的使用習慣,1/3是指Mar 1st,還是Jan 3rd真的要處理的對也不容易。
下面這張Wiki借來的圖,這些國家使用的日期格式大概分類如下:
- 青色:DMY
- 黃色:YMD
- 綠色:混用
要能正確格式化這些日期格式,不只是單位換算上,就連顯示格式都可能因地區不同有不同的作法。
所以要怎麼辦?
問題我們明白了,但是要怎麼辦?有沒有一個準則或做法是能讓我們善加利用別人造好的輪子,但是在我們自己寫的應用程式中卻又能正確操作DateTime的處理?
讓我們來看一下下面這兩個日期時間的表示,他們是相等的嗎?
- 2021-10-13 10:00:00 (UTC+8)
- 2021-10-13 02:00:00 (UTC+0)
作為字串看起來當然不同,但其實是指相同的時間,他們則應該要是相等的。相信看到這裡應該有些頭緒,其實處理日期時間的問題並不那麼複雜,我們只要將其分開成兩個問題看待:
- 怎麼存DateTime
- 存成絕對格式,例如UTC Timezone的timestamp
- 怎麼顯示DateTime
- 盡可能推遲轉換時間,在接近使用者的呈現層才作序列化(Serialize)
若是商業邏輯在處理過程中需要對DateTime進行運算,先把它轉換成具有處理這些複雜問題能力的物件再進行;在要傳遞或顯示出來之前,將DateTime物件序列化(Seralize)成可被對方讀懂的格式
能夠處理這些問題的DateTime物件在各個語言中都存在,例如:
序列化與反序列化
而在序列化的格式中,最具權威性的標準就是ISO8601,也因為它是一系列的日期格式標準,幾乎能在所有的語言與平台中被正確的處理。在ISO8601中,帶日期時間與時區的表示方法長這樣
2021-10-13T10:00:00+08:00
2021-10-13T02:00:00+00:00
或如果你是UTC時區,你也可以直接將時區改成Z
2021-10-13T02:00:00Z
而在轉換方面,如果在Javascript內,Date()
原生的toISOString()
幾乎幫你包辦了這件事。更方便的是,在反序列化時,將字串讀入成為DateTime物件,直接將toISOString()
的輸出字串拿回來給new Date()
進行反序列化回物件,你其實會拿到一樣的DateTime物件
例如下面這個Demo,同樣的DateTime物件now
被序列化成ISOString作為顯示之用;而再度使用Date
將他們反序列化回DateTime物件,程式認為他們是相等的:
function DisplayDateTime() { const [dt, setDt] = useState(new Date()) const [isoString, setIsoString] = useState('') useEffect(() => { const timer = setInterval(() => { const now = new Date() setDt(now) setIsoString(now.toISOString()) }, 1000) return () => clearInterval(timer) }, []) return ( <> <p> ISO String: {isoString} </p> <p> DT as timestamp: {dt.getTime()} </p> <p> Unseralize still equal: { (new Date(isoString)).getTime() == dt.getTime() ? 'yes' : 'no' } </p> </> ); }
仔細觀察你會發現,這個序列化出來的ISO8601字串是以Z結尾,也就是說他其實是UTC 0的時區在表示你當下的local time,也就是說它其實是帶時區資訊的,瀏覽器處理時是會讀到作業系統所設定的時區。好好掌握這個特性,要在瀏覽器依照使用者的時區來顯示時間,其實是非常方便的。
另外要注意的是,並不是所有的序列化格式,甚至是ISO8601本身,都能夠正確地保留DateTime物件所包含了資訊。在ISO8601的例子中是因為其實ISO8601對於其格式中能保留的訊息精確度是可以選擇的,例如你可以只拿它來表示某個日期,而不是表示到分秒
所以在將DateTime物件序列化成ISO8601時,也得注意是否將訊息的精確度給捨棄了。
而其他的序列化方式,例如在Javascript的toUTCString()
,因為他序列化出來的格式沒辦法儲存到毫秒,所以要是你拿它做上面的例子,你會發現反序列化回來的結果會因為秒以下的資訊被捨棄,導致最終反序列化回來的結果不是相等 的。
function DisplayDateTime() { const [dt, setDt] = useState(new Date()) const [utcString, setUtcString] = useState('') useEffect(() => { const timer = setInterval(() => { const now = new Date() setDt(now) setUtcString(now.toUTCString()) }, 1000) return () => clearInterval(timer) }, []) return ( <> <p> UTC String: {utcString} </p> <p> DT to timestamp: {dt.getTime()} <br/> UTC String Unseralize Timestamp: {(new Date(utcString)).getTime()} </p> <p> Unseralize still equal: { (new Date(utcString)).getTime() == dt.getTime() ? 'yes' : 'no' } </p> </> ); }
兩種情境
在繼續討論前,有必要提一下SSoT (Single Source of Truth)的概念。資料在不同的process與不同的機器間傳遞,過程中因為格式轉換、傳輸延遲、避免被篡改的安全考量、甚至是錯誤的實作(Bug),都有可能導致同一份資料在不同的時間或地方而有不同的值
舉例來說,前端的14:00,在後端因為時區不同或是主機的時間校正有問題有可能是13:01
SSoT的概念指的是:到底哪一份資料是最具權威性的?在資料有差異的時候可以以它為準?
要能夠正確的處理DateTime,識別哪一份資料是SSoT是最重要的一個步驟
舉例來說,以3-tier的Web Application為例子,三個最主要產生資料落差的SSoT候選人就是那三個tier。
- Frontend
- Backend
- Database
在不同的情境下,SSoT可能會在不同的地方,以下就建立與查詢的情境進行討論