4

My server call gives me JSON data with a date time per piece of data and I want to calculate the time elapsed between now and then and load it into my data structure. The way I'm doing it now takes way to long, is there a different design practice I should be using? The follow function is what I am using right now

func dateDiff(_ dateStr:String) -> String {
    var timeAgo = "10m"

    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd' 'HH:mm:ss"
    formatter.timeZone = NSTimeZone(name: "AST") as! TimeZone


    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "yyyy-MM-dd' 'HH:mm:ss"
    dateFormatter.timeZone = NSTimeZone(name: "AST") as! TimeZone

    let now = formatter.string(from: Date())

    if let date = formatter.date(from: dateStr){
        if let nowDate = formatter.date(from: now){
        let components = Calendar.current.dateComponents([.day,.hour,.minute,.second], from: date, to: nowDate)
        let sec = components.second
        let min = components.minute
        let hours = components.hour
        let days = components.day
        if (sec! > 0){
            if let secc = sec {
                timeAgo = "\(secc)s"
            }
        }
        if (min! > 0){
            if let minn = min {
                timeAgo = "\(minn)m"
            }            }
        if(hours! > 0){
            if let hourss = hours {
                timeAgo = "\(hourss)h"
            }
        }
        if(days! > 0){
            if let dayss = days {
                timeAgo = "\(dayss)d"
            }
        }
    }
  }
    return timeAgo
}
Sean
  • 51
  • 5
  • Where is the difference between `formatter` and `dateFormatter`? The latter seems to be unused. Why do you convert `Date()` to a string and back to a `Date`? – Martin R Aug 20 '17 at 02:32
  • Have a look at [`DateComponentsFormatter`](https://developer.apple.com/documentation/foundation/datecomponentsformatter) – Martin R Aug 20 '17 at 02:34

3 Answers3

1

Your code was too verbose to begin with, you could have shortened it significantly:

if let day = components.day, day > 0 {
    timeAgo = "\(day)d"
} else if let hour = components.hour, hour > 0 {
    timeAgo = "\(hour)h"
} else if let minute = components.minute, minute > 0 {
    timeAgo = "\(minute)m"
} else if let second = components.second, second > 0 {
    timeAgo = "\(second)s"
}

But the better way to do it is to use DateComponentsFormatter, available since iOS 8 and OS X 10.10:

let componentsFormatter = DateComponentsFormatter()
componentsFormatter.allowedUnits = [.second, .minute, .hour, .day]
componentsFormatter.maximumUnitCount = 1
componentsFormatter.unitsStyle = .abbreviated

let timeAgo = componentsFormatter.string(from: dateComponents)!

Here are some example of what it does for you:

let dateComponents1 = DateComponents(calendar: .current, day: 1, hour: 4, minute: 6, second: 3)
let dateComponents2 = DateComponents(calendar: .current, day: 0, hour: 4, minute: 6, second: 3)
let dateComponents3 = DateComponents(calendar: .current, day: 0, hour: 0, minute: 6, second: 3)
let dateComponents4 = DateComponents(calendar: .current, day: 0, hour: 0, minute: 0, second: 3)

print(componentsFormatter.string(from: dateComponents1)!) // 1d
print(componentsFormatter.string(from: dateComponents2)!) // 4h
print(componentsFormatter.string(from: dateComponents3)!) // 6m
print(componentsFormatter.string(from: dateComponents4)!) // 3s
Code Different
  • 90,614
  • 16
  • 144
  • 163
0

It's somewhat expensive to create and then DateFormatter objects each time dateDiff is called. If the object that owns dateDiff is created once per app launch or JSON load, one thing you can do to optimize is to make your date formatters a member of the class, so you don't recreate it every single time you call dateDiff.

In other words, put:

var formatter : DateFormatter
var dateFormatter : DateFormatter 

at the top of your class and then initialize it in an init function.

Michael Dautermann
  • 88,797
  • 17
  • 166
  • 215
0

For performance reasons, you should pull the instantiation of the date formatter out of the method, because that's notoriously computationally intensive.

I'd also suggest using a DateComponentsFormatter to simplify the formatting of the elapsed time.

So, define your two formatters:

let dateFormatter: DateFormatter = {
    let _formatter = DateFormatter()
    _formatter.dateFormat = "yyyy-MM-dd' 'HH:mm:ss"
    _formatter.locale = Locale(identifier: "en_US_POSIX")
    _formatter.timeZone = TimeZone(abbreviation: "AST")  // Curious; we usually use `TimeZone(secondsFromGMT: 0)` (i.e. GMT/UTC/Zulu)
    return _formatter
}()

let componentsFormatter: DateComponentsFormatter = {
    let _formatter = DateComponentsFormatter()
    _formatter.maximumUnitCount = 1
    _formatter.unitsStyle = .abbreviated
    return _formatter
}()

And then your function is considerably simplified:

func dateDiff(_ string: String) -> String? {
    guard let date = dateFormatter.date(from: string) else { return nil }

    return componentsFormatter.string(from: date, to: Date())
}

Also note that:

  • I used TimeZone directly, rather round-tripping through NSTimeZone;
  • I set the locale to be en_US_POSIX, which you should always use if the source of the date strings is a web service or database;
  • I eliminated the conversion of “now” to a string and back; just use Date() directly;

The only other thing that looks suspicious is the use of AST for the timezone. Usually date strings are saved in GMT/UTC/Zulu (e.g., RFC 3339 or ISO 8601). If you have control over that, that's probably best practice, avoiding problems if users change time zones;

Rob
  • 415,655
  • 72
  • 787
  • 1,044