1

Is there a way to create a DateComponentFormatter for TimeInterval that outputs minutes, seconds and milliseconds (bonus, if I could specify how many fractional places after seconds).

let t: TimeInterval = 124.344657 // 124 seconds, 345 milliseconds

// output as 2m 4s 345ms

I tried the following:

let formatter = DateComponentsFormatter()
formatter.unitsStyle = .abbreviated
formatter.allowedUnits = [.minute, .second]
formatter.allowsFractionalUnits = true

print("\(formatter.string(from: t)!)") // outputs 2m 4s

I tried playing with more parameters, e.g. like adding .nanosecond, but to no effect.

What's the right approach here?

New Dev
  • 48,427
  • 12
  • 87
  • 129
  • 1
    Does this answer your question? [How to convert TimeInterval into Minutes, Seconds and Milliseconds in Swift](https://stackoverflow.com/questions/30771820/how-to-convert-timeinterval-into-minutes-seconds-and-milliseconds-in-swift) – Amr Sep 05 '21 at 13:28
  • 1
    @Amr, thanks... Sort of, but not really. My question was specific to the use of a formatter, so that it could adjust to locales. I updated the question to reflect more of a need for a formatter. – New Dev Sep 05 '21 at 13:35
  • 1
    If you only include `.nanosecond`, the formatter gives you nil, and there is no such thing as `NSCalendar.Unit.millisecond` in the first place, so I don't think it supports what you want. After all, the formatter is supposed to produce a "user-readable string", and milliseconds and nanoseconds are _arguably_ not that. Also, as far as I know, only very few applications have the need to do this, so IMO you can't blame them for not supporting this either. – Sweeper Sep 05 '21 at 13:38
  • That said, I'm sure someone will come up with some way of abusing one of the other formatters to do this... – Sweeper Sep 05 '21 at 13:40
  • @Sweeper, I see. I thought that maybe there was a trick to it. The use case is in sports applications, where milliseconds count (though, I agree about the user readability of milliseconds) – New Dev Sep 05 '21 at 13:40
  • https://stackoverflow.com/a/44910553/6576315 – RTXGamer Sep 05 '21 at 13:58

2 Answers2

0

I think the way to look at this is that it's a misuse of a date components formatter. This isn't a date of any kind. It's a string consisting of a certain number of minutes, seconds, and milliseconds. Unlike date math, that's a calculation you can perform, and then you are free to present the string however you like.

If you want to use a formatter to help you with user locales and so forth, then you are looking for a measurement formatter (for each of the substrings).

Example (using the new Swift 5.5 formatter notation):

let t1 = Measurement<UnitDuration>(value: 2, unit: .minutes)
let t2 = Measurement<UnitDuration>(value: 4, unit: .seconds)
let t3 = Measurement<UnitDuration>(value: 345, unit: .milliseconds)
let s1 = t1.formatted(.measurement(width: .narrow))
let s2 = t2.formatted(.measurement(width: .narrow))
let s3 = t3.formatted(.measurement(width: .narrow))
let result = "\(s1) \(s2) \(s3)" // "2m 4s 345ms"

Addendum: You say in a comment that you're having trouble deriving the number milliseconds. Here's a possible way. Start with seconds and let the Measurement do the conversion. Then format the resulting value in the formatter. Like this:

let t3 = Measurement<UnitDuration>(value: 0.344657, unit: .seconds)
    .converted(to: .milliseconds)
// getting the `0.xxx` from `n.xxx` is easy and not shown here
let s3 = t3.formatted(.measurement(
    width: .narrow, 
    numberFormatStyle: .number.precision(.significantDigits(3))))

You might have to play around a little with the number-formatter part of that, but the point is that a measurement formatter lets you dictate the number format and thus get the truncation / rounding behavior you're after.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 1
    I don't think this is a “misuse.” A date components formatter has a dedicated method which takes a *time interval* (measured in seconds) as input. – Martin R Sep 05 '21 at 14:43
  • @MartinR I stand by my answer nonetheless. I produced the desired string and it is Locale-savvy. I didn't show the arithmetic for deriving the values from a TimeInterval but that's trivial. – matt Sep 05 '21 at 14:57
  • Hmm.. that's an interesting approach, @matt - not quite what I wanted, but it *is* locale-aware! I hoped to achieved this with a formatter, that could be passed as a parameter into an interpolation function, which would make it easier to switch/update parameters. Btw, getting the millisecond part `345` as an Int isn't quite as trivial as I also thought. A combination of `truncatingRemainder` and multiplication can cause some weirdness with floating point numbers, where you might get `1000ms` – New Dev Sep 05 '21 at 15:41
  • That sounds like a different question. :) I'll add something about it in my answer, though. – matt Sep 05 '21 at 15:57
  • Ugh... `.formatted` is only available in iOS15; I don't have that installed yet, so couldn't even test it out :( ... anyway, I'll accept this, since I think it best answers the locale aspect of the question. – New Dev Sep 05 '21 at 16:28
  • Well I _said_ I was using the new notation. Everything I did can be done with an old-fashioned MeasurementFormatter. But surely you don't need me to translate backwards. – matt Sep 05 '21 at 16:32
0

An update to matt's answer which correctly displays all the requested units using purely Foundation

let string = Duration
    .seconds(Date().timeIntervalSince(startDate))
    .formatted(.units(
        allowed: [.minutes, .seconds, .milliseconds],
        width: .condensedAbbreviated
    ))

Will display something like:

1 min 3 sec 198 ms

If you prefer to show milliseconds as a decimal:

let string = Duration
    .seconds(date.timeIntervalSince(startDate))
    .formatted(.units(
        allowed: [.minutes, .seconds],
        width: .condensedAbbreviated,
        fractionalPart: .show(length: 2)
    ))

Which displays:

1 min 0.54 sec
Luis
  • 951
  • 2
  • 12
  • 27