MeasurementFormatter string without unit

Is it possible to get string from MeasurementFormatter without the unit? For example


import Foundation

let weight = Measurement<UnitMass>(value: 100.0, unit: .kilograms)
let formatter = MeasurementFormatter()
formatter.unitStyle = .short // Maybe Formatter.UnitStyle should have .none case?
formatter.string(for: weight) // Now returns 100kg


I know I can use Measurement value directly but then I would lose preferred unit for localization.

Hi,


AFAIK, this is currently not possible. If I understand you right, you want to use a MeasureMentFormatter to convert a value with a given unit into the unit for the current locale? So you actualy need something like a MeasurementConverter.


I suggest filing a bugreport at least.


Dirk

>> then I would lose preferred unit for localization


Do you have a use-case for this?


It's a really odd requirement. If you don't know the unit of the resulting string, nor will anyone looking at it in the UI.


I mean, if you're displaying a column of weights, you'd have to say what the numbers meant, somewhere. If do know that, then you don't need a measurement formatter (just use a numeric formatter on the number). If you don't know that, your numbers are meaningless.

My use-case is a UI where value and unit are styled differently. Actually now when I think about it again, what I really want is to MeasurementFormatter to return value and unit separated. Something like this


extension MeasurementFormatter {
    func stringComponents<UnitType: Unit>(for measurement: Measurement<UnitType>) -> (measurement: String, unit: String)? {
        guard var measurementString = string(for: measurement) else {
            return nil
        }
        let unitString = string(from: measurement.unit)
       
        guard let unitSringRange = measurementString.range(of: unitString) else {
            return nil
        }
       
        measurementString.removeSubrange(unitSringRange)
        let trimmedMeasurementString = measurementString.trimmingCharacters(in: .whitespaces)
       
        return (measurement: trimmedMeasurementString, unit: unitString)
    }
}

Stating it like that shows that your goal is impossible. You've exposed your own false assumptions:


— that any formatted measurement consists of 2 discrete parts: the formatted number and the units


— that any "spacing" between the parts is composed of whitespace characters only


— (worst of all) that the order of the parts isn't locale-specific (you don't return anything that indicates where the unit string was found in relation to the measurement string).


These conditions might be satisfied in some locales, but it's wrong to believe they must hold everywhere.


(What would you expect if the locale-specific string for 98 degrees Celsius was [the equivalent of] "2 degrees below boiling"?)


If you have a need to separate the quantity from the units, you can use a NumberFormatter for the number, and MeasurementFormatter.string(from:Unit) for the units. This would be appropriate if, for example, you were displaying a table with the numbers in one column and the units in another column. It wouldn't be appropriate for applying different styling in connected, localized text.


It is in the nature of programming for multiple locales that global assumptions must be very general. If you have a compelling need to do something more specific, you're going to have to write locale-specific code yourself.

I just had the same problem and i used regex to split the value from unit. If you stick to simple formats i think will not fail:


extension MeasurementFormatter {


    func stringComponents(for measurement: Measurement) -> MeasurementComponents? {


        guard var measurementString = string(for: measurement) else {
            return nil
        }
        guard let unitsRegex = try? NSRegularExpression(pattern: "[A-Za-z/]+", options: []) else {
            return nil
        }
        guard let match = unitsRegex.firstMatch(in: measurementString, options: [], range: NSRange(location: 0, length: measurementString.count)) else {
            return nil
        }
        let unitString = (measurementString as NSString).substring(with: match.range)
        guard let unitSringRange = measurementString.range(of: unitString) else {
            return nil
        }
        measurementString.removeSubrange(unitSringRange)
        let valueString = measurementString.trimmingCharacters(in: .whitespaces)


        return (value: valueString, unit: unitString)

   }
}
typealias MeasurementComponents = (value: String, unit: String)

And you use it like


let formatter = MeasurementFormatter()
formatter.unitStyle = .short
formatter.locale = Locale.current
formatter.numberFormatter.maximumFractionDigits = 1
let measurement = Measurement(value: speed, unit: UnitSpeed.kilometersPerHour)
return formatter.stringComponents(for: measurement) ?? (value: "", unit: "")

Of course, never do that in prod. The code will break unexpectedly as soon as you start adding exotic locale…

(For starter, the unit won’t pass the [A-Za-z/] regex.)

I did this:

formatter.numberFormatter.string( from: weight.value as NSNumber )!

instead of this:

formatter.string(for: weight)

MeasurementFormatter string without unit
 
 
Q