zoukankan      html  css  js  c++  java
  • (转)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
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 相关阅读:
    JavaScript对原始数据类型的拆装箱操作
    Javascript继承(原始写法,非es6 class)
    动态作用域与词法作用域
    自行车的保养
    探索JS引擎工作原理 (转)
    C语言提高 (7) 第七天 回调函数 预处理函数DEBUG 动态链接库
    C语言提高 (6) 第六天 文件(续) 链表的操作
    C语言提高 (5) 第五天 结构体,结构体对齐 文件
    C语言提高 (4) 第四天 数组与数组作为参数时的数组指针
    C语言提高 (3) 第三天 二级指针的三种模型 栈上指针数组、栈上二维数组、堆上开辟空间
  • 原文地址:https://www.cnblogs.com/zhangmingcheng/p/15075360.html
Copyright © 2011-2022 走看看