Golang
日期格式化
熱身
在講這個(gè)問(wèn)題之前撵儿,先來(lái)看一道代碼題:
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"))
}
這段代碼的輸出是什么(假定運(yùn)行時(shí)刻的時(shí)間是2017-09-07 18:05:32)?
什么壮池?你已經(jīng)知道答案了薯酝?那你是大神叛买,可以跳過(guò)這篇文章了。
一惜互、神奇的日期
剛接觸Golang時(shí)布讹,閱讀代碼的時(shí)候總會(huì)在代碼中發(fā)現(xiàn)這么一個(gè)日期,
2006-01-02 15:04:05
剛看到這段代碼的時(shí)候训堆,我當(dāng)時(shí)想:這個(gè)人好隨便啊描验,隨便寫(xiě)一個(gè)日期在這里,但是又感覺(jué)還挺方便的坑鱼,格式清晰一目了然膘流。也沒(méi)有更多的在意了。
之后一次做需求的時(shí)候輪到自己要格式化時(shí)間了姑躲,仿照它的樣子睡扬,寫(xiě)了一個(gè)日期格式來(lái)格式化,差不多就是上面代碼題上寫(xiě)的那樣黍析。殊不知卖怜,運(yùn)行完畢后,結(jié)果令人驚呆阐枣。马靠。奄抽。
運(yùn)行結(jié)果如下:
2017-09-07 18:06:43
7097-09+08 98:43:67
頓時(shí)就犯糊涂了:怎么就變成這個(gè)鳥(niǎo)樣子了?format不認(rèn)識(shí)我的日期甩鳄?這么標(biāo)準(zhǔn)的日期都不認(rèn)識(shí)逞度?
二、開(kāi)始探究
查閱了資料妙啃,發(fā)現(xiàn)原來(lái)這個(gè)日期就是寫(xiě)死的一個(gè)日期档泽,不是這個(gè)日期就不認(rèn)識(shí),就不能正確的格式化揖赴。記住就好了馆匿。
但是,還是覺(jué)得有點(diǎn)納悶燥滑。為什么輸出日期是這個(gè)亂的渐北?仔細(xì)觀察這個(gè)日期,06年铭拧,1月2日下午3點(diǎn)4分5秒赃蛛,查閱相關(guān)資料還有 -7時(shí)區(qū),Monday搀菩,數(shù)字1~7都有了呕臂,而且都不重復(fù)。難道有什么深刻含義秕磷?還是單純的為了方便記憶诵闭?
晚上睡覺(jué)前一直在心里想。突然想到:這些數(shù)字全都不重復(fù)澎嚣,那豈不就是說(shuō),每個(gè)數(shù)字就能代表你需要格式化的屬性了瘟芝?比如易桃,解析格式化字符串的時(shí)候,遇到了1锌俱,就說(shuō)明這個(gè)地方要填的是月份晤郑,遇到了4,說(shuō)明這個(gè)位置是分鐘贸宏?
不禁覺(jué)得造寝,發(fā)明這串時(shí)間數(shù)字的人還是很聰明的。2006-01-02 15:04:05這個(gè)日期吭练,不但挺好記的诫龙,而且用起來(lái)也比較方便。這個(gè)比其他編程語(yǔ)言的yyyy-MM-dd HH:mm:ss這種東西好記多了鲫咽。(樓主就曾經(jīng)把yyyy大小寫(xiě)弄錯(cuò)了签赃,弄出一個(gè)大bug谷异,寫(xiě)成YYYY,結(jié)果锦聊,當(dāng)時(shí)沒(méi)測(cè)出來(lái)歹嘹,到了十二月左右的時(shí)候,年份多了一年孔庭。尺上。。)
三圆到、深入探究
為了一窺這個(gè)時(shí)間格式化的究竟尖昏,我們還是得閱讀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
上面就是所能見(jiàn)到的所有關(guān)于日期時(shí)間的片段抽诉。基本能夠涵蓋所有的關(guān)于日期格式化的請(qǐng)求吐绵。
可以總結(jié)如下:
格式 | 含義 |
---|---|
01迹淌、 1、Jan己单、January | 月 |
02唉窃、 2、_2 | 日纹笼,這個(gè)_2表示如果日期是只有一個(gè)數(shù)字纹份,則表示出來(lái)的日期前面用個(gè)空格占位。 |
03廷痘、 3蔓涧、15 | 時(shí) |
04、4 | 分 |
05笋额、5 | 秒 |
2006元暴、06、6 | 年 |
-070000兄猩、 -07:00:00茉盏、 -0700、 -07:00枢冤、 -07 Z070000鸠姨、Z07:00:00、 Z0700淹真、 Z07:00 |
時(shí)區(qū) |
PM讶迁、pm | 上下午 |
Mon、Monday | 星期 |
MST | 美國(guó)時(shí)間趟咆,如果機(jī)器設(shè)置的是中國(guó)時(shí)間則表示為UTC |
看完了這些添瓷,心里對(duì)日期格式問(wèn)題已經(jīng)有數(shù)了梅屉。
所以,我們回頭看一下開(kāi)頭的問(wèn)題鳞贷,我用
2017-09-07 18:05:32
這串?dāng)?shù)字來(lái)格式化這個(gè)日期
2017-09-07 18:05:32
得到的結(jié)果就是
7097-09+08 98:43:67
看了這個(gè)我就在想坯汤,如果是我,我會(huì)怎么解析這個(gè)格式呢搀愧?不禁想起來(lái)了學(xué)習(xí)《編譯原理》時(shí)候的詞法分析器惰聂,這個(gè)肯定需要構(gòu)造一個(gè)語(yǔ)法樹(shù)。至于文法什么的咱筛,暫時(shí)我也還弄不清搓幌。既然這樣,那不如我們直接看GO源代碼一窺究竟迅箩,看看golang語(yǔ)言團(tuán)隊(duì)的人是怎么解析的:
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, ""
}
這段代碼有點(diǎn)長(zhǎng)溉愁,不過(guò)邏輯還是很清楚的,我們吧上面表格中的那些常用項(xiàng)的先進(jìn)行排序饲趋,然后根據(jù)排序結(jié)果拐揭,對(duì)首個(gè)字符進(jìn)行分類(lèi),相同首字符的項(xiàng)放在一個(gè)case里面判斷處理奕塑√梦郏看起來(lái)這里是簡(jiǎn)單的進(jìn)行判斷處理,其實(shí)這就是編譯里面詞法分析的一個(gè)步驟(分詞)龄砰。
縱觀整個(gè)format.go文件盟猖,其實(shí)這個(gè)日期處理還是挺復(fù)雜的,包括日期計(jì)算换棚,格式解析式镐,對(duì)日期進(jìn)行格式化等。
本來(lái)想引申開(kāi)來(lái)講一下編譯原理的詞法分析的圃泡。無(wú)奈發(fā)現(xiàn)自己現(xiàn)在也有點(diǎn)記不清楚了碟案。一個(gè)很簡(jiǎn)單的問(wèn)題,還是花了不少時(shí)間來(lái)寫(xiě)颇蜡。真是紙上得來(lái)終覺(jué)淺,絕知此事要躬行傲究鳌风秤!
如果你喜歡這篇文章,請(qǐng)打賞支持我扮叨!如果文中有什么錯(cuò)誤還望指出缤弦!