Skip to content

Commit 84a0493

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 59f786c commit 84a0493

12 files changed

Lines changed: 479 additions & 517 deletions

File tree

README.md

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

119119
## Custom Units
120120

121-
To extend this package, users can define their own custom units using `Unit.define`:
121+
The unit system is backed by a `Registry` that maps unit symbols to their metadata. To add new units, use `RegistryBuilder`:
122122

123123
```swift
124-
let centifoot = try Unit.define(
124+
let registryBuilder = RegistryBuilder()
125+
registryBuilder.addUnit(
125126
name: "centifoot",
126127
symbol: "cft",
127128
dimension: [.Length: 1],
128129
coefficient: 0.003048 // This is the conversion to meters
129130
)
131+
let registry = registryBuilder.registry()
132+
let centifoot = try Unit(fromSymbol: "cft", registry: registry)
130133

131134
let measurement = Measurement(value: 5, unit: centifoot)
132135
print(5.measured(in: .foot).convert(to: centifoot))
133136
```
134137

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

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

139152
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:
140153

141154
```swift
142-
let apple = try Unit.define(
155+
let registryBuilder = RegistryBuilder()
156+
try registryBuilder.addUnit(
143157
name: "apple",
144158
symbol: "apple",
145159
dimension: [.Amount: 1],
146160
coefficient: 1
147161
)
148-
149-
let carton = try Unit.define(
162+
try registryBuilder.addUnit(
150163
name: "carton",
151164
symbol: "carton",
152165
dimension: [.Amount: 1],
153166
coefficient: 48
154167
)
168+
let registry = registryBuilder.registry()
169+
170+
let apple = try Unit(fromSymbol: "apple", registry: registry)
171+
let carton = try Unit(fromSymbol: "carton", registry: registry)
155172

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

162179
```swift
163-
let person = try Unit.define(
180+
try registryBuilder.addUnit(
164181
name: "person",
165182
symbol: "person",
166183
dimension: [.Amount: 1],
167184
coefficient: 1
168185
)
186+
let person = try Unit(fromSymbol: "person", registry: registryBuilder.registry())
169187

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

176-
### Adding custom units to the Registry
177-
178-
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:)`
179-
180-
If these features are absolutely needed, and the implications are understood, custom units can be added to the registry using `Unit.register`:
181-
182-
```swift
183-
let centifoot = try Unit.register(
184-
name: "centifoot",
185-
symbol: "cft",
186-
dimension: [.Length: 1],
187-
coefficient: 0.003048 // This is the conversion to meters
188-
)
189-
```
190-
191-
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)`.
192-
193-
To simplify access, `Unit` may be extended with a static property:
194-
195-
```swift
196-
extension Unit {
197-
public static var centifoot = try! Unit.fromSymbol("cft")
198-
}
199-
200-
let measurement = 5.measured(in: .centifoot)
201-
```
202-
203-
Again, unless strictly necessary, `Unit.define` is preferred over `Unit.register`.
204-
205194
## CLI
206195

207196
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)