SwiftのDateFormatterに年末の値を渡す時には気を付けよう！

# 概要
SwiftでDateFormatterに年末の日付を渡したらそんな挙動するの！？ということがあったので記録を残しておきます。

# 詳細


早速ですが、以下の二つのformatDateTime関数に`2022-12-31 00:00:00 +0000`を渡した際の返り値が違うことをご存知でしょうか？

```swift
func formatDateTime(_ date: Date) -> String {
    let formatter = DateFormatter()
    formatter.dateFormat = "YYYY-MM-dd"
    
    return formatter.string(from: date)
}

func formatDateTime2(_ date: Date) -> String {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd"
    
    return formatter.string(from: date)
}

```

上の関数は`2023-12-31`が返され、下の関数は`2022-12-31`が返されます。

それぞれ、`dateFormat`が
```
YYYY-MM-dd
```
であるか、

```
yyyy-MM-dd
```

であるかの違いが存在します。
一見同じようなフォーマットに見えますが、明確に振る舞いが異なります。

どういった違いがあるかというと、

- `YYYY`は週の年(Week of Year)を指し、与えられた日付が属する年を返します。

- `yyyy`は暦年を指し、与えられた日付が属する暦年を返します。

週の年とは、[ISO 8601](https://ja.wikipedia.org/wiki/ISO_8601)の規格で定義されている概念です。

この規格では、

>最初の木曜日を含む週が、その年の第1週である。
>週の最初の日は、月曜日である[14]。

となっており、その週が属する年を「週の年」と定義しています。

この規格を踏まえた上で具体例を見てみましょう。
例えば、**2024年12月31日は2025年1月の週**になっています。

Playgroundで境界値となる値に変換をかける以下のようなコードを実行してみます。

```swift:Playground
import UIKit

func formatDateTime(_ date: Date) -> String {
    let formatter = DateFormatter()
    formatter.dateFormat = "YYYY-MM-dd"
    
    return formatter.string(from: date)
}

func formatDateTime2(_ date: Date) -> String {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd"
    
    return formatter.string(from: date)
}

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"

if let date = dateFormatter.date(from: "2024-12-29 00:00:00") {
    print(formatDateTime(date)) // "2024-12-29\n"
    print(formatDateTime2(date)) // "2024-12-29\n"
}

if let date = dateFormatter.date(from: "2024-12-30 00:00:00") {
    print(formatDateTime(date)) // "2025-12-30\n"
    print(formatDateTime2(date)) // "2024-12-30\n"
}

if let date = dateFormatter.date(from: "2024-12-31 00:00:00") {
    print(formatDateTime(date)) // "2025-12-31\n"
    print(formatDateTime2(date)) // "2024-12-31\n"
}

```

週の始まりが月曜日、最初の木曜日を含む週が第1週である、と定義されている通り、
`"2024-12-29 00:00:00"`をそれぞれの関数に渡した時は二つとも同様の結果を返しましたが、
`"2024-12-30 00:00:00"`をそれぞれの関数に渡した時には異なる結果を返しました。

このようなケースでは`yyyy`を使わないとデータの不整合が生じてしまう可能性があると言えます。

仮に`Y`(週の年)の状態でリリースし、データが入力された後にこの仕様に気付いたとしましょう。
その場合、`2025年12月31日`と入力されているデータは`2024年12月31日`と入力したかったのか、それとも`2024年12月31日`と入力したかったのかというのは入力者本人しか分からないため、データを修正することが困難、といった状況になりかねません。


ちなみに、どちらのフォーマットを使用しても大丈夫なケースもあります。
例えば、`2017年12月31日`を見てみると、こちらは12月の最終週の中に31日まで収まっています。

2024年のケースと同様に以下のようなコードを実行し、結果を見てみます。


```swift:Playground
import UIKit

func formatDateTime(_ date: Date) -> String {
    let formatter = DateFormatter()
    formatter.dateFormat = "YYYY-MM-dd"
    
    return formatter.string(from: date)
}

func formatDateTime2(_ date: Date) -> String {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd"
    
    return formatter.string(from: date)
}

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"

if let date = dateFormatter.date(from: "2017-12-28 00:00:00") {
    print(formatDateTime(date)) // "2017-12-28\n"
    print(formatDateTime2(date)) // "2017-12-28\n"
}

if let date = dateFormatter.date(from: "2017-12-29 00:00:00") {
    print(formatDateTime(date)) // "2017-12-29\n"
    print(formatDateTime2(date)) // "2017-12-29\n"
}

if let date = dateFormatter.date(from: "2017-12-30 00:00:00") {
    print(formatDateTime(date)) // "2017-12-30\n"
    print(formatDateTime2(date)) // "2017-12-30\n"
}

if let date = dateFormatter.date(from: "2017-12-31 00:00:00") {
    print(formatDateTime(date)) // "2017-12-31\n"
    print(formatDateTime2(date)) // "2017-12-31\n"
}

```

このような場合は仮に`Y`(週の年)をフォーマットとして使用しても、`yyyy`(暦年)を使用してもきっちりと`2017年12月31日`が返されます。


ただ、どのような場合でも対応できることを考えると、特殊な事情がない限りは`yyyy`(暦年)を使用することが賢明でしょう。


一通り調べた後にAppleの公式ドキュメントを読んでいたらきっちり書かれていました。
[Data Formatting Guide](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/DataFormatting/Articles/dfDateFormatting10_4.html)

原理をそこまで知らずに使っていたので勉強になりました。