Skip to content

Commit 80d55e0

Browse files
committed
integrated Percent into solver and added Unit tests
1 parent 0319a5a commit 80d55e0

6 files changed

Lines changed: 134 additions & 37 deletions

File tree

Sources/Units/Expression.swift

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -107,19 +107,27 @@ public final class Expression {
107107
while let next = left.next {
108108
let right = next.node
109109
switch (left.value, right.value) {
110-
case let (.measurement(leftMeasurement), .measurement(rightMeasurement)):
111-
switch next.op {
112-
case .add, .subtract: // Skip over operation
113-
left = right
114-
case .multiply: // Compute and absorb right node into left
115-
left.value = .measurement(leftMeasurement * rightMeasurement)
116-
left.next = right.next
117-
case .divide: // Compute and absorb right node into left
118-
left.value = .measurement(leftMeasurement / rightMeasurement)
119-
left.next = right.next
120-
}
121-
default:
122-
fatalError("Parentheses still present during multiplication phase")
110+
case let (.measurement(leftMeasurement), .measurement(rightMeasurement)):
111+
switch next.op {
112+
case .add, .subtract: // Skip over operation
113+
left = right
114+
case .multiply: // Compute and absorb right node into left
115+
if let percent = rightMeasurement.asPercent {
116+
left.value = .measurement(leftMeasurement * percent)
117+
} else {
118+
left.value = .measurement(leftMeasurement * rightMeasurement)
119+
}
120+
left.next = right.next
121+
case .divide: // Compute and absorb right node into left
122+
if let percent = rightMeasurement.asPercent {
123+
left.value = .measurement(leftMeasurement / percent)
124+
} else {
125+
left.value = .measurement(leftMeasurement / rightMeasurement)
126+
}
127+
left.next = right.next
128+
}
129+
default:
130+
fatalError("Parentheses still present during multiplication phase")
123131
}
124132
}
125133

@@ -130,11 +138,21 @@ public final class Expression {
130138
switch (left.value, right.value) {
131139
case let (.measurement(leftMeasurement), .measurement(rightMeasurement)):
132140
switch next.op {
141+
133142
case .add: // Compute and absorb right node into left
134-
left.value = try .measurement(leftMeasurement + rightMeasurement)
143+
// NOTE: Exceptional handling of Percent
144+
if let percent = rightMeasurement.asPercent {
145+
left.value = .measurement(leftMeasurement + percent)
146+
} else {
147+
left.value = try .measurement(leftMeasurement + rightMeasurement)
148+
}
135149
left.next = right.next
136150
case .subtract: // Compute and absorb right node into left
137-
left.value = try .measurement(leftMeasurement - rightMeasurement)
151+
if let percent = rightMeasurement.asPercent {
152+
left.value = .measurement(leftMeasurement - percent)
153+
} else {
154+
left.value = try .measurement(leftMeasurement - rightMeasurement)
155+
}
138156
left.next = right.next
139157
case .multiply, .divide:
140158
fatalError("Multiplication still present during addition phase")

Sources/Units/Measurement/Percent+Measurement.swift

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
//
2-
// Percent.swift
3-
// Units
4-
//
5-
// Created by Jason Jobe on 9/5/25.
6-
//
7-
81
import Foundation
92
/*
103
NOTE: Should consider introducing `protocol Scalar`
@@ -72,17 +65,22 @@ public struct Percent: Numeric, Equatable, Codable {
7265
}
7366
}
7467

68+
extension Measurement {
69+
public var isPercent: Bool {
70+
self.unit == Percent.unit
71+
}
72+
73+
public var asPercent: Percent? {
74+
isPercent ? Percent(magnitude: self.value/100) : nil
75+
}
76+
}
77+
7578
// MARK: Percent as Unit
7679
extension Percent {
7780
public var unit: Unit { Self.unit }
7881

7982
public static let unit = Unit(
80-
definedBy: try! DefinedUnit(
81-
name: "percent",
82-
symbol: "%",
83-
dimension: [:],
84-
coefficient: 0.01
85-
))
83+
definedBy: DefaultUnits.percent)
8684
}
8785

8886
// MARK: Numeric Conformance
@@ -129,11 +127,11 @@ extension BinaryFloatingPoint {
129127
// AdditiveArithmetic operations `*` and `/`
130128

131129
public extension Measurement {
132-
/// Calculate the percentage of the Measurement
130+
/// Adds a percentage to a measurement by increasing its value by the given percent.
133131
/// - Parameters:
134-
/// - lhs: The left-hand-side measurement
135-
/// - rhs: The right-hand-side measurement
136-
/// - Returns: A new measurement with the summed scalar values and the same unit of measure
132+
/// - lhs: The base measurement.
133+
/// - rhs: The percentage to add.
134+
/// - Returns: A new `Measurement` with its value increased by the given percentage.
137135
@_disfavoredOverload
138136
static func + (lhs: Measurement, rhs: Percent) -> Measurement {
139137
return Measurement(
@@ -142,6 +140,11 @@ public extension Measurement {
142140
)
143141
}
144142

143+
/// Subtracts a percentage from a measurement by decreasing its value by the given percent.
144+
/// - Parameters:
145+
/// - lhs: The base measurement.
146+
/// - rhs: The percentage to subtract.
147+
/// - Returns: A new `Measurement` with its value decreased by the given percentage.
145148
@_disfavoredOverload
146149
static func - (lhs: Measurement, rhs: Percent) -> Measurement {
147150
return Measurement(
@@ -150,35 +153,51 @@ public extension Measurement {
150153
)
151154
}
152155

156+
/// Increases a measurement in place by the given percentage.
157+
/// - Parameters:
158+
/// - lhs: The measurement to modify.
159+
/// - rhs: The percentage to add.
153160
@_disfavoredOverload
154161
static func += (lhs: inout Measurement, rhs: Percent) {
155162
lhs = lhs + rhs
156163
}
157-
164+
165+
/// Decreases a measurement in place by the given percentage.
166+
/// - Parameters:
167+
/// - lhs: The measurement to modify.
168+
/// - rhs: The percentage to subtract.
158169
@_disfavoredOverload
159170
static func -= (lhs: inout Measurement, rhs: Percent) {
160171
lhs = lhs - rhs
161172
}
162-
173+
163174
}
164175

165176
// Scalar operations `*` and `/`
166177
public extension Measurement {
167-
178+
/// Multiplies a measurement by a percentage, treating the percent as a scalar (e.g., 25% = 0.25).
179+
/// - Parameters:
180+
/// - lhs: The base measurement to scale.
181+
/// - rhs: The percentage factor.
182+
/// - Returns: A new `Measurement` whose value is `lhs.value * rhs.magnitude` with the same unit.
168183
@_disfavoredOverload
169184
static func * (lhs: Measurement, rhs: Percent) -> Measurement {
170185
return Measurement(
171186
value: lhs.value * rhs.magnitude,
172187
unit: lhs.unit
173188
)
174189
}
175-
190+
191+
/// Divides a measurement by a percentage, treating the percent as a scalar (e.g., 25% = 0.25).
192+
/// - Parameters:
193+
/// - lhs: The base measurement to scale.
194+
/// - rhs: The percentage divisor.
195+
/// - Returns: A new `Measurement` whose value is `lhs.value / rhs.magnitude` with the same unit.
176196
@_disfavoredOverload
177197
static func / (lhs: Measurement, rhs: Percent) -> Measurement {
178198
return Measurement(
179199
value: lhs.value / rhs.magnitude,
180200
unit: lhs.unit
181201
)
182202
}
183-
184203
}

Sources/Units/RegistryBuilder.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,9 @@ public class RegistryBuilder {
249249
DefaultUnits.troyOunces,
250250
DefaultUnits.slug,
251251

252+
// MARK: Percent
253+
DefaultUnits.percent,
254+
252255
// MARK: Power
253256

254257
DefaultUnits.watt,

Sources/Units/Unit/DefaultUnits.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,15 @@ enum DefaultUnits {
818818
coefficient: 14.5939
819819
)
820820

821+
// MARK: Percent
822+
823+
static let percent = try! DefinedUnit(
824+
name: "percent",
825+
symbol: "%",
826+
dimension: [:],
827+
coefficient: 0.01
828+
)
829+
821830
// MARK: Power
822831

823832
// Base unit: watt

Sources/Units/Unit/Unit+DefaultUnits.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,10 @@ public extension Unit {
190190
static let troyOunces = Unit(definedBy: DefaultUnits.troyOunces)
191191
static let slug = Unit(definedBy: DefaultUnits.slug)
192192

193+
// MARK: Percent
194+
195+
static let percent = Unit(definedBy: DefaultUnits.percent)
196+
193197
// MARK: Power
194198

195199
static let watt = Unit(definedBy: DefaultUnits.watt)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//
2+
// Test.swift
3+
// Units
4+
//
5+
// Created by Jason Jobe on 9/13/25.
6+
//
7+
8+
@testable import Units
9+
import XCTest
10+
11+
final class PercentTests: XCTestCase {
12+
func testParse() throws {
13+
XCTAssertEqual(
14+
try Expression("10m + 25%"),
15+
Expression(node: .init(.measurement(10.measured(in: .meter))))
16+
.append(op: .add, node: .init(.measurement(25.measured(in: .percent))))
17+
)
18+
}
19+
20+
func testSolutions() throws {
21+
22+
XCTAssertEqual(
23+
try Expression("10m + 25%").solve(),
24+
12.5.measured(in: .meter)
25+
)
26+
27+
XCTAssertEqual(
28+
try Expression("10m - 25%").solve(),
29+
7.5.measured(in: .meter)
30+
)
31+
32+
XCTAssertEqual(
33+
try Expression("10m * 25%").solve(),
34+
2.5.measured(in: .meter)
35+
)
36+
37+
XCTAssertEqual(
38+
try Expression("10m / 25%").solve(),
39+
40.measured(in: .meter)
40+
)
41+
42+
}
43+
}
44+

0 commit comments

Comments
 (0)