Skip to content

Commit 2a02e76

Browse files
feat: Add/Subtract support dimensionally equivalent units
More specifically, addition and subtraction no longer require that units match exactly. Instead, the units must only be dimensionally equivalent, and the left-hand unit is preserved as the unit of the output measurement.
1 parent a9bc961 commit 2a02e76

3 files changed

Lines changed: 29 additions & 9 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ This package has no other dependencies.
2323

2424
## Usage
2525

26-
Users should interact primarily with the `Measurement` struct. Here are a few usage examples:
26+
Users should interact primarily with the `Measurement` struct. Here are a examples of arithmetic:
2727

2828
```swift
2929
let drivingSpeed = 60.measured(in: .mile / .hour)
@@ -34,6 +34,8 @@ let drivingDistance = drivingSpeed * drivingTime
3434
print(drivingDistance.convert(to: .mile)) // Prints 30 mi
3535
```
3636

37+
Note that a measurement may be multiplied or divided by another measurement with any unit, resulting in a measurement that has a new-dimensioned unit (5 meters / 10 seconds ✅). However, addition and subtraction requires that both measurements have the same dimensionality (5 meters - 10 seconds ❌), otherwise a runtime error is thrown. If adding or subtracting two measurements with different units but the same dimensionality, the result retains the first measurement's unit (5 meters - 5 millimeters = 4.995 meters).
38+
3739
The type names in this package align closely with the unit system provided by `Foundation`. This was intentional to provide a
3840
familiar nomenclature for Swift developers. The APIs have been designed to avoid namespace ambiguity in files where both `Units`
3941
and `Foundation` are imported as much as possible. However, if an issue arises, just qualify the desired package like so:

Sources/Units/Measurement/Measurement.swift

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ public struct Measurement: Equatable, Codable {
3838
/// - Parameter newUnit: The unit to convert this measurement to
3939
/// - Returns: A new measurement with the converted scalar value and provided unit of measure
4040
public func convert(to newUnit: Unit) throws -> Measurement {
41+
if unit == newUnit {
42+
// No conversion needed
43+
return self
44+
}
4145
guard unit.isDimensionallyEquivalent(to: newUnit) else {
4246
throw UnitError.incompatibleUnits(message: "Cannot convert \(unit) to \(newUnit)")
4347
}
@@ -52,9 +56,9 @@ public struct Measurement: Equatable, Codable {
5256
/// - rhs: The right-hand-side measurement
5357
/// - Returns: A new measurement with the summed scalar values and the same unit of measure
5458
public static func + (lhs: Measurement, rhs: Measurement) throws -> Measurement {
55-
try checkSameUnit(lhs, rhs)
59+
let rhsValue = try rhs.convert(to: lhs.unit).value
5660
return Measurement(
57-
value: lhs.value + rhs.value,
61+
value: lhs.value + rhsValue,
5862
unit: lhs.unit
5963
)
6064
}
@@ -64,8 +68,8 @@ public struct Measurement: Equatable, Codable {
6468
/// - lhs: The left-hand-side measurement
6569
/// - rhs: The right-hand-side measurement
6670
public static func += (lhs: inout Measurement, rhs: Measurement) throws {
67-
try checkSameUnit(lhs, rhs)
68-
lhs.value = lhs.value + rhs.value
71+
let rhsValue = try rhs.convert(to: lhs.unit).value
72+
lhs.value = lhs.value + rhsValue
6973
}
7074

7175
/// Subtract one measurement from another. The measurements must have the same unit.
@@ -75,9 +79,9 @@ public struct Measurement: Equatable, Codable {
7579
/// - Returns: A new measurement with the subtracted scalar values and the same unit of measure
7680
/// and the same unit of measure
7781
public static func - (lhs: Measurement, rhs: Measurement) throws -> Measurement {
78-
try checkSameUnit(lhs, rhs)
82+
let rhsValue = try rhs.convert(to: lhs.unit).value
7983
return Measurement(
80-
value: lhs.value - rhs.value,
84+
value: lhs.value - rhsValue,
8185
unit: lhs.unit
8286
)
8387
}
@@ -87,8 +91,8 @@ public struct Measurement: Equatable, Codable {
8791
/// - lhs: The left-hand-side measurement
8892
/// - rhs: The right-hand-side measurement
8993
public static func -= (lhs: inout Measurement, rhs: Measurement) throws {
90-
try checkSameUnit(lhs, rhs)
91-
lhs.value = lhs.value - rhs.value
94+
let rhsValue = try rhs.convert(to: lhs.unit).value
95+
lhs.value = lhs.value - rhsValue
9296
}
9397

9498
/// Multiply the measurements. The measurements may have different units.

Tests/UnitsTests/MeasurementTests.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ final class MeasurementTests: XCTestCase {
3636
accuracy: accuracy
3737
)
3838

39+
// Test that adding different units of the same dimension works
40+
XCTAssertEqual(
41+
try 5.measured(in: .meter) + 5.measured(in: .millimeter),
42+
5.005.measured(in: .meter),
43+
)
44+
45+
// Test that adding different dimensions throws an error
3946
XCTAssertThrowsError(
4047
try 5.measured(in: .meter) + 5.measured(in: .second)
4148
)
@@ -62,6 +69,13 @@ final class MeasurementTests: XCTestCase {
6269
accuracy: accuracy
6370
)
6471

72+
// Test that subtracting different units of the same dimension works
73+
XCTAssertEqual(
74+
try 5.measured(in: .meter) - 5.measured(in: .millimeter),
75+
4.995.measured(in: .meter),
76+
)
77+
78+
// Test that subtracting different dimensions throws an error
6579
XCTAssertThrowsError(
6680
try 5.measured(in: .meter) - 5.measured(in: .second)
6781
)

0 commit comments

Comments
 (0)