Swift: unexpected behavior when setting DateComponents year - swift

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.

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!)")

Related

Time and Date Calculations

Using date format "EEEE:dd:MMM:HH:mm" returns the correct date and time, however, the day of the week is incorrect. For example a 750 nautical mile voyage conducted at 7.5 knots will take 100 hours. If I use my code to calculate the arrival time using a start of, say, Friday 1 Nov at 12:00 it returns Sunday 5 Nov at 16:00. Time and date are correct but day of the week is not. Should be Tuesday.
#IBAction func Calculate(_ sender: UIButton) {
let userCalendar = Calendar.current
let dateMakerFormatter = DateFormatter()
dateMakerFormatter.dateFormat = "EEEE:dd:MMM:HH:mm"
let distance = (Distance.text! as NSString).floatValue
let speed = (GndSpd.text! as NSString).floatValue
let calcDT = ((distance / speed) * 3600)
if var date = dateMakerFormatter.date(from: (DTG.text!)) {
date = dateMakerFormatter.date(from: (DTG.text!))!
var timeAdj = DateComponents()
timeAdj.second = Int(calcDT)
if var adj = userCalendar.date(byAdding: timeAdj, to: date) {
adj = userCalendar.date(byAdding: timeAdj, to: date)!
CalcDTG.text = dateMakerFormatter.string(from: adj)
}
}
}
You should use d for Day, not D
dateMakerFormatter.dateFormat = "EEEE:dd:MMM:HH:mm"
DateFormatter
You can't say the day of the week is incorrect when you're not giving a year.
The date formatter seems to ignore the day of the week when creating a date:
let dateMakerFormatter = DateFormatter()
dateMakerFormatter.dateFormat = "EEEE:dd:MMM:HH:mm"
let date = dateMakerFormatter.date(from: "Friday:01:Nov:12:00")!
print(date) -> 2000-11-01 12:00:00 +0000
print(dateMakerFormatter.string(from: date)) -> Wednesday:01:Nov:12:00
Hey presto, you're now in the year 2000, where 5 November did fall on a Sunday.
The important takeaway you need is that you should never, ever, ever, use strings to pass around date values in your code. Use Date. If you're getting a date from an API response, change it to a date on ingestion. If you're getting one from user entry, use a date picker or other control. If you're getting one from a string the user is typing in, I'd politely suggest you're making unnecessary work for yourself, but do make sure you fill in all the details the user doesn't give you.

Xcode playground prints wrong date value despite showing the correct value in the sidebar

After creating DateComponents() and fetching the Calendar.current and the Date(), I altered some date components and created a new date variable based on the altered components. The value of this variable displays correctly in the sidebar on Xcode playground but when I print this value it shows the wrong date. The same happens in the app code too (i.e. not just in the Playground).
// Initial set-up
var dateComponents = DateComponents()
let date = Date()
let calendar = Calendar.current
// Getting some components
var month = calendar.component(.month, from: date)
var year = calendar.component(.year, from: date)
var day = calendar.component(.day, from: date)
// Some sample code
if day > 15 {
day = 1
if month != 12 {
month += 1
} else {
month == 1
}
} else {
day = 15
}
// Create date from components
// Generic
dateComponents.hour = 00
dateComponents.minute = 00
// Specific
dateComponents.day = day
dateComponents.month = month
dateComponents.year = year
// Finalise the next resetDate
let resetDate = calendar.date(from: dateComponents)
resetDate // Displays correct value in the side bar
print(resetDate!) // Prints wrong value
The expected result of course is that the value of resetDate in both the last two lines should agree. On 31 August 2019 this is what I get in the sidebar:
"Sep 1, 2019 at 12:00 AM"
versus what the print statement displays:
2019-08-31 18:30:00 +0000
Output is correct. Please note that print date object direct alway received result on timezone +0 UTC. In your brain, just plus with your timezone is time in your timezone.

Swift - Date from components is incorrect when printed. Date has 2 values?

I'm trying to store a time of day in a Date:
let calendar = NSCalendar.init(identifier: .gregorian)
let components = NSDateComponents()
components.hour = 7
components.minute = 0
var newDate = calendar?.date(from: components as DateComponents)!
print(newDate!)
However, this yields some bizarre results when I try to print or otherwise use the value. Here's a Swift Playground of the results:
How can newDate be both 7:00AM and 11:56AM at the same time?
You didn't specify a time zone (by setting the timeZone property of either components or calendar). So the system used your local time zone to convert components to a date.
The playground system used your local time zone to convert newDate to the string “Jan 1, 1 at 7:00 AM”. (The playground system has special-case code for displaying Date objects. The special-case code uses your local time zone.)
The print function used the UTC time zone to convert newDate to the string “0001-01-01 11:56:02 +0000”. (The print function uses the CustomStringConvertible protocol. Date's CustomStringConvertible implementation uses the UTC time zone.)
I deduce that your local time zone is US/Eastern time, also known as America/New_York:
import Foundation
var calendar = Calendar(identifier: .gregorian)
let components = NSDateComponents()
components.hour = 7
components.minute = 0
for timeZoneIdentifier in TimeZone.knownTimeZoneIdentifiers {
calendar.timeZone = TimeZone(identifier: timeZoneIdentifier)!
let date = calendar.date(from: components as DateComponents)!
let dateString = "\(date)"
if dateString == "0001-01-01 11:56:02 +0000" {
print("\(timeZoneIdentifier) \(date)")
}
}
// Only one line of output: America/New_York 0001-01-01 11:56:02 +0000
So why the weird minutes and seconds in UTC? Because at noon on November 18, 1883, the US and Canada railway companies began using a new, standard time system, which is the time system we still use today. Observe:
import Foundation
let formatter = ISO8601DateFormatter()
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(identifier: "America/New_York")!
let components = NSDateComponents()
components.year = 1883
components.month = 11
components.day = 18
components.hour = 11
print(formatter.string(from: calendar.date(from: components as DateComponents)!))
// prints 1883-11-18T15:56:02Z
components.hour = 12
print(formatter.string(from: calendar.date(from: components as DateComponents)!))
// prints 1883-11-18T17:00:00Z
Prior to noon, the difference from UTC time is 4:56:02.
Before the advent of standard railway time, we typically defined local time based on local apparent noon (the moment when the sun is highest in the sky and shadows point exactly north or south or disappear entirely if the sun is directly overhead).
If we look at the definition of America/New_York in the tz Time Zone Database, we find this:
# From Paul Eggert (2014-09-06):
# Monthly Notices of the Royal Astronomical Society 44, 4 (1884-02-08), 208
# says that New York City Hall time was 3 minutes 58.4 seconds fast of
# Eastern time (i.e., -4:56:01.6) just before the 1883 switch. Round to the
# nearest second.
and a bit further down:
Zone America/New_York -4:56:02 - LMT 1883 Nov 18 12:03:58
This explains the difference we see above prior to noon on November 18, 1883.

Saving Date Components in Swift 3

I have troubles saving a time. Let me show you a screenshot, it will make clear what I'm trying to achieve here.
I am saving a day as an Int and from that I create a new Date, that is handled. Only thing what is missing is adding time to that date. How can I save AM or PM in a date?
Also, will that affect users who use 24-hour time in their iPhones? If I save 9 PM will their system know that I really mean 21 hrs for them?
Okay, I forgot about the code, here it is:
This is a piece of code where I am scheduling a notification
let calendar = Calendar.autoupdatingCurrent
guard let lastPeriod = RealmManager.sharedInstance.queryLastPeriod(), let predictionDate = lastPeriod.predictionDate else {
return
}
let day = predictionDate.day - DefaultsManager.getNotificationDays()
let newComponents = DateComponents(calendar: calendar, timeZone: .current, year: predictionDate.year, month: predictionDate.month, day: day, hour: time, minute: 0)
As you can see that hour part of newComponents is tricky. I only need to provide is it 9AM, 12AM or 9PM. How do I do that? (btw. time is just an Int I'm manually inputting for testing)
As far as I can tell, you use 24h-style hours (what else?).
let dc = DateComponents(year: 2016, month: 01, day: 11, hour: 14, minute: 20)
let d = Calendar.current.date(from: dc)
//> d: Date? = 2016-01-11 13:20:00 UTC
This is correct since my current timezone is CET (UTC+1).
You can use DateFormatter to print dates in different formats. For example:
let df = DateFormatter()
df.dateStyle = DateFormatter.Style.short
df.timeStyle = DateFormatter.Style.short
df.string(from: d!)
//> R4: String = "11/01/16 14:20"
Back to my timezone!
DateFormatter also has a method date(from: String) which you can use to parse dates:
df.date(from: "11/01/16 14:20")
//> R5: Date? = 2016-01-11 13:20:00 UTC
You will have to use settings of Calendar and/or DateFormatter to get 12h-style output. On the input site, you can just map xxAM to Int("xx")! and xxPM to 12 + Int("xx")!.

NSDateComponents weekOfYear returns wrong date

so i am making a basic week picker, where you can pick a week number and year, and get the corresponding date from the weeks startpoint.
A basic test scenario
let calendar = NSCalendar.currentCalendar()
let components = NSDateComponents()
components.year = 2015
components.weekOfYear = 15
print(calendar.dateFromComponents(components)!)
You can see, that i set the wanted year to 2015 and the wanted week to 15, but the result i get is: "2014-12-31 23:00:00 +0000"
... and thats really gets me hanging. No matter what week and year i choose, it will always be off and in december.
Thank you in advance for your help.
weekOfYear works in conjunction with yearForWeekOfYear:
let calendar = NSCalendar.currentCalendar()
let components = NSDateComponents()
components.yearForWeekOfYear = 2015
components.weekOfYear = 15
print(calendar.dateFromComponents(components)!)
// 2015-04-05 22:00:00 +0000
Update for Swift 3 and later:
let calendar = Calendar.current
var components = DateComponents()
components.yearForWeekOfYear = 2015
components.weekOfYear = 15
print(calendar.date(from: components)!)