swiftlang / swift-foundation

The Foundation project
Apache License 2.0
2.39k stars 155 forks source link

Calendar nextDate/enumerateDates provides wrong dates when using backward search direction #348

Open alpennec opened 10 months ago

alpennec commented 10 months ago

Feedback: FB13462533 Post on Swift Forums: https://forums.swift.org/t/calendar-nextdate-enumeratedates-provides-wrong-dates/68943


In my app, I need to get previous dates that matches specific months in the year (think of it like recurring events, and I want to find the last occurence of the recurrence based on the current date and time: every month on the 13th day, or every month on the 3rd Friday -> what is the previous date that matches these components).

It mainly works but for an unknown reason, I get wrong dates under certains circumstances.

It does not work as expected for the 9nth month in the Gregorian calendar (which is September). When the after date is Date.now (currently in December), I expect the previous September month to be in 2023. But the first date returned is in 1995.

var calendar: Calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone.autoupdatingCurrent

let matchingDateComponents: DateComponents = DateComponents(month: 09)

let date: Date? = calendar.nextDate(
    after: Date.now,
    matching: matchingDateComponents,
    matchingPolicy: .nextTime,
    direction: .backward
)

I tested with other time zones: for some, the result are corrects, for others, it's not. I used the following code to print the result for all time zones:

for zone in TimeZone.knownTimeZoneIdentifiers {
    var calendar: Calendar = Calendar(identifier: .gregorian)
    calendar.timeZone = TimeZone(identifier: zone) ?? .autoupdatingCurrent

    let matchingDateComponents: DateComponents = DateComponents(month: 9)

    let date: Date? = calendar.nextDate(
        after: Date.now,
        matching: matchingDateComponents,
        matchingPolicy: .nextTime,
        direction: .backward
    )

    print(date, zone)
}

I also tested with other date components to see if the dates provided by these methods are correct when searching backward. I tested using a .nextTime and .strict matchingPolicy.

If we assume that these methods provide the first day of the month for a forward and backward search when we only use a month component (see additional notes below regarding this), here are some results:

DateComponents(month: 1) Correct years, wrong days. I mainly get dates around January the 5th. But the years seem to be correct. In this case, only the America/Los_Angeles date seems to be correct. Even GMT is wrong.

DateComponents(month: 2) Correct years, correct days

DateComponents(month: 3) Correct years, wrong days

DateComponents(month: 4) Correct years, correct days

DateComponents(month: 5) Correct years, wrong days. I mainly get dates around May the 3rd.

DateComponents(month: 6) Correct years, correct days

DateComponents(month: 7) Correct years, wrong days

DateComponents(month: 8) Correct years, wrong days

DateComponents(month: 9) Wrong years, correct days

DateComponents(month: 10) Correct years, wrong days

DateComponents(month: 11) Correct years, correct days

DateComponents(month: 12) Correct years, correct days if we expect to get the start of the December from the previous year when we already are in December (and not the start of the current month).

If I additionally specify a day in the date components like DateComponents(month: 1, day: 1) or DateComponents(month: 4, day: 20), I get the correct results for all time zones, except for September (still wrong years).


Should we avoid using these methods with a backward direction?


Additional notes

When searching .forward, and only specifying a month in the date components, we expect to get the next start of the month. So if I request the nextDate that matches December after December the 12th in 2023, I expect to get December the 1st in 2024. This is the actual result provided by the nextDate method.

It seems that when we search .backward, we also get the start of the month when we only specify the month. I was expecting to get the last day of the specified month, not the first one. To me, the next date that matches only a month when searching backward is the last day (first day encountering the specified month if we go back day by day).

vashpan commented 1 week ago

Hello, I encountered very similar and probably related issue, with date search going backward:

On month October of each year after 1995, year that is returned is always 1995 for some reason.

https://gist.github.com/vashpan/8eda15eee0954b2ca582fda27f6307a5