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

[インデックス 10336] ファイルの概要

このコミットは、Go言語の標準ライブラリであるtimeパッケージに、ISO 8601規格に基づく週番号を計算するISOWeekメソッドを追加するものです。これにより、日付からISO週の年と週番号を正確に取得できるようになります。

コミット

commit d98970963081585c3c2e85fa68740cc854d08f92
Author: Volker Dobler <dr.volker.dobler@gmail.com>
Date:   Thu Nov 10 12:40:50 2011 -0800

    time: add ISOWeek method to Time
    
    As the ISO 8601 week number is untrivial to compute a new method
    on *Time provides year and number of week.
    
    R=golang-dev, rsc, r, r
    CC=golang-dev
    https://golang.org/cl/5316074

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/d98970963081585c3c2e85fa68740cc854d08f92

元コミット内容

time: add ISOWeek method to Time

ISO 8601週番号の計算は自明ではないため、*Time型に新しいメソッドを追加し、年と週番号を提供する。

変更の背景

日付と時刻の処理において、ISO 8601規格に基づく週番号は、特にビジネスや国際的な文脈で広く利用されています。しかし、この週番号の計算は、一般的なグレゴリオ暦の週の概念とは異なり、特定のルール(例えば、年の最初の週の定義や、年末年始の週の扱い)に従うため、単純な計算では正確に求めることができませんでした。

Go言語のtimeパッケージには、日付や時刻に関する基本的な機能は提供されていましたが、ISO 8601週番号を直接取得する機能は存在しませんでした。このため、開発者はISO週番号を計算するために、自身で複雑なロジックを実装するか、外部ライブラリに依存する必要がありました。

このコミットは、このような背景から、ISO 8601週番号の計算をtimeパッケージの標準機能として提供することで、開発者の利便性を向上させ、正確な日付計算を容易にすることを目的としています。特に、ISO 8601週番号の計算が「自明ではない (untrivial)」と明記されていることから、その複雑性をライブラリ側で吸収することの重要性が強調されています。

前提知識の解説

ISO 8601 週番号

ISO 8601は、日付と時刻の表記に関する国際標準です。この標準には、週番号の定義も含まれており、以下の重要なルールがあります。

  1. 週の始まり: 週は月曜日から始まります。
  2. 第1週の定義: その年の最初の木曜日を含む週が、その年の第1週(Week 01)と定義されます。これは以下のいずれかの条件を満たす週です。
    • 1月1日が月曜日、火曜日、水曜日、または木曜日である場合、その週が第1週です。
    • 1月1日が金曜日、土曜日、または日曜日である場合、その週は前年の最後の週(Week 52または53)の一部とみなされ、その年の第1週は1月4日を含む週になります。
  3. 週の数: 1年は通常52週ですが、一部の年は53週になります。53週になるのは、その年の最初の木曜日が1月1日、2日、3日のいずれかである場合、またはその年の最後の木曜日が12月29日、30日、31日のいずれかである場合です。
  4. 年の境界: 年末年始の週は、異なる年にまたがることがあります。例えば、12月29日から31日までの日付が翌年の第1週に属したり、1月1日から3日までの日付が前年の最後の週に属したりすることがあります。このため、ISO週の年とグレゴリオ暦の年は異なる場合があります。

ユリウス日 (Julian Day Number, JDN)

ユリウス日(JDN)は、紀元前4713年1月1日正午(グリニッジ標準時)を起点(ユリウス日0)として、そこからの日数を数える連続した日付システムです。天文学や歴史学で日付の計算を簡素化するために広く使用されています。

ユリウス日を使用する利点は、異なる暦法(グレゴリオ暦、ユリウス暦など)間の変換や、特定の日付間の日数の計算が容易になる点です。連続した整数であるため、日付の加算や減算が直接行えます。

ユリウス日からグレゴリオ暦への変換、またはその逆の変換には、特定のアルゴリズムが必要です。このコミットでは、julianDayNumber関数がグレゴリオ暦の日付をユリウス日に変換するために使用されています。

Go言語の time パッケージ

Go言語の標準ライブラリであるtimeパッケージは、日付と時刻を扱うための基本的な機能を提供します。これには、現在時刻の取得、特定の日付の作成、日付の加算・減算、タイムゾーンの処理、日付のフォーマットなどが含まれます。time.Time構造体は、特定の日付と時刻を表すために使用されます。

技術的詳細

このコミットで追加されたISOWeekメソッドは、ISO 8601週番号の計算を正確に行うために、以下の3つの補助関数と連携して動作します。

  1. julianDayNumber(year int64, month, day int) int64:

    • この関数は、与えられたグレゴリオ暦の年、月、日から、対応するユリウス日番号を計算して返します。
    • 計算式は、グレゴリオ暦からユリウス日への標準的な変換アルゴリズムに基づいています。具体的には、以下の式が使用されています。
      a := (14 - month) / 12
      y := year + 4800 - a
      m := month + 12*a - 3
      return day + (153*m+2)/5 + 365*y + y/4 - y/100 + y/400 - 32045
      
    • この関数は、日付を連続した数値に変換することで、週番号の計算を簡素化するための基盤となります。
  2. startOfFirstWeek(year int64) (d int64):

    • この関数は、与えられた年のISO 8601における第1週の最初の日のユリウス日番号を計算して返します。
    • まず、その年の1月1日のユリウス日番号をjulianDayNumberを使って取得します。
    • 次に、1月1日の曜日(weekday)を計算します(ユリウス日番号を7で割った余り+1)。
    • ISO 8601のルールに従い、第1週の最初の木曜日を含む週が第1週となるため、1月1日の曜日によって第1週の開始日を調整します。
      • もし1月1日の曜日が木曜日(weekday <= 4)であれば、その週が第1週となるため、1月1日からその週の月曜日(jan01 - weekday + 1)が第1週の開始日となります。
      • もし1月1日の曜日が金曜日、土曜日、日曜日(weekday > 4)であれば、その週は前年の最後の週の一部とみなされ、その年の第1週は翌週の月曜日(jan01 + 8 - weekday)から始まります。
  3. dayOfWeek(year int64, month, day int) int:

    • この関数は、与えられた年、月、日の曜日を返します。
    • 内部的には、time.Time構造体を作成し、そのWeekday()メソッドを呼び出すことで曜日を取得しています。これは、timeパッケージが既に提供している曜日計算の機能を利用するものです。

ISOWeek() メソッドのロジック

ISOWeek()メソッドは、time.Time型のレシーバtに対して呼び出され、その日付のISO週の年と週番号を返します。

  1. 現在日付のユリウス日番号の取得:

    • まず、対象となる日付tのユリウス日番号djulianDayNumber(t.Year, t.Month, t.Day)を使って計算します。
  2. 現在の年の第1週の開始日の取得:

    • 次に、t.YearのISO第1週の開始日week1StartstartOfFirstWeek(t.Year)を使って計算します。
  3. 週番号の判定ロジック:

    • ケース1: 現在の日付が現在の年の第1週の開始日よりも前の場合 (d < week1Start):
      • これは、現在の日付がグレゴリオ暦では現在の年だが、ISO週の定義では前年の最後の週(52週または53週)に属することを示します。
      • yeart.Year - 1(前年)に設定されます。
      • weekは、前年の1月1日または12月31日の曜日が木曜日であるか(つまり、前年が53週を持つ年であるか)によって、53または52に設定されます。これは、ISO 8601のルールで53週を持つ年の条件(1月1日または12月31日が木曜日)をチェックしています。
    • ケース2: 現在の日付が現在の年の第1週の開始日以降、かつ翌年の第1週の開始日よりも前の場合 (d < startOfFirstWeek(t.Year+1)):
      • これは、現在の日付が現在の年のISO週に属することを示します。
      • yeart.Year(現在の年)に設定されます。
      • weekは、week1Startからの日数を7で割って1を加えることで計算されます。int((d-week1Start)/7 + 1)。これは、week1Startからの経過週数を求めるものです。
    • ケース3: 上記のいずれでもない場合:
      • これは、現在の日付がグレゴリオ暦では現在の年だが、ISO週の定義では翌年の第1週に属することを示します。
      • yeart.Year + 1(翌年)に設定されます。
      • weekは常に1に設定されます。

このロジックにより、ISO 8601の複雑な週番号の定義(特に年の境界をまたぐケース)が正確に処理されます。

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

src/pkg/time/time.go

// julianDayNumber returns the time's Julian Day Number
// relative to the epoch 12:00 January 1, 4713 BC, Monday.
func julianDayNumber(year int64, month, day int) int64 {
	a := int64(14-month) / 12
	y := year + 4800 - a
	m := int64(month) + 12*a - 3
	return int64(day) + (153*m+2)/5 + 365*y + y/4 - y/100 + y/400 - 32045
}

// startOfFirstWeek returns the julian day number of the first day
// of the first week of the given year.
func startOfFirstWeek(year int64) (d int64) {
	jan01 := julianDayNumber(year, 1, 1)
	weekday := (jan01 % 7) + 1
	if weekday <= 4 {
		d = jan01 - weekday + 1
	} else {
		d = jan01 + 8 - weekday
	}
	return
}

// dayOfWeek returns the weekday of the given date.
func dayOfWeek(year int64, month, day int) int {
	t := Time{Year: year, Month: month, Day: day}
	return t.Weekday()
}

// ISOWeek returns the time's year and week number according to ISO 8601. 
// Week ranges from 1 to 53. Jan 01 to Jan 03 of year n might belong to 
// week 52 or 53 of year n-1, and Dec 29 to Dec 31 might belong to week 1 
// of year n+1.
func (t *Time) ISOWeek() (year int64, week int) {
	d := julianDayNumber(t.Year, t.Month, t.Day)
	week1Start := startOfFirstWeek(t.Year)

	if d < week1Start {
		// Previous year, week 52 or 53
		year = t.Year - 1
		if dayOfWeek(t.Year-1, 1, 1) == 4 || dayOfWeek(t.Year-1, 12, 31) == 4 {
			week = 53
		} else {
			week = 52
		}
		return
	}

	if d < startOfFirstWeek(t.Year+1) {
		// Current year, week 01..52(,53)
		year = t.Year
		week = int((d-week1Start)/7 + 1)
		return
	}

	// Next year, week 1
	year = t.Year + 1
	week = 1
	return
}

src/pkg/time/time_test.go

type ISOWeekTest struct {
	year       int64 // year
	month, day int   // month and day
	yex        int64 // expected year
	wex        int   // expected week
}

var isoWeekTests = []ISOWeekTest{
	{1981, 1, 1, 1981, 1}, {1982, 1, 1, 1981, 53}, {1983, 1, 1, 1982, 52},
	// ... (多数のテストケースが続く) ...
	{2039, 1, 1, 2038, 52}, {2040, 1, 1, 2039, 52},
}

func TestISOWeek(t *testing.T) {
	// Selected dates and corner cases
	for _, wt := range isoWeekTests {
		dt := &Time{Year: wt.year, Month: wt.month, Day: wt.day}
		y, w := dt.ISOWeek()
		if w != wt.wex || y != wt.yex {
			t.Errorf("got %d/%d; expected %d/%d for %d-%02d-%02d",
				y, w, wt.yex, wt.wex, wt.year, wt.month, wt.day)
		}
	}

	// The only real invariant: Jan 04 is in week 1
	for year := int64(1950); year < 2100; year++ {
		if y, w := (&Time{Year: year, Month: 1, Day: 4}).ISOWeek(); y != year || w != 1 {
			t.Errorf("got %d/%d; expected %d/1 for Jan 04", y, w, year)
		}
	}
}

コアとなるコードの解説

src/pkg/time/time.go

  • julianDayNumber 関数:

    • この関数は、グレゴリオ暦の日付(年、月、日)を受け取り、対応するユリウス日番号を計算します。
    • ユリウス日番号は、紀元前4713年1月1日正午からの日数を表す連続した整数であり、日付計算の基盤として利用されます。
    • 計算式は、日付をユリウス日に変換するための標準的なアルゴリズムを実装しています。
  • startOfFirstWeek 関数:

    • ISO 8601規格では、年の最初の木曜日を含む週がその年の第1週と定義されます。この関数は、このルールに基づいて、指定された年の第1週の最初の日のユリウス日番号を計算します。
    • まず、その年の1月1日のユリウス日番号を取得し、その曜日を計算します。
    • 1月1日の曜日が木曜日(1~4)であれば、その週が第1週となるため、1月1日を含む週の月曜日を第1週の開始日とします。
    • 1月1日の曜日が金曜日、土曜日、日曜日(5~7)であれば、その週は前年の最後の週の一部とみなされ、翌週の月曜日を第1週の開始日とします。
  • dayOfWeek 関数:

    • この関数は、指定された年、月、日の曜日を返します。
    • 内部的には、time.Time構造体を作成し、既存のWeekday()メソッドを利用して曜日を取得しています。これは、timeパッケージの既存機能を再利用する良い例です。
  • ISOWeek メソッド:

    • *Time型のレシーバtに対して呼び出され、その日付のISO週の年と週番号を返します。
    • まず、対象日付のユリウス日番号と、現在の年のISO第1週の開始日のユリウス日番号を計算します。
    • 年の境界処理:
      • もし対象日付が現在の年のISO第1週の開始日よりも前であれば、その日付はISO週の定義では前年の最後の週(52週または53週)に属します。この場合、ISO週の年は前年となり、週番号は前年が53週を持つ年であるかどうかに応じて53または52に設定されます。
      • もし対象日付が現在の年のISO第1週の開始日以降、かつ翌年のISO第1週の開始日よりも前であれば、その日付は現在の年のISO週に属します。この場合、ISO週の年は現在の年となり、週番号は第1週の開始日からの経過週数として計算されます。
      • それ以外の場合(対象日付が翌年のISO第1週の開始日以降であれば)、その日付はISO週の定義では翌年の第1週に属します。この場合、ISO週の年は翌年となり、週番号は1に設定されます。
    • このロジックにより、ISO 8601の複雑なルール(特に年末年始の週が異なる年にまたがるケース)が正確に処理されます。

src/pkg/time/time_test.go

  • ISOWeekTest 構造体:

    • テストケースを定義するための構造体で、テスト対象の年、月、日と、期待されるISO週の年、週番号を保持します。
  • isoWeekTests 変数:

    • ISOWeekTest構造体のスライスで、ISO 8601週番号の計算における様々な日付(特に年の境界や特殊なケース)に対する期待値が網羅的に定義されています。これにより、ISOWeekメソッドの正確性が検証されます。
  • TestISOWeek 関数:

    • ISOWeekメソッドのテスト関数です。
    • 選択された日付とコーナーケースのテスト: isoWeekTestsに定義された各テストケースをループし、ISOWeek()メソッドの戻り値が期待値と一致するかどうかを検証します。不一致があればエラーを報告します。
    • 不変条件のテスト: 「1月4日は常に第1週に属する」というISO 8601の重要な不変条件を検証するために、1950年から2100年までの各年の1月4日に対してISOWeek()メソッドを呼び出し、結果が期待通り(年が現在の年、週番号が1)であるかをチェックします。これは、ISO 8601週番号の定義の核となる部分を直接検証する強力なテストです。

これらのコード変更により、Go言語のtimeパッケージは、ISO 8601週番号の計算という、日付処理における重要な機能を正確かつ効率的に提供できるようになりました。

関連リンク

参考にした情報源リンク

  • ISO 8601に関する一般的な情報源(Wikipediaなど)
  • ユリウス日計算に関する情報源
  • Go言語のtimeパッケージの既存のドキュメントとソースコード
  • コミットメッセージと変更されたコード自体
  • Go言語のコードレビューシステム (Gerrit) の変更リスト (CL): https://golang.org/cl/5316074 (コミットメッセージに記載)