Skip to content

Commit

Permalink
Merge pull request #3 from NeedleInAJayStack/feature/protocol-conform…
Browse files Browse the repository at this point in the history
…ance

Unitless Support / Quality of Life improvements
  • Loading branch information
NeedleInAJayStack authored Jan 29, 2023
2 parents c05f5be + ae98df6 commit 5b44e99
Show file tree
Hide file tree
Showing 17 changed files with 701 additions and 104 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ jobs:
- uses: fwal/setup-swift@v1
- uses: actions/checkout@v2
- name: Run tests
run: swift test
run: swift test --skip PerformanceTests
4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,9 @@ let package = Package(
name: "UnitsTests",
dependencies: ["Units"]
),
.testTarget(
name: "PerformanceTests",
dependencies: ["Units"]
),
]
)
70 changes: 69 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,35 @@ and `Foundation` are imported as much as possible. However, if an issue arises,

```swift
let measurement = Units.Measurement(value: 5, unit: .mile)
```
```

## Scalars

This package provides a special `none` unit that represents a unitless scalar measurement:

```swift
let unitless = 5.measured(in: .none)
```

Measurements whose arithmetic has cancelled out units are also represented as unitless:

```swift
let a = 6.measured(in: .meter) / 2.measured(in: .meter)
print(a.unit) // Prints 'none'
```

Measurements are also representible through `Int` or `Float` scalars, which are automatically interpreted as unitless:

```swift
let m: Measurement = 6 // Has unit of 'none'
```

This allows including scalar values directly in arithmetic equations:

```swift
let distance: 5.measured(in: .meter) * 3 / 1.measured(in: .second)
print(distance) // Prints '15 m/s'
```

## Conversion

Expand Down Expand Up @@ -92,6 +120,46 @@ print(5.measured(in: .foot).convert(to: centifoot))

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

### Non-scientific Units

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:

```swift
let apple = try Unit.define(
name: "apple",
symbol: "apple",
dimension: [.Amount: 1],
coefficient: 1
)

let carton = try Unit.define(
name: "carton",
symbol: "carton",
dimension: [.Amount: 1],
coefficient: 48
)

let harvest = 288.measured(in: apple)
print(harvest.convert(to: carton)) // Prints '6.0 carton'
```

We can extend this example to determine how many cartons a group of people can pick in a week:

```swift
let person = try Unit.define(
name: "person",
symbol: "person",
dimension: [.Amount: 1],
coefficient: 1
)

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

### Adding custom units to the Registry

To support deserialization and runtime querying of available units, this package keeps a global
Expand Down
1 change: 1 addition & 0 deletions Scripts/dev_setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
echo "./Scripts/git_commit_hook.sh" > .git/hooks/pre-commit
1 change: 1 addition & 0 deletions Scripts/git_commit_hook.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
swiftformat .
1 change: 0 additions & 1 deletion Sources/Units/Measurement/Double+Measurement.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
public extension Double {

/// Creates a new measurement from the Double with the provided unit.
/// - Parameter unit: The unit to use in the resulting measurement
/// - Returns: A new measurement with a scalar value of the Double and the
Expand Down
1 change: 0 additions & 1 deletion Sources/Units/Measurement/Int+Measurement.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
public extension Int {

/// Creates a new measurement from the Int with the provided unit.
/// - Parameter unit: The unit to use in the resulting measurement
/// - Returns: A new measurement with a scalar value of the Int and the
Expand Down
110 changes: 95 additions & 15 deletions Sources/Units/Measurement/Measurement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import Foundation

/// A numeric scalar value with a unit of measure
public struct Measurement: Equatable, Codable {
public let value: Double
public let unit: Unit
public private(set) var value: Double
public private(set) var unit: Unit

/// Create a new measurement
/// - Parameters:
/// - value: The magnitude of the measurement
Expand Down Expand Up @@ -52,33 +52,45 @@ public struct Measurement: Equatable, Codable {
/// - rhs: The right-hand-side measurement
/// - Returns: A new measurement with the summed scalar values and the same unit of measure
public static func + (lhs: Measurement, rhs: Measurement) throws -> Measurement {
guard lhs.unit == rhs.unit else {
throw UnitError.incompatibleUnits(message: "Incompatible units: \(lhs.unit) != \(rhs.unit)")
}

try checkSameUnit(lhs, rhs)
return Measurement(
value: lhs.value + rhs.value,
unit: lhs.unit
)
}

/// Adds the measurements and stores the result in the left-hand-side variable. The measurements must have the same unit.
/// - Parameters:
/// - lhs: The left-hand-side measurement
/// - rhs: The right-hand-side measurement
public static func += (lhs: inout Measurement, rhs: Measurement) throws {
try checkSameUnit(lhs, rhs)
lhs.value = lhs.value + rhs.value
}

/// Subtract one measurement from another. The measurements must have the same unit.
/// - Parameters:
/// - lhs: The left-hand-side measurement
/// - rhs: The right-hand-side measurement
/// - Returns: A new measurement with a scalar value of the left-hand-side value minus the right-hand-side value
/// - Returns: A new measurement with the subtracted scalar values and the same unit of measure
/// and the same unit of measure
public static func - (lhs: Measurement, rhs: Measurement) throws -> Measurement {
guard lhs.unit == rhs.unit else {
throw UnitError.incompatibleUnits(message: "Incompatible units: \(lhs.unit) != \(rhs.unit)")
}

try checkSameUnit(lhs, rhs)
return Measurement(
value: lhs.value - rhs.value,
unit: lhs.unit
)
}

/// Subtracts the measurements and stores the result in the left-hand-side variable. The measurements must have the same unit.
/// - Parameters:
/// - lhs: The left-hand-side measurement
/// - rhs: The right-hand-side measurement
public static func -= (lhs: inout Measurement, rhs: Measurement) throws {
try checkSameUnit(lhs, rhs)
lhs.value = lhs.value - rhs.value
}

/// Multiply the measurements. The measurements may have different units.
/// - Parameters:
/// - lhs: The left-hand-side measurement
Expand All @@ -91,19 +103,36 @@ public struct Measurement: Equatable, Codable {
)
}

/// Multiplies the measurements and stores the result in the left-hand-side variable. The measurements may have different units.
/// - Parameters:
/// - lhs: The left-hand-side measurement
/// - rhs: The right-hand-side measurement
public static func *= (lhs: inout Measurement, rhs: Measurement) {
lhs.value = lhs.value * rhs.value
lhs.unit = lhs.unit * rhs.unit
}

/// Divide the measurements. The measurements may have different units.
/// - Parameters:
/// - lhs: The left-hand-side measurement
/// - rhs: The right-hand-side measurement
/// - Returns: A new measurement with a scalar value of the left-hand-side value divided by the right-hand-side value
/// and a combined unit of measure
/// - Returns: A new measurement with the divided scalar value and a combined unit of measure
public static func / (lhs: Measurement, rhs: Measurement) -> Measurement {
return Measurement(
value: lhs.value / rhs.value,
unit: lhs.unit / rhs.unit
)
}

/// Divide the measurements and stores the result in the left-hand-side variable. The measurements may have different units.
/// - Parameters:
/// - lhs: The left-hand-side measurement
/// - rhs: The right-hand-side measurement
public static func /= (lhs: inout Measurement, rhs: Measurement) {
lhs.value = lhs.value / rhs.value
lhs.unit = lhs.unit / rhs.unit
}

/// Exponentiate the measurement. This is equavalent to multiple `*` operations.
/// - Parameter raiseTo: The exponent to raise the measurement to
/// - Returns: A new measurement with an exponentiated scalar value and an exponentiated unit of measure
Expand All @@ -113,11 +142,62 @@ public struct Measurement: Equatable, Codable {
unit: unit.pow(raiseTo)
)
}

private static func checkSameUnit(_ lhs: Measurement, _ rhs: Measurement) throws {
guard lhs.unit == rhs.unit else {
throw UnitError.incompatibleUnits(message: "Incompatible units: \(lhs.unit) != \(rhs.unit)")
}
}
}

extension Measurement: CustomStringConvertible {
/// Displays the measurement as a string of the value and unit symbol
public var description: String {
return "\(value) \(unit)"
if unit == .none {
return "\(value)"
} else {
return "\(value) \(unit)"
}
}
}

extension Measurement: LosslessStringConvertible {
public init?(_ description: String) {
let valueEndIndex = description.firstIndex(of: " ") ?? description.endIndex
guard let value = Double(description[..<valueEndIndex]) else {
return nil
}
self.value = value

if valueEndIndex != description.endIndex {
guard let unit = Unit(String(
description[description.index(after: valueEndIndex) ..< description.endIndex]
)) else {
return nil
}
self.unit = unit
} else {
unit = .none
}
}
}

extension Measurement: Hashable {}

extension Measurement: ExpressibleByIntegerLiteral {
public init(integerLiteral value: Int64) {
self = Self(value: Double(value), unit: .none)
}

public typealias IntegerLiteralType = Int64
}

extension Measurement: ExpressibleByFloatLiteral {
public init(floatLiteral value: Double) {
self = Self(value: value, unit: .none)
}

public typealias FloatLiteralType = Double
}

extension Measurement: Sendable {}
2 changes: 1 addition & 1 deletion Sources/Units/Quantity.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// A dimension of measurement. These may be combined to form composite dimensions and measurements
public enum Quantity {
public enum Quantity: Sendable {
// TODO: Consider changing away from enum for extensibility

// Base ISQ quantities: https://en.wikipedia.org/wiki/International_System_of_Quantities#Base_quantities
Expand Down
Loading

0 comments on commit 5b44e99

Please sign in to comment.