[インデックス 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週の定義: その年の最初の木曜日を含む週が、その年の第1週(Week 01)と定義されます。これは以下のいずれかの条件を満たす週です。
- 1月1日が月曜日、火曜日、水曜日、または木曜日である場合、その週が第1週です。
- 1月1日が金曜日、土曜日、または日曜日である場合、その週は前年の最後の週(Week 52または53)の一部とみなされ、その年の第1週は1月4日を含む週になります。
- 週の数: 1年は通常52週ですが、一部の年は53週になります。53週になるのは、その年の最初の木曜日が1月1日、2日、3日のいずれかである場合、またはその年の最後の木曜日が12月29日、30日、31日のいずれかである場合です。
- 年の境界: 年末年始の週は、異なる年にまたがることがあります。例えば、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つの補助関数と連携して動作します。
-
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
- この関数は、日付を連続した数値に変換することで、週番号の計算を簡素化するための基盤となります。
-
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
)から始まります。
- もし1月1日の曜日が木曜日(
-
dayOfWeek(year int64, month, day int) int
:- この関数は、与えられた年、月、日の曜日を返します。
- 内部的には、
time.Time
構造体を作成し、そのWeekday()
メソッドを呼び出すことで曜日を取得しています。これは、time
パッケージが既に提供している曜日計算の機能を利用するものです。
ISOWeek()
メソッドのロジック
ISOWeek()
メソッドは、time.Time
型のレシーバt
に対して呼び出され、その日付のISO週の年と週番号を返します。
-
現在日付のユリウス日番号の取得:
- まず、対象となる日付
t
のユリウス日番号d
をjulianDayNumber(t.Year, t.Month, t.Day)
を使って計算します。
- まず、対象となる日付
-
現在の年の第1週の開始日の取得:
- 次に、
t.Year
のISO第1週の開始日week1Start
をstartOfFirstWeek(t.Year)
を使って計算します。
- 次に、
-
週番号の判定ロジック:
- ケース1: 現在の日付が現在の年の第1週の開始日よりも前の場合 (
d < week1Start
):- これは、現在の日付がグレゴリオ暦では現在の年だが、ISO週の定義では前年の最後の週(52週または53週)に属することを示します。
year
はt.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週に属することを示します。
year
はt.Year
(現在の年)に設定されます。week
は、week1Start
からの日数を7で割って1を加えることで計算されます。int((d-week1Start)/7 + 1)
。これは、week1Start
からの経過週数を求めるものです。
- ケース3: 上記のいずれでもない場合:
- これは、現在の日付がグレゴリオ暦では現在の年だが、ISO週の定義では翌年の第1週に属することを示します。
year
はt.Year + 1
(翌年)に設定されます。week
は常に1に設定されます。
- ケース1: 現在の日付が現在の年の第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: https://ja.wikipedia.org/wiki/ISO_8601
- ユリウス通日 - Wikipedia: https://ja.wikipedia.org/wiki/%E3%83%A6%E3%83%AA%E3%82%A6%E3%82%B9%E9%80%9A%E6%97%A5
- Go言語
time
パッケージ公式ドキュメント: https://pkg.go.dev/time
参考にした情報源リンク
- ISO 8601に関する一般的な情報源(Wikipediaなど)
- ユリウス日計算に関する情報源
- Go言語の
time
パッケージの既存のドキュメントとソースコード - コミットメッセージと変更されたコード自体
- Go言語のコードレビューシステム (Gerrit) の変更リスト (CL): https://golang.org/cl/5316074 (コミットメッセージに記載)