.NETでISO 8601の週番号を使うならISOWeekクラスを使う

# 3行まとめ

* .NETで、ISO 8601の週番号を扱いたい場合、[ISOWeek](https://learn.microsoft.com/ja-jp/dotnet/api/system.globalization.isoweek?view=net-9.0)クラスを使えばOK
* ISOWeekクラスには、.NET 10で、DateOnlyのメソッド・オーバーロードが追加予定
* .NET 9において、週番号に対応した書式指定子はない。.NET 10 preview 1段階でもなさそう

※ 本投稿では、「ISO 8601」の週に関してのみを対象とします。
※ 本投稿は、明記しない場合.NET 9時点の情報について基づいて作成しています。
※ 本投稿は、一部.NET 10 Preview 1の情報を含みます。正式リリース前に変更の可能性がある点に注意してください。

# 前提

ISO 8601は、日付、時刻、継続時間を規定する国際規格です。

このISO 8601の中で、「その年の第何週」というものが定められています。

* 週の開始曜日は、月曜日
* その年の第1週は、その年の最初の木曜日を含む週です

2020年の年末・2021年の年始のカレンダーの一部は、次のような感じです。
2021年の1月1日・2日・3日は、2021年ですが「前年である2020年の第53週」に属していることに注目してください。

| 年と週 | 月 | 火 | 水 | 木 | 金 | 土 | 日 |
|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| 2020年 第53週 | 12/28 | 12/29 | 12/30 | 12/31 | 1/1 | 1/2 | 1/3 |
| 2021年 第1週 | 1/4 | 1/5 | 1/6 | 1/7 | 1/8 | 1/9 | 1/10 |

2014年の年末・2015年の年始のカレンダーの一部は、次のような感じです。
2014年の12月29日・30日・31日は、2014年ですが「翌年である2015年の第1週」に属していることに注目してください。

| 年と週 | 月 | 火 | 水 | 木 | 金 | 土 | 日
:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| 2014年 第52週 | 12/22 | 12/23 | 12/24 | 12/25 | 12/26 | 12/27 | 12/28 | 
| 2015年 第1週 | 12/29 | 12/30 | 12/31 | 1/1 | 1/2 | 1/3 | 1/4 | 

こんな感じで「その年の第何週」という概念、週番号があります。

# ISOWeekクラスを使って週番号や年を求める

.NETの[ISOWeek](https://learn.microsoft.com/ja-jp/dotnet/api/system.globalization.isoweek?view=net-9.0)クラスは、DateTime型に対してISO 8601の週番号を求める[GetWeekOfYear](https://learn.microsoft.com/ja-jp/dotnet/api/system.globalization.isoweek.getweekofyear?view=net-9.0#system-globalization-isoweek-getweekofyear(system-datetime))メソッドなどが定義されているクラスです。次にISOWeekクラスを使って、「〇〇年第XX週」のように表示するためのコードを示します。

```csharp
DateTime date = DateTime.Parse("2021-01-01");

// 「2020年 第53週」と出力される
// 週番号を求めるには、GetWeekOfYearメソッドを使う
// その週の属する年を求めるときは、ISOWeek.GetYearメソッドを使うことに注意
Console.WriteLine(date.ToString($"{ISOWeek.GetYear(date)}年 第{ISOWeek.GetWeekOfYear(date)}週"));
```

DateTime型に対してISO 8601の週を求めて、「〇〇年第XX週」と表示する時は、「年」にも注意してください。Yearプロパティーを使って表示すると不具合を生んでしまいます。ISOWeekクラスの[GetYear](https://learn.microsoft.com/ja-jp/dotnet/api/system.globalization.isoweek.getyear?view=net-9.0#system-globalization-isoweek-getyear(system-datetime))メソッドを使いましょう。

先に示した通り2021年1月1日は、2021年でありながら、前年である2020年の第53週に属しています。そのため、Yearプロパティーを使った次のようなコードでは、期待した「2020年 第53週」という出力をしてくれず、「2021年 第53週」と出力してしまいます。

```csharp
DateTime date = DateTime.Parse("2021-01-01");

// 次のコードは間違い
// 「2021年 第53週」と出力されてしまう
// 2021年1月1日は、2020年の第53週なので間違い
Console.WriteLine(date.ToString($"{date.Year}年 第{ISOWeek.GetWeekOfYear(date)}週"));
```

ISOWeekクラスにはこれ以外にも、週関連の[便利なメソッド](https://learn.microsoft.com/ja-jp/dotnet/api/system.globalization.isoweek?view=net-9.0#methods)が定義されています。

# .NET 10で、DateOnly関連のオーバーロード・メソッドが加わりそう

※ 本節は、[.NET 10 Preview 1](https://devblogs.microsoft.com/dotnet/dotnet-10-preview-1/)の情報に基づいています。.NET 10の正式リリース前に変わる可能性がある点に注意してください。

.NET 10で、ISOWeekクラスに、DateOnly構造体を引数に取るオーバーロードとメソッドが加わる予定です。

DateOnly構造体は、.NET 6で追加されました。ISOWeekクラスは、.NET Core 3.0で追加されたクラスであり、登場時点でDateOnly構造体はまだなかったため、.NET 10での対応になったようです。

前節で紹介した

* [ISOWeek.GetYear](https://learn.microsoft.com/ja-jp/dotnet/api/system.globalization.isoweek.getyear?view=net-9.0#system-globalization-isoweek-getyear(system-datetime))
* [ISOWeek.GetWeekOfYear](https://learn.microsoft.com/ja-jp/dotnet/api/system.globalization.isoweek.getweekofyear?view=net-9.0#system-globalization-isoweek-getweekofyear(system-datetime))

の引数としてDateTimeではなく、DateOnlyを引数に取るオーバーロードが追加される予定です。（確かにこのメソッド、意味的には時刻を司るDateTimeじゃなくて、日付を司るDateOnlyを引数に取る方が適切な気がしますね。）

```csharp
DateOnly date = DateOnly.Parse("2021-01-01");
// 2021年 第53週
Console.WriteLine(date.ToString($"{ISOWeek.GetYear(date)}年 第{ISOWeek.GetWeekOfYear(date)}週"));
```

ISOWeekには、年・週番号・曜日を引数に取り、DateTime型を返す「[ISOWeek.ToDateTime](https://learn.microsoft.com/ja-jp/dotnet/api/system.globalization.isoweek.todatetime?view=net-9.0#system-globalization-isoweek-todatetime(system-int32-system-int32-system-dayofweek))」メソッドがあります。

これとよく似たDateOnlyを返すメソッド「ISOWeek.ToDateOnly」が追加予定です。

```csharp
DateTime dateTime = ISOWeek.ToDateTime(year: 2020,  week: 53, dayOfWeek: DayOfWeek.Friday);
// 2021/01/01 0:00:00
Console.WriteLine(dateTime);

DateOnly dateOnly = ISOWeek.ToDateOnly(year: 2020,  week: 53, dayOfWeek: DayOfWeek.Friday);
// 2021/01/01 0:00:00
Console.WriteLine(dateOnly);
```

* [追加PR](https://github.com/dotnet/runtime/pull/108479)
* [GitHubの.NET 10 Preview1リリースノートのISOWeekクラスの紹介](https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview1/libraries.md#new-method-overloads-in-isoweek-for-dateonly-type)
* [.NET 10 Preview 1紹介ブログ](https://devblogs.microsoft.com/dotnet/dotnet-10-preview-1/)

# 週番号に対応した書式指定子はない

.NET 9において、週番号に対応した書式指定子はなさそうです。

* [標準の日時書式指定文字列](https://learn.microsoft.com/ja-jp/dotnet/standard/base-types/standard-date-and-time-format-strings)
* [Standard date and time format strings](https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings)
* [カスタム日時形式文字列](https://learn.microsoft.com/ja-jp/dotnet/standard/base-types/custom-date-and-time-format-strings)
* [Custom date and time format strings](https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings)

.NET 10 Preview 1でも、[コード](https://github.com/dotnet/runtime/blame/v10.0.0-preview.1.25080.5/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeFormat.cs)見る限りはぱっと見なさそう。

（書式指定子って、後から足せるんだろうか？足したら破壊的な変更になるから、難しい？）

# まとめ

* .NETで、ISO 8601の週番号を扱いたい場合、[ISOWeek](https://learn.microsoft.com/ja-jp/dotnet/api/system.globalization.isoweek?view=net-9.0)クラスを使えばOK
* ISOWeekクラスには、.NET 10で、DateOnlyのメソッド・オーバーロードが追加予定
* .NET 9において、週番号に対応した書式指定子はない。.NET 10 preview 1段階でもなさそう