Go言語で日時と文字列を相互変換するライブラリtimefmtを作りました

Go言語でstrftimestrptime相当の関数を提供するライブラリを実装しました。

t, _ := timefmt.Parse("2020/07/24 09:07:29", "%Y/%m/%d %H:%M:%S")
fmt.Println(t) // 2020-07-24 09:07:29 +0000 UTC

str := timefmt.Format(t, "%Y/%m/%d %H:%M:%S")
fmt.Println(str) // 2020/07/24 09:07:29

str = timefmt.Format(t, "%a, %d %b %Y %T %z")
fmt.Println(str) // Fri, 24 Jul 2020 09:07:29 +0000

なぜ作ったか

Go言語の標準ライブラリには日時と文字列を変換する関数がありますが、2006年1月2日の15:04:05でフォーマットを指定するという独自の仕様をとっています。 しかしPythonRubyのようにstrftime(3)strptime(3)のフォーマットをGo言語でも使いたいという人は多く、様々なライブラリが作られてきました。

これらの先人には敬意を表しますが、どのライブラリにも満足できないところがありました。

  • %F %T%r といった複数の情報を含むものが実装されていない
  • %-y-%-m-%-d%_y-%_m-%_dのようにpaddingを消したりスペースにしたりすることができない
  • %10A, %10B %2k:%Mのような幅の指定、%^a %^bのような大文字への変換ができない
  • cgoを使っており、クロスビルドできない
  • 文字列への変換と文字列からの変換は、同じライブラリーで提供したい
    • strconv.Atoistrconv.Itoaがあるように

どのライブラリを改善するにも微妙なところがあり、自分で作ろうと思い至ったわけです。

以上が表向きの理由ですが、本当の理由はgojqに必要だったからです。 jqにはstrftimeとstrptimeがありますので、これをgojqで実現するためにはGo言語でも同じ関数が必須です。 つまり単に日時と文字列を変換したいわけではなくて (それなら標準ライブラリを使えばよい)、strftimestrptimeそのものが必要だったのです。 これまでlestrrat-go/strftimepbnjay/strptimeを使っていました。 lestrrat-go/strftimeにはほとんど不満はありませんでしたが、pbnjay/strptimeは機能的に不十分なこと (%cでパースできないなど) や、古いライブラリでライセンスファイルが置かれていないことなど不安要素が多く、新しいライブラリを作るのには十分な理由となりました。

timefmtは、Parse(source, format string)Format(t time.Time, format string)を提供しています。 strftime(3)strptime(3)のほぼ全てのフォーマット指定子、paddingの調整や幅の指定に対応しています。

パフォーマンス

timefmtライブラリを作り始めたときは、パフォーマンスはそこまで重視していませんでした。 strftimeのライブラリのベンチマーク結果は以下のようになっています。

多くのフォーマットで現状最速のライブラリだと思います。 以下の方針で実装していくと、それなりの速度がでました。

  • フォーマット指定文字列をone-passで辿る
  • 月日や時刻など二桁以下の数字の文字列化を高速化する
  • bytes.Bufferではなく[]byteを使う
  • 文字列の結合を避け、strconv.AppendIntを使う
  • モリーの確保を極力減らす

他のライブラリでは標準ライブラリのフォーマットに変換していたり、メモリー確保に気を使っていなかったりしていました。 標準ライブラリは二文字か三文字読まないとどの指定子か分からないのが遅くなる原因だと思います。 この点はstrftimeのフォーマットは優れていますね。

timefmtはこれ以上チューニングする予定はありません。 今のtimefmt.Formatが遅いという場面では、fmt.Sprintfのような関数も使ってはいけないほどパフォーマンスに厳しい場面でしょう。 loggerのように時刻を固定フォーマットで高速に出力する場面では、フォーマット指定文字列を使わずに直接手で書いてしまえば良いでしょう。 このように気楽に構えておくことによってパフォーマンスチューニングの沼に踏み込みすぎないというのは、ライブラリをシンプルに保つ一つのコツだと思います。

まとめ

Go言語で時刻と文字列を相互に変換するライブラリtimefmtを作りました。 作っておいてなんですが、ほとんどの場面では標準ライブラリを使っておくと良いでしょう。 strftime自体が欲しい場面や、標準ライブラリより少し速いライブラリが欲しいときに役に立つと思います。 gojqへの組み込みは成功し、jqとの互換性は上がったので満足しています。

strftimeには多くの指定子があり、なかなか覚えられない方もいらっしゃるかと思います。 最低限 %Y-%m-%d %H:%M:%S をそらで書けるようになりましょう。 追加で %a %b %e %I %p %Z あたりを覚えておくと便利です。 今回timefmtを作ったことで全て覚えてしまいました。 やはり再実装は深い理解への近道ですね。