Format byte rate

Is there a way to format a byte rate (e.g. bytes per second) as a string?

None of the built-in stuff supports this, and I can't find any 3rd party packages which provide this either.

I see plenty of examples of naive implementations of this, e.g. formatting using ByteCountFormatStyle and then appending "/s" to the end, or similar, but that's invalid in many locales.

1 Like

Does Foundation Unit Measurement have anything to help you? It has bits bytes etc. not sure it it has rate per sec.

As far as I can tell it does not. You can create custom units but only linearly and only in terms of the existing units.

1 Like

You could create a custom unit:

class UnitBytesPerSecond: Dimension {
    static let bytesPerSecond = UnitBytesPerSecond(symbol: NSLocalizedString("bytes/s", comment: "bytes/s"), converter: UnitConverterLinear(coefficient: 1.0))
    static let kbytesPerSecond = UnitBytesPerSecond(symbol: NSLocalizedString("kbytes/s", comment: "kbytes/s"), converter: UnitConverterLinear(coefficient: 1000))
    override class func baseUnit() -> Self { Self(symbol: NSLocalizedString("bytes/s", comment: "bytes/s")) }
}

but it doesn't help much: you'd need to provide relevant localisations and the logic to convert from bytes to kilobytes, etc similar to how ByteCountFormatter is doing that (or somehow using ByteCountFormatter directly for that piece of the logic).

Quick & dirty prototype of the above idea:

class BoundrateUnit: Dimension {
    var factor: Int { fatalError() }
    class var symbolKey: String { fatalError() }
    class var localizedSymbol: String {
        NSLocalizedString(symbolKey, comment: symbolKey)
    }
    override var symbol: String {
        Self.localizedSymbol
    }
    override class func baseUnit() -> Self {
        Self(symbol: localizedSymbol)
    }
}
class BytesPerSecondUnit: BoundrateUnit {
    override class var symbolKey: String { "bytes/s" }
    override var factor: Int { 1 }
}
class KBytesPerSecondUnit: BoundrateUnit {
    override class var symbolKey: String { "KB/s" }
    override var factor: Int { 1024 }
}
class MBytesPerSecondUnit: BoundrateUnit {
    override class var symbolKey: String { "MB/s" }
    override var factor: Int { 1024*1024 }
}
class GBytesPerSecondUnit: BoundrateUnit {
    override class var symbolKey: String { "GB/s" }
    override var factor: Int { 1024*1024*1024 }
}
func boundrateMeasurement(for value: Int) -> Measurement<BoundrateUnit> {
    let unit = if value < 1024 {
        BytesPerSecondUnit(forLocale: .current)
    }
    else if value < 1024*1024 {
        KBytesPerSecondUnit(forLocale: .current)
    }
    else if value < 1024*1024*1024 {
        MBytesPerSecondUnit(forLocale: .current)
    }
    else {
        GBytesPerSecondUnit(forLocale: .current)
    }
    return Measurement(value: Double(value / unit.factor), unit: unit)
}

Not sure if the above standalone function boundrateMeasurement could be refactored into some magic "ChooseUnitsAutomatically" units.

Usage example:

boundrateMeasurement(for: 0).formatted()             // 0 bytes/s
boundrateMeasurement(for: 1).formatted()             // 1 bytes/s
boundrateMeasurement(for: 2).formatted()             // 2 bytes/s
boundrateMeasurement(for: 123).formatted()           // 123 bytes/s
boundrateMeasurement(for: 1245).formatted()          // 1 KB/s
boundrateMeasurement(for: 12456).formatted()         // 12 KB/s
boundrateMeasurement(for: 124567).formatted()        // 121 KB/s
boundrateMeasurement(for: 1245678).formatted()       // 1 MB/s
boundrateMeasurement(for: 12456789).formatted()      // 11 MB/s
boundrateMeasurement(for: 124567890).formatted()     // 118 MB/s
boundrateMeasurement(for: 1245678901).formatted()    // 1 GB/s
boundrateMeasurement(for: 12456789012).formatted()   // 11 GB/s

Right, but the limitation of all these rolling-your-own methods is no support for languages other than English. Granted that stretches further than normal in this case since the presentation is the same in quite a few locales beyond the English-speaking ones, but far from all of them.

I have an imperfect solution already based on the exiting ByteCountFormatStyle, but it doesn't work if suffixing "/s" is incorrect.

The above does support other languages... it uses NSLocalizedString and it's just a matter of providing localisations for "bytes/s", etc. Understandably a better solution would be the one that doesn't require that, but OTOH this approach is quite flexible.

I don't know if those locales exist but "KB/s" could be written as, say, "kbps" in certain locales, so if you take, say, "KB" from the byte count unit and append "/" and then a unit for duration that would give a different result.

Haha, yeah, just being the imperative word. :slightly_smiling_face:

Granted it wouldn't be impractical or necessarily the worst idea to just generate those semi-automatically based on the output of ByteCountFormatStyle and Date.ComponentsFormatStyle, using those as a guide for what locales are right-to-left or use Arabic digits etc. But it's still possible that the result is wrong in some locales, because you're still guessing blindly about how to combine the two (I'd almost put money on "X/s" not being correct in every locale).

The problem is, even ICU appears to not support byte rates, which makes it hard for Swift to support it (short of diving into reinventing ICU, or at least significant parts thereof).

1 Like

Yes, but:

  1. If the app is already properly localised it should not be too much trouble adding those extra strings, right?

  2. And if it' not – why bother localising bytes/s to begin with?

  3. And if you are a library/component provider don't provide those strings yourself, make sure you are using localisable string API's and let app developers bother with them if it's (1).

  4. After those strings are localised once, with the permission of those who did it you could expose the strings and make them publicly available so other app developers won't have to do this again.


I was also thinking of using some similar available "per second" unit (like meters per second), then get out "meters" and "bytes" separately, somehow find where "meters" are in "meters per second" and substitute that for bytes. Ideally not as a textual search / replace but more directly, pseudocode:

func valueFormattedAsBytesPerSecond(_ value: Int) -> String {
    let s = value.formattedAsSpeed().attributedString()
    let newAttributes = attributedString.attributes.map { (key, value) in 
        (key == .meters ? .bytes : key, value)
    }
    return AttributedString(s.string, newAttributes).string
}

No idea if that's possible or not.

No app is ever localised into every locale (whereas ICU does support practically every locale, for the limited set of capabilities it has).

In some apps there isn't really much custom text other than formatted numbers, like file sizes and transfer rates (and standard menu items etc that are localised automatically by the system). With care you can actually get away with not having to explicitly localise anything.

And even if not, it's still better to have more localisation for any given locale than less, or none.

Are they really? I don't think that happens in AppKit / UIKit / SwiftUI... AFAIK there's no "standardImage" analogue for standard strings like "File" / "New" / "Cancel", etc.

If I were to do this I'd print the relevant strings for "m/s" from the languages I need to support and see what they do, and do the same for "bytes/s" (switching to KB, MB, etc similar to how ByteCountFormatter doing that). Would be a pain, for sure, especially if you want to support 200 languages/regions but shan't take more than a day methinks.