Skip to content

Instantly share code, notes, and snippets.

@ole
Last active January 27, 2017 18:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ole/74167eb1683584fae461ebd67ea46008 to your computer and use it in GitHub Desktop.
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.
// 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