[インデックス 11000] ファイルの概要
このコミットは、Go言語の標準ライブラリである time
パッケージに ParseDuration
関数を追加するものです。この関数は、"300ms" や "1.5h" のような人間が読みやすい形式の文字列から、Goの time.Duration
型の値を解析(パース)する機能を提供します。これにより、設定ファイルやコマンドライン引数などで時間間隔を指定する際の利便性が大幅に向上します。
コミット
commit f298d0ce29cdd6a3521cb6a062a9fc4a104392fc
Author: David Symonds <dsymonds@golang.org>
Date: Fri Dec 23 16:28:56 2011 +1100
time: add ParseDuration.
R=rsc, r, r
CC=golang-dev
https://golang.org/cl/5489111
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f298d0ce29cdd6a3521cb6a062a9fc4a104392fc
元コミット内容
time: add ParseDuration.
R=rsc, r, r
CC=golang-dev
https://golang.org/cl/5489111
変更の背景
Go言語の time
パッケージには、時間の期間を表す Duration
型が既に存在していました。しかし、この Duration
型の値を文字列から生成する標準的なメカニズムが不足していました。例えば、ユーザーがアプリケーションの設定ファイルでタイムアウト値を "30s" (30秒) のように指定したい場合、開発者はその文字列を自力で解析し、time.Duration
型に変換する必要がありました。これは、文字列解析のロジックを各アプリケーションで重複して実装することになり、エラーの温床となる可能性がありました。
ParseDuration
の追加は、このような一般的なニーズに応えるものです。これにより、開発者は複雑な文字列解析ロジックを記述することなく、標準的かつ堅牢な方法で時間間隔の文字列を Duration
型に変換できるようになります。これは、Goの「バッテリー同梱 (batteries included)」という設計哲学にも合致しており、一般的なタスクに対する標準的なソリューションを提供することで、開発者の生産性を向上させます。
前提知識の解説
time.Duration
型
Go言語の time
パッケージにおける Duration
型は、ナノ秒単位で時間の期間を表す整数型です。これは int64
のエイリアスとして定義されており、負の値も取り得ます。例えば、time.Second
は1秒を表す Duration
定数であり、5 * time.Second
は5秒の期間を表します。
Duration
型は、時間の加算・減算、比較、文字列への変換(String()
メソッド)など、様々な操作をサポートしています。しかし、このコミット以前は、文字列から Duration
型への変換は、開発者が手動で実装する必要がありました。
時間単位の表現
Goの time
パッケージでは、以下のような時間単位が定数として定義されています。
Nanosecond
Microsecond
Millisecond
Second
Minute
Hour
これらの定数は、それぞれ対応する時間単位のナノ秒数を Duration
型で表します。例えば、time.Second
は 1_000_000_000
ナノ秒(1秒)に相当します。
文字列解析の一般的な課題
文字列から数値や特定のフォーマットのデータを解析する際には、以下のような一般的な課題があります。
- フォーマットの多様性: ユーザーが入力する文字列は、様々な形式を取り得ます(例: "1h30m", "90m", "1.5h")。これらすべてに対応する必要があります。
- エラーハンドリング: 無効な入力文字列(例: "abc", "10xyz")が与えられた場合に、適切にエラーを検出し、報告する必要があります。
- 数値解析: 整数だけでなく、浮動小数点数(例: "1.5h")も正確に解析できる必要があります。
- 単位の認識: "h", "m", "s" などの単位を正しく認識し、対応する時間値に変換する必要があります。
- 符号の処理: 負の期間(例: "-5s")も正しく処理できる必要があります。
ParseDuration
はこれらの課題を包括的に解決するためのものです。
技術的詳細
ParseDuration
関数は、以下のフォーマットの期間文字列を解析します。
[-+]?([0-9]*(\.[0-9]*)?[a-z]+)+
これは正規表現のような形式で、以下の要素から構成されます。
[-+]?
: オプションの符号(-
または+
)。([0-9]*(\.[0-9]*)?[a-z]+)+
: 1つ以上の数値と単位の組み合わせ。[0-9]*
: 整数部分(0個以上の数字)。(\.[0-9]*)?
: オプションの小数部分(.
の後に0個以上の数字)。[a-z]+
: 単位(1つ以上のアルファベット)。
有効な時間単位は以下の通りです。
"ns"
: ナノ秒 (Nanosecond)"us"
: マイクロ秒 (Microsecond)"µs"
: マイクロ秒 (Microsecond) - U+00B5 (micro symbol)"μs"
: マイクロ秒 (Microsecond) - U+03BC (Greek letter mu)"ms"
: ミリ秒 (Millisecond)"s"
: 秒 (Second)"m"
: 分 (Minute)"h"
: 時間 (Hour)
ParseDuration
は、入力文字列を左から順に解析し、数値部分と単位部分を抽出します。各数値と単位のペアは、対応する Duration
値に変換され、最終的にそれらの合計が返されます。
内部的には、以下のヘルパー関数とマップが使用されています。
leadingInt(s string) (x int, rem string, err error)
: 文字列s
の先頭から連続する数字を整数として解析し、残りの文字列とエラーを返します。これは、ParseDuration
が数値部分を抽出するために使用されます。unitMap = map[string]float64
: 各単位文字列(例: "ns", "us", "s")に対応するtime.Duration
定数(ナノ秒単位の浮動小数点値)を格納するマップです。これにより、単位の変換が効率的に行われます。
解析プロセスは、以下のステップで進行します。
- 符号の処理: 文字列の先頭に
-
または+
があれば、それを処理し、残りの文字列を解析対象とします。 - "0" の特殊処理: 文字列が "0" の場合は、即座に0の
Duration
を返します。 - ループによる解析: 文字列が空になるまで、以下の処理を繰り返します。
- 数値部分の抽出:
leadingInt
を使用して、整数部分と小数部分を解析します。小数部分は、float64
に変換する際に適切にスケールされます。 - 単位の抽出: 数値部分の直後にあるアルファベットのシーケンスを単位として抽出します。
- 単位の変換:
unitMap
を使用して、抽出した単位に対応するfloat64
値を取得します。 - 合計への加算: 抽出した数値と単位の値を乗算し、全体の合計
float64
値に加算します。
- 数値部分の抽出:
- 最終的な符号の適用: 最初に見つかった符号(もしあれば)を最終的な合計に適用します。
Duration
型への変換: 最終的なfloat64
の合計をtime.Duration
型にキャストして返します。
エラーハンドリングも組み込まれており、無効なフォーマットの文字列が与えられた場合(例: 数字がない、単位がない、不明な単位など)には、適切なエラーが返されます。
コアとなるコードの変更箇所
このコミットでは、主に以下の2つのファイルが変更されています。
-
src/pkg/time/format.go
:- 既存の
atoi
関数が、より汎用的なleadingInt
関数を使用するように変更されています。これは、ParseDuration
が数値部分を解析する際にleadingInt
を利用するためです。 errLeadingInt
という新しいエラー変数が追加されています。leadingInt
関数が新しく追加されています。unitMap
という新しいグローバルマップが追加されています。ParseDuration
関数が新しく追加されています。
- 既存の
-
src/pkg/time/time_test.go
:ParseDuration
関数のテストケースを定義するparseDurationTests
という新しいスライスが追加されています。TestParseDuration
関数が追加されており、parseDurationTests
を使用してParseDuration
の基本的な機能とエラーケースをテストします。TestParseDurationRoundTrip
関数が追加されており、Duration.String()
とParseDuration
の間のラウンドトリップ(変換して元に戻す)が正しく行われるかをテストします。
コアとなるコードの解説
src/pkg/time/format.go
の変更
atoi
関数の変更
// Duplicates functionality in strconv, but avoids dependency.
func atoi(s string) (x int, err error) {
neg := false
if s != "" && s[0] == '-' {
neg = true
s = s[1:]
}
x, rem, err := leadingInt(s) // ここが変更点
if err != nil || rem != "" {
return 0, atoiError
}
if neg {
x = -x
}
return x, nil
}
既存の atoi
関数は、文字列全体が整数であると仮定していましたが、leadingInt
を呼び出すように変更されました。これにより、atoi
は leadingInt
の堅牢な数値解析ロジックを利用し、文字列の先頭から数字を解析し、残りの文字列が空であることを確認するようになりました。
leadingInt
関数の追加
var errLeadingInt = errors.New("time: bad [0-9]*") // never printed
// leadingInt consumes the leading [0-9]* from s.
func leadingInt(s string) (x int, rem string, err error) {
i := 0
for ; i < len(s); i++ {
c := s[i]
if c < '0' || c > '9' {
break
}
if x >= (1<<31-10)/10 { // 32-bit int overflow check
// overflow
return 0, "", errLeadingInt
}
x = x*10 + int(c) - '0'
}
return x, s[i:], nil
}
leadingInt
は、文字列の先頭から数字のシーケンスを解析し、整数 x
と残りの文字列 rem
を返します。これは、ParseDuration
が数値部分(整数部と小数部)を抽出するために使用する基本的なパーサーです。オーバーフローチェックも含まれています。
unitMap
の追加
var unitMap = map[string]float64{
"ns": float64(Nanosecond),
"us": float64(Microsecond),
"µs": float64(Microsecond), // U+00B5 = micro symbol
"μs": float64(Microsecond), // U+03BC = Greek letter mu
"ms": float64(Millisecond),
"s": float64(Second),
"m": float64(Minute),
"h": float64(Hour),
}
unitMap
は、各時間単位の文字列表現と、それに対応する Duration
値(float64
にキャストされたナノ秒単位の値)をマッピングします。これにより、ParseDuration
は単位文字列を効率的に数値に変換できます。特に、マイクロ秒には2種類のUnicode文字(µとμ)がサポートされている点に注目です。
ParseDuration
関数の追加
// ParseDuration parses a duration string.
// A duration string is a possibly signed sequence of
// decimal numbers, each with optional fraction and a unit suffix,
// such as "300ms", "-1.5h" or "2h45m".
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
func ParseDuration(s string) (Duration, error) {
// [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+
orig := s
f := float64(0)
neg := false
// Consume [-+]?
if s != "" {
c := s[0]
if c == '-' || c == '+' {
neg = c == '-'
s = s[1:]
}
}
// Special case: if all that is left is "0", this is zero.
if s == "0" {
return 0, nil
}
if s == "" {
return 0, errors.New("time: invalid duration " + orig)
}
for s != "" {
g := float64(0) // this element of the sequence
var x int
var err error
// The next character must be [0-9.]
if !(s[0] == '.' || ('0' <= s[0] && s[0] <= '9')) {
return 0, errors.New("time: invalid duration " + orig)
}
// Consume [0-9]* (整数部)
pl := len(s)
x, s, err = leadingInt(s)
if err != nil {
return 0, errors.New("time: invalid duration " + orig)
}
g = float64(x)
pre := pl != len(s) // whether we consumed anything before a period
// Consume (\\.[0-9]*)? (小数部)
post := false
if s != "" && s[0] == '.' {
s = s[1:]
pl := len(s)
x, s, err = leadingInt(s)
if err != nil {
return 0, errors.New("time: invalid duration " + orig)
}
scale := 1
for n := pl - len(s); n > 0; n-- {
scale *= 10
}
g += float64(x) / float64(scale)
post = pl != len(s)
}
if !pre && !post {
// no digits (e.g. ".s" or "-.s")
return 0, errors.New("time: invalid duration " + orig)
}
// Consume unit. (単位)
i := 0
for ; i < len(s); i++ {
c := s[i]
if c == '.' || ('0' <= c && c <= '9') {
break
}
}
if i == 0 {
return 0, errors.New("time: missing unit in duration " + orig)
}
u := s[:i]
s = s[i:]
unit, ok := unitMap[u]
if !ok {
return 0, errors.New("time: unknown unit " + u + " in duration " + orig)
}
f += g * unit // 合計に加算
}
if neg {
f = -f
}
return Duration(f), nil
}
ParseDuration
は、前述の技術的詳細で説明した解析ロジックを実装しています。符号の処理、"0" の特殊ケース、leadingInt
を利用した数値(整数部と小数部)の抽出、unitMap
を利用した単位の変換、そしてそれらを合計して最終的な Duration
を構築する一連の処理が含まれています。エラーハンドリングも各ステップで適切に行われています。
src/pkg/time/time_test.go
の変更
parseDurationTests
の追加
var parseDurationTests = []struct {
in string
ok bool
want Duration
}{
// simple
{"0", true, 0},
{"5s", true, 5 * Second},
// ... (多数のテストケース)
// errors
{"", false, 0},
{"3", false, 0},
// ... (多数のエラーケース)
}
このスライスは、ParseDuration
の様々な入力文字列 (in
)、期待される成功/失敗 (ok
)、そして成功した場合の期待される Duration
値 (want
) を定義しています。これにより、関数の挙動が網羅的にテストされます。
TestParseDuration
の追加
func TestParseDuration(t *testing.T) {
for _, tc := range parseDurationTests {
d, err := ParseDuration(tc.in)
if tc.ok && (err != nil || d != tc.want) {
t.Errorf("ParseDuration(%q) = %v, %v, want %v, nil", tc.in, d, err, tc.want)
} else if !tc.ok && err == nil {
t.Errorf("ParseDuration(%q) = _, nil, want _, non-nil", tc.in)
}
}
}
TestParseDuration
は、parseDurationTests
の各テストケースをループし、ParseDuration
を呼び出して結果を検証します。期待される結果と実際の結果が異なる場合、またはエラーの有無が期待と異なる場合にテストが失敗します。
TestParseDurationRoundTrip
の追加
func TestParseDurationRoundTrip(t *testing.T) {
for i := 0; i < 100; i++ {
// Resolutions finer than milliseconds will result in
// imprecise round-trips.
d0 := Duration(rand.Int31()) * Millisecond
s := d0.String()
d1, err := ParseDuration(s)
if err != nil || d0 != d1 {
t.Errorf("round-trip failed: %d => %q => %d, %v", d0, s, d1, err)
}
}
}
このテストは、ランダムな Duration
値を生成し、それを String()
メソッドで文字列に変換し、さらにその文字列を ParseDuration
で元の Duration
に戻すという「ラウンドトリップ」テストを行います。これにより、String()
と ParseDuration
の間の互換性と正確性が保証されます。コメントにあるように、ミリ秒より細かい精度では浮動小数点演算の性質上、不正確なラウンドトリップになる可能性があるため、テストではミリ秒単位でランダムな値を生成しています。
関連リンク
- Go CL 5489111: https://golang.org/cl/5489111
参考にした情報源リンク
- Go
time
パッケージのドキュメント: https://pkg.go.dev/time - Go言語のソースコード (timeパッケージ): https://github.com/golang/go/tree/master/src/time
- Go言語の
Duration
型に関する公式ドキュメント (Go 1.0以降): https://pkg.go.dev/time#Duration - Go言語の
ParseDuration
関数に関する公式ドキュメント (Go 1.0以降): https://pkg.go.dev/time#ParseDuration