読者です 読者をやめる 読者になる 読者になる

YAMAGUCHI::weblog

土足で窓から失礼いたします。今日からあなたの息子になります。 当年とって92歳、下町の発明王、エジソンです。

Goで良い感じに日時をパースするライブラリdatemakiの話とGo 1.6

Go

はじめに

こんにちは、Go界の京成舎人ライナーです。このエントリは Go Advent Calendar 2015 の22日目の記事です。

今回のアドベントカレンダーに向けて、タイトルとは別のことをいろいろとやってみてたんですが、OS X 10.11 (El Capitan) でのみ発生する謎事象の原因をいろいろ調べてたらどうもGoと全然関係ないことのようで、まったくGo Advent Calendarに関係ない記事になりそうなのでやめました。

というわけで、今回はGoでの日時のパースについてです。

Goにおける日付のパース処理

いうまでもないおさらいなのですが、念の為に。Goでは日付の処理は timeパッケージ で行っています。他のプログラミング言語とは異なるフォーマットなので、初めて使うときは少々戸惑いますが、慣れてくると「2006年1月2日3時4分5秒」という決まった時刻のどの数字がどこに来るかを書くだけなので案外覚えやすいものです。

http://play.golang.org/p/wq8jrZKLjq

package main

import (
    "fmt"
    "time"
)

func main() {
    s := "2015/12/22 10:00:30"
    layout := "2006/01/02 15:04:05"
    t, err := time.Parse(layout, s)
    if err != nil {
        panic(err)
    }

    layout2 := "06年01月02日 15時04分05秒"
    fmt.Println(t.Format(layout2))
}

実行するとこのようになります。

15年12月22日 10時00分30秒

相対時間と絶対時間

timeパッケージのパース関数やフォーマット関数は上記のような数字だけの絶対時間を扱うのはとても楽なのですが、文字が入った日時などを扱おうとすると面倒です。たとえば "2015 December 21st" とかです。

また相対時間を扱うときも結構面倒だったりします。たとえば "yesterday" とか "3 days ago" とかそういうやつです。

いろいろなパターンについて @shibukawa がgistにまとめてくれています。

これを眺めるだけでも「うぁあああああ」という気持ちになるのですが、「うぁあああああ」とばかりも言ってられないので、とりあえずなんとか力技でもいいから扱えるようにしとくかー、と思ってライブラリを数カ月前に作ったのを忘れてたんですが、今日切羽詰まって思い出しました。

datemaki

なんとなく「dateが入った名前がいいなあ」と思って、伊達巻にしました。いまの季節にぴったりですね!

github.com

datemakiを使うと、日時をパースするのがどう楽になるのか、サンプルコードでご覧ください。

package main

import (
    "fmt"

    "github.com/ymotongpoo/datemaki"
)

func main() {
    exps := []string{
        "3 days ago",
        "2015 Dec 22nd 23:00:00",
        "yesterday 14:00",
    }

    for _, e := range exps {
        t, err := datemaki.Parse(e)
        if err != nil {
            fmt.Println(err)
            continue
        }
        fmt.Println(t)
    }
}

これを実行するとこのような結果になります。

# 実行時の日時は 2015/12/21 23:16:49
% go run main.go
2015-12-18 23:16:49.782843129 +0900 JST
2015-12-22 23:00:00 +0900 JST
2015-12-20 14:00:00 +0900 JST

Parse() という雑な関数に投げただけでちゃんと返してくれました!

datemakiの今後

最初の実装はかたっぱしから正規表現かけたり、序数のサフィックスを持ってるかを適当に判定したりして、漏れがかなりありそうです。テストケースも結構雑なので多分バグります。またいまは実行時間からの相対時間などしかとれないですが、ログを解析するなどの場合はログの取得時からの相対時間もとれないといけないので、ちゃんと状態管理用の構造体を持たせないといけないなあと実装しながら思いました。

Pull Requestなどは随時募集してますが、気が向いた時にしか反応しないと思います。ごめんなさい。(先に謝っていくスタイル)

Go 1.6でのtimeパッケージの変更

ところで、みなさんGo 1.6 beta1はすでにチェケラしてますかぁ~!?Go 1.6のリリースノートをチラ見すると超下の方にちっさくtimeパッケージについての変更予定が書いてあります。(まだリリースしてないので変更しないかもしれない可能性もある。たぶんそんなことないけど。)

  • Go 1.6 Release Notes DRAFT - The Go Programming Language

    The time package's Parse function has always rejected any day of month larger than 31, such as January 32. In Go 1.6, Parse now also rejects February 29 in non-leap years, February 30, February 31, April 31, June 31, September 31, and November 31.

書いてあるままを読むと「Go 1.6ではこれまでエラーにしてなかったうるう年でない2月29日等々もエラーにしますよ」って書いてあります。試しに次のコードをGo 1.5.2とGo 1.6 beta1で実行してみます。

package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    fmt.Println("Go version", runtime.Version())
    s1 := "2015/04/31 10:00:00"
    s2 := "2015/04/31 25:00:00"

    layout := "2006/01/02 15:04:05"

    t1, err := time.Parse(layout, s1)
    if err != nil {
        fmt.Println("[s1]", err)
    }
    fmt.Println("[s1]", s1, "->", t1)

    t2, err := time.Parse(layout, s2)
    if err != nil {
        fmt.Println("[s2]", err)
    }
    fmt.Println("[s2]", s2, "->", t2)
}
  • Go 1.5.2
% PATH=/opt/go/go1.5.2/bin:$PATH GOROOT=/opt/go/go1.5.2 go run main.go
Go version go1.5.2
[s1] 2015/04/31 10:00:00 -> 2015-05-01 10:00:00 +0000 UTC
[s2] parsing time "2015/04/31 25:00:00": hour out of range
[s2] 2015/04/31 25:00:00 -> 0001-01-01 00:00:00 +0000 UTC
  • Go 1.6 beta1
PATH=/opt/go/go1.6.b1/bin:$PATH GOROOT=/opt/go/go1.6.b1 go run main.go
Go version go1.6beta1
[s1] parsing time "2015/04/31 10:00:00": day of month out of range
[s1] 2015/04/31 10:00:00 -> 0001-01-01 00:00:00 +0000 UTC
[s2] parsing time "2015/04/31 25:00:00": hour out of range
[s2] 2015/04/31 25:00:00 -> 0001-01-01 00:00:00 +0000 UTC

そのようになってます。しかし、俺が一番ほしいのは時刻の部分の繰上げです。たとえばテレビ欄とかに出てくる「25:30」みたいなのをパースする機能なので逆方向に行っちゃってますね。

というわけでこの辺ちょっと本家にパッチ送るモチベーション湧いてきました。とりあえずdatemakiを良い感じにしつつ、ぼちぼちやってきます。みなさん、お正月に伊達巻見たらdatemaki思い出してください。

明日のラインナップは次の皆さんです!!

(訂正)2016.01.05 完全に1.6のリリースノートを逆に読み違えてました。1.6では日付のチェックがより厳しくなりました、というのが正です。