Calendar.nextDate() is acting really weird when us

2019-04-27 17:49发布

问题:

In my app I need to get the previous 4am, not the 4am of the current date.
For example:

  • if it's March 05, 10:00 am then I should expect to get back: March 05, 4:00 am
  • if it's March 05, 02:00 am then I should expect to get back: March 04, 4:00 am

To achieve that I'm using Calendar.nextDate. It works well for everyday of the month except the last day of each month.

This is an example of the happy path, where it works as expected:

let components = DateComponents(hour: 4, minute: 0, second: 0)

let date =  Calendar.current.date(from: DateComponents(year: 2018,
                                                       month: 03, 
                                                       day: 05, 
                                                       hour: 13, 
                                                       minute: 25, 
                                                       second: 0))!
let result = Calendar.current.nextDate(after: date, 
                                       matching: components,
                                       matchingPolicy: .nextTime,
                                       direction: .backward)

Output:

date = "Mar 5, 2018 at 1:25 PM"
result = "Mar 5, 2018 at 4:00 AM"

This is an example of the dark sad path, where it does NOT act as expected:

let components = DateComponents(hour: 4, minute: 0, second: 0)

let date =  Calendar.current.date(from: DateComponents(year: 2018,
                                                       month: 03, 
                                                       day: 31, 
                                                       hour: 13, 
                                                       minute: 25, 
                                                       second: 0))!
let result = Calendar.current.nextDate(after: date, 
                                       matching: components,
                                       matchingPolicy: .nextTime,
                                       direction: .backward)

Output:

date = "Mar 31, 2018 at 1:25 PM"
result = "Mar 30, 2018 at 4:00 AM"
// it should give me this instead: "Mar 31, 2018 at 4:00 AM"

Any idea why this is happening? is it a bug in Apple's code, should I report it? or am I using this wrong?
If it's an Apple bug, is there another way of achieving what I need in the meantime?

Update:

After looking at some Swift source code, I noticed that the issue is not with the Calendar.nextDate() function specifically. nextDate() uses Calendar.enumerateDates() and that's where the real issue is. I couldn't find the source code for that function though. This is the same bug using Calendar.enumerateDates():

let components = DateComponents(hour: 4, minute: 0, second: 0)

let date =  Calendar.current.date(from: DateComponents(year: 2018,
                                                       month: 03, 
                                                       day: 31, 
                                                       hour: 13, 
                                                       minute: 25, 
                                                       second: 0))!

Calendar.current.enumerateDates(startingAfter: inputDate,
                                matching: components, 
                                matchingPolicy: .nextTime, 
                                direction: .backward) { (date, exactMatch, stop) in
    let result = date
    // result = "Mar 30, 2018 at 4:00 AM"
    stop = true
}

Also, this is a similar issue: Swift's Calendar.enumerateDates gives incorrect result when starting on February

回答1:

This does appear to be a bug that should be reported to Apple. Be sure you include a runnable test app demonstrating the issue. Note that this bug only appears in iOS. In macOS (at least a macOS playground), the code works as expected.

In the meantime there is a fairly simple workaround.

Change your code to use .forward instead of .backward. This will give the next date instead of the previous date. There does not seem to be any bugs going in that direction. Then use:

Calendar.current.date(byAdding: .day, value: -1, date: result)

to get your desired result by going back one day.