Initializing a Unit from String

1.4k views Asked by At

I'm looking for a Unit(symbol: String) kind of initializer that would recognize, not define a symbol.

Here's a code snippet to illustrate my problem by providing a limited solution to it.

Although the solution below works, explicitly listing all units predefined in the Foundation framework is tedious and does not have implicit forward compatibility should Apple release new units in upcoming iOS updates.

func matchedUnit<S: Sequence>(symbol: String, valid sequence: S) -> S.Iterator.Element? where S.Iterator.Element: Unit {
    let symbols = sequence.map { $0.symbol };
    let candidate = zip(sequence, symbols).first {
        return $0.1 == symbol;
        }?.0
    return candidate;
}

let knownUnitsOfLength = [UnitLength.meters, UnitLength.centimeters, UnitLength.kilometers, UnitLength.decimeters];

if let aUnit = matchedUnit(symbol: "dm", valid: knownUnitsOfLength) {
    let measurement = Measurement(value: 1, unit: aUnit);
}

Perhaps I'm missing an obvious piece of the puzzle in the SDK - an initializer that recognizes a symbol rather than defines one?

UPDATE:

It has been confirmed that there is no Apple provided API to do that. I ended up using Objective-C runtime to find all classes that inherit from NSDimension and iterate over all of their class properties that begin with NSUnit in their names. I mapped all the symbols, class and property names and can create NSUnit instance at runtime based on a user provided symbol.

Granted, it is not at the same level with NSMeasurementFormatter when it comes to language support. BUT I automatically gain access to any new units and symbols that are introduced by Apple with future releases.

Below is a sample client code:

NSUnit *kg = [NSUnit unitRecognizingSymbol:@"kg"];
NSMeasurement *mkg = [[NSMeasurement alloc] initWithDoubleValue:3.0 unit:kg];

NSUnit *g = [NSUnit unitRecognizingSymbol:@"g"];
NSMeasurement *mg = [[NSMeasurement alloc] initWithDoubleValue:200 unit:g];

NSMeasurement *total = [mkg measurementByAddingMeasurement:mg];

NSMeasurementFormatter *formatter = [[NSMeasurementFormatter alloc] init];
[formatter setUnitOptions:NSMeasurementFormatterUnitOptionsProvidedUnit];

NSLog(@"%@", [formatter stringFromMeasurement:total]);
// 3.2 kg

And here's what the core part of the implementation side looks like.

// Count receives its value from objc_copyClassList().
unsigned int classCount;
Class *classes = objc_copyClassList(&classCount);
// Inspect all available classes.
for (int classIterator = 0; classIterator < classCount; classIterator++) {
    char const *className = class_getName(classes[classIterator]);

    // Classes we are looking for are derived from NSDimension or NSUnit.
    Class superClass = class_getSuperclass(classes[classIterator]);
    char const *superClassName = class_getName(superClass);

    // Single out NSDimension exception because NSDimension itself inherits from NSUnit.
    if ((strcmp(superClassName, "NSDimension") == 0 || strcmp(superClassName, "NSUnit") == 0) && strcmp(className, "NSDimension") != 0) {
        // To access available class properties, gain access to meta class.
        Class meta = objc_getMetaClass(class_getName(classes[classIterator]));

        // Count receives its value from class_copyPropertyList()
        unsigned int propertyCount = 0;
        objc_property_t *properties = class_copyPropertyList(meta, &propertyCount);
        // Inspect all available class properties.
        for (int propertyIterator = 0; propertyIterator < propertyCount; propertyIterator++) {
            char const *propertyName = property_getName(properties[propertyIterator]);
        // ...
    }
    // ...
}
// ...
1

There are 1 answers

2
matt On BEST ANSWER

There's no better way than what you're doing, because NSMeasurementFormatter only runs one way — it makes a string from a measurement, not the other way around. Your approach is likely to be highly flawed because you are not taking account of locale, which a measurement formatter knows how to do (for the one direction in which it goes). You could file an enhancement request with Apple if this is something you'd like to see built in.