热身

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

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. timeString := time.Now().Format("2006-01-02 15:04:05")
  8. fmt.Println(timeString)
  9. fmt.Println(time.Now().Format("2017-09-07 18:05:32"))
  10. }

这段代码的输出是什么(假定运行时刻的时间是2017-09-07 18:05:32)?
什么?你已经知道答案了?那你是大神,可以跳过这篇文章了。

一、神奇的日期

刚接触Golang时,阅读代码的时候总会在代码中发现这么一个日期,

2006-01-02 15:04:05

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

  1. 2017-09-07 18:06:43
  2. 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文件中,我们可以找到如下代码:

  1. const (
  2. _ = iota
  3. stdLongMonth = iota + stdNeedDate // "January"
  4. stdMonth // "Jan"
  5. stdNumMonth // "1"
  6. stdZeroMonth // "01"
  7. stdLongWeekDay // "Monday"
  8. stdWeekDay // "Mon"
  9. stdDay // "2"
  10. stdUnderDay // "_2"
  11. stdZeroDay // "02"
  12. stdHour = iota + stdNeedClock // "15"
  13. stdHour12 // "3"
  14. stdZeroHour12 // "03"
  15. stdMinute // "4"
  16. stdZeroMinute // "04"
  17. stdSecond // "5"
  18. stdZeroSecond // "05"
  19. stdLongYear = iota + stdNeedDate // "2006"
  20. stdYear // "06"
  21. stdPM = iota + stdNeedClock // "PM"
  22. stdpm // "pm"
  23. stdTZ = iota // "MST"
  24. stdISO8601TZ // "Z0700" // prints Z for UTC
  25. stdISO8601SecondsTZ // "Z070000"
  26. stdISO8601ShortTZ // "Z07"
  27. stdISO8601ColonTZ // "Z07:00" // prints Z for UTC
  28. stdISO8601ColonSecondsTZ // "Z07:00:00"
  29. stdNumTZ // "-0700" // always numeric
  30. stdNumSecondsTz // "-070000"
  31. stdNumShortTZ // "-07" // always numeric
  32. stdNumColonTZ // "-07:00" // always numeric
  33. stdNumColonSecondsTZ // "-07:00:00"
  34. stdFracSecond0 // ".0", ".00", ... , trailing zeros included
  35. 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. func nextStdChunk(layout string) (prefix string, std int, suffix string) {
  2. for i := 0; i < len(layout); i++ {
  3. switch c := int(layout[i]); c {
  4. case 'J': // January, Jan
  5. if len(layout) >= i+3 && layout[i:i+3] == "Jan" {
  6. if len(layout) >= i+7 && layout[i:i+7] == "January" {
  7. return layout[0:i], stdLongMonth, layout[i+7:]
  8. }
  9. if !startsWithLowerCase(layout[i+3:]) {
  10. return layout[0:i], stdMonth, layout[i+3:]
  11. }
  12. }
  13. case 'M': // Monday, Mon, MST
  14. if len(layout) >= i+3 {
  15. if layout[i:i+3] == "Mon" {
  16. if len(layout) >= i+6 && layout[i:i+6] == "Monday" {
  17. return layout[0:i], stdLongWeekDay, layout[i+6:]
  18. }
  19. if !startsWithLowerCase(layout[i+3:]) {
  20. return layout[0:i], stdWeekDay, layout[i+3:]
  21. }
  22. }
  23. if layout[i:i+3] == "MST" {
  24. return layout[0:i], stdTZ, layout[i+3:]
  25. }
  26. }
  27. case '0': // 01, 02, 03, 04, 05, 06
  28. if len(layout) >= i+2 && '1' <= layout[i+1] && layout[i+1] <= '6' {
  29. return layout[0:i], std0x[layout[i+1]-'1'], layout[i+2:]
  30. }
  31. case '1': // 15, 1
  32. if len(layout) >= i+2 && layout[i+1] == '5' {
  33. return layout[0:i], stdHour, layout[i+2:]
  34. }
  35. return layout[0:i], stdNumMonth, layout[i+1:]
  36. case '2': // 2006, 2
  37. if len(layout) >= i+4 && layout[i:i+4] == "2006" {
  38. return layout[0:i], stdLongYear, layout[i+4:]
  39. }
  40. return layout[0:i], stdDay, layout[i+1:]
  41. case '_': // _2, _2006
  42. if len(layout) >= i+2 && layout[i+1] == '2' {
  43. //_2006 is really a literal _, followed by stdLongYear
  44. if len(layout) >= i+5 && layout[i+1:i+5] == "2006" {
  45. return layout[0 : i+1], stdLongYear, layout[i+5:]
  46. }
  47. return layout[0:i], stdUnderDay, layout[i+2:]
  48. }
  49. case '3':
  50. return layout[0:i], stdHour12, layout[i+1:]
  51. case '4':
  52. return layout[0:i], stdMinute, layout[i+1:]
  53. case '5':
  54. return layout[0:i], stdSecond, layout[i+1:]
  55. case 'P': // PM
  56. if len(layout) >= i+2 && layout[i+1] == 'M' {
  57. return layout[0:i], stdPM, layout[i+2:]
  58. }
  59. case 'p': // pm
  60. if len(layout) >= i+2 && layout[i+1] == 'm' {
  61. return layout[0:i], stdpm, layout[i+2:]
  62. }
  63. case '-': // -070000, -07:00:00, -0700, -07:00, -07
  64. if len(layout) >= i+7 && layout[i:i+7] == "-070000" {
  65. return layout[0:i], stdNumSecondsTz, layout[i+7:]
  66. }
  67. if len(layout) >= i+9 && layout[i:i+9] == "-07:00:00" {
  68. return layout[0:i], stdNumColonSecondsTZ, layout[i+9:]
  69. }
  70. if len(layout) >= i+5 && layout[i:i+5] == "-0700" {
  71. return layout[0:i], stdNumTZ, layout[i+5:]
  72. }
  73. if len(layout) >= i+6 && layout[i:i+6] == "-07:00" {
  74. return layout[0:i], stdNumColonTZ, layout[i+6:]
  75. }
  76. if len(layout) >= i+3 && layout[i:i+3] == "-07" {
  77. return layout[0:i], stdNumShortTZ, layout[i+3:]
  78. }
  79. case 'Z': // Z070000, Z07:00:00, Z0700, Z07:00,
  80. if len(layout) >= i+7 && layout[i:i+7] == "Z070000" {
  81. return layout[0:i], stdISO8601SecondsTZ, layout[i+7:]
  82. }
  83. if len(layout) >= i+9 && layout[i:i+9] == "Z07:00:00" {
  84. return layout[0:i], stdISO8601ColonSecondsTZ, layout[i+9:]
  85. }
  86. if len(layout) >= i+5 && layout[i:i+5] == "Z0700" {
  87. return layout[0:i], stdISO8601TZ, layout[i+5:]
  88. }
  89. if len(layout) >= i+6 && layout[i:i+6] == "Z07:00" {
  90. return layout[0:i], stdISO8601ColonTZ, layout[i+6:]
  91. }
  92. if len(layout) >= i+3 && layout[i:i+3] == "Z07" {
  93. return layout[0:i], stdISO8601ShortTZ, layout[i+3:]
  94. }
  95. case '.': // .000 or .999 - repeated digits for fractional seconds.
  96. if i+1 < len(layout) && (layout[i+1] == '0' || layout[i+1] == '9') {
  97. ch := layout[i+1]
  98. j := i + 1
  99. for j < len(layout) && layout[j] == ch {
  100. j++
  101. }
  102. // String of digits must end here - only fractional second is all digits.
  103. if !isDigit(layout, j) {
  104. std := stdFracSecond0
  105. if layout[i+1] == '9' {
  106. std = stdFracSecond9
  107. }
  108. std |= (j - (i + 1)) << stdArgShift
  109. return layout[0:i], std, layout[j:]
  110. }
  111. }
  112. }
  113. }
  114. return layout, 0, ""
  115. }

这段代码有点长,不过逻辑还是很清楚的,我们吧上面表格中的那些常用项的先进行排序,然后根据排序结果,对首个字符进行分类,相同首字符的项放在一个case里面判断处理。看起来这里是简单的进行判断处理,其实这就是编译里面词法分析的一个步骤(分词)。
纵观整个format.go文件,其实这个日期处理还是挺复杂的,包括日期计算,格式解析,对日期进行格式化等。

作者:FredGan
链接:https://www.jianshu.com/p/c7f7fbb16932
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。