Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 44 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ let registryBuilder = RegistryBuilder()
registryBuilder.addUnit(
name: "centifoot",
symbol: "cft",
dimension: [.Length: 1],
dimension: [.length: 1],
coefficient: 0.003048 // This is the conversion to meters
)
let registry = registryBuilder.registry()
Expand Down Expand Up @@ -159,13 +159,13 @@ let registryBuilder = RegistryBuilder()
try registryBuilder.addUnit(
name: "apple",
symbol: "apple",
dimension: [.Amount: 1],
dimension: [.amount: 1],
coefficient: 1
)
try registryBuilder.addUnit(
name: "carton",
symbol: "carton",
dimension: [.Amount: 1],
dimension: [.amount: 1],
coefficient: 48
)
let registry = registryBuilder.registry()
Expand All @@ -183,7 +183,7 @@ We can extend this example to determine how many cartons a group of people can p
try registryBuilder.addUnit(
name: "person",
symbol: "person",
dimension: [.Amount: 1],
dimension: [.amount: 1],
coefficient: 1
)
let person = try Unit(fromSymbol: "person", registry: registryBuilder.registry())
Expand All @@ -194,6 +194,46 @@ let weeklyCartons = try (workforce * personPickRate).convert(to: carton / .week)
print(weeklyCartons) // Prints '350.0 carton/week'
```

### Custom Dimensions

The built-in `Quantity` dimensions (`.length`, `.mass`, `.time`, etc.) cover the ISQ base
quantities, but you can define your own dimension when none of them fit — for example, a
`Money` dimension for cost calculations. Because `Quantity` is `RawRepresentable`, you can
add one with a static extension:

```swift
extension Quantity {
static let money = Quantity(rawValue: "Acme.Money")
}
```

Your dimension then behaves like any built-in one. Here a concrete rate of `$/m^3` is
multiplied by a volume in `m^3`, and the volume cancels to leave a pure cost:

```swift
let registry = try RegistryBuilder().addUnit(
name: "dollar",
symbol: "$",
dimension: [.money: 1]
).registry()
let dollar = try Unit(fromSymbol: "$", registry: registry)

let rate = Measurement(value: 180, unit: dollar / .meter.pow(3)) // $180 per m^3
let volume = Measurement(value: 24, unit: .meter.pow(3)) // 24 m^3
print(rate * volume) // Prints '4320.0 $'
```

A dedicated dimension is safer than repurposing an unrelated base quantity such as `.amount`,
which would silently cancel against unrelated units that share it. But the `rawValue` namespace
is **global**: a `Quantity` is identified solely by its raw string, across every module linked
into the program. If two modules each define a dimension with the same raw value — say both use
`"Money"` — they are treated as *the same dimension* and will silently cancel against each other,
producing wrong results with no error. There is no central registry to detect the clash.

To avoid this, namespace every custom `rawValue` with a prefix unique to your module or
organization (e.g. `"Acme.Money"`, not `"Money"`), exactly as the example above does. Reserve
bare names like `"Money"` for nothing, since you cannot know what another dependency has chosen.

## CLI

The easiest way to install the CLI is with brew:
Expand Down
77 changes: 65 additions & 12 deletions Sources/Units/Quantity.swift
Original file line number Diff line number Diff line change
@@ -1,17 +1,70 @@
/// A dimension of measurement. These may be combined to form composite dimensions and measurements
public enum Quantity: String, Sendable {
// TODO: Consider changing away from enum for extensibility
/// A dimension of measurement. These may be combined to form composite dimensions and measurements.
///
/// `Quantity` is `RawRepresentable` rather than an enum so that callers can define their own
/// dimensions without modifying this package. Add one with a static extension:
///
/// ```swift
/// extension Quantity {
/// static let money = Quantity(rawValue: "Money")
Comment thread
NeedleInAJayStack marked this conversation as resolved.
/// }
/// ```
///
/// Equality and hashing are based on `rawValue`, so each distinct dimension needs a unique
/// raw string. Because the namespace is global across every module that links this package,
/// two modules that independently pick the same raw value are treated as the *same* dimension
/// and silently cancel against one another. Prefix custom raw values to avoid collisions —
/// e.g. `"Acme.Money"` rather than `"Money"`.
public struct Quantity: RawRepresentable, Hashable, Sendable {
public let rawValue: String

public init(rawValue: String) {
self.rawValue = rawValue
}

// Base ISQ quantities: https://en.wikipedia.org/wiki/International_System_of_Quantities#Base_quantities
case Amount
case Current
case Length
case Mass
case Temperature
case Time
case LuminousIntensity
public static let amount = Quantity(rawValue: "Amount")
public static let current = Quantity(rawValue: "Current")
public static let length = Quantity(rawValue: "Length")
public static let mass = Quantity(rawValue: "Mass")
public static let temperature = Quantity(rawValue: "Temperature")
public static let time = Quantity(rawValue: "Time")
public static let luminousIntensity = Quantity(rawValue: "LuminousIntensity")

// Extended SI Units: https://en.wikipedia.org/wiki/Non-SI_units_mentioned_in_the_SI
case Angle
case Data
public static let angle = Quantity(rawValue: "Angle")
public static let data = Quantity(rawValue: "Data")
}

// MARK: - Deprecated PascalCase aliases

// The previous `enum Quantity` spelled its cases in PascalCase. These aliases keep existing
// call sites compiling against the renamed lowerCamelCase properties (Swift API Design
// Guidelines) and can be removed in a future major release.
public extension Quantity {
@available(*, deprecated, renamed: "amount")
static let Amount = Quantity.amount
@available(*, deprecated, renamed: "current")
static let Current = Quantity.current
@available(*, deprecated, renamed: "length")
static let Length = Quantity.length
@available(*, deprecated, renamed: "mass")
static let Mass = Quantity.mass
@available(*, deprecated, renamed: "temperature")
static let Temperature = Quantity.temperature
@available(*, deprecated, renamed: "time")
static let Time = Quantity.time
@available(*, deprecated, renamed: "luminousIntensity")
static let LuminousIntensity = Quantity.luminousIntensity
@available(*, deprecated, renamed: "angle")
static let Angle = Quantity.angle
@available(*, deprecated, renamed: "data")
static let Data = Quantity.data
}

// MARK: - CustomStringConvertible

extension Quantity: CustomStringConvertible {
// Preserve the enum's behavior: interpolating a Quantity yields its raw value
// (e.g. "Length"), not the synthesized struct description.
public var description: String { rawValue }
}
Comment thread
NeedleInAJayStack marked this conversation as resolved.
Loading
Loading