Skip to content

Commit 55df9cc

Browse files
Merge pull request #19 from NeedleInAJayStack/feat/swift-6
Breaking: Swift 6 Compatibility
2 parents 152b28a + 170f963 commit 55df9cc

19 files changed

Lines changed: 579 additions & 540 deletions

.github/workflows/test.yml

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,38 @@ on:
44
push: { branches: [ main ] }
55

66
jobs:
7-
test:
7+
macos:
8+
name: Test on macOS
9+
runs-on: macos-latest
10+
steps:
11+
- uses: maxim-lobanov/setup-xcode@v1
12+
with:
13+
xcode-version: latest-stable
14+
- uses: actions/checkout@v4
15+
- name: Build and test
16+
run: |
17+
swift test \
18+
--parallel \
19+
--skip PerformanceTests
20+
21+
linux:
22+
name: Test on Linux - ${{ matrix.swift-image }}
823
strategy:
924
matrix:
10-
os: [ubuntu-latest, macos-latest]
11-
runs-on: ${{ matrix.os }}
25+
swift-image:
26+
- "swift:5.10-jammy"
27+
- "swift:5.10-noble"
28+
- "swift:6.0-jammy"
29+
- "swift:6.0-noble"
30+
- "swift:6.1-jammy"
31+
- "swift:6.1-noble"
32+
runs-on: ubuntu-latest
33+
container: ${{ matrix.swift-image }}
1234
steps:
13-
- name: Checkout
14-
uses: actions/checkout@v4
15-
- name: Set up Swift
16-
uses: swift-actions/setup-swift@v2
17-
- name: Run tests
18-
run: swift test --skip PerformanceTests
35+
- name: Checkout
36+
uses: actions/checkout@v4
37+
- name: Test
38+
run: |
39+
swift test \
40+
--parallel \
41+
--skip PerformanceTests

Migration.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# v0 to v1
2+
3+
## Registry singleton removal
4+
5+
To avoid data races, the internal `Registry` singleton has been removed, instead preferring an explicit user-defined `Registry`. For example, when parsing or querying units from Strings, a registry instance should be passed:
6+
7+
```swift
8+
let meter = try Unit(fromSymbol: "m") // old
9+
let meter = try Unit(fromSymbol: "m", registry: registry) // new
10+
```
11+
12+
Note that if registry is omitted from these functions, the default unit database is used, which should provide a relatively smooth transition in the common case where custom units are not used.
13+
14+
## Registry builder
15+
16+
Registries should be defined and instantiated during startup, and must not be changed after creation. To enforce this lifecycle, a `RegistryBuilder` has been introduced that custom units may be registered to.
17+
18+
```swift
19+
// old
20+
let centifoot = try Unit.define(
21+
name: "centifoot",
22+
symbol: "cft",
23+
dimension: [.Length: 1],
24+
coefficient: 0.003048
25+
)
26+
27+
// new
28+
let registryBuilder = RegistryBuilder()
29+
registryBuilder.addUnit(
30+
name: "centifoot",
31+
symbol: "cft",
32+
dimension: [.Length: 1],
33+
coefficient: 0.003048
34+
)
35+
let registry = registryBuilder.registry()
36+
```
37+
38+
## Registry Encode/Decode support
39+
40+
To provide `Registry` lookup support inside `Encode`/`Decode` processes, a `userInfo` key has been added:
41+
42+
```swift
43+
let encoder = JSONEncoder()
44+
encoder.userInfo[Unit.registryUserInfoKey] = Registry.default
45+
try encoder.encode(Unit.meter / .second)
46+
47+
let decoder = JSONDecoder()
48+
decoder.userInfo[Unit.registryUserInfoKey] = Registry.default
49+
try decoder.decode(Unit.self, from: "\"m\\/s\"".data(using: .utf8)!)
50+
```

Package.resolved

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,6 @@ let package = Package(
3737
name: "PerformanceTests",
3838
dependencies: ["Units"]
3939
),
40-
]
40+
],
41+
swiftLanguageVersions: [.v5, .version("6")]
4142
)

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: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import ArgumentParser
22
import Units
33

44
struct Convert: ParsableCommand {
5-
static var configuration = CommandConfiguration(
5+
static let configuration = CommandConfiguration(
66
abstract: "Convert a measurement expression to a specified unit.",
77
discussion: """
88
Run `unit list` to see the supported unit symbols and names. Unless arguments are wrapped \
@@ -39,18 +39,20 @@ struct Convert: ParsableCommand {
3939
}
4040
}
4141

42-
extension Expression: ExpressibleByArgument {
42+
let registry = Units.Registry.default
43+
44+
extension Units.Expression: ArgumentParser.ExpressibleByArgument {
4345
public convenience init?(argument: String) {
4446
let argument = argument.replacingOccurrences(of: "_", with: " ")
4547
try? self.init(argument)
4648
}
4749
}
4850

49-
extension Units.Unit: ExpressibleByArgument {
51+
extension Units.Unit: ArgumentParser.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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import ArgumentParser
22
import Units
33

44
struct List: ParsableCommand {
5-
static var configuration = CommandConfiguration(
5+
static let configuration = CommandConfiguration(
66
abstract: "Print a table of the available units, their symbols, and their dimensionality."
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/CLI/Unit.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import ArgumentParser
22

33
struct Unit: ParsableCommand {
4-
static var configuration = CommandConfiguration(
4+
static let configuration = CommandConfiguration(
55
abstract: "A utility for performing unit conversions.",
66
subcommands: [Convert.self, List.self]
77
)

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

0 commit comments

Comments
 (0)