Skip to content

Commit 0211648

Browse files
Merge pull request #17 from NeedleInAJayStack/feat/add-dimensionally-equivalent-units
2 parents bc5782d + cf97e6b commit 0211648

10 files changed

Lines changed: 179 additions & 191 deletions

File tree

README.md

Lines changed: 29 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
# Units 📏
22

3-
Units is a Swift package to manipulate, compare, and convert between physical quantities. This package models measurements,
4-
which are a numerical value with a unit of measure. It has been designed so that users don't need to worry whether they are
5-
using a defined unit (like `Newton`) or a complex composite unit (like `kg*m/s^2`). Both should be easy to convert to and from
6-
different units, perform arithmetic operations, check dimensionality, or serialize to and from a string format.
3+
Units is a Swift package to manipulate, compare, and convert between physical quantities. This package models measurements, which are a numerical value with a unit of measure. It has been designed so that users don't need to worry whether they are using a defined unit (like `Newton`) or a complex composite unit (like `kg*m/s^2`). Both should be easy to convert to and from different units, perform arithmetic operations, check dimensionality, or serialize to and from a string format.
74

8-
This approach allows us to easily handle any permutation of units. You want to convert `12 km³/hr/N` to
9-
`ft²*s/lb`? We've got you covered!
5+
This approach allows us to easily handle any permutation of units. You want to convert `12 km³/hr/N` to `ft²*s/lb`? We've got you covered!
106

117
Included is a convenient command-line tool for performing quick unit conversions. See the [CLI section](#cli) for details.
128

@@ -23,7 +19,7 @@ This package has no other dependencies.
2319

2420
## Usage
2521

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

2824
```swift
2925
let drivingSpeed = 60.measured(in: .mile / .hour)
@@ -34,9 +30,9 @@ let drivingDistance = drivingSpeed * drivingTime
3430
print(drivingDistance.convert(to: .mile)) // Prints 30 mi
3531
```
3632

37-
The type names in this package align closely with the unit system provided by `Foundation`. This was intentional to provide a
38-
familiar nomenclature for Swift developers. The APIs have been designed to avoid namespace ambiguity in files where both `Units`
39-
and `Foundation` are imported as much as possible. However, if an issue arises, just qualify the desired package like so:
33+
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).
34+
35+
The type names in this package align closely with the unit system provided by `Foundation`. This was intentional to provide a familiar nomenclature for Swift developers. The APIs have been designed to avoid namespace ambiguity in files where both `Units` and `Foundation` are imported as much as possible. However, if an issue arises, just qualify the desired package like so:
4036

4137
```swift
4238
let measurement = Units.Measurement(value: 5, unit: .mile)
@@ -72,41 +68,25 @@ print(distance) // Prints '15 m/s'
7268

7369
## Conversion
7470

75-
Only linear conversions are supported. The vast majority of unit conversions are simply changes in scale, represented by a single
76-
conversion coefficient, sometimes with a constant shift. Units that don't match this format (like currency conversions, which are
77-
typically time-based functions) are not supported.
71+
Only linear conversions are supported. The vast majority of unit conversions are simply changes in scale, represented by a single conversion coefficient, sometimes with a constant shift. Units that don't match this format (like currency conversions, which are typically time-based functions) are not supported.
7872

79-
Composite units are those that represent complex quantities and dimensions. A good example is `horsepower`, whose quantity is
80-
`mass * length^2 / time^2`.
73+
Composite units are those that represent complex quantities and dimensions. A good example is `horsepower`, whose quantity is `mass * length^2 / time^2`.
8174

8275
### Coefficients
8376

84-
Each quantity has a single "base unit", through which the units of that quantity may be converted. SI units have been
85-
chosen to be these base units for all quantities.
77+
Each quantity has a single "base unit", through which the units of that quantity may be converted. SI units have been chosen to be these base units for all quantities.
8678

87-
Non-base units require a conversion coefficient to convert between them and other units of the same dimension. This coefficient
88-
is the number of base units there are in one of the defined unit. For example, `kilometer` has a coefficient of `1000`
89-
because there are 1000 meters in 1 kilometer.
79+
Non-base units require a conversion coefficient to convert between them and other units of the same dimension. This coefficient is the number of base units there are in one of the defined unit. For example, `kilometer` has a coefficient of `1000` because there are 1000 meters in 1 kilometer.
9080

91-
Composite units must have a coefficient that converts to the composte SI units of those dimensions. That is, `horsepower` should
92-
have a conversion to `kilogram * meter^2 / second^2` (otherwise known as `watt`). This is natural for SI quantities and units, but
93-
care should be taken that a single, absolute base unit is chosen for all non-SI quantities since they will impact all composite
94-
conversions.
81+
Composite units must have a coefficient that converts to the composte SI units of those dimensions. That is, `horsepower` should have a conversion to `kilogram * meter^2 / second^2` (otherwise known as `watt`). This is natural for SI quantities and units, but care should be taken that a single, absolute base unit is chosen for all non-SI quantities since they will impact all composite conversions.
9582

9683
### Constants
9784

98-
Units that include a constant value, such as Fahrenheit, cannot be used within composite unit conversions. For example,
99-
you may not convert `5m/°F` to `m/°C` because its unclear how to handle their shifted scale. Instead use the
100-
non-shifted Kelvin and Rankine temperature units to refer to temperature differentials.
85+
Units that include a constant value, such as Fahrenheit, cannot be used within composite unit conversions. For example, you may not convert `5m/°F` to `m/°C` because its unclear how to handle their shifted scale. Instead use the non-shifted Kelvin and Rankine temperature units to refer to temperature differentials.
10186

10287
## Serialization
10388

104-
Each defined unit must have a unique symbol, which is used to identify and serialize/deserialize it. Defined unit symbols are not
105-
allowed to contain the `*`, `/`, `^`, or ` ` characters because those are used in the symbol representation of complex units.
106-
Complex units are represented by their arithmetic combination of simple units using `*` for multiplication, `/` for division,
107-
and `^` for exponentiation. Order of operations treats exponentiation first, and multiplication and division equally, from left-
108-
to-right. This means that, unless negatively exponentiated, units following a `*` can always be considered to be "in the numerator",
109-
and those following `/` can always be considered to be "in the denominator".
89+
Each defined unit must have a unique symbol, which is used to identify and serialize/deserialize it. Defined unit symbols are not allowed to contain the `*`, `/`, `^`, or ` ` characters because those are used in the symbol representation of complex units. Complex units are represented by their arithmetic combination of simple units using `*` for multiplication, `/` for division, and `^` for exponentiation. Order of operations treats exponentiation first, and multiplication and division equally, from left-to-right. This means that, unless negatively exponentiated, units following a `*` can always be considered to be "in the numerator", and those following `/` can always be considered to be "in the denominator".
11090

11191
Here are a few examples:
11292

@@ -125,7 +105,7 @@ Expressions are a mathematical combination of measurements. Arithemetic operator
125105
- `5.3 m + 3.8 m`
126106
- `5m^2/s + (1m + 2m)^2 / 5s`
127107

128-
There are few expression parsing rules to keep in mind:
108+
There are few expression parsing rules to keep in mind:
129109

130110
- All parentheses must be matched
131111
- All measurement operators must have a leading and following space. i.e. ` * `
@@ -134,8 +114,7 @@ There are few expression parsing rules to keep in mind:
134114

135115
## Default Units
136116

137-
For a list of the default units and their conversion factors, see the
138-
[`DefaultUnits.swift file`](https://github.com/NeedleInAJayStack/Units/blob/main/Sources/Units/Unit/DefaultUnits.swift)
117+
For a list of the default units and their conversion factors, see the [`DefaultUnits.swift file`](https://github.com/NeedleInAJayStack/Units/blob/main/Sources/Units/Unit/DefaultUnits.swift)
139118

140119
## Custom Units
141120

@@ -157,8 +136,7 @@ This returns a Unit object that can be used in arithmetic, conversions, and seri
157136

158137
### Non-scientific Units
159138

160-
For "non-scientific" units, it is typically appropriate to use the `Amount` quantity. Through this
161-
approach, you can easily build up an impromptu conversion system on the fly. For example:
139+
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:
162140

163141
```swift
164142
let apple = try Unit.define(
@@ -197,13 +175,9 @@ print(weeklyCartons) // Prints '350.0 carton/week'
197175

198176
### Adding custom units to the Registry
199177

200-
To support deserialization and runtime querying of available units, this package keeps a global
201-
registry of the default units. The `Unit.define` method does not insert new definitions into this
202-
registry. While this avoids conflicts and prevents race conditions, it also means that units created
203-
using `Unit.define` cannot be deserialized correctly or looked up using `Unit(fromSymbol:)`
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:)`
204179

205-
If these features are absolutely needed, and the implications are understood, custom units can be
206-
added to the registry using `Unit.register`:
180+
If these features are absolutely needed, and the implications are understood, custom units can be added to the registry using `Unit.register`:
207181

208182
```swift
209183
let centifoot = try Unit.register(
@@ -214,8 +188,7 @@ let centifoot = try Unit.register(
214188
)
215189
```
216190

217-
Note that you may only register the unit once globally, and afterwards it should be accessed
218-
either by the assigned variable or using `Unit(fromSymbol: String)`.
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)`.
219192

220193
To simplify access, `Unit` may be extended with a static property:
221194

@@ -231,8 +204,14 @@ Again, unless strictly necessary, `Unit.define` is preferred over `Unit.register
231204

232205
## CLI
233206

234-
The command-line interface can be built and installed by running the command below. Note that
235-
[swift](https://www.swift.org/download/) must be installed.
207+
The easiest way to install the CLI is with brew:
208+
209+
```sh
210+
brew tap NeedleInAJayStack/tap
211+
brew install units
212+
```
213+
214+
Alternatively, you can build it from source and install it to `/usr/local/bin/` using the install script. Note that [swift](https://www.swift.org/download/) must be installed, and you need write permissions to `/usr/local/bin/`.
236215

237216
```bash
238217
./install.sh
@@ -252,9 +231,7 @@ You can then perform unit conversions using the `unit convert` command:
252231
unit convert 5m/s mi/hr # Returns 11.184681460272012 mi/hr
253232
```
254233

255-
This command uses the unit and expression [serialization format](#serialization). Note that for
256-
convenience, you may use an underscore `_` to represent the normally serialized space. Also,
257-
`*` characters may need to be escaped.
234+
This command uses the unit and expression [serialization format](#serialization). Note that for convenience, you may use an underscore `_` to represent the normally serialized space. Also, `*` characters may need to be escaped.
258235

259236
You can also evaulate math in the first argument. For example:
260237

@@ -272,6 +249,4 @@ unit list
272249

273250
## Contributing
274251

275-
Contributions are absolutely welcome! If you find yourself using a custom unit a lot, feel free
276-
to stick it in an MR, and we can add it to the default list!
277-
252+
Contributions are absolutely welcome! If you find yourself using a custom unit a lot, feel free to stick it in an MR, and we can add it to the default list!

Sources/Units/Expression.swift

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@
22
public final class Expression {
33
// Implemented as a linked list of ExpressionNodes. This allows us to indicate operators,
44
// and iteratively solve by reducing the list according to the order of operations.
5-
5+
66
var first: ExpressionNode
77
var last: ExpressionNode
88
var count: Int
9-
9+
1010
init(node: ExpressionNode) {
11-
self.first = node
12-
self.last = node
11+
first = node
12+
last = node
1313
count = 1
1414
}
15-
15+
1616
/// Initializes an expression from a string.
1717
///
1818
/// Parsing rules:
@@ -29,25 +29,25 @@ public final class Expression {
2929
/// - Parameter expr: The string expression to parse.
3030
public init(_ expr: String) throws {
3131
let parsed = try Parser(expr).parseExpression()
32-
self.first = parsed.first
33-
self.last = parsed.last
34-
self.count = parsed.count
32+
first = parsed.first
33+
last = parsed.last
34+
count = parsed.count
3535
}
36-
36+
3737
/// Reduces the expression to a single measurement, respecting the [order of operations](https://en.wikipedia.org/wiki/Order_of_operations)
3838
public func solve() throws -> Measurement {
3939
let copy = self.copy()
4040
return try copy.computeAndDestroy()
4141
}
42-
42+
4343
@discardableResult
4444
func append(op: Operator, node: ExpressionNode) -> Self {
4545
last.next = .init(op: op, node: node)
4646
last = node
4747
count = count + 1
4848
return self
4949
}
50-
50+
5151
func copy() -> Expression {
5252
// Copy the expression list so the original is not destroyed
5353
let copy = Expression(node: first.copy())
@@ -58,12 +58,11 @@ public final class Expression {
5858
}
5959
return copy
6060
}
61-
61+
6262
/// Reduces the expression to a single measurement, respecting the [order of operations](https://en.wikipedia.org/wiki/Order_of_operations)
6363
///
6464
/// NOTE: This flattens the list, destroying it. Use `solve` for non-destructive behavior.
6565
private func computeAndDestroy() throws -> Measurement {
66-
6766
// SubExpressions
6867
func computeSubExpression(node: ExpressionNode) throws {
6968
switch node.value {
@@ -81,7 +80,7 @@ public final class Expression {
8180
}
8281
try computeSubExpression(node: left)
8382
// At this point, there should be no more sub expressions
84-
83+
8584
// Exponentals
8685
func exponentiate(node: ExpressionNode) throws {
8786
guard let exponent = node.exponent else {
@@ -102,7 +101,7 @@ public final class Expression {
102101
left = next.node
103102
}
104103
try exponentiate(node: left)
105-
104+
106105
// Multiplication
107106
left = first
108107
while let next = left.next {
@@ -123,15 +122,15 @@ public final class Expression {
123122
fatalError("Parentheses still present during multiplication phase")
124123
}
125124
}
126-
125+
127126
// Addition
128127
left = first
129128
while let next = left.next {
130129
let right = next.node
131130
switch (left.value, right.value) {
132131
case let (.measurement(leftMeasurement), .measurement(rightMeasurement)):
133132
switch next.op {
134-
case .add: // Compute and absorb right node into left
133+
case .add: // Compute and absorb right node into left
135134
left.value = try .measurement(leftMeasurement + rightMeasurement)
136135
left.next = right.next
137136
case .subtract: // Compute and absorb right node into left
@@ -144,7 +143,7 @@ public final class Expression {
144143
fatalError("Parentheses still present during addition phase")
145144
}
146145
}
147-
146+
148147
if first.next != nil {
149148
fatalError("Expression list reduction not complete")
150149
}
@@ -194,15 +193,15 @@ class ExpressionNode {
194193
var value: ExpressionNodeValue
195194
var exponent: Int?
196195
var next: ExpressionLink?
197-
196+
198197
init(_ value: ExpressionNodeValue, exponent: Int? = nil, next: ExpressionLink? = nil) {
199198
self.value = value
200199
self.exponent = exponent
201200
self.next = next
202201
}
203-
202+
204203
func copy() -> ExpressionNode {
205-
return .init(value.copy(), exponent: self.exponent)
204+
return .init(value.copy(), exponent: exponent)
206205
}
207206
}
208207

@@ -216,7 +215,7 @@ extension ExpressionNode: Equatable {
216215
enum ExpressionNodeValue {
217216
case measurement(Measurement)
218217
case subExpression(Expression)
219-
218+
220219
func copy() -> ExpressionNodeValue {
221220
switch self {
222221
case let .measurement(measurement):
@@ -254,7 +253,7 @@ extension ExpressionNodeValue: Equatable {
254253
class ExpressionLink {
255254
let op: Operator
256255
let node: ExpressionNode
257-
256+
258257
init(op: Operator, node: ExpressionNode) {
259258
self.op = op
260259
self.node = node

0 commit comments

Comments
 (0)