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

热身

在讲这个问题之前,先来看一道代码题: 

1
2
3
4
5
6
7
8
9
10
11
12
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

刚看到这段代码的时候,我当时想:这个人好随便啊,随便写一个日期在这里,但是又感觉还挺方便的,格式清晰一目了然。也没有更多的在意了。
之后一次做需求的时候轮到自己要格式化时间了,仿照它的样子,写了一个日期格式来格式化,差不多就是上面代码题上写的那样。殊不知,运行完毕后,结果令人惊呆。。。

运行结果如下:

1
2
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文件中,我们可以找到如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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语言团队的人是怎么解析的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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 @   人艰不拆_zmc  阅读(308)  评论(0编辑  收藏  举报
编辑推荐:
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 张高兴的大模型开发实战:(一)使用 Selenium 进行网页爬虫
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
历史上的今天:
2016-07-29 ant简述
2016-07-29 Docker镜像的获取与删除
2016-07-29 ubuntu14.04安装Docker
2016-07-29 简述Docker镜像、容器、仓库概念
2014-07-29 POJ3414—Pots(bfs加回溯)
点击右上角即可分享
微信分享提示