Swift Digest

partial range で recurrence を検索する

Search for recurrence in partial ranges

Proposal
SF-0032
Authors
Hristo Staykov
Review Manager
Tina L
Status
Accepted

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

01 何が問題だったのか

SF-0009 で導入された Calendar.RecurrenceRule には、繰り返しイベントの発生日を列挙するための recurrences(of:in:) メソッドがあります。第 2 引数に Range<Date> を渡すことで、その範囲内の発生日だけを取り出せます。

let birthday   = Date(timeIntervalSince1970: 813283200.0)  // 1995-10-10T00:00:00-0000
let rangeStart = Date(timeIntervalSince1970: 946684800.0)  // 2000-01-01T00:00:00-0000
let rangeEnd   = Date(timeIntervalSince1970: 1293840000.0) // 2011-01-01T00:00:00-0000

let recurrence = Calendar.RecurrenceRule(calendar: .current, frequency: .yearly)
for date in recurrence.recurrences(of: birthday, in: rangeStart..<rangeEnd) {
    // 2000 年から 2010 年までの birthday の発生日
}

しかし、上端あるいは下端だけを指定する partial range(rangeStart... のような形)には対応していませんでした。たとえば「2000 年以降のすべての発生日」を取り出すには、範囲を指定せずに列挙して条件で弾くか、

for date in recurrence.recurrences(of: birthday) where date >= rangeStart {
    // 2000 年以降のすべての発生日
}

Date.distantPastDate.distantFuture を上下端に充てて全範囲扱いにする必要がありました。

for date in recurrence.recurrences(of: birthday, in: rangeStart..<Date.distantFuture) {
    // 2000 年以降のすべての発生日
}

これらの書き方は冗長なだけでなく、必要のない範囲についても発生日を計算してしまうため、性能面でも無駄が生じていました。

02 どのように解決されるのか

Calendar.RecurrenceRulerecurrences(of:in:) に、partial range と ClosedRange<Date> を受け取るオーバーロードが追加されます。FoundationPreview 6.3 以降で利用できます。

extension Calendar.RecurrenceRule {
    @available(FoundationPreview 6.3, *)
    public func recurrences(of start: Date,
                            in range: PartialRangeThrough<Date>
                            ) -> some (Sequence<Date> & Sendable)

    @available(FoundationPreview 6.3, *)
    public func recurrences(of start: Date,
                            in range: PartialRangeTo<Date>
                            ) -> some (Sequence<Date> & Sendable)

    @available(FoundationPreview 6.3, *)
    public func recurrences(of start: Date,
                            in range: PartialRangeFrom<Date>
                            ) -> some (Sequence<Date> & Sendable)

    @available(FoundationPreview 6.3, *)
    public func recurrences(of start: Date,
                            in range: ClosedRange<Date>
                            ) -> some (Sequence<Date> & Sendable)
}

これにより、「ある日付以降のすべての発生日」を素直に書けるようになります。

for date in recurrence.recurrences(of: birthday, in: rangeStart...) {
    // 2000 年以降のすべての発生日
}

...rangeEndPartialRangeThrough<Date>)や ..<rangeEndPartialRangeTo<Date>)、rangeStart...rangeEndClosedRange<Date>)も同様に渡せます。Date.distantPastDate.distantFuture を端に詰めたり、列挙後にフィルタしたりする必要はなくなります。

これは単に書き方が短くなるだけでなく、性能面でも有利です。範囲外の発生日を計算してから捨てるのではなく、指定された範囲に必要な分だけを生成するため、不要な計算を省けます。

API としては既存の recurrences(of:in:) にオーバーロードを追加するだけなので、既存コードへの影響はありません。