相対的な日時の一覧(Swift)

## はじめに

本記事は [SwiftWednesday Advent Calendar 2023](https://qiita.com/advent-calendar/2023/swift-wednesday) の3日目の記事です。
昨日は @ojun_9 さんで [Xcode 15のPreview Macroにおける依存注入について](https://zenn.dev/dena/articles/9165f8c4e12acc) でした。

`date.formatted(.relative(presentation: .named))` を使うと、「32 秒前」や「4 週間前」のように日時を相対的に表せます。
具体的な日時の範囲と文言が気になり、調べて一覧にしたので紹介します。

## 環境

- OS：macOS Sonoma 14.0（23A344）
- Swift：5.9

## 相対的な日時の一覧

相対的な日時の一覧です。

- 「0 秒前」を「2023/12/01(金) 17:11:52」とする
- 未来の日時は使用頻度が低いため「1 秒後」のみ確認している
- 3 年より前は確認していない

|日時の範囲 / ロケール|`ja_JP` <br>日本|`en_US` <br>アメリカ|
|:--|:--|:--|
|1 秒後|1 秒後|in 1 second|
|0 秒前|今|now|
|1 秒前|1 秒前|1 second ago|
|2 秒前|2 秒前|2 seconds ago|
|…|…|…|
|59 秒前|59 秒前|59 seconds ago|
|1 分 〜 1 分 29 秒前|1 分前|1 minute ago|
|1 分 30 秒 〜 2 分 29 秒前|2 分前|2 minutes ago|
|…|…|…|
|59 分 30 秒 〜 59 分 59 秒前|60 分前|60 minutes ago|
|1 時間前 〜 1 時間 29 分 59 秒前|1 時間前|1 hour ago|
|1 時間 30 分 〜 2 時間 29 分 59 秒前|2 時間前	|2 hours ago|
|…|…|…|
|23 時間 30 分 〜 23 時間 59 分 59 秒前|24 時間前|24 hours ago|
|1 日前 〜 2023/11/30(木) 00:00:00|昨日|yesterday|
|2023/11/29(水) 23:59:59 〜 2023/11/29(水) 00:00:00|一昨日|2 days ago|
|2023/11/28(火) 23:59:59 〜 2023/11/28(火) 00:00:00|3 日前|3 days ago|
|…|…|…|
|2023/11/25(土) 23:59:59 〜 2023/11/25(土) 00:00:00|6 日前|6 days ago|
|2023/11/24(金) 23:59:59 〜 2023/11/20(月) 00:00:00|先週|last week|
|2023/11/19(日) 23:59:59 〜 2023/11/13(月) 00:00:00|2 週間前|2 weeks ago|
|2023/11/12(日) 23:59:59 〜 2023/11/06(月) 00:00:00|3 週間前|3 weeks ago|
|2023/11/05(日) 23:59:59 〜 2023/11/04(土) 00:00:00|4 週間前|4 weeks ago|
|2023/11/03(金) 23:59:59 〜 2023/11/01(水) 00:00:00|先月|last month|
|2023/10/31(火) 23:59:59 〜 2023/10/01(日) 00:00:00|2 ヶ月前|2 months ago|
|…|…|…|
|2023/01/31(火) 23:59:59 〜 2023/01/01(日) 00:00:00|11 ヶ月前|11 months ago|
|2022/12/31(土) 23:59:59 〜 2022/01/01(土) 00:00:00|昨年|last year|
|2021/12/31(金) 23:59:59 〜 2021/01/01(金) 00:00:00|2 年前|2 years ago|
|2020/12/31(木) 23:59:59 〜 2020/01/01(水) 00:00:00|3 年前|3 years ago|
|…|…|…|

思ったより複雑で面白い結果になりました。

おそらく以下のルールとなっています。

- 1 秒前 〜 59 秒前 → 「○○ 秒前」
- 1 分前 〜 59 分 59 秒前 → 「○○ 分前」（30 秒が境界値）
- 1 時間前 〜 23 時間 59 分 59 秒前 → 「○○ 時間前」（30 分が境界値）
- 1 日前 〜 6 日前 → 「○○ 日前」（「昨日」や「一昨日」などの例外あり）
- 1 週間前 〜 4 週間前 → 「○○ 週間前」（月曜始まり、「先週」などの例外あり）
- 5 週間前 〜 先月 → 「先月」
- 先々月 〜 今年1月1日 → 「○○ ヶ月前」
- 昨年 → 「昨年」
- 2 年前 〜 → 「○○ 年前」

## ソースコード

検証に使ったソースコードを紹介します。
1秒単位で確認するために実際はもっと汚いコードでしたが、それ以外は変えていません。

様々なロケールで確認したかったので [`Date.RelativeFormatStyle`](https://developer.apple.com/documentation/foundation/date/relativeformatstyle) を使っていますが、 `date.formatted(.relative(presentation: .named))` でも結果は同様だと思います。

```swift
import Foundation

printRelativeDate(timeIntervalSinceNow: 1) // 1 秒後
printRelativeDate(timeIntervalSinceNow: 0) // 0 秒前
printRelativeDate(timeIntervalSinceNow: -1) // 1 秒前
printRelativeDate(timeIntervalSinceNow: -2) // 2 秒前
printRelativeDate(timeIntervalSinceNow: -(60 - 1)) // 59 秒前
printRelativeDate(timeIntervalSinceNow: -60) // 1 分前
printRelativeDate(timeIntervalSinceNow: -(60 + 1)) // 1 分 1 秒前
printRelativeDate(timeIntervalSinceNow: -((60 + 30) - 1)) // 1 分 29 秒前
printRelativeDate(timeIntervalSinceNow: -(60 + 30)) // 1 分 30 秒前
printRelativeDate(timeIntervalSinceNow: -(((2 * 60) + 30) - 1)) // 2 分 29 秒前
printRelativeDate(timeIntervalSinceNow: -((2 * 60) + 30)) // 2 分 30 秒前
printRelativeDate(timeIntervalSinceNow: -(((59 * 60) + 30) - 1)) // 59 分 29 秒前
printRelativeDate(timeIntervalSinceNow: -((59 * 60) + 30)) // 59 分 30 秒前
printRelativeDate(timeIntervalSinceNow: -((60 * 60) - 1)) // 59 分 59 秒前
printRelativeDate(timeIntervalSinceNow: -(60 * 60)) // 1 時間前
printRelativeDate(timeIntervalSinceNow: -(((60 + 30) * 60) - 1)) // 1 時間 29 分 59 秒前
printRelativeDate(timeIntervalSinceNow: -((60 + 30) * 60)) // 1 時間 30 分前
printRelativeDate(timeIntervalSinceNow: -((((2 * 60) + 30) * 60) - 1)) // 2 時間 29 分 59 秒前
printRelativeDate(timeIntervalSinceNow: -(((2 * 60) + 30) * 60)) // 2 時間 30 分前
printRelativeDate(timeIntervalSinceNow: -((((23 * 60) + 30) * 60) - 1)) // 23 時間 29 分 59 秒前
printRelativeDate(timeIntervalSinceNow: -(((23 * 60) + 30) * 60)) // 23 時間 30 分前
printRelativeDate(timeIntervalSinceNow: -((24 * 60 * 60) - 1)) // 23 時間 59 分 59 秒前
printRelativeDate(timeIntervalSinceNow: -(24 * 60 * 60)) // 1 日前
printRelativeDate(timeIntervalSinceNow: -(2 * 24 * 60 * 60)) // 2 日前
printRelativeDate(timeIntervalSinceNow: -(3 * 24 * 60 * 60)) // 3 日前
printRelativeDate(timeIntervalSinceNow: -(6 * 24 * 60 * 60)) // 4 日前
printRelativeDate(timeIntervalSinceNow: -(7 * 24 * 60 * 60)) // 7 日前
printRelativeDate(timeIntervalSinceNow: -(11 * 24 * 60 * 60)) // 11 日前
printRelativeDate(timeIntervalSinceNow: -(12 * 24 * 60 * 60)) // 12 日前
printRelativeDate(timeIntervalSinceNow: -(18 * 24 * 60 * 60)) // 18 日前
printRelativeDate(timeIntervalSinceNow: -(19 * 24 * 60 * 60)) // 19 日前
printRelativeDate(timeIntervalSinceNow: -(25 * 24 * 60 * 60)) // 25 日前
printRelativeDate(timeIntervalSinceNow: -(26 * 24 * 60 * 60)) // 26 日前
printRelativeDate(timeIntervalSinceNow: -(27 * 24 * 60 * 60)) // 27 日前
printRelativeDate(timeIntervalSinceNow: -(28 * 24 * 60 * 60)) // 28 日前
printRelativeDate(timeIntervalSinceNow: -(30 * 24 * 60 * 60)) // 30 日前
printRelativeDate(timeIntervalSinceNow: -(31 * 24 * 60 * 60)) // 31 日前
printRelativeDate(timeIntervalSinceNow: -(61 * 24 * 60 * 60)) // 61 日前
printRelativeDate(timeIntervalSinceNow: -(62 * 24 * 60 * 60)) // 62 日前
printRelativeDate(timeIntervalSinceNow: -(303 * 24 * 60 * 60)) // 303 日前
printRelativeDate(timeIntervalSinceNow: -(304 * 24 * 60 * 60)) // 304 日前
printRelativeDate(timeIntervalSinceNow: -(334 * 24 * 60 * 60)) // 334 日前
printRelativeDate(timeIntervalSinceNow: -(335 * 24 * 60 * 60)) // 335 日前
printRelativeDate(timeIntervalSinceNow: -((335 + (365 - 1)) * 24 * 60 * 60)) // 699 日前
printRelativeDate(timeIntervalSinceNow: -((335 + 365) * 24 * 60 * 60)) // 700 日前
printRelativeDate(timeIntervalSinceNow: -((335 + (365 * 2) - 1) * 24 * 60 * 60)) // 1064 日前
printRelativeDate(timeIntervalSinceNow: -((335 + (365 * 2)) * 24 * 60 * 60)) // 1065 日前
printRelativeDate(timeIntervalSinceNow: -((335 + (365 * 3)) * 24 * 60 * 60)) // 1430 日前
printRelativeDate(timeIntervalSinceNow: -((335 + (365 * 3) + 1) * 24 * 60 * 60)) // 1431 日前

// MARK: - Privates

private func printRelativeDate(timeIntervalSinceNow: Int) {
    let calendar = Calendar.current
    let date = calendar.date(byAdding: .second, value: timeIntervalSinceNow, to: .now)!
    let formatStyle = Date.RelativeFormatStyle(
        presentation: .named,
        locale: Locale(identifier: "ja_JP"),
        calendar: calendar
    )
    print("\(date.formatted(date: .complete, time: .standard)): \(formatStyle.format(date))")
}
```

## おわりに

相対的な日時についての理解が深まりました。
使用を検討している場合、本記事が参考になれば幸いです :relaxed:

以上 [SwiftWednesday Advent Calendar 2023](https://qiita.com/advent-calendar/2023/swift-wednesday) の3日目の記事でした。
明日も @ojun_9 さんで [@Observable Macro を構造体へ適用させる難しさについて](https://zenn.dev/dena/articles/6e70c30d148523) です。