(转)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:4

顿时就犯糊涂了:怎么就变成这个鸟样子了?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文件,其实这个日期处理还是挺复杂的,包括日期计算,格式解析,对日期进行格式化等。

本来想引申开来讲一下编译原理的词法分析的。无奈发现自己现在也有点记不清楚了。一个很简单的问题,还是花了不少时间来写。真是纸上得来终觉浅,绝知此事要躬行啊!


作者:FredGan
链接:https://www.jianshu.com/p/c7f7fbb16932
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
posted @ 2021-07-29 15:34  人艰不拆_zmc  阅读(270)  评论(0编辑  收藏  举报