みずりゅの自由帳

主に参加したイベントやソフトウェア技術/開発について記録しています

GolangのParseとParseInLocationで「0001-01-01 00:00:00 +0000 UTC」にならない日時対応

※Goのバージョンは1.9.1で確認しています。

Go言語で特定の文字列から時刻(Time型)を生成する際には、パッケージ「time」の「Parse」や「ParseInLocation」を利用します。
違いは、ParseがUTC固定で、ParseInLocationがLocation(「”Asia/Tokyo”」等)を指定できる点です。
また、どちらも第1引数に「layout」というフォーマット用の文字列を指定しますが、他の言語のように「%Y%m%d」な表記ではなく「2006-01-02 15:04:05」のような特定の値を指定する必要があります。
残念ながら、この値を利用者が好きな値を変える(例えば、layoutの指定文字を「2018-08-31 21:22:23」にする)ことはできません。

ここでハマりどころなのは、ちょっとでも書式に合わない文字列を指定すると、「0001-01-01 00:00:00 +0000 UTC」を返してくる点です。


原因としては、以下の2点があります。

layoutの指定が悪い場合:

序盤にハマりやすいケースです。主に動作検証をしている時に遭遇します。

上述したように、第1引数の「layout」は特定の値で指定する必要があります。

最初に挙げた、「2006-01-02 15:04:05」を例にとってみます。
この場合ですと、「2006」が「年」を4桁で、「01」が「月」を、「02」が「日」を、「15」が「時」を24時表記で、「04」が「分」を、「05」が「秒」を表します。(ちなみに、「時」を12時表記にしたい場合には「03」となります。)

また、別の例として「2006-01-02T15:04:05.000 -07:00」を挙げます。
年月日時分秒の部分は「2006-01-02 15:04:05」と同じです。違いとして、タイムゾーン指定としての「T」、ミリ秒の「000」、UTCとの差分時間として「-07:00」を表しています。

なお、「何故この値なのか」についてはオンラインマニュアルに記載されています。
https://golang.org/pkg/time/#pkg-constants

また、Qiitaで日本語で説明されているページもあります。
qiita.com


指定するlayout用の文字列については、ひとまず日本でよく使われる形式である、以下の3つを覚えておけば差し支えないかと思います。

  • 2006-01-02」:年月日のみの指定。時刻は00:00:00.000
  • 2006-01-02 15:04:05」:年月日時刻秒の指定。ミリ秒は000
  • 「2006-01-02T15:04:05.000 -07:00」:年月日時刻秒とミリ秒、タイムゾーンの指定。

以下、いくつか実行例を挙げます。
例では、「Parse」で実施していますが、「ParseInLocation」でも同じです。


まずは、簡単な例でlayoutに「2006-01-02」と「2006-02-01」(月と日を入れ替えた)を指定した場合。
この場合、該当する箇所の値として処理できる場合には変換はされますが、もちろん意図した値にはなりません。

t1, _ := time.Parse("2006-01-02", "2018-11-12")
fmt.Println("t1-1:", t1)

t2, _ := time.Parse("2006-02-01", "2018-11-12")
fmt.Println("t1-2:(mm<-->dd)", t2)

t3, _  = time.Parse("2006-02-01", "2018-11-13")
fmt.Println("t1-3:(mm<-->dd)", t3)

以下、出力結果です。

t1-1: 2018-11-12 00:00:00 +0000 UTC
t1-2:(mm<-->dd) 2018-12-11 00:00:00 +0000 UTC
t1-3:(mm<-->dd) 0001-01-01 00:00:00 +0000 UTC

時分秒は指定がないため、「0」で出力されています。
なお、「t1-2」では、月と日をlayoutで逆に指定しているため、第二引数で渡した値の月と日の出力箇所が逆(「11-12」の予定が「12-11」)に出力されています。
また、「t1-3」でも同様に月と日をlayoutで逆に指定していますが、第二引数で渡した値の「日」の13が「月」の範囲を超えているため、「0001-01-01 00:00:00 +0000 UTC」で出力されています。



続いて、layoutに「2006-01-02 15:04:05」を指定する例です。
「時」と「分」の入れ替え、「03」指定の際に時に「13」と「12」を設定している場合の実行例です。

t4, _ := time.Parse("2006-01-02 15:04:05", "2018-11-12 13:14:15")
fmt.Println("t4:", t4)

t4, _ = time.Parse("2006-01-02 15:05:04", "2018-11-12 13:14:15")
fmt.Println("t4-1(mi<->ss):", t4)

t4, _ = time.Parse("2006-01-02 03:04:05", "2018-11-12 13:14:15")
fmt.Println("t4-2(mi->am/pm):", t4)

t4, _ = time.Parse("2006-01-02 03:04:05", "2018-11-12 12:14:15")
fmt.Println("t4-3(mi->am/pm 12):", t4)

以下、出力結果です。

t4: 2018-11-12 13:14:15 +0000 UTC
t4-1(mi<->ss): 2018-11-12 13:15:14 +0000 UTC
t4-2(mi->am/pm): 0001-01-01 00:00:00 +0000 UTC
t4-3(mi->am/pm 12): 2018-11-12 12:14:15 +0000 UTC

「4-1」では「分」と「秒」が入れ替わっています。
「4-2」では、12時表記の箇所に変換可能範囲を超える「13」を渡してしまったので書式にあっていないと判断されて「0001-01-01 00:00:00 +0000 UTC」になってしまっています。
「4-3」では、12時表記の箇所に変換可能範囲内の「12」を渡してしているので値がそのまま変換できています。


どうしてこういう動きになるかの詳細については、オンラインマニュアルやソースコードを直接確認した方が理解が進むと思います。
https://golang.org/src/time/format.go


変換対象の文字列が悪い場合:

こちらは、どちらかというと実行時に発生しやすいかも知れません。

この場合の対処は簡単です。
「layaout」で定義した書式と同じ書式で変換したい文字列を、第2引数「value」へ指定してください。

layout9 := "2006-01-02T15:04:05.000000 -07:00"

t9, _  := time.Parse(layout9, "2018-11-12T13:14:15.000010 +09:00")
fmt.Println("t9-1:", t9)

t9, _ = time.Parse(layout9, "2018-11-12T13:14:15.00001 +09:00")
fmt.Println("t9-2:", t9)

t9, _  = time.Parse(layout9, "2018-11-12T13:14:15.000010 09:00")
fmt.Println("t9-3:", t9)

上記の実行結果は以下になります。

t9-1: 2018-11-12 13:14:15.00001 +0900 JST
t9-2: 0001-01-01 00:00:00 +0000 UTC
t9-3: 0001-01-01 00:00:00 +0000 UTC

この例では、「t9-2」側はlayoutと比較してミリ秒の桁が1つ足り無いために、「0001-01-01 00:00:00 +0000 UTC」となっています。
また、「t9-3」側はUTCとの時差に「+記号」が無いために、やはり「0001-01-01 00:00:00 +0000 UTC」となっています。