Search code examples
swiftdateswift3nsdatensdatecomponents

Swift: unexpected behavior when setting DateComponents year


The example code below gets DateComponents from the current Date, modifies the components, and creates a new Date from the modified components. It also shows creating a new DateComponents object, filling it out, and then creating a new Date from that.

import Foundation

let utcHourOffset = -7.0
let tz = TimeZone(secondsFromGMT: Int(utcHourOffset*60.0*60.0))!
let calendar = Calendar(identifier: .gregorian)
var now = calendar.dateComponents(in: tz, from: Date())

// Get and display current date
print("\nCurrent Date:")
print("\(now.month!)/\(now.day!)/\(now.year!) \(now.hour!):\(now.minute!):\(now.second!)   \(now.timeZone!)")
let curDate = calendar.date(from: now)
print("\(curDate!)")

// Modify and display current date
now.year = 2010
now.month = 2
now.day = 24
now.minute = 0
print("\nModified Date:")
print("\(now.month!)/\(now.day!)/\(now.year!) \(now.hour!):\(now.minute!):\(now.second!)   \(now.timeZone!)")
let modDate = calendar.date(from: now)
print("\(modDate!)")

// Create completely new date
var dc = DateComponents()
dc.year = 2014
dc.month = 12
dc.day = 25
dc.hour = 10
dc.minute = 12
dc.second = 34
print("\nNew Date:")
print("\(dc.month!)/\(dc.day!)/\(dc.year!) \(dc.hour!):\(dc.minute!):\(dc.second!)   \(now.timeZone!)")
let newDate = calendar.date(from: dc)
print("\(newDate!)")

In the case where I modify the components, setting different year, month, day, etc., then use the components to get a date, I get the unexpected result that the new date has all the modified components except the year, which remains unchanged.

In the case where I create a DateComponents object and fill it out then create a Date from it, it works as expected.

The output of the code is shown below:

Current Date:
3/9/2017 19:5:30   GMT-0700 (fixed)
2017-03-10 02:05:30 +0000

Modified Date:
2/24/2010 19:0:30   GMT-0700 (fixed)
2017-02-25 02:00:30 +0000

New Date:
12/25/2014 10:12:34   GMT-0700 (fixed)
2014-12-25 17:12:34 +0000

I expected the modified Date to be 2010-02-25 02:00:30 +0000 rather than 2017-02-25 02:00:30 +0000. Why isn't it? Why does it work in the second case?

The docs for DateComponents say: "An instance of NSDateComponents is not responsible for answering questions about a date beyond the information with which it was initialized...". Being that the DateComponents object was initialized with a year, it doesn't seem like this would apply, but it's the only thing I saw in the docs that might explain the behavior I observe.


Solution

  • If you log now and dc you will see the problem. now is being created from a Date. This fills in all of the date components including yearForWeekOfYear and several of the weekday related components. These components are causing modDate to come out incorrectly.

    newDate works as expected because only the specific components are being set.

    You can get modDate to come out correctly if you reset some of the extra components. Specifically, adding:

    now.yearForWeekOfYear = nil
    

    just before creating modDate will result in the expected date for modDate. Of course the best solution is to create a new instance of DateComponents and use specific values from the previous DateComponents as needed:

    let mod = DateComponents()
    mod.timeZone = now.timeZone
    mod.year = 2010
    mod.month = 2
    mod.day = 24
    mod.hour = now.hour
    mod.minute = 0
    mod.second = now.second
    print("\nModified Date:")
    print("\(mod.month!)/\(mod.day!)/\(mod.year!) \(mod.hour!):\(mod.minute!):\(mod.second!)   \(mod.timeZone!)")
    let modDate = calendar.date(from: mod)
    print("\(modDate!)")