Calendar's date(bySetting:value:of:) returns nil when changing year

I'd like to change the year component of a Date. I tried this code, but date2 is nil. How do I change the year component of a Date, and why does this code return nil?

let date = Date()
print(date)
let date2 = Calendar.current.date(bySetting: .year, value: 2020, of: date)
print(date2)

Hmmm, this is a tricky one. The date(bySetting:value:of:) method is more-or-less a wrapper around the Objective-C -nextDateAfterDate:matchingUnit:value:options: method. By default this searches forwards, with the options parameter letting you request a backwards search (NSCalendarSearchBackwards). So things (kinda) work if you try to set the year to 2022:

let date2 = Calendar.current.date(bySetting: .year, value: 2022, of: date)
print(date2)
// -> Optional(2022-01-01 00:00:00 +0000)

However, this still isn’t the result you’re looking for, because the semantics of this method don’t really align with your requirements. Try this instead:

let c = Calendar(identifier: .gregorian)
var dc = c.dateComponents([.era, .year, .month, .day, .hour, .minute, .second], from: date)
dc.year = 2020
let date2 = c.date(from: dc)
print(date2)
// -> Optional(2020-07-29 08:17:33 +0000)

There are some serious caveats here:

  • In my code I hard-wired the calendar to Gregorian. The year 2020 only makes sense in that calendar. If you use a year like that and work in the current calendar and the user’s current calendar is something non-Gregorian you will run into problems. To see this in action, tweak the code above to use .buddhist (-:

  • There’s no guarantee that date2 won’t be nil. The date you’re looking for may not exist, or the current time might not exist within that date (due to a time discontinuity, typically a daylight saving time jump). Most calendars try to avoid returning nil and give you back something that’s kinda close, but nil is always a possibility.

If you can explain more about your high-level goal I may be able to suggest an alternative. For example, if you’re trying to calculate this time last year, date(byAdding:value:to:) is often the best choice.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Accepted Answer

I thought two dropdown buttons would be nicer, so I used two NSPopUpButtons, one which contains a list on month names, and one with a list of years.

In that case I recommend that you use these values to build a set of date components and then run that through the calendar to produce a new date each time. That’s much easier (and more performant) than dealing with date(bySetting:value:of:).

For example:

let m = 7
let y = 2020
let dc = DateComponents(year: y, month: m, day: 1, hour: 12)
let d = Calendar(identifier: .gregorian).date(from: dc)
print(d)
// -> Optional(2020-07-01 11:00:00 +0000)

Note how I’m pinning the day and hour. Pinning the day isn’t really necessary but pinning the hour is important because, depending on your time zone, you may end up landing on a date that doesn’t exist (some folks transition to DST at midnight).

If you want the start of the day, call Calendar.startOfDay(for:).

Also, I’m hard wiring Gregorian here. If you use Calendar.current you will encounter users who use non-Gregorian calendars. That’s fine, and you can definitely support that if you want, but it’s a bunch of extra work.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Calendar's date(bySetting:value:of:) returns nil when changing year
 
 
Q