I am using the built in DateComponentsFormatter to figure out the number of years between the current day and another date. If the date gets too far in the past for example 1940, the returned units are wrong:
let dateComponents = DateComponents(year: 1940, month: 1, day: 2, hour: 1, minute: 1, second: 1)
let date = Calendar.current.date(from: dateComponents)!
let dateComponentsFormatter = DateComponentsFormatter()
dateComponentsFormatter.allowedUnits = [.year]
dateComponentsFormatter.unitsStyle = .full
print(dateComponentsFormatter.string(from: abs(date.timeIntervalSinceNow)))
This prints -58 years which is incorrect. Is this an Apple bug? Am I doing something wrong?
It is a bug in DateComponentsFormatter's implementation.
They apparently are using Int32 internally to represent time intervals.
I ran into the same issue, it's very easy to reproduce:
let interval: TimeInterval = ... some number here
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.second]
formatter.unitsStyle = .abbreviated
print(formatter.string(from: interval) ?? "")
This is supposed to output just the number of seconds, without any special calculations.
Even that only works as expected up until 2147483647 (which happens to be Int32.max). Everything over that simply overflows and produces incorrect results.
This maximum "supported" number of seconds is 68y 2w 4d 3h 14m 7s, which explains why you are seeing it also.
UPD
Someone already filed a radar: http://www.openradar.me/32513237 and mentioned a workaround there, and it works!
In your case that would be instead using:
let date = Calendar.current.date(from: dateComponents)!
let now = Date()
dateComponentsFormatter.string(from: date, to: now)
Related
I have datecomponent objects that represent some time in the future. I want to calculate how many dates from now until that date. I'm also including representation of the dates simply as dates. What I'm finding is that when I am trying to show how many there are to a date that is 'tomorrow' it's showing 0. To my mind it should be showing 1. I can try a hacky way of just adding 1 to my count but I'm wondering is it because it's trying to round to the nearest 24 hours or something? If so how can I 'fix' it?
Here is my sample code:
let myPreviousRelevantDate = self.datePickerOutlet.date
let nextDate = Date(timeInterval: Double(86400 * (myDurationInDaysAsInt)), since: myPreviousRelevantDate!)
let daysToNextDate = Calendar.current.dateComponents([.day], from: Date(), to: nextDate).day!
What I'd like to avoid is the number of days to the target date changing during the day also - i.e. regardless of the timestamp of my target date - the number of days to that day remaining constant until midnight is reached.
If your intent is to calculate the number of days using a timeless calendrical calculation what you need is to use noon time. Note that not every day has 24 hours, you should always use calendar method to add days to a date:
extension Date {
var noon: Date {
Calendar(identifier: .iso8601)
.date(
bySettingHour: 12,
minute: 0,
second: 0,
of: self
)!
}
}
let daysToNextDate = Calendar.current.dateComponents([.day], from: Date().noon, to: nextDate.noon).day!
Very new to IOS, I need to set a Date hour and minutes, I do like this:
let date = Calendar.current.date(bySettingHour: prayerTimeHour, minute: prayerTimeMinutes + Int(adjustement)! , second: 0, of: Date())!
prayerTimeMinutes + Int(adjustement)! in some cases are more than 60 so the app crash because the Date is nil
Let the Calendar do all the math for you. There is no such time as "09:61" and Calendar is telling you so.
// It's not clear that you want the current calendar here. It may not be Gregorian.
let calendar = Calendar(identifier: .gregorian)
// Using ! means you're promising prayerTimeHour and prayerTimeMinutes are valid.
let baseDate = calendar.date(bySettingHour: prayerTimeHour,
minute: prayerTimeMinutes,
second: 0,
of: Date())!
// ! should be safe because the Gregorian calendar will have a date for any number
// of minutes into the future
let adjustedData = calendar.date(byAdding: .minute,
value: adjustement,
to: baseDate)!
I just ran into something strange with the Calendar API when trying to get the first day of a particular month.
import Foundation
//2017-12-01 23:00:00 +0000
let date = Date(timeIntervalSinceReferenceDate: 533862000.0)
let secondDate = Calendar.current.date(bySetting: .day, value: 2, of: date)
let thirdDate = Calendar.current.date(bySetting: .day, value: 1, of: date)
print("Passed")
In a Swift 4 Playground, the property observer on the right displays values for date and secondDate, but enters in an infinite loop when evaluating the let thirdDate = ... expression.
Activity Indicator reports of 99% CPU usage and RAM usage increases at around +100MB/second, and so does the Debug Navigator in XCode when ran inside an iOS project (until the app is killed at around 1.3GB RAM usage)
This is not happening for all dates, but is consistent with this particular date.
I've gone through the docs of Calendar but I can't figure out why this happens.
I came up with a workaround like this :
let date = Calendar.current.startOfDay(for: someDate)
var dc = Calendar.current.dateComponents([.month, .year, .day], from: date)
dc.day = 1
let firstDayOfMonth = Calendar.current.date(from: dc)
But I'd very much like to get an explanation as to why Calendar.current.date(bySetting: .day, value: 1, of: date) is failing that hard. Is there something I missed here?
Edit: it happens for the timezone GMT+2, but doesn't anymore if I change my system settings to GMT+1.
Consider the following code:
import UIKit
let date = Date()
guard let nycTimeZone = TimeZone(abbreviation: "EST"),
let nzTimeZone = TimeZone(abbreviation: "NZDT") else {
fatalError()
}
var nycCalendar = Calendar(identifier: .gregorian)
nycCalendar.timeZone = nycTimeZone
var nzCalendar = Calendar(identifier: .gregorian)
nzCalendar.timeZone = nzTimeZone
let now = Date()
let nycDayOfEra = nycCalendar.ordinality(of: .day, in: .era, for: now)
let nycDayOfYear = nycCalendar.ordinality(of: .day, in: .year, for: now)
var nzDayOfEra = nzCalendar.ordinality(of: .day, in: .era, for: now)
var nzDayOfYear = nzCalendar.ordinality(of: .day, in: .year, for: now)
As I write this, NYC time and Aukland NZ time give different days. That's the case I'm interested in.
With the code above, the results for nycDayOfYear and nzDayOfYear are different (as of this writing I get nycDayOfYear=42 and nzDayOfYear=43.)
That is as expected, and as desired. (I was working to answer a "how do I calculate the number of days of difference in two Dates evaluated in different time zones?" question.)
However, it would take a bunch of messy adjustments to make the above day-of-year calculation and figure out the number of days of difference between those local dates when they span year boundaries.
I therefore tried to do the calculations using ordinality(of: .day, in: .era, for: date).
However, the calculations based on calendar era give the same value regardless of the time zone of the calendar used to make do the calculation.
Why is that?
What would be a simpler way to calculate the number of calendar days difference between two dates WHEN EXPRESSED IN DIFFERENT LOCAL TIME ZONES? Like I said, my code that calculates the day of year would need additional logic added to handle dates that span calendar year boundaries.
Note that this is a different question than "How many days difference is there between 2 dates". In my question I want both dates to be expressed in different local time zones, and I'm interested in the difference in the calendar date of each of those date values.
Martin's comment about calendar calculations over long intervals giving unexpected results is as good an answer as any as to why it doesn't work.
I did come up with code that calculates the desired difference in calendar date values between 2 dates expressed in specific time zones:
let date = Date()
guard let nycTimeZone = TimeZone(abbreviation: "EST"),
let nzTimeZone = TimeZone(abbreviation: "NZDT") else {
fatalError()
}
var nycCalendar = Calendar(identifier: .gregorian)
nycCalendar.timeZone = nycTimeZone
var nzCalendar = Calendar(identifier: .gregorian)
nzCalendar.timeZone = nzTimeZone
let now = Date()
let nycDateComponents = nycCalendar.dateComponents([.month, .day, .year], from: now)
let nzDateComponents = nzCalendar.dateComponents([.month, .day, .year], from: now)
let difference = Calendar.current.dateComponents([.day],
from: nycDateComponents,
to: nzDateComponents)
let daysDifference = difference.days
First I convert the 2 dates to month/day/year DateComponents values using calendars set to their specific time zone.
Then I use the Calendar function dateComponents(_:from:to:), which lets you calculate the difference between 2 DateComponents values, in whatever units you want to use to compare them. (days, in this case)
When i get the components like seconds in:
NSCalendar.currentCalendar().components(.Second, fromDate: savedDate, toDate: todayDate, options: []).second
I get the seconds since that date.
But i am trying to get all the components like days and hours it gives me the total in each one of them.
If i get the hours lets say 36h and the days will show one day.
1 I want it to show 1 day and 12 hours.
2. How to start a counter or a timer with these components?
You have to specify all units in the components parameter you want to be considered.
let components = NSCalendar.currentCalendar().components([.Second, .Minute, .Hour, .Day], fromDate: savedDate, toDate: todayDate, options: [])
print(components)
Alternatively use NSDateComponentsFormatter, it can create a (localized) string. Specify the units to be displayed and the style.
let formatter = NSDateComponentsFormatter()
formatter.allowedUnits = [.Day, .Hour, .Minute, .Second ]
formatter.unitsStyle = .Full
let dateString = formatter.stringFromDate(savedDate, toDate: todayDate)!
print(dateString)
Regarding your second question let the counter count in seconds and format the seconds for human reading accordingly.