Last active
January 27, 2017 18:04
-
-
Save ole/74167eb1683584fae461ebd67ea46008 to your computer and use it in GitHub Desktop.
Parse measurement expressions like `"5 m²"` and convert them into `Measurement` values. Uses the Objective-C runtime to find the known and valid symbols for a given unit (such as `UnitArea`). Adopts `ExpressibleByStringLiteral` for easy initialization.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Parse measurement expressions like `"5 m²"` and convert them into `Measurement` values. | |
// Uses the Objective-C runtime to find the known and valid symbols for a given unit | |
// (such as `UnitArea`). Adopts `ExpressibleByStringLiteral` for easy initialization. | |
import ObjectiveC | |
enum ObjectiveCRuntime { | |
class Class { | |
let base: AnyClass | |
init(base: AnyClass) { | |
self.base = base | |
} | |
deinit { | |
_properties?.deallocate(capacity: _propertiesCount + 1) | |
} | |
private var _properties: UnsafeMutablePointer<objc_property_t?>? = nil | |
private var _propertiesCount: Int = 0 // not including NULL terminator | |
var properties: [Property] { | |
if _properties == nil { | |
var count: UInt32 = 0 | |
_properties = class_copyPropertyList(base, &count) | |
_propertiesCount = Int(count) | |
} | |
guard let _properties = _properties else { return [] } | |
let buffer = UnsafeBufferPointer(start: _properties, count: _propertiesCount + 1) | |
return buffer.flatMap { property in property.map(Property.init(base:)) } | |
} | |
} | |
class Property { | |
let base: objc_property_t | |
init(base: objc_property_t) { | |
self.base = base | |
} | |
deinit { | |
_attributes?.deallocate(capacity: _attributesCount) | |
} | |
var name: String { | |
return String(cString: property_getName(base)) | |
} | |
var attributeString: String { | |
return String(cString: property_getAttributes(base)) | |
} | |
var _attributes: UnsafeMutablePointer<objc_property_attribute_t>? = nil | |
var _attributesCount: Int = 0 | |
var attributes: [String: String] { | |
if _attributes == nil { | |
var count: UInt32 = 0 | |
_attributes = property_copyAttributeList(base, &count) | |
_attributesCount = Int(count) | |
} | |
guard let _attributes = _attributes else { return [:] } | |
let buffer = UnsafeBufferPointer(start: _attributes, count: _attributesCount) | |
var result: [String: String] = [:] | |
for attribute in buffer { | |
let name = String(cString: attribute.name) | |
let value = String(cString: attribute.value) | |
result[name] = value | |
} | |
return result | |
} | |
var typeEncoding: String { | |
return attributes["T"] ?? "" | |
} | |
} | |
} | |
import Foundation | |
extension Unit { | |
class func units() -> [Unit] { | |
let metaclass = ObjectiveCRuntime.Class(base: object_getClass(self)) | |
let className = String(describing: self) | |
let unitNames = metaclass.properties | |
// Only consider properties whose return type is self, | |
// e.g. UnitLength if self is UnitLength | |
.filter { property in | |
property.typeEncoding == "@\"\(className)\"" | |
}.map { $0.name } | |
return unitNames.flatMap { className -> Unit? in | |
let unit = self.value(forKey: className) | |
return unit as? Unit | |
} | |
} | |
class func knownSymbols() -> [String: Unit] { | |
var symbolMapping: [String: Unit] = [:] | |
for unit in units() { | |
symbolMapping[unit.symbol] = unit | |
} | |
return symbolMapping | |
} | |
} | |
extension Measurement { | |
static func parse(_ expression: String) -> Measurement? { | |
let scanner = Scanner(string: expression) | |
let whitespace = CharacterSet.whitespaces | |
scanner.scanCharacters(from: whitespace, into: nil) | |
var value: Double = 0.0 | |
guard scanner.scanDouble(&value) else { return nil } | |
scanner.scanCharacters(from: whitespace, into: nil) | |
var symbol: NSString? = nil | |
guard scanner.scanUpToCharacters(from: whitespace, into: &symbol), | |
let sym = symbol as? String, | |
let unit = UnitType.knownSymbols()[sym] as? UnitType | |
else { return nil } | |
return Measurement(value: value, unit: unit) | |
} | |
} | |
extension Measurement: ExpressibleByStringLiteral { | |
public init(stringLiteral value: String) { | |
guard let result = Measurement.parse(value) else { | |
fatalError("Unable to parse expression \"\(value)\". Valid symbols are \(Array(UnitType.knownSymbols().keys))") | |
} | |
self = result | |
} | |
public init(unicodeScalarLiteral value: String) { | |
self.init(stringLiteral: value) | |
} | |
public init(extendedGraphemeClusterLiteral value: String) { | |
self.init(stringLiteral: value) | |
} | |
} | |
// This is how you use it: | |
let fiveMeters: Measurement<UnitLength> = "5 m" | |
let tenSeconds: Measurement<UnitDuration> = "10s" | |
let threePointFiveSquareFeet: Measurement<UnitArea> = "3.5 ft²" | |
let minusTenPointTwoDegrees: Measurement<UnitTemperature> = "-10.2 °C" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment