45

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).

Quantaliinuxite
  • 3,133
  • 4
  • 18
  • 32

6 Answers6

100

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
Code Different
  • 90,614
  • 16
  • 144
  • 163
  • Amazing! Exactly what I needed. – Quantaliinuxite Mar 14 '16 at 22:59
  • Nice answer. (Voted, since it's more complete than mine.) I did some tinkering with this. Curiously, setting the `allowsFractionalUnits` flag on the formatter does not give "3.75 hr" for 13500 seconds like you might expect. You have to set `maximumUnitCount` to 2 and the you get `3 hr 45 min` (which is not what the OP wanted.) I wonder why? Any idea, @CodeDifferent? – Duncan C Mar 14 '16 at 23:14
  • Why `.string(from:)` returns `Optional`? In what case it can return `nil`? – cakraww Feb 19 '19 at 05:28
  • This looks great! However, is there any way to get a duration interval? e.g. "1-2 hrs" ? – Alexandru Motoc Feb 06 '20 at 14:27
  • Warning! This behaves somewhat strangely at 59:30 - 59:59 (seconds 3570 - 3599) - for any of that values, it returns `0d`. So counting down, you will get something like `2h`, `1h`, `0d`, `59m`, `58m`... Am I the only one to find this strange? – Michal Šrůtek Oct 04 '20 at 14:24
24

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
Diego Carrera
  • 2,245
  • 1
  • 13
  • 16
Kesava
  • 1,049
  • 14
  • 22
18

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)
  }
}
Yura Voevodin
  • 412
  • 6
  • 10
Roman Barzyczak
  • 3,785
  • 1
  • 30
  • 44
1

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

beyowulf
  • 15,101
  • 2
  • 34
  • 40
1

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.

Duncan C
  • 128,072
  • 22
  • 173
  • 272
-1

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).

owlswipe
  • 19,159
  • 9
  • 37
  • 82
  • 1
    Your answer does not handle different locales as the OP asked. CodeDifferent's answer is the best. – Duncan C Mar 14 '16 at 23:16