ioslocalizationnstimeintervalnsdatecomponentsformatter

iOS DateComponentsFormatter - get used units


I would like to formate time from seconds to positional format like 2:05 min or 1:23 h or 19 s. I'm having problem of retrieving localised abbreviated unit of time. Here is my code.

let secs: Double = 3801
let stopWatchFormatter = DateComponentsFormatter()
stopWatchFormatter.unitsStyle = .positional
stopWatchFormatter.maximumUnitCount = 2
stopWatchFormatter.allowedUnits = [.hour, .minute, .second]
print(stopWatchFormatter.string(from: secs)) // 1:03
stopWatchFormatter.unitsStyle = .short
print(stopWatchFormatter.string(from: secs)) // 1 hr, 3 min

As you can see 3801 seconds is formatted to 1:03, which is fine but I don't know whether DateComponentsFormatter used hours or minutes etc. I may use simple MOD logic to check it, but then comes hard part of localisation. Also notice that MOD solution is worthless if I set collapsesLargestUnit to false.


Solution

  • DateComponentsFormatter doesn't directly support the format you want which is essentially the positional format but with the first unit of the short format shown at the end.

    The following helper function combines those two separate results into what you want. This should work with any locale but thorough testing needs to be done to confirm that.

    func formatStopWatchTime(seconds: Double) -> String {
        let stopWatchFormatter = DateComponentsFormatter()
        stopWatchFormatter.unitsStyle = .positional
        stopWatchFormatter.maximumUnitCount = 2
        stopWatchFormatter.allowedUnits = [.hour, .minute, .second]
        var pos = stopWatchFormatter.string(from: seconds)!
    
        // Work around a bug where some values return 3 units despite only requesting 2 units
        let parts = pos.components(separatedBy: CharacterSet.decimalDigits.inverted)
        if parts.count > 2 {
            let seps = pos.components(separatedBy: .decimalDigits).filter { !$0.isEmpty }
            pos = parts[0..<2].joined(separator: seps[0])
        }
    
        stopWatchFormatter.maximumUnitCount = 1
        stopWatchFormatter.unitsStyle = .short
        let unit = stopWatchFormatter.string(from: seconds)!
    
        // Replace the digits in the unit result with the pos result
        let res = unit.replacingOccurrences(of: "[\\d]+", with: pos, options: [.regularExpression])
    
        return res
    }
    
    print(formatStopWatchTime(seconds: 3801))
    

    Output:

    1:03 hr