1. 程式人生 > >Golang神奇的2006-01-02 15:04:05

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檔案,其實這個日期處理還是挺複雜的,包括日期計算,格式解析,對日期進行格式化等。

本來想引申開來講一下編譯原理的詞法分析的。無奈發現自己現在也有點記不清楚了。一個很簡單的問題,還是花了不少時間來寫。真是紙上得來終覺淺,絕知此事要躬行啊!