Skip to content

Commit 78ffb04

Browse files
Merge pull request #21 from wildthink/percent
added Percent (not as Measurement)
2 parents 55df9cc + 80d55e0 commit 78ffb04

10 files changed

Lines changed: 364 additions & 16 deletions

File tree

Justfile

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Units Justfile
2+
# Command line support
3+
4+
5+
# About Units Commands
6+
_default:
7+
@echo '{{ style("warning") }}Units Script Commands{{ NORMAL }}'
8+
@echo @{{source_file()}}
9+
@echo ""
10+
@just -f {{source_file()}} --list
11+
12+
# Install 'units' in /usr/local/bin
13+
install:
14+
swift build -c release
15+
cp .build/release/unit /usr/local/bin/
16+
17+
# rm 'units' from /usr/local/bin
18+
uninstall:
19+
rm /usr/local/bin/unit
20+
21+
# Add swiftformat to git/pre-commit
22+
dev_setup:
23+
echo "./Scripts/git_commit_hook.sh" > .git/hooks/pre-commit
24+
25+
# Open Documentation
26+
docs:
27+
open https://swiftpackageindex.com/NeedleInAJayStack/Units/v1.0.0/documentation/units
28+
29+
git_origin := `git remote get-url origin`
30+
31+
# repo/origin
32+
repo:
33+
@echo {{git_origin}}
34+
35+
# open repo/origin
36+
open-repo:
37+
open {{git_origin}}
38+

Sources/CLI/Convert.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ struct Convert: ParsableCommand {
3131
@Argument(help: """
3232
The unit to convert to. This can either be a unit name, a unit symbol, or an equation of \
3333
unit symbols.
34+
Example: unit convert 1_ft meter -> 0.3048 m
3435
""")
3536
var to: Units.Unit
3637

Sources/CLI/List.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ struct List: ParsableCommand {
66
abstract: "Print a table of the available units, their symbols, and their dimensionality."
77
)
88

9+
@Option(name: .shortAndLong,
10+
help: "Substring to filter on dimensions and symbols")
11+
var filter: String? = nil
12+
913
func run() throws {
1014
let units = registry.allUnits().sorted { u1, u2 in
1115
u1.name <= u2.name
@@ -17,7 +21,9 @@ struct List: ParsableCommand {
1721
"dimension",
1822
]
1923

20-
let rows = units.map { unit in
24+
let rows = units
25+
.filter({ $0.contains(filter)})
26+
.map { unit in
2127
[
2228
unit.name,
2329
unit.symbol,
@@ -59,3 +65,11 @@ struct List: ParsableCommand {
5965
}
6066
}
6167
}
68+
69+
extension Units.Unit {
70+
func contains(_ substring: String?) -> Bool {
71+
guard let substring else { return true }
72+
return self.symbol.contains(substring)
73+
|| self.dimensionDescription().contains(substring)
74+
}
75+
}

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")
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import Foundation
2+
/*
3+
NOTE: Should consider introducing `protocol Scalar`
4+
based on `VectorArithmetic`
5+
*/
6+
7+
/**
8+
Math operators with percentages treat the percent as its decimal equivalent (e.g., 25% = 0.25)
9+
but in the case of `+` and `-` the calculation is less direct.
10+
11+
Here’s how math operators work with percentages in typical calculations:
12+
13+
Multiplication (100 * 25%)
14+
15+
When you multiply a number by a percentage, you’re finding that percent of the number.
16+
• 25% is the same as 0.25.
17+
• So, 100 * 25% = 100 * 0.25 = 25.
18+
19+
Division (100 / 30%)
20+
21+
Dividing by a percentage means dividing by its decimal form.
22+
• 30% is 0.3.
23+
• So, 100 / 30% = 100 / 0.3 ≈ 333.33.
24+
25+
Addition (100 + 10%)
26+
27+
Adding a percentage to a number is less direct, but usually means increasing the number by that percent.
28+
• 10% of 100 is 10.
29+
• So, 100 + 10% = 100 + (100 * 0.10) = 110.
30+
31+
General Rule
32+
• Percent means “per hundred,” so 25% = 25/100 = 0.25.
33+
• Replace the percent with its decimal equivalent before performing the operation.
34+
35+
Example Table
36+
===========
37+
Expression Decimal Form Result
38+
------------------------------
39+
100 * 25% 100 * 0.25 25
40+
100 / 30% 100 / 0.3 333.33
41+
100 + 10% 100 + (100*0.10) 110
42+
43+
44+
If you see a percent sign in a calculation, just convert it to a decimal and proceed as usual. If you want to know how subtraction works with percentages, or how to handle more complex expressions, let me know!
45+
*/
46+
public struct Percent: Numeric, Equatable, Codable {
47+
48+
public private(set) var magnitude: Double
49+
50+
/// Create a new Percent
51+
/// - Parameters:
52+
/// - value: The magnitude of the percent
53+
public init(
54+
magnitude: Double
55+
) {
56+
self.magnitude = magnitude
57+
}
58+
59+
func percent(of measure: Measurement) -> Measurement {
60+
.init(value: magnitude * measure.value, unit: measure.unit)
61+
}
62+
63+
func percent(of other: Double) -> Double {
64+
magnitude * other
65+
}
66+
}
67+
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+
78+
// MARK: Percent as Unit
79+
extension Percent {
80+
public var unit: Unit { Self.unit }
81+
82+
public static let unit = Unit(
83+
definedBy: DefaultUnits.percent)
84+
}
85+
86+
// MARK: Numeric Conformance
87+
public extension Percent {
88+
init(integerLiteral value: Double) {
89+
magnitude = value
90+
}
91+
92+
static func *= (lhs: inout Percent, rhs: Percent) {
93+
lhs.magnitude *= rhs.magnitude
94+
}
95+
96+
static func - (lhs: Percent, rhs: Percent) -> Percent {
97+
Percent(magnitude: lhs.magnitude - rhs.magnitude)
98+
}
99+
100+
init?<T>(exactly source: T) where T : BinaryInteger {
101+
magnitude = Double(source)
102+
}
103+
104+
static func * (lhs: Percent, rhs: Percent) -> Percent {
105+
Percent(magnitude: lhs.magnitude * rhs.magnitude)
106+
}
107+
108+
static func + (lhs: Percent, rhs: Percent) -> Percent {
109+
Percent(magnitude: lhs.magnitude + rhs.magnitude)
110+
}
111+
}
112+
113+
postfix operator %
114+
115+
extension BinaryInteger {
116+
public static postfix func % (value: Self) -> Percent {
117+
Percent(magnitude: Double(value)/100)
118+
}
119+
}
120+
121+
extension BinaryFloatingPoint {
122+
public static postfix func % (value: Self) -> Percent {
123+
Percent(magnitude: Double(value)/100)
124+
}
125+
}
126+
127+
// AdditiveArithmetic operations `*` and `/`
128+
129+
public extension Measurement {
130+
/// Adds a percentage to a measurement by increasing its value by the given percent.
131+
/// - Parameters:
132+
/// - lhs: The base measurement.
133+
/// - rhs: The percentage to add.
134+
/// - Returns: A new `Measurement` with its value increased by the given percentage.
135+
@_disfavoredOverload
136+
static func + (lhs: Measurement, rhs: Percent) -> Measurement {
137+
return Measurement(
138+
value: lhs.value + rhs.percent(of: lhs.value),
139+
unit: lhs.unit
140+
)
141+
}
142+
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.
148+
@_disfavoredOverload
149+
static func - (lhs: Measurement, rhs: Percent) -> Measurement {
150+
return Measurement(
151+
value: lhs.value - rhs.percent(of: lhs.value),
152+
unit: lhs.unit
153+
)
154+
}
155+
156+
/// Increases a measurement in place by the given percentage.
157+
/// - Parameters:
158+
/// - lhs: The measurement to modify.
159+
/// - rhs: The percentage to add.
160+
@_disfavoredOverload
161+
static func += (lhs: inout Measurement, rhs: Percent) {
162+
lhs = lhs + rhs
163+
}
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.
169+
@_disfavoredOverload
170+
static func -= (lhs: inout Measurement, rhs: Percent) {
171+
lhs = lhs - rhs
172+
}
173+
174+
}
175+
176+
// Scalar operations `*` and `/`
177+
public extension Measurement {
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.
183+
@_disfavoredOverload
184+
static func * (lhs: Measurement, rhs: Percent) -> Measurement {
185+
return Measurement(
186+
value: lhs.value * rhs.magnitude,
187+
unit: lhs.unit
188+
)
189+
}
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.
196+
@_disfavoredOverload
197+
static func / (lhs: Measurement, rhs: Percent) -> Measurement {
198+
return Measurement(
199+
value: lhs.value / rhs.magnitude,
200+
unit: lhs.unit
201+
)
202+
}
203+
}

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)

0 commit comments

Comments
 (0)