Golang神奇的2006-01-02 15:04:05

Golang 日期格式化

熱身

在講這個(gè)問(wèn)題之前,先來(lái)看一道代碼題:

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"))
}

這段代碼的輸出是什么(假定運(yùn)行時(shí)刻的時(shí)間是2017-09-07 18:05:32)?

什么?你已經(jīng)知道答案了?那你是大神,可以跳過(guò)這篇文章了。

一、神奇的日期

剛接觸Golang時(shí),閱讀代碼的時(shí)候總會(huì)在代碼中發(fā)現(xiàn)這么一個(gè)日期,

2006-01-02 15:04:05

剛看到這段代碼的時(shí)候,我當(dāng)時(shí)想:這個(gè)人好隨便啊,隨便寫一個(gè)日期在這里,但是又感覺(jué)還挺方便的,格式清晰一目了然。也沒(méi)有更多的在意了。
之后一次做需求的時(shí)候輪到自己要格式化時(shí)間了,仿照它的樣子,寫了一個(gè)日期格式來(lái)格式化,差不多就是上面代碼題上寫的那樣。殊不知,運(yùn)行完畢后,結(jié)果令人驚呆。。。

運(yùn)行結(jié)果如下:

2017-09-07 18:06:43
7097-09+08 98:43:67

頓時(shí)就犯糊涂了:怎么就變成這個(gè)鳥樣子了?format不認(rèn)識(shí)我的日期?這么標(biāo)準(zhǔn)的日期都不認(rèn)識(shí)?

二、開(kāi)始探究

查閱了資料,發(fā)現(xiàn)原來(lái)這個(gè)日期就是寫死的一個(gè)日期,不是這個(gè)日期就不認(rèn)識(shí),就不能正確的格式化。記住就好了。

但是,還是覺(jué)得有點(diǎn)納悶。為什么輸出日期是這個(gè)亂的?仔細(xì)觀察這個(gè)日期,06年,1月2日下午3點(diǎn)4分5秒,查閱相關(guān)資料還有 -7時(shí)區(qū),Monday,數(shù)字1~7都有了,而且都不重復(fù)。難道有什么深刻含義?還是單純的為了方便記憶?

晚上睡覺(jué)前一直在心里想。突然想到:這些數(shù)字全都不重復(fù),那豈不就是說(shuō),每個(gè)數(shù)字就能代表你需要格式化的屬性了?比如,解析格式化字符串的時(shí)候,遇到了1,就說(shuō)明這個(gè)地方要填的是月份,遇到了4,說(shuō)明這個(gè)位置是分鐘?

不禁覺(jué)得,發(fā)明這串時(shí)間數(shù)字的人還是很聰明的。2006-01-02 15:04:05這個(gè)日期,不但挺好記的,而且用起來(lái)也比較方便。這個(gè)比其他編程語(yǔ)言的yyyy-MM-dd HH:mm:ss這種東西好記多了。(樓主就曾經(jīng)把yyyy大小寫弄錯(cuò)了,弄出一個(gè)大bug,寫成YYYY,結(jié)果,當(dāng)時(shí)沒(méi)測(cè)出來(lái),到了十二月左右的時(shí)候,年份多了一年。。。)

三、深入探究

為了一窺這個(gè)時(shí)間格式化的究竟,我們還是得閱讀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

上面就是所能見(jiàn)到的所有關(guān)于日期時(shí)間的片段?;灸軌蚝w所有的關(guān)于日期格式化的請(qǐng)求。

可以總結(jié)如下:

格式 含義
01、 1、Jan、January
02、 2、_2 日,這個(gè)_2表示如果日期是只有一個(gè)數(shù)字,則表示出來(lái)的日期前面用個(gè)空格占位。
03、 3、15 時(shí)
04、4
05、5
2006、06、6
-070000、 -07:00:00、 -0700、 -07:00、 -07
Z070000、Z07:00:00、 Z0700、 Z07:00
時(shí)區(qū)
PM、pm 上下午
Mon、Monday 星期
MST 美國(guó)時(shí)間,如果機(jī)器設(shè)置的是中國(guó)時(shí)間則表示為UTC

看完了這些,心里對(duì)日期格式問(wèn)題已經(jīng)有數(shù)了。
所以,我們回頭看一下開(kāi)頭的問(wèn)題,我用

2017-09-07 18:05:32

這串?dāng)?shù)字來(lái)格式化這個(gè)日期

2017-09-07 18:05:32

得到的結(jié)果就是

7097-09+08 98:43:67

看了這個(gè)我就在想,如果是我,我會(huì)怎么解析這個(gè)格式呢?不禁想起來(lái)了學(xué)習(xí)《編譯原理》時(shí)候的詞法分析器,這個(gè)肯定需要構(gòu)造一個(gè)語(yǔ)法樹。至于文法什么的,暫時(shí)我也還弄不清。既然這樣,那不如我們直接看GO源代碼一窺究竟,看看golang語(yǔ)言團(tuán)隊(duì)的人是怎么解析的:

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, ""
}

這段代碼有點(diǎn)長(zhǎng),不過(guò)邏輯還是很清楚的,我們吧上面表格中的那些常用項(xiàng)的先進(jìn)行排序,然后根據(jù)排序結(jié)果,對(duì)首個(gè)字符進(jìn)行分類,相同首字符的項(xiàng)放在一個(gè)case里面判斷處理??雌饋?lái)這里是簡(jiǎn)單的進(jìn)行判斷處理,其實(shí)這就是編譯里面詞法分析的一個(gè)步驟(分詞)。

縱觀整個(gè)format.go文件,其實(shí)這個(gè)日期處理還是挺復(fù)雜的,包括日期計(jì)算,格式解析,對(duì)日期進(jìn)行格式化等。

本來(lái)想引申開(kāi)來(lái)講一下編譯原理的詞法分析的。無(wú)奈發(fā)現(xiàn)自己現(xiàn)在也有點(diǎn)記不清楚了。一個(gè)很簡(jiǎn)單的問(wèn)題,還是花了不少時(shí)間來(lái)寫。真是紙上得來(lái)終覺(jué)淺,絕知此事要躬行啊!

如果你喜歡這篇文章,請(qǐng)打賞支持我!如果文中有什么錯(cuò)誤還望指出!

收款碼.JPG
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,319評(píng)論 25 708
  • Go最新資料匯總鏈接 Golang資料集 《Platform-native GUI library for Go》...
    Albert陳凱閱讀 5,921評(píng)論 0 148
  • 作者:gabriel theodoropoulos,原文鏈接,原文日期:2015-10-18譯者:ray16897...
    梁杰_numbbbbb閱讀 4,011評(píng)論 0 11
  • 該資源的github地址:Qix 《Platform-native GUI library for Go》 介紹:...
    ty4z2008閱讀 5,001評(píng)論 5 121
  • 在生活這場(chǎng)默劇里,每個(gè)人都有保持沉默的機(jī)會(huì),但是,肆意飛揚(yáng)的年代,我們?nèi)钥梢杂梦覀儤O力張大的嘴型告訴世界我們需要什...
    冰依潔穎閱讀 262評(píng)論 0 0

友情鏈接更多精彩內(nèi)容