Golang神奇的2006-01-02 15:04:05
熱身
在講這個問題之前,先來看一道程式碼題:
package main
import (
"fmt"
"time"
)
func main() {
timeString := time.Now().Format("2006-01-02 15:04:05")
fmt.Println(timeString)
fmt.Println(time.Now().Format("2017-09-07 18:05:32"))
}
這段程式碼的輸出是什麼(假定執行時刻的時間是2017-09-07 18:05:32)?
什麼?你已經知道答案了?那你是大神,可以跳過這篇文章了。
一、神奇的日期
剛接觸Golang時,閱讀程式碼的時候總會在程式碼中發現這麼一個日期,
2006-01-02 15:04:05
剛看到這段程式碼的時候,我當時想:這個人好隨便啊,隨便寫一個日期在這裡,但是又感覺還挺方便的,格式清晰一目瞭然。也沒有更多的在意了。 之後一次做需求的時候輪到自己要格式化時間了,仿照它的樣子,寫了一個日期格式來格式化,差不多就是上面程式碼題上寫的那樣。殊不知,執行完畢後,結果令人驚呆。。。
執行結果如下:
2017-09-07 18:06:43
7097-09+08 98:43:67
頓時就犯糊塗了:怎麼就變成這個鳥樣子了?format不認識我的日期?這麼標準的日期都不認識?
二、開始探究
查閱了資料,發現原來這個日期就是寫死的一個日期,不是這個日期就不認識,就不能正確的格式化。記住就好了。
但是,還是覺得有點納悶。為什麼輸出日期是這個亂的?仔細觀察這個日期,06年,1月2日下午3點4分5秒,查閱相關資料還有 -7時區,Monday,數字1~7都有了,而且都不重複。難道有什麼深刻含義?還是單純的為了方便記憶?
晚上睡覺前一直在心裡想。突然想到:這些數字全都不重複,那豈不就是說,每個數字就能代表你需要格式化的屬性了?比如,解析格式化字串的時候,遇到了1,就說明這個地方要填的是月份,遇到了4,說明這個位置是分鐘?
不禁覺得,發明這串時間數字的人還是很聰明的。2006-01-02 15:04:05這個日期,不但挺好記的,而且用起來也比較方便。這個比其他程式語言的yyyy-MM-dd HH:mm:ss這種東西好記多了。(樓主就曾經把yyyy大小寫弄錯了,弄出一個大bug,寫成YYYY,結果,當時沒測出來,到了十二月左右的時候,年份多了一年。。。)
三、深入探究
為了一窺這個時間格式化的究竟,我們還是得閱讀go的time包原始碼。在$GOROOT/src/time/format.go檔案中,我們可以找到如下程式碼:
const (
_ = iota
stdLongMonth = iota + stdNeedDate // "January"
stdMonth // "Jan"
stdNumMonth // "1"
stdZeroMonth // "01"
stdLongWeekDay // "Monday"
stdWeekDay // "Mon"
stdDay // "2"
stdUnderDay // "_2"
stdZeroDay // "02"
stdHour = iota + stdNeedClock // "15"
stdHour12 // "3"
stdZeroHour12 // "03"
stdMinute // "4"
stdZeroMinute // "04"
stdSecond // "5"
stdZeroSecond // "05"
stdLongYear = iota + stdNeedDate // "2006"
stdYear // "06"
stdPM = iota + stdNeedClock // "PM"
stdpm // "pm"
stdTZ = iota // "MST"
stdISO8601TZ // "Z0700" // prints Z for UTC
stdISO8601SecondsTZ // "Z070000"
stdISO8601ShortTZ // "Z07"
stdISO8601ColonTZ // "Z07:00" // prints Z for UTC
stdISO8601ColonSecondsTZ // "Z07:00:00"
stdNumTZ // "-0700" // always numeric
stdNumSecondsTz // "-070000"
stdNumShortTZ // "-07" // always numeric
stdNumColonTZ // "-07:00" // always numeric
stdNumColonSecondsTZ // "-07:00:00"
stdFracSecond0 // ".0", ".00", ... , trailing zeros included
stdFracSecond9 // ".9", ".99", ..., trailing zeros omitted
上面就是所能見到的所有關於日期時間的片段。基本能夠涵蓋所有的關於日期格式化的請求。
可以總結如下:
格式 | 含義 |
---|---|
01、 1、Jan、January | 月 |
02、 2、_2 | 日,這個_2表示如果日期是隻有一個數字,則表示出來的日期前面用個空格佔位。 |
03、 3、15 | 時 |
04、4 | 分 |
05、5 | 秒 |
2006、06、6 | 年 |
-070000、 -07:00:00、 -0700、 -07:00、 -07 Z070000、Z07:00:00、 Z0700、 Z07:00 | 時區 |
PM、pm | 上下午 |
Mon、Monday | 星期 |
MST | 美國時間,如果機器設定的是中國時間則表示為UTC |
看完了這些,心裡對日期格式問題已經有數了。 所以,我們回頭看一下開頭的問題,我用
2017-09-07 18:05:32
這串數字來格式化這個日期
2017-09-07 18:05:32
得到的結果就是
7097-09+08 98:43:67
看了這個我就在想,如果是我,我會怎麼解析這個格式呢?不禁想起來了學習《編譯原理》時候的詞法分析器,這個肯定需要構造一個語法樹。至於文法什麼的,暫時我也還弄不清。既然這樣,那不如我們直接看GO原始碼一窺究竟,看看golang語言團隊的人是怎麼解析的:
func nextStdChunk(layout string) (prefix string, std int, suffix string) {
for i := 0; i < len(layout); i++ {
switch c := int(layout[i]); c {
case 'J': // January, Jan
if len(layout) >= i+3 && layout[i:i+3] == "Jan" {
if len(layout) >= i+7 && layout[i:i+7] == "January" {
return layout[0:i], stdLongMonth, layout[i+7:]
}
if !startsWithLowerCase(layout[i+3:]) {
return layout[0:i], stdMonth, layout[i+3:]
}
}
case 'M': // Monday, Mon, MST
if len(layout) >= i+3 {
if layout[i:i+3] == "Mon" {
if len(layout) >= i+6 && layout[i:i+6] == "Monday" {
return layout[0:i], stdLongWeekDay, layout[i+6:]
}
if !startsWithLowerCase(layout[i+3:]) {
return layout[0:i], stdWeekDay, layout[i+3:]
}
}
if layout[i:i+3] == "MST" {
return layout[0:i], stdTZ, layout[i+3:]
}
}
case '0': // 01, 02, 03, 04, 05, 06
if len(layout) >= i+2 && '1' <= layout[i+1] && layout[i+1] <= '6' {
return layout[0:i], std0x[layout[i+1]-'1'], layout[i+2:]
}
case '1': // 15, 1
if len(layout) >= i+2 && layout[i+1] == '5' {
return layout[0:i], stdHour, layout[i+2:]
}
return layout[0:i], stdNumMonth, layout[i+1:]
case '2': // 2006, 2
if len(layout) >= i+4 && layout[i:i+4] == "2006" {
return layout[0:i], stdLongYear, layout[i+4:]
}
return layout[0:i], stdDay, layout[i+1:]
case '_': // _2, _2006
if len(layout) >= i+2 && layout[i+1] == '2' {
//_2006 is really a literal _, followed by stdLongYear
if len(layout) >= i+5 && layout[i+1:i+5] == "2006" {
return layout[0 : i+1], stdLongYear, layout[i+5:]
}
return layout[0:i], stdUnderDay, layout[i+2:]
}
case '3':
return layout[0:i], stdHour12, layout[i+1:]
case '4':
return layout[0:i], stdMinute, layout[i+1:]
case '5':
return layout[0:i], stdSecond, layout[i+1:]
case 'P': // PM
if len(layout) >= i+2 && layout[i+1] == 'M' {
return layout[0:i], stdPM, layout[i+2:]
}
case 'p': // pm
if len(layout) >= i+2 && layout[i+1] == 'm' {
return layout[0:i], stdpm, layout[i+2:]
}
case '-': // -070000, -07:00:00, -0700, -07:00, -07
if len(layout) >= i+7 && layout[i:i+7] == "-070000" {
return layout[0:i], stdNumSecondsTz, layout[i+7:]
}
if len(layout) >= i+9 && layout[i:i+9] == "-07:00:00" {
return layout[0:i], stdNumColonSecondsTZ, layout[i+9:]
}
if len(layout) >= i+5 && layout[i:i+5] == "-0700" {
return layout[0:i], stdNumTZ, layout[i+5:]
}
if len(layout) >= i+6 && layout[i:i+6] == "-07:00" {
return layout[0:i], stdNumColonTZ, layout[i+6:]
}
if len(layout) >= i+3 && layout[i:i+3] == "-07" {
return layout[0:i], stdNumShortTZ, layout[i+3:]
}
case 'Z': // Z070000, Z07:00:00, Z0700, Z07:00,
if len(layout) >= i+7 && layout[i:i+7] == "Z070000" {
return layout[0:i], stdISO8601SecondsTZ, layout[i+7:]
}
if len(layout) >= i+9 && layout[i:i+9] == "Z07:00:00" {
return layout[0:i], stdISO8601ColonSecondsTZ, layout[i+9:]
}
if len(layout) >= i+5 && layout[i:i+5] == "Z0700" {
return layout[0:i], stdISO8601TZ, layout[i+5:]
}
if len(layout) >= i+6 && layout[i:i+6] == "Z07:00" {
return layout[0:i], stdISO8601ColonTZ, layout[i+6:]
}
if len(layout) >= i+3 && layout[i:i+3] == "Z07" {
return layout[0:i], stdISO8601ShortTZ, layout[i+3:]
}
case '.': // .000 or .999 - repeated digits for fractional seconds.
if i+1 < len(layout) && (layout[i+1] == '0' || layout[i+1] == '9') {
ch := layout[i+1]
j := i + 1
for j < len(layout) && layout[j] == ch {
j++
}
// String of digits must end here - only fractional second is all digits.
if !isDigit(layout, j) {
std := stdFracSecond0
if layout[i+1] == '9' {
std = stdFracSecond9
}
std |= (j - (i + 1)) << stdArgShift
return layout[0:i], std, layout[j:]
}
}
}
}
return layout, 0, ""
}
這段程式碼有點長,不過邏輯還是很清楚的,我們吧上面表格中的那些常用項的先進行排序,然後根據排序結果,對首個字元進行分類,相同首字元的項放在一個case裡面判斷處理。看起來這裡是簡單的進行判斷處理,其實這就是編譯裡面詞法分析的一個步驟(分詞)。
縱觀整個format.go檔案,其實這個日期處理還是挺複雜的,包括日期計算,格式解析,對日期進行格式化等。
本來想引申開來講一下編譯原理的詞法分析的。無奈發現自己現在也有點記不清楚了。一個很簡單的問題,還是花了不少時間來寫。真是紙上得來終覺淺,絕知此事要躬行啊!