Skip to content

Commit c9a95ff

Browse files
feat!: Removes reliance on registry singleton
This does so by introducing a RegistryBuilder that custom units must be attached to, and is used for all Unit lookups. This was (rightfully) required by Swift 6 memory safety, since the existing implementation had issues with shared mutable state. I attempted to minimize breaking changes, but some were required.
1 parent 4322236 commit c9a95ff

14 files changed

Lines changed: 486 additions & 521 deletions

README.md

Lines changed: 24 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -121,40 +121,57 @@ For a list of the default units and their conversion factors, see the [`DefaultU
121121

122122
## Custom Units
123123

124-
To extend this package, users can define their own custom units using `Unit.define`:
124+
The unit system is backed by a `Registry` that maps unit symbols to their metadata. To add new units, use `RegistryBuilder`, and pass it to any operation that converts from a `String` to a `Unit`:
125125

126126
```swift
127-
let centifoot = try Unit.define(
127+
let registryBuilder = RegistryBuilder()
128+
registryBuilder.addUnit(
128129
name: "centifoot",
129130
symbol: "cft",
130131
dimension: [.Length: 1],
131132
coefficient: 0.003048 // This is the conversion to meters
132133
)
134+
let registry = registryBuilder.registry()
135+
let centifoot = try Unit(fromSymbol: "cft", registry: registry)
133136

134137
let measurement = Measurement(value: 5, unit: centifoot)
135138
print(5.measured(in: .foot).convert(to: centifoot))
136139
```
137140

138141
This returns a Unit object that can be used in arithmetic, conversions, and serialization.
139142

143+
### Encoding and Decoding
144+
145+
When using custom units, you must provide the custom registry to the encoder or decoder using `userInfo`:
146+
147+
```swift
148+
let decoder = JSONDecoder()
149+
decoder.userInfo[Unit.registryUserInfoKey] = registry // Required to recognize custom units.
150+
try decoder.decode(Unit.self, from: #""cft/s""#.data(using: .utf8))
151+
```
152+
140153
### Non-scientific Units
141154

142155
For "non-scientific" units, it is typically appropriate to use the `Amount` quantity. Through this approach, you can easily build up an impromptu conversion system on the fly. For example:
143156

144157
```swift
145-
let apple = try Unit.define(
158+
let registryBuilder = RegistryBuilder()
159+
try registryBuilder.addUnit(
146160
name: "apple",
147161
symbol: "apple",
148162
dimension: [.Amount: 1],
149163
coefficient: 1
150164
)
151-
152-
let carton = try Unit.define(
165+
try registryBuilder.addUnit(
153166
name: "carton",
154167
symbol: "carton",
155168
dimension: [.Amount: 1],
156169
coefficient: 48
157170
)
171+
let registry = registryBuilder.registry()
172+
173+
let apple = try Unit(fromSymbol: "apple", registry: registry)
174+
let carton = try Unit(fromSymbol: "carton", registry: registry)
158175

159176
let harvest = 288.measured(in: apple)
160177
print(harvest.convert(to: carton)) // Prints '6.0 carton'
@@ -163,48 +180,20 @@ print(harvest.convert(to: carton)) // Prints '6.0 carton'
163180
We can extend this example to determine how many cartons a group of people can pick in a week:
164181

165182
```swift
166-
let person = try Unit.define(
183+
try registryBuilder.addUnit(
167184
name: "person",
168185
symbol: "person",
169186
dimension: [.Amount: 1],
170187
coefficient: 1
171188
)
189+
let person = try Unit(fromSymbol: "person", registry: registryBuilder.registry())
172190

173191
let personPickRate = 600.measured(in: apple / .day / person)
174192
let workforce = 4.measured(in: person)
175193
let weeklyCartons = try (workforce * personPickRate).convert(to: carton / .week)
176194
print(weeklyCartons) // Prints '350.0 carton/week'
177195
```
178196

179-
### Adding custom units to the Registry
180-
181-
To support deserialization and runtime querying of available units, this package keeps a global registry of the default units. The `Unit.define` method does not insert new definitions into this registry. While this avoids conflicts and prevents race conditions, it also means that units created using `Unit.define` cannot be deserialized correctly or looked up using `Unit(fromSymbol:)`
182-
183-
If these features are absolutely needed, and the implications are understood, custom units can be added to the registry using `Unit.register`:
184-
185-
```swift
186-
let centifoot = try Unit.register(
187-
name: "centifoot",
188-
symbol: "cft",
189-
dimension: [.Length: 1],
190-
coefficient: 0.003048 // This is the conversion to meters
191-
)
192-
```
193-
194-
Note that you may only register the unit once globally, and afterwards it should be accessed either by the assigned variable or using `Unit(fromSymbol: String)`.
195-
196-
To simplify access, `Unit` may be extended with a static property:
197-
198-
```swift
199-
extension Unit {
200-
public static var centifoot = try! Unit.fromSymbol("cft")
201-
}
202-
203-
let measurement = 5.measured(in: .centifoot)
204-
```
205-
206-
Again, unless strictly necessary, `Unit.define` is preferred over `Unit.register`.
207-
208197
## CLI
209198

210199
The easiest way to install the CLI is with brew:

Sources/CLI/Convert.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ struct Convert: ParsableCommand {
3939
}
4040
}
4141

42+
let registry = Units.Registry.default
43+
4244
extension Expression: @retroactive ExpressibleByArgument {
4345
public convenience init?(argument: String) {
4446
let argument = argument.replacingOccurrences(of: "_", with: " ")
@@ -48,9 +50,9 @@ extension Expression: @retroactive ExpressibleByArgument {
4850

4951
extension Units.Unit: @retroactive ExpressibleByArgument {
5052
public init?(argument: String) {
51-
if let unit = try? Self(fromName: argument) {
53+
if let unit = try? Self(fromName: argument, registry: registry) {
5254
self = unit
53-
} else if let unit = try? Self(fromSymbol: argument) {
55+
} else if let unit = try? Self(fromSymbol: argument, registry: registry) {
5456
self = unit
5557
} else {
5658
return nil

Sources/CLI/List.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ struct List: ParsableCommand {
77
)
88

99
func run() throws {
10-
let units = Units.Unit.allDefined().sorted { u1, u2 in
10+
let units = registry.allUnits().sorted { u1, u2 in
1111
u1.name <= u2.name
1212
}
1313

Sources/Units/Expression.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ public final class Expression {
2727
/// - `5m^2/s + (1m + 2m)^2 / 5s`
2828
///
2929
/// - Parameter expr: The string expression to parse.
30-
public init(_ expr: String) throws {
31-
let parsed = try Parser(expr).parseExpression()
30+
public init(_ expr: String, registry: Registry = .default) throws {
31+
let parsed = try Parser(expr, registry: registry).parseExpression()
3232
first = parsed.first
3333
last = parsed.last
3434
count = parsed.count

Sources/Units/Measurement/Measurement.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,9 @@ extension Measurement: CustomStringConvertible {
165165
}
166166
}
167167

168-
extension Measurement: LosslessStringConvertible {
169-
public init?(_ description: String) {
170-
guard let parsed = try? Parser(description).parseMeasurement() else {
168+
public extension Measurement {
169+
init?(_ description: String, registry _: Registry = .default) {
170+
guard let parsed = try? Parser(description, registry: .default).parseMeasurement() else {
171171
return nil
172172
}
173173
value = parsed.value

Sources/Units/Parser.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Foundation
22

33
class Parser {
4+
var registry: Registry
45
var data: [UnicodeScalar]
56
var position = 0
67

@@ -18,8 +19,9 @@ class Parser {
1819
return Character(UnicodeScalar(data[position + 1]))
1920
}
2021

21-
init(_ string: String) {
22+
init(_ string: String, registry: Registry) {
2223
data = Array(string.unicodeScalars)
24+
self.registry = registry
2325
}
2426

2527
func parseMeasurement() throws -> Measurement {
@@ -206,7 +208,7 @@ class Parser {
206208
unitString.append(cur)
207209
consume()
208210
}
209-
let unit = try Unit(fromSymbol: unitString)
211+
let unit = try Unit(fromSymbol: unitString, registry: registry)
210212
return .unit(unit)
211213
}
212214
}

0 commit comments

Comments
 (0)