Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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.Second1_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 定数(ナノ秒単位の浮動小数点値)を格納するマップです。これにより、単位の変換が効率的に行われます。

解析プロセスは、以下のステップで進行します。

  1. 符号の処理: 文字列の先頭に - または + があれば、それを処理し、残りの文字列を解析対象とします。
  2. "0" の特殊処理: 文字列が "0" の場合は、即座に0の Duration を返します。
  3. ループによる解析: 文字列が空になるまで、以下の処理を繰り返します。
    • 数値部分の抽出: leadingInt を使用して、整数部分と小数部分を解析します。小数部分は、float64 に変換する際に適切にスケールされます。
    • 単位の抽出: 数値部分の直後にあるアルファベットのシーケンスを単位として抽出します。
    • 単位の変換: unitMap を使用して、抽出した単位に対応する float64 値を取得します。
    • 合計への加算: 抽出した数値と単位の値を乗算し、全体の合計 float64 値に加算します。
  4. 最終的な符号の適用: 最初に見つかった符号(もしあれば)を最終的な合計に適用します。
  5. Duration 型への変換: 最終的な float64 の合計を time.Duration 型にキャストして返します。

エラーハンドリングも組み込まれており、無効なフォーマットの文字列が与えられた場合(例: 数字がない、単位がない、不明な単位など)には、適切なエラーが返されます。

コアとなるコードの変更箇所

このコミットでは、主に以下の2つのファイルが変更されています。

  1. src/pkg/time/format.go:

    • 既存の atoi 関数が、より汎用的な leadingInt 関数を使用するように変更されています。これは、ParseDuration が数値部分を解析する際に leadingInt を利用するためです。
    • errLeadingInt という新しいエラー変数が追加されています。
    • leadingInt 関数が新しく追加されています。
    • unitMap という新しいグローバルマップが追加されています。
    • ParseDuration 関数が新しく追加されています。
  2. 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 を呼び出すように変更されました。これにより、atoileadingInt の堅牢な数値解析ロジックを利用し、文字列の先頭から数字を解析し、残りの文字列が空であることを確認するようになりました。

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 の間の互換性と正確性が保証されます。コメントにあるように、ミリ秒より細かい精度では浮動小数点演算の性質上、不正確なラウンドトリップになる可能性があるため、テストではミリ秒単位でランダムな値を生成しています。

関連リンク

参考にした情報源リンク