Have I found a bug in DateComponentsFormatter? - ios

In answering this question on how to create a "xxx <largest_time_units> ago" string based on comparing two dates, I wrote out a complete solution to the problem.
It involves using a DateComponentsFormatter with maximumUnitCount set to 1 and unitsStyle set to .full. Here is the code to create the DateComponentsFormatter:
var timeFormatter:DateComponentsFormatter = {
let temp = DateComponentsFormatter()
temp.allowedUnits = [.year, .month, .weekOfMonth, .day, .hour, .minute, .second]
temp.maximumUnitCount = 1
temp.unitsStyle = .full
return temp
}()
Then I wrote a function that uses the DateComponentsFormatter to output "xxx units ago" strings:
//Use our DateComponentsFormatter to generate a string showing "n <units> ago" where units is the largest units of the difference
//Between the now date and the specified date in te past
func timefromDateToNow(_ pastDate: Date) -> String {
if let output = timeFormatter.string(from: pastDate, to: now) {
return output + " ago"
} else {
return "error"
}
}
I calculate dates in the past with code like this:
let value = Double.random(in: min ... max)
let past = now.addingTimeInterval(value)
Strangely, for certain values of value, I'm getting the string "0 months ago". The value I was able to capture was -408754.0, which is about 4 days, 17 hours.
Why would calling timeFormatter.string(from: Date(timeIntervalSinceNow: -408754.0), to: Date()) return a string of 0 months? It should display a result of "4 days"!

If you want a reference to how far long ago, consider RelativeDateTimeFormatter, e.g.
let formatter = RelativeDateTimeFormatter()
formatter.dateTimeStyle = .named
formatter.unitsStyle = .spellOut
formatter.formattingContext = .beginningOfSentence
let date = Date()
let longTimeAgo = date.addingTimeInterval(-10_000_000)
print(formatter.localizedString(for: longTimeAgo, relativeTo: date)) // Three months ago
let veryLongTimeAgo = date.addingTimeInterval(-100_000_000)
print(formatter.localizedString(for: veryLongTimeAgo, relativeTo: date)) // Three years ago

Related

Why can DateComponentsFormatter produce different results if I pass a TimeInterval VS let it calculate the TimeInterval itself?

I've noticed what appears to be an inconsistency when using the DateComponentsFormatter class. Perhaps I'm using it incorrectly or maybe this is actually what's supposed to happen.
In the below code I create 2 dates and then create an interval string. However if I use method 1 which is currently commented I get a different result than method 2.
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss"
let date1 = dateFormatter.date(from: "2000/01/01 00:00:00") ?? Date()
let date2 = dateFormatter.date(from: "2001/02/02 01:01:01") ?? Date()
let dateComponentsFormatter = DateComponentsFormatter()
dateComponentsFormatter.unitsStyle = .abbreviated
dateComponentsFormatter.allowedUnits = [.year, .month, .day, .hour, .minute, .second]
// Method 1 - produces "1y 1mo 1d 1h 1min. 1s."
// let outputString = dateComponentsFormatter.string(from: date1, to: date2)
// Method 2 - produces "1y 1mo 2d 1h 1min. 1s."
let timeInterval: TimeInterval = date2.timeIntervalSince(date1)
let outputString = dateComponentsFormatter.string(from: timeInterval)
Even more interestingly, if I reverse the dates in both methods, method 1 remains consistent just adding a negative sign, where as method 2 adds a negative sign but also changes from 2 days to 1 day which is the same result as method 1.
So the question is, why does method 2 not work consistently? Using the following random dates I get either 1 or 0 days depending on which method I use.
let date1 = dateFormatter.date(from: "2020/11/24 17:02:39") ?? Date()
let date2 = dateFormatter.date(from: "2021/12/25 18:03:40") ?? Date()
Is there an explanation for this or am I doing something wrong? What am I missing? It seems like it's method 2 that's the problem. Could it be that the timeIntervalSince function is at fault? But shouldn't it have the same implementation as the dateComponentsFormatter function?
Thanks.

Calculate average time between an array of dates

I have an array of objects which the app gets from a WebService, each object has a createdTime and objects are created randomly from 6 in the morning to midnight.
I want to know what is the average time between each object creation.
What is the best and most efficient way to implement it?
The dates are in this format: "CreatedTime": "2019-02-18T22:06:30.523"
The average date interval is the time elapsed between the first and last date and divide by n-1, the number of intervals. That’s going to be most efficient.
This works because the average is equal to the sum of the intervals divided by the number of intervals. But the sum of all the intervals is equal to the difference between the first and last date.
Assuming your date strings are already in order, just grab the first and last, calculate the difference and divide.
let dateStrings = ["2019-02-18T18:06:30.523", "2019-02-18T19:06:30.523", "2019-02-18T21:06:30.523"]
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) // I’m going to assume it’s GMT; what is it really?
guard dateStrings.count > 1,
let lastDateString = dateStrings.last,
let lastDate = dateFormatter.date(from: lastDateString),
let firstDateString = dateStrings.first,
let firstDate = dateFormatter.date(from: firstDateString) else { return }
let average = lastDate.timeIntervalSince(firstDate) / Double(dateStrings.count - 1)
That’s in seconds. If you’d like a nice string format and don’t care about milliseconds, the DateComponentsFormatter is convenient for localized strings:
let dateComponentsFormatter = DateComponentsFormatter()
dateComponentsFormatter.allowedUnits = [.hour, .minute, .second]
dateComponentsFormatter.unitsStyle = .full
let string = dateComponentsFormatter.string(from: average)
That produces:
"1 hour, 30 minutes"
Or you can, less efficiently, build the dates array:
let dateStrings = ["2019-02-18T18:06:30.523", "2019-02-18T19:06:30.523", "2019-02-18T21:06:30.523"]
guard dateStrings.count > 1 else { return }
let dates = dateStrings.map { dateFormatter.date(from: $0)! }
Then you could build an array of intervals between those dates:
var intervals: [TimeInterval] = []
for index in 1 ..< dates.count {
intervals.append(dates[index].timeIntervalSince(dates[index-1]))
}
And then average them:
let average = intervals.reduce(0.0, +) / Double(intervals.count)
And format to taste:
let dateComponentsFormatter = DateComponentsFormatter()
dateComponentsFormatter.allowedUnits = [.hour, .minute, .second]
dateComponentsFormatter.unitsStyle = .full
let string = dateComponentsFormatter.string(from: average)

how to get this weekend date in swift? [duplicate]

This question already has answers here:
How do I find the beginning of the week from an NSDate?
(7 answers)
Closed 4 years ago.
If current date is 3 August 2018 (friday), i want to get 4 & 5 august 2018 (saturday and sunday)
if current date is 4 august (saturday), I still want to get 4 & 5 august 2018 (saturday and sunday)
if current date is 6 august (monday) then I want to get 11 & 12 August (saturday and sunday)
how to do that in swift?
Calendar has convenience methods for that
dateIntervalOfWeekend(containing:start:interval:) checks if the given date is in a weekend and returns the startDate and interval(duration in seconds) in the inout parameters. The Bool return value is true if the given date is within a weekend.
nextWeekend(startingAfter:start:interval:) returns startDate und interval in the inout parameters for the upcoming (.forward parameter) or passed (.backward) weekend.
let now = Date()
var startDate = Date()
var interval : TimeInterval = 0.0
if !Calendar.current.dateIntervalOfWeekend(containing: now, start: &startDate, interval: &interval) {
Calendar.current.nextWeekend(startingAfter: now, start: &startDate, interval: &interval, direction: .forward)
}
print(startDate, startDate.addingTimeInterval(interval))
If you need start of Saturday and start of Sunday then replace the last line with
let endDate = startDate.addingTimeInterval(interval-1)
print(startDate, Calendar.current.startOfDay(for: endDate))
Alternatively – suggested by Martin R (thanks) – use dateIntervalOfWeekend(containing:) / nextWeekend(startingAfter:) which both return a DateInterval object containing start, end and duration
let now = Date()
let calendar = Calendar.current
let weekEndInterval = calendar.dateIntervalOfWeekend(containing: now) ?? calendar.nextWeekend(startingAfter: now)!
let startDate = weekEndInterval.start
let endDate = startDate.addingTimeInterval(weekEndInterval.duration-1)
print(startDate, calendar.startOfDay(for: endDate))
let (nextSaturday,orderTotal) = getWeekends()
print(nextSaturday)
print(nextSunday)
func getWeekends() -> (Date,Date) {
let today = Date()
let calendar = Calendar.current
let todayWeekday = calendar.component(.weekday, from: today)
let addWeekdays = 7 - todayWeekday
var components = DateComponents()
components.weekday = addWeekdays
let nextSaturday = calendar.date(byAdding: components, to: today)
components.weekday = addWeekdays + 1
let nextSunday = calendar.date(byAdding: components, to: today)
return (nextSaturday,nextSunday)
}

Date from string is not coming properly and two dates difference also using Swift 3?

I have a date in string format, example:- "2017-07-31" or can be multiple dates (any) in string format. My requirement is to check this date to current date and if it is greater than 0 and less than 15, then that time I have to do another operation.
So first I am converting that date string to in date format. But it is giving one day ago date. Here is my code:
//Date from string
func dateFromString(date : String) -> Date {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let currentDate = (dateFormatter.date(from: date))//(from: date))
return currentDate!
}
Ex. my date is "2017-08-30" and this function is returning 2017-08-29 18:30:00 +0000 in date format. It means 1 day ago. I am little bit confuse about dates operation. I read so many blogs also.
After that I have to check this date to current date if it is in between 0 < 15 than I will do other operation.
Comparing two dates:
extension Date {
func daysBetweenDate(toDate: Date) -> Int {
let components = Calendar.current.dateComponents([.day], from: self, to: toDate)
return components.day ?? 0
}
}
If my date is today date and comparing to tomorrow date then also it is giving 0 days difference. Why?
If – for example – the current date is 2017-07-31 at 11AM then the
difference to 2017-08-01 (midnight) is 0 days and 13 hours, and that's
why you get "0 days difference" as result.
What you probably want is to compare the difference between the start
of the current day and the other date in days:
extension Date {
func daysBetween(toDate: Date) -> Int {
let cal = Calendar.current
let startOfToday = cal.startOfDay(for: self)
let startOfOtherDay = cal.startOfDay(for: toDate)
return cal.dateComponents([.day], from: startOfToday, to: startOfOtherDay).day!
}
}
Try this method for convert string to date:
func dateFromString(date : String) -> Date {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
dateFormatter.timeZone = TimeZone.init(abbreviation: "UTC")
let currentDate = (dateFormatter.date(from: date))//(from: date))
return currentDate!
}
Try this to compare the time between two dates in seconds :
var seconds = Calendar.current.dateComponents([.second], from: date1!, to: date2!).second ?? 0
seconds = abs(seconds)
let min = seconds/60 // this gives you the number of minutes between two dates
let hours = seconds/3600 // this gives you the number of hours between two dates
let days = seconds/3600*24 // this gives you the number of days between two dates

How to format time intervals for user display (social network like) in swift?

I have a time interval, say, 12600, which is equivalent to 3 hours 30 minutes. How could I format any such time interval so that only the highest part of the interval (for example in this case figure, the hours) is kept and have the correct locale abbreviation be appended to the number. For example 10m (10 minutes), 3d (3 days), 1y (1 years).
EDIT: Here are some examples:
Time interval in: 90000 Whole string: 1d String out: 1d
Time interval in: 900 Whole string: 15m String out: 15m
Time interval in: 13500 Whole String: 3h 45m String out: 4h
As a general rule, apply the normal rounding rules (3.4 rounds down, 3.6 rounds up).
If you are targeting newer OS versions (iOS 13.5+, OS X 10.15+), you can use RelativeDateTimeFormatter:
let formatter = RelativeDateTimeFormatter()
formatter.dateTimeStyle = .named
for d in [-12600.0, -90000.0, -900.0, 13500.0] {
let str = formatter.localizedString(fromTimeInterval: d)
print("\(d): \(str)")
}
// Output
-12600.0: 3 hours ago
-90000.0: yesterday
-900.0: 15 minutes ago
13500.0: in 3 hours
For older OS versions, use DateComponentFormatter, available since iOS 8:
func format(duration: TimeInterval) -> String {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.day, .hour, .minute, .second]
formatter.unitsStyle = .abbreviated
formatter.maximumUnitCount = 1
return formatter.string(from: duration)!
}
for d in [12600.0, 90000.0, 900.0, 13500.0] {
let str = format(duration: d)
print("\(d): \(str)")
}
This prints:
12600.0: 4h
90000.0: 1d
900.0: 15m
13500.0: 4h
Just in case anyone wants it.. Swift 4
extension TimeInterval {
func format(using units: NSCalendar.Unit) -> String? {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = units
formatter.unitsStyle = .abbreviated
formatter.zeroFormattingBehavior = .pad
return formatter.string(from: self)
}
}
Example usage:
let value:TimeInterval = 12600.0
print("\(value.format(using: [.hour, .minute, .second])!)")
and the result will be:
3h 30m 0s
Swift 3 extension:
extension TimeInterval {
func format() -> String? {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.day, .hour, .minute, .second, .nanosecond]
formatter.unitsStyle = .abbreviated
formatter.maximumUnitCount = 1
return formatter.string(from: self)
}
}
Take a look at the NSDateComponentsFormatter class. It lets you calculate whatever units you want either using 2 dates or using an NSTimeInterval, and supports different languages and locales automatically. There have been a couple of posts here in SO on the subject.
You can use NSDate and NSCalendar. You can say something like:
let timeInterval:Double = 12600
let calendar = NSCalendar.currentCalendar()
let date = NSDate(timeInterval: -timeInterval, sinceDate: NSDate())
let components = calendar.components([.Year,.Day,.Hour, .Minute, .Second, .Nanosecond], fromDate: date, toDate: NSDate(), options: [])
let hour = components.hour //3
let minute = components.minute //30
Per duncan's and rmaddy's suggestions use NSDateComponentsFormatter
I created a function for you! I hope you like it. And this is super easy to implement and very customizable.
func totime(time: Double) -> (String) {
var timex = time
var fancytime: String = "a while"
if time < 61 {
fancytime = "\(timex)s"
} else if time < 3601 {
timex = timex/60
timex = round(timex)
fancytime = "\(timex)m"
} else if time < 86401 {
timex = timex/3600
timex = round(timex)
fancytime = "\(timex)h"
} else if Double(time) < 3.15576E+07 {
timex = timex/86400
timex = round(timex)
fancytime = "\(timex)d"
} else {
fancytime = "more than one year"
}
fancytime = fancytime.stringByReplacingOccurrencesOfString(".0", withString: "")
return fancytime
}
Tested, and it works flawlessly:
print(totime(90000)) // prints "1d"
print(totime(900)) // prints "15m"
print(totime(13500)) // prints "4h"
Just call it with totime(Double).

Resources