Calculate average time between an array of dates - ios

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)

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.

Have I found a bug in DateComponentsFormatter?

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

How to calculate time (minutes) between two dates in swift?

What do we got: Date+time (format yyyy-mm-dd hh:mm a)
What are we looking for: Time difference in minutes
What operation: NewDate - OldDate
So, I wonder how I could accomplish above goal? I would like to format the date and time to US, regardless from which locale the user has. How can I do that?
Then I will save the 'oldTime' into UserDefaults, and use it for later calculation. The goal is to put the user on delay for 5 minutes and the calculations will be performed to determine if user should be on delay or not.
Just make a function that takes two dates and compares them like this.
import UIKit
func minutesBetweenDates(_ oldDate: Date, _ newDate: Date) -> CGFloat {
//get both times sinces refrenced date and divide by 60 to get minutes
let newDateMinutes = newDate.timeIntervalSinceReferenceDate/60
let oldDateMinutes = oldDate.timeIntervalSinceReferenceDate/60
//then return the difference
return CGFloat(newDateMinutes - oldDateMinutes)
}
//Usage:
let myDateFormatter = DateFormatter()
myDateFormatter.dateFormat = "yyyy-MM-dd HH:mm"
//You'll need both dates to compare, you can get them by just storing a Date object when you first start the timer.
//Then when you need to check it, compare it to Date()
let oldDate: Date = myDateFormatter.date(from: String("2019-06-22 11:25"))
func validateRefresh() {
//do the comparison between the old date and the now date like this.
if minutesBetweenDates(oldDate, Date()) > 5 {
//Do whatever
}
}
You can, of course, change the .dateFormat value on the date formatter to be whatever format you'd like. A great website for finding the right format is: https://nsdateformatter.com/.
You say:
I would like to format the date and time to US, regardless from which locale the user has. How can I do that?
Specify a Locale of en_US_POSIX:
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd hh:mm a"
formatter.locale = Locale(identifier: "en_US_POSIX")
The locale is not the only question.
There’s also a timezone question. For example, you're driving out of Chicago and go from Central to Eastern timezones; do you really want to consider that one hour has passed?
Do you really want to discard seconds? If you do that, the 59 seconds between going from 8:00:00pm to 8:00:59pm will be considered “zero minutes” but the one second between 8:00:59pm and 8:01:00pm will be considered “one minute”.
Frankly, if I wanted to save a locale and timezone invariant date string, I’d suggest using ISO8601DateFormatter.
Then I will save the 'oldTime' into UserDefaults, and use it for later calculation.
If that’s why you’re using this DateFormatter, I’d suggest saving the Date object directly.
UserDefaults.standard.set(oldTime, forKey: "oldTime")
And to retrieve it:
if let oldTime = UserDefaults.standard.object(forKey: "oldTime") as? Date {
...
}
In terms of calculating the number of minutes between two Date objects
let minutes = Calendar.current
.dateComponents([.minute], from: date1, to: date2)
.minute
If you want the number of seconds, you can also use timeIntervalSince:
let seconds = date2.timeIntervalSince(date1)
And if you wanted to show the amount of elapsed time as a nice localized string:
let intervalFormatter = DateComponentsFormatter()
intervalFormatter.allowedUnits = [.minute, .second]
intervalFormatter.unitsStyle = .full
let string = intervalFormatter.string(from: date1, to: date2)
I'm not convinced that your question is the best way to go about accomplishing your aim, but the code below will work.
let dateFormatterNow = DateFormatter()
dateFormatterNow.dateFormat = "yyyy-MM-dd hh:mm a"
dateFormatterNow.timeZone = TimeZone(abbreviation: "EST")
let oldDateString = "2019-06-23 12:44 p"
let oldDate = dateFormatterNow.date(from: oldDateString)
let newDateString = "2019-06-23 12:54 p"
let newDate = dateFormatterNow.date(from: newDateString)
if let oldDate = oldDate, let newDate = newDate {
let diffInMins = Calendar.current.dateComponents([.minute], from: oldDate, to: newDate).minute
print(diffInMins)
}

Difference between boarding time and current time in UTC in iOS , Swift

I am getting Boarding Time from service ( lets say BT- Boarding Time)
I need to find out the differnce between Boarding Time and current time and then find out the difference in Hour , Min.
The condition is user may check the difference between these from any country in the world. so i used UTC to calculate but its giving correct result , kindly help me in this.
func dayStringFromTime() -> String {
let currentTimeUnix = Date().timeIntervalSince1970
let date = NSDate(timeIntervalSince1970: currentTimeUnix)
let dateFormatter = DateFormatter()
// dateFormatter.dateFormat = "HH:mm:ss"
return date.description
}
let CT = dayStringFromTime() //time1
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-mm-dd HH:mm:ss"
let CTDate = formatter.date(from: CT)
let time1 = boardingDateTime//timeformatter.date(from: CT)
let time2 = CT_Date//timeformatter.date(from: ETD)
//You can directly use from here if you have two dates
let interval = time1.timeIntervalSince(time2! as Date)
let hour = (interval ) / 3600;
let minute = interval.truncatingRemainder(dividingBy: 3600) / 60
let intervalInt = Int(interval)
print("\(intervalInt < 0 ? "-" : "+") \(Int(hour)) Hours \(Int(Int(minute))) Minutes")
let minText = Int(minute) > 0 && Int(minute) != 0 ? " \(Int(minute)) min" : (Int(minute) < 0 ? " \(Int(abs(minute))) min" : "")
let hrText = Int(hour) > 0 && Int(hour) != 0 ? " \(Int(hour)) hr" : (Int(hour) < 0 ? " \(Int(abs(hour))) hr" : "")
this url https://stackoverflow.com/a/28608779/3400991 shows the exact problem about this result, kindly help
This is way easier that you have made it out to be:
let boardingTime = Date().addingTimeInterval(3200) // the `addingTimeInterval` is for demonstration purposes only.
let now = Date()
let difference = Calendar.current.dateComponents([.hour, .minute, .second], from: now, to: boardingTime)
print("Boarding will be in: \(difference.hour!):\(difference.minute!):\(difference.second!)")
First of all, be very careful with date/time mathematics, it's not a straight linear conversion, there are lots and lots of rules which go around it and make it ... complicated.
The first thing you need is to calculate the difference between the two times, lucky for you, this is relatively easy...
var boardingTime = Date()
boardingTime = bordingTime.addingTimeInterval(Double.random(in: 0.0..<86400.0))
let now = Date()
let difference = boardingTime.timeIntervalSince(now)
This gives you the number of seconds between these two values (a positive value been the time till, a negative value been the time after)
Next, you need the hours/minutes in some form of human readable notation. It might seem tempting to just start by multiplying and dividing everything by 60, but that would be a mistake and lead you into bad habits (sure over a short range it's not bad, but you need to be very careful)
A better solution would be to use a DateComponentsFormatter...
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.hour, .minute]
formatter.unitsStyle = .abbreviated
formatter.string(from: difference)
Which will take care of all the "rules" for you, but, it will also localise the results, always a bonus.
The above example will print something like...
10h 28m

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