[SF-0001] Calendar Sequence Enumeration の解説
この記事はSwiftWednesday Advent Calendar 2023の21日目の記事です。
昨日は @sugiy さんの「矯正治療 - アプリ開発編2」でした。
はじめに
macOS 14
およびiOS 17
において、Calendar
クラスがSwift
に書き換えられました。この更新により、従来のenumerateDates
メソッドと比較して、Swift
の特性を活かした、より直感的で使いやすいCalendar API
の開発が可能となりました。
この記事では、最近提案された [SF-0001] Calendar Sequence Enumeration に焦点を当てています。(この提案は、Calendar API
の機能拡張と使い勝手の向上に関する内容を含んでいます。)特に、追加された3つのメソッドと1つのプロパティに注目し、それらが従来の実装とどのように異なるか、その利点を具体的な例とともに解説します。
Calendar API
のメソッドと機能の詳細
新しいこのセクションでは、Calendar API
に新たに追加された3つの主要な機能に焦点を当てます。これらの機能は日付操作をより効率的かつ直感的に行えるように設計されています。
プロポーザルでは、日付操作をシンプルかつ効率的に行うための3つの新しいメソッドが含まれています。これらのメソッドは、日付の加算、マッチング、および日付Sequence
の生成を容易にします。具体的には以下の通りです
1. 日付の加算を行うメソッド
// DateComponentsを使用して日付に加算するメソッド
public func dates(
byAdding components: DateComponents,
startingAt start: Date,
in range: Range<Date>? = nil,
wrappingComponents: Bool = false
) -> some (Sequence<Date> & Sendable)
// 単一のCalendar.Componentを使用して日付に加算するメソッド
public func dates(
byAdding component: Calendar.Component,
value: Int = 1,
startingAt start: Date,
in range: Range<Date>? = nil,
wrappingComponents: Bool = false
) -> some (Sequence<Date> & Sendable)
これらのメソッドは、例えば「今から3日後までの毎日の日付」や「今から1時間ごとの次の5時間の日付」など、特定の期間にわたる日付のSequence
を生成する際に便利です。
既存のAPIにdate(byAdding:to:options:)
が存在しますがこれとは異なります。
2. 特定の条件にマッチする日付を見つけるメソッド
指定されたDateComponents
にマッチする日付のSequence
を生成します。
// 指定されたDateComponentsにマッチする日付の`Sequence`を返すメソッド
public func dates(
byMatching components: DateComponents,
startingAt start: Date,
in range: Range<Date>? = nil,
matchingPolicy: MatchingPolicy = .nextTime,
repeatedTimePolicy: RepeatedTimePolicy = .first,
direction: SearchDirection = .forward
) -> some (Sequence<Date> & Sendable)
このメソッドは、特定の時間(例:毎時0分)、特定の日(例:毎月1日)、または特定の曜日(例:毎週月曜日)にマッチする日付を見つける際に特に有用です。
この記事では、便宜上これらの新しいメソッドを「新しいbyAdding
メソッド」と「新しいbyMatching
メソッド」と称します。
3. 年の中の特定の日(dayOfYear
)を扱う新しいプロパティとケース
新しいdayOfYear
プロパティとケースは、年の中で特定の日を識別しやすくします。これにより、特定の日付が年の何日目にあたるかを簡単に判断できるようになります。
// Calendar.Componentに新しいcaseとしてdayOfYearが追加される
extension Calendar {
public enum Component : Sendable {
// ...
case dayOfYear
}
}
// DateComponentsにもdayOfYearプロパティが追加される
extension DateComponents {
// ...
public var dayOfYear: Int?
}
このプロパティ及びケースの導入により、年の中で特定の日(例:年の100日目や250日目)を指定したり、その日が何曜日であるかを簡単に特定することが可能になります。これは、年間イベントの計画や、特定の日付に関連するデータ分析など、多くのiOSアプリで役立ちます。
byAdding
メソッドについて
新しい新しいbyAdding
メソッドは、指定された日付コンポーネントを開始日付に加算し特定の日付コンポーネントを初期日付に加え、連続する日付のSequence
を生成します。このメソッドは、日付計算を簡素化し、開発者がより直感的に日付を操作できるようにします。
従来の実装との比較
従来の日付計算は、繰り返しの処理や複雑なロジックを必要としていました。例えば、現在の時刻から1時間ごとに次の3時間の日付を計算する場合、以下のような実装が考えられます。(enumerateDates
を利用せずにwhile
等を利用して実装することも可能です)
func nextThreeHourlyDatesWithEnumrate() {
let startDate = Date.now
let components = Calendar.current.dateComponents(
[.hour, .minute, .second],
from: startDate
)
var result: [Date] = []
Calendar.current.enumerateDates(
startingAfter: startDate,
matching: components,
matchingPolicy: .nextTime
) { (date, _, stop) in
if let date = date, result.count < 3 {
result.append(date)
} else {
stop = true
}
}
print(startDate)
// 2023-12-29 02:40:26 +0000
print(result)
// [2023-12-30 02:40:26 +0000,
// 2023-12-31 02:40:26 +0000,
// 2024-01-01 02:40:26 +0000]
}
このコード例では、ループ内でstop
フラグを管理し、配列の長さを監視する必要があるため、コードの複雑さが増し、可読性が低下しています。
byAdding
メソッドの利点
新しい新しいbyAdding
メソッドを使うと、同じ操作がはるかに簡潔になります。
func nextThreeHourlyDates() {
let startDate = Date.now
let result = Calendar.current.dates(
byAdding: .hour,
startingAt: startDate
).prefix(3)
print(startDate)
// 2023-12-29 02:40:26 +0000
print(Array(result))
// [2023-12-30 02:40:26 +0000,
// 2023-12-31 02:40:26 +0000,
// 2024-01-01 02:40:26 +0000]
}
ここでは、新しいbyAdding
メソッドで1時間ずつ加算された日付のSequence
を生成し、.prefix(3)
を使って最初の3つの日付だけを取得しています。この方法は、コードが読みやすく、効率的です。
複雑な加算の例
さらに複雑な加算が必要な場合、DateComponents
を引数とするbyAdding
メソッドを使用することが有効です。例として、現在の時刻から1時間、1分、1秒ずつ加算を繰り返す場合を考えます。
func complexDateComponentsAddition() {
let startDate = Date.now
let endDate = startDate + (3600 * 4) // 4時間後
let dateComponents = DateComponents(
hour: 1,
minute: 1,
second: 1
)
let result = Calendar.current.dates(
byAdding: dateComponents,
startingAt: startDate,
in: startDate..<endDate
)
print(startDate)
// 2023-12-28 14:39:54 +0000
print(Array(result))
// [2023-12-28 15:40:55 +0000,
// 2023-12-28 16:41:56 +0000,
// 2023-12-28 17:42:57 +0000]
}
このコードでは、出力から1時間、1分、1秒が順次加算されていることがわかります。ただし、新しいbyAdding
メソッドを使用する際には、in:
パラメータで指定された範囲のendDate
が含まれないことに注意が必要です。また、このパラメータを省略すると、メソッドは終了しないため、範囲の指定が重要です。
byAdding
メソッドの戻り値の型
新しい以下のsequenceDate
の型はsome Sendable & Sequence<Date>
となります。
これはSwiftの型システムにおける2つの概念、「不透明型(Opaque Type
)」と「プロトコル合成(Protocol Composition
)」を組み合わせたものです。
Sendable
が付与されていることにより変数が複数のスレッド間で安全に送信できるようになります。
let sequenceDate: some Sendable & Sequence<Date> = Calendar.current.dates(
// ...
)
sequenceDate
の型がsome Sendable & Sequence<Date>
となるのは、新しいbyAdding
メソッドの戻り値の型がsome (Sequence<Date> & Sendable)
であるからです。
byMatching
メソッドについて
新しい新しいbyMatching
メソッドはメソッドを使えば、指定したコンポーネントにマッチする日付のSequence
を簡単に作成できます。これは、特定の日付パターン(例えば毎週の特定の曜日や毎月の特定の日など)にマッチする日付を効率的に見つける際に非常に有用です。
従来の実装との比較
以前は、特定のパターンにマッチする日付を見つけるためには、手動での繰り返し処理が必要でした。例えば、毎時0分にマッチする日付を見つけるためには、以下のようなコードが必要でした。(enumerateDates
を利用しても実現可能です。)
func nextThreeHourlyDates() {
let startDate = Date.now
let dateComponents = DateComponents(minute: 0, second: 0)
var nextDate = startDate
var result: [Date] = []
while let matchingDate = Calendar.current.nextDate(
after: nextDate,
matching: dateComponents,
matchingPolicy: .nextTime
) {
result.append(matchingDate)
if result.count > 3 { break }
nextDate = matchingDate
}
print(startDate)
// 2023-12-29 02:40:26 +0000
print(result)
// [2023-12-29 03:00:00 +0000,
// 2023-12-29 04:00:00 +0000,
// 2023-12-29 05:00:00 +0000, 2023-12-29 06:00:00 +0000]
}
このコードは、ループを使って次のマッチする日付を繰り返し検索しています。これには複数の手順と状態管理が必要で、コードが複雑になりがちです。
byMatching
メソッドの利点
新しい新しいbyMatching
メソッドの導入により、このプロセスが大幅に簡素化されます。
func hourlyDatesAtZeroMinute() {
let startDate = Date.now
let dateComponents = DateComponents(
minute: 0,
second: 0
)
let result = Calendar.current.dates(
byMatching: dateComponents,
startingAt: startDate,
matchingPolicy: .nextTime
).prefix(3)
print(startDate)
// 2023-12-29 02:40:26 +0000
print(Array(result))
// [2023-12-29 03:00:00 +0000,
// 2023-12-29 04:00:00 +0000,
// 2023-12-29 05:00:00 +0000]
}
このメソッドでは、指定したコンポーネントにマッチする日付のSequence
を直接生成します。例えば、毎時0分にマッチする日付を見つける場合、単に新しいbyMatching
メソッドを使用して、条件にマッチする日付のSequence
を取得するだけです。
ユースケース
新しいbyMatching
メソッドは、特定の時間帯や曜日に繰り返されるイベントのスケジューリング、毎月特定の日に行われるタスクのリマインダーの設定、特定の時間帯にマッチするデータの分析など、さまざまなユースケースに適用できます。例えば、毎週月曜日の9時にマッチする日付のSequence
を生成する場合、以下のように簡単に記述できます。
func mondayDates() -> some Sendable & Sequence<Date> {
let startDate = Date.now
let components = DateComponents(
hour: 19,
weekday: 2
)
let mondayDates = Calendar.current.dates(
byMatching: components,
startingAt: startDate,
matchingPolicy: .nextTime
)
return mondayDates
}
このように、新しいbyMatching
メソッドは、日付の検索と生成をより効率的で読みやすいものにし、より柔軟かつ効率的に日付に関連するタスクを処理できるようになります。
期間指定と出力順序の設定
特定の時間パターンにマッチする日付を見つけ、その結果を新しい順(逆順)で得ることもできます。
例えば、毎時0分にマッチする日付を3時間分探し、新しい順で結果を取得するには以下のように記述します。
func reverseOrderHourlyDatesAtZeroMinute() {
let startDate = Date.now
let endDate = startDate + (3600 * 3) + 1 // 3時間1秒後
let dateComponents = DateComponents(minute: 0)
let result = Calendar.current.dates(
byMatching: dateComponents,
startingAt: endDate,
in: startDate..<endDate,
matchingPolicy: .nextTime,
direction: .backward
)
print(startDate)
// 2023-12-29 02:40:26 +0000
print(Array(result))
// [2023-12-29 05:00:00 +0000,
// 2023-12-29 04:00:00 +0000,
// 2023-12-29 03:00:00 +0000]
}
ここでのin:
パラメータによって、特定の期間(startDate
からendDate
まで)を指定しています。加えて、.backward
オプションを用いることで、日付のSequence
を逆順(新しい順)で取得します。この場合、startingAt
のパラメータはendDate
であることが重要です。これにより、期間内の最新の日付から遡って、条件に合致する日付を探索します。
この機能は、特定の期間内で最新のイベントやデータを逆順に確認したい場合など、さまざまなシナリオで有効です。利用者は期間を自由に設定し、指定した条件に合致する日付を簡単かつ柔軟に取得できます。
direction
の指定はは新しいbyAdding
メソッドでも可能です
dayOfYear
プロパティとケースの活用
dayOfYear
プロパティとケースは、ある特定の日付がその年の中で何日目にあたるかを示します。このプロパティの導入により、日付に関連する計算がより直感的かつ柔軟に行えるようになりました。
今日が年の何日目かの取得
これまでは、年の中での特定の日付の位置を求めるには、ordinality(of:in:for:)
メソッドを使用していました。このメソッドは、.day
と.year
を指定する必要があり、戻り値がオプショナル型でした。
let dayOfYear: Int? = Calendar.current.ordinality(
of: .day,
in: .year,
for: Date.now
)
print(dayOfYear) // 363
新しいdayOfYear
とcomponent(_:from:)
メソッドを組み合わせることで、以下のようによりシンプルに実装できます。戻り値もオプショナル型ではなくなります。
let dayOfYear: Int = Calendar.current.component(
.dayOfYear,
from: Date.now
)
print(dayOfYear) // 363
年の特定の日を取得
例えば、今年の100日目が何日であるかを知りたい場合、従来は年の初めから99日を加算する方法を取っていました。
func dateOfHundredthDayOfCurrentYear() {
let calendar = Calendar.current
let currentYear = calendar.component(.year, from: Date.now)
let startOfYear = calendar.date(from: DateComponents(year: currentYear))!
let result = calendar.date(
byAdding: DateComponents(day: 99),
to: startOfYear
)
print(result) // 2023-04-09 15:00:00 +0000
}
新しいbySetting
メソッドを使用すると、この計算がより明確かつ簡潔になります。ここでは、年の初めを取得した後、dayOfYear
プロパティを用いて直接100日目の日付を求めます。
func dateOfHundredthDayOfCurrentYear() {
let calendar = Calendar.current
let currentYear = calendar.component(.year, from: Date.now)
let startOfYear = calendar.date(from: DateComponents(year: currentYear))!
let result = calendar.date(
bySetting: .dayOfYear,
value: 100,
of: startOfYear
)
print(result) // 2023-04-09 15:00:00 +0000
}
このように、dayOfYear
プロパティとケースの導入は、年内の特定の日付を取得する際のコードをより直感的かつ効率的にします。
まとめ
この記事では [SF-0001] Calendar Sequence Enumeration のプロポーザルについて詳しく見てきました。このプロポーザルは、macOS 14
とiOS 17
のアップデートでSwift
に書き換えられたCalendar
クラスに基づいています。重要な点は以下の通りです。
- 新しいAPIの導入
- 以下の3つのメソッドにより、日付に関する操作が簡単かつ効率的に行えるようになりました。これにより、開発者は繰り返し処理や複雑な日付計算をシンプルに実装できるようになります。
dates(byAdding:value:startingAt:in:wrappingComponents:)
dates(byAdding:startingAt:in:wrappingComponents:)
dates(byMatching:startingAt:in:matchingPolicy:repeatedTimePolicy:direction:)
- 以下の3つのメソッドにより、日付に関する操作が簡単かつ効率的に行えるようになりました。これにより、開発者は繰り返し処理や複雑な日付計算をシンプルに実装できるようになります。
- 利便性と効率の向上
- 新しいAPIは、
Sequence
ベースのアプローチを採用しており、Swift
の標準ライブラリとの互換性が高いです。これにより、開発者はより読みやすく、保守しやすいコードを書くことが可能になります。
- 新しいAPIは、
-
dayOfYear
プロパティとケースの導入- このプロパティとケースにより、年内の特定の日付をより簡単に取得できます。これは、特に日付に関連する複雑な計算を必要とするアプリケーションにとって大きな利点です。
このプロポーザルは、日付と時間に関する操作を行うSwift開発者にとって大きなメリットをもたらします。日付の計算や繰り返し処理は、多くのアプリケーションで一般的なタスクであり、このプロポーザルにより、これらのタスクの実装がより簡単かつ効率的になります。
また、プロポーザルのレビュー期間はすでに終了しており、最新の情報は公式のプロポーザルやフォーラムで確認することが推奨されます。このアップデートにより、日付と時間に関連する開発作業を大きく前進させることでしょう。
開発環境
- Swift compiler version info:
Apple Swift version 5.9 (swiftlang-5.9.0.128.108 clang-1500.0.40.1)
- Xcode version info:
Xcode 15.0 Build version 15A240d
- Calendar Sequence Enumeration info:
参考引用文献
- https://github.com/apple/swift-foundation/blob/main/Proposals/0001-calendar-improvements.md#calendar-sequence-enumeration
- https://forums.swift.org/t/pitch-calendar-sequence-enumeration/68521
- https://github.com/apple/swift-foundation/pull/322
- https://developer.apple.com/documentation/foundation/nscalendar/1409577-date
- https://developer.apple.com/documentation/foundation/calendar/2293661-enumeratedates
- https://developer.apple.com/documentation/foundation/nscalendar/1409577-date
- https://github.com/ojun9/date_test
Discussion