Skip to content

RecurrenceRule produces incorrect sequences for start dates with non-zero nanosecond components #1283

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
lukaskollmer opened this issue May 8, 2025 · 0 comments · May be fixed by #1284
Open

Comments

@lukaskollmer
Copy link

lukaskollmer commented May 8, 2025

SF-0009 states that the first element in a sequence of Date occurrences computed from a RecurrenceRule is always the start date (see SF-0009#Usage):

A recurrence rule of a given frequency repeats the start date with the interval of that frequency. For example, assume that now it is February 09 2024, 13:43. Creating a daily recurrence would yield a result for each following date at the same time:

var recurrence = Calendar.RecurrenceRule(calendar: .current, frequency: .daily)
for date in recurrence.recurrences(of: .now) {
    // 2024-02-09, 13:43
    // 2024-02-10, 13:43
    // 2024-02-11, 13:43
    // 2024-02-12, 13:43
    // ...
}

However, this currently is only the case for start dates with a 0 nanosecond component. Any other start date (e.g. also Date.now, unless you happen to be creating the date exactly at the start of a second, which admittedly is rather unlikely), will result in recurrences(of:) returning a sequence that will skip the start date.

The reason for this is that Calendar.DatesByRecurring.Iterator internally uses Calendar._dates(startingAfter:), which in turn uses Calendar._unadjustedDates(after:), which only produces Dates with whole-integer seconds.

The tests currently don't catch this, since all RecurrenceRule tests only use dates with whole-integer components as input.

lukaskollmer added a commit to StanfordSpezi/SpeziScheduler that referenced this issue May 12, 2025
# `observeNewOutcomes` observation API, fix `.once` schedules

## ♻️ Current situation & Problem
This PR marks the Scheduler's `sinkDidSavePublisher(into:)` function as
`@_spi(APISupport) public`, thereby allowing other Spezi packages to
observe changes to the scheduler's internal ModelContext.
The specific use case here is SpeziStudy wanting to be able to run code
in response to specific `Event`s getting marked as completed.
It also adds a new `@_spi(APISupport) public func observeNewOutcomes(_
handler: @mainactor @escaping (Outcome) -> Void) -> AnyObject` to the
Scheduler, which allows registering a callback that will get invoked
every time a new `Outome` is added to the scheduler.
Note: This API is intentionally closure-based, rather than being eg an
`AsyncStream` as discussed below. The reason for this is that `Outcome`
isn't sendable, but the `AsyncStream.Continuation`'s yield function
operates on a `sending` parameter.

Additionally, this PR attempts to fix #56 and fix #64.

Additionally, this PR attempts to work around
swiftlang/swift-foundation#1283, which
currently results in using eg `Date.now` as the start date of some
`Schedule`s producing incorrect schedule occurrences.

### Alternatives considered
Instead of directly exposing the "raw" did change notification (which
IMO we should still do, anwyay), we could also give the `Scheduler`
directly a nice API for observing changes to it's underlying SwiftData
model.
For example, we could have a function `func completions(of task: Task)
-> some AsyncSequence<Outcome, Never>` that returns an `AsyncStream`
which yields individual `Outcomes` for the task, as they are saved into
the context.
(The immediate first issue here would bw how this should handle new Task
versions getting introduced while there still are active observers on
the now-outdated previous version...)
IMO we should go with the simpler change from this PR first, and then
maybe add a more sophisticated API in the future, should we need to.

## ⚙️ Release Notes
- add `@_spi(APISupport) public func observeNewOutcomes(_ handler:
@mainactor @escaping (Outcome) -> Void) -> AnyObject`
- restored the ability to use `.once` schedules (it no longer crashes)
- removed several `Duration` extensions (e.g.: `Duration.days(_:)`,
`.weeks(_:)`, etc). these have been moved to
[SpeziFoundation](https://github.com/StanfordSpezi/SpeziFoundation)


## 📚 Documentation
is documented


## ✅ Testing
- the UITests now schedule a `.once` task; the fact that this isn't
crashing the app is in itself already a regression test
- additionally, we have a View that registers a new-outcome observer
with the scheduler, completes an event, and checks that the observer has
been called with an outcome belonging to the just-completed event


## 📝 Code of Conduct & Contributing Guidelines
By creating and submitting this pull request, you agree to follow our
[Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant