(转)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
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。