Skip to content

Commit bfa5aac

Browse files
author
Jairon Terrero
committed
feature: Fractional Exponents
1 parent 4e14254 commit bfa5aac

7 files changed

Lines changed: 231 additions & 22 deletions

File tree

Sources/Units/Measurement/Measurement.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,9 @@ public struct Measurement: Equatable, Codable {
136136
/// Exponentiate the measurement. This is equavalent to multiple `*` operations.
137137
/// - Parameter raiseTo: The exponent to raise the measurement to
138138
/// - Returns: A new measurement with an exponentiated scalar value and an exponentiated unit of measure
139-
public func pow(_ raiseTo: Int) -> Measurement {
139+
public func pow(_ raiseTo: Fraction) -> Measurement {
140140
return Measurement(
141-
value: Foundation.pow(value, Double(raiseTo)),
141+
value: Foundation.pow(value, raiseTo.asDouble),
142142
unit: unit.pow(raiseTo)
143143
)
144144
}

Sources/Units/Registry.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ internal class Registry {
2929

3030
/// Returns a list of defined units and their exponents, given a composite unit symbol. It is expected that the caller has
3131
/// verified that this is a composite unit.
32-
internal func compositeUnitsFromSymbol(symbol: String) throws -> [DefinedUnit: Int] {
32+
internal func compositeUnitsFromSymbol(symbol: String) throws -> [DefinedUnit: Fraction] {
3333
let symbolsAndExponents = try deserializeSymbolicEquation(symbol)
3434

35-
var compositeUnits = [DefinedUnit: Int]()
35+
var compositeUnits = [DefinedUnit: Fraction]()
3636
for (definedUnitSymbol, exponent) in symbolsAndExponents {
3737
guard exponent != 0 else {
3838
continue
@@ -70,7 +70,7 @@ internal class Registry {
7070
internal func addUnit(
7171
name: String,
7272
symbol: String,
73-
dimension: [Quantity: Int],
73+
dimension: [Quantity: Fraction],
7474
coefficient: Double = 1,
7575
constant: Double = 0
7676
) throws {

Sources/Units/Unit/DefinedUnit.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
struct DefinedUnit: Hashable, Sendable {
33
let name: String
44
let symbol: String
5-
let dimension: [Quantity: Int]
5+
let dimension: [Quantity: Fraction]
66
let coefficient: Double
77
let constant: Double
88

9-
init(name: String, symbol: String, dimension: [Quantity: Int], coefficient: Double = 1, constant: Double = 0) throws {
9+
init(name: String, symbol: String, dimension: [Quantity: Fraction], coefficient: Double = 1, constant: Double = 0) throws {
1010
guard !symbol.isEmpty else {
1111
throw UnitError.invalidSymbol(message: "Symbol cannot be empty")
1212
}

Sources/Units/Unit/Equations.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
/// - spaceAroundOperators: Whether to include space characters before and after multiplication and division characters.
1515
/// - Returns: A string that represents the equation of the object symbols and their respective exponentiation.
1616
func serializeSymbolicEquation<T>(
17-
of dict: [T: Int],
17+
of dict: [T: Fraction],
1818
symbolPath: KeyPath<T, String>,
1919
spaceAroundOperators: Bool = false
2020
) -> String {
@@ -93,19 +93,19 @@ func serializeSymbolicEquation<T>(
9393
/// - Returns: A dictionary containing object symbols and exponents
9494
func deserializeSymbolicEquation(
9595
_ equation: String
96-
) throws -> [String: Int] {
96+
) throws -> [String: Fraction] {
9797
let expSymbol = OperatorSymbols.exp.rawValue
9898
let multSymbol = OperatorSymbols.mult.rawValue
9999
let divSymbol = OperatorSymbols.div.rawValue
100100

101-
var result = [String: Int]()
101+
var result = [String: Fraction]()
102102
for multChunks in equation.split(separator: multSymbol, omittingEmptySubsequences: false) {
103103
for (index, divChunks) in multChunks.split(separator: divSymbol, omittingEmptySubsequences: false).enumerated() {
104104
let symbolChunks = divChunks.split(separator: expSymbol, omittingEmptySubsequences: false)
105105
let subSymbol = String(symbolChunks[0]).trimmingCharacters(in: .whitespaces)
106-
var exp = 1
106+
var exp: Fraction = 1
107107
if symbolChunks.count == 2 {
108-
guard let expInt = Int(String(symbolChunks[1])) else {
108+
guard let expInt = Fraction(String(symbolChunks[1])) else {
109109
throw UnitError.invalidSymbol(message: "Symbol '^' must be followed by an integer: \(equation)")
110110
}
111111
exp = expInt

Sources/Units/Unit/Fraction.swift

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
2+
3+
public struct Fraction: Hashable, Equatable, Sendable {
4+
let numerator: Int
5+
let denominator: Int
6+
7+
init(numerator: Int, denominator: Int) {
8+
let gcd = Self.gcd(numerator, denominator)
9+
self.numerator = numerator / gcd
10+
self.denominator = denominator / gcd
11+
}
12+
13+
var positive: Bool {
14+
switch (numerator, denominator) {
15+
// 0/0 is not positive in this logic
16+
case let (n, d) where n >= 0 && d > 0: true
17+
case let (n, d) where n < 0 && d < 0: true
18+
default: false
19+
}
20+
}
21+
}
22+
23+
private extension Fraction {
24+
static func gcd(_ a: Int, _ b: Int) -> Int {
25+
// See: https://en.wikipedia.org/wiki/Euclidean_algorithm
26+
var latestRemainder = max(a, b)
27+
var previousRemainder = min(a, b)
28+
29+
while latestRemainder != 0 {
30+
let tmp = latestRemainder
31+
latestRemainder = previousRemainder % latestRemainder
32+
previousRemainder = tmp
33+
}
34+
return previousRemainder
35+
}
36+
}
37+
38+
39+
extension Fraction {
40+
public static func * (lhs: Self, rhs: Self) -> Self {
41+
Self(numerator: lhs.numerator * rhs.numerator, denominator: lhs.denominator * rhs.denominator)
42+
}
43+
44+
public static func / (lhs: Self, rhs: Self) -> Self {
45+
Self(numerator: lhs.numerator * rhs.denominator, denominator: lhs.denominator * rhs.numerator)
46+
}
47+
48+
public static func + (lhs: Self, rhs: Self) -> Self {
49+
Self(numerator: (lhs.numerator * rhs.denominator) + (rhs.numerator * lhs.denominator), denominator: lhs.denominator * rhs.denominator)
50+
}
51+
52+
public static func - (lhs: Self, rhs: Self) -> Self {
53+
Self(numerator: (lhs.numerator * rhs.denominator) - (rhs.numerator * lhs.denominator), denominator: lhs.denominator * rhs.denominator)
54+
}
55+
}
56+
extension Fraction {
57+
public static func * (lhs: Self, rhs: Int) -> Self {
58+
lhs * Self(integerLiteral: rhs)
59+
}
60+
61+
public static func / (lhs: Self, rhs: Int) -> Self {
62+
lhs / Self(integerLiteral: rhs)
63+
}
64+
65+
public static func * (lhs: Int, rhs: Self) -> Self {
66+
Self(integerLiteral: lhs) * rhs
67+
}
68+
69+
public static func / (lhs: Int, rhs: Self) -> Self {
70+
Self(integerLiteral: lhs) * rhs
71+
}
72+
73+
public static func + (lhs: Self, rhs: Int) -> Self {
74+
lhs + Self(integerLiteral: rhs)
75+
}
76+
77+
public static func - (lhs: Self, rhs: Int) -> Self {
78+
lhs - Self(integerLiteral: rhs)
79+
}
80+
81+
public static func + (lhs: Int, rhs: Self) -> Self {
82+
Self(integerLiteral: lhs) + rhs
83+
}
84+
85+
public static func - (lhs: Int, rhs: Self) -> Self {
86+
Self(integerLiteral: lhs) + rhs
87+
}
88+
}
89+
90+
extension Fraction: ExpressibleByIntegerLiteral {
91+
public typealias IntegerLiteralType = Int
92+
93+
public init(integerLiteral value: Int) {
94+
self.init(numerator: value, denominator: 1)
95+
}
96+
}
97+
98+
extension Fraction: SignedNumeric {
99+
100+
public init?<T>(exactly source: T) where T : BinaryInteger {
101+
self.init(integerLiteral: Int(source))
102+
}
103+
104+
public static func *= (lhs: inout Fraction, rhs: Fraction) {
105+
lhs = lhs * rhs
106+
}
107+
108+
public var magnitude: Fraction {
109+
Self(numerator: abs(numerator), denominator: abs(denominator))
110+
}
111+
112+
public typealias Magnitude = Self
113+
114+
}
115+
116+
extension Fraction {
117+
var asDouble: Double {
118+
Double(numerator) / Double(denominator)
119+
}
120+
}
121+
122+
extension Fraction: Comparable {
123+
public static func < (lhs: Fraction, rhs: Fraction) -> Bool {
124+
return lhs.numerator < rhs.numerator
125+
}
126+
}
127+
128+
extension Fraction: LosslessStringConvertible {
129+
public init?(_ description: String) {
130+
if
131+
description.first == "(",
132+
description.last == ")"
133+
{
134+
let parts = description.dropFirst().dropLast().split(separator: "|").compactMap({ Int(String($0)) })
135+
guard
136+
parts.count == 2,
137+
let numerator = parts.first,
138+
let denominator = parts.last
139+
else {
140+
return nil
141+
}
142+
self.init(numerator: numerator, denominator: denominator)
143+
} else if let number = Int(description) {
144+
self.init(integerLiteral: number)
145+
} else {
146+
return nil
147+
}
148+
}
149+
150+
public var description: String {
151+
if denominator == 1 {
152+
"\(numerator)"
153+
} else {
154+
"(\(numerator)|\(denominator))"
155+
}
156+
}
157+
}
158+
159+
extension SignedInteger {
160+
func over<T: SignedInteger>(_ denominator: T) -> Fraction {
161+
Fraction(numerator: Int(self), denominator: Int(denominator))
162+
}
163+
}

Sources/Units/Unit/Unit.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public struct Unit {
5555
/// Create a new from the sub-unit dictionary.
5656
/// - Parameter subUnits: A dictionary of defined units and exponents. If this dictionary has only a single unit with an exponent of one,
5757
/// we return that defined unit directly.
58-
internal init(composedOf subUnits: [DefinedUnit: Int]) {
58+
internal init(composedOf subUnits: [DefinedUnit: Fraction]) {
5959
if subUnits.count == 1, let subUnit = subUnits.first, subUnit.value == 1 {
6060
type = .defined(subUnit.key)
6161
} else {
@@ -88,7 +88,7 @@ public struct Unit {
8888
public static func define(
8989
name: String,
9090
symbol: String,
91-
dimension: [Quantity: Int],
91+
dimension: [Quantity: Fraction],
9292
coefficient: Double = 1,
9393
constant: Double = 0
9494
) throws -> Unit {
@@ -123,7 +123,7 @@ public struct Unit {
123123
public static func register(
124124
name: String,
125125
symbol: String,
126-
dimension: [Quantity: Int],
126+
dimension: [Quantity: Fraction],
127127
coefficient: Double = 1,
128128
constant: Double = 0
129129
) throws -> Unit {
@@ -144,14 +144,14 @@ public struct Unit {
144144
}
145145

146146
/// The dimension of the unit in terms of base quanties
147-
public var dimension: [Quantity: Int] {
147+
public var dimension: [Quantity: Fraction] {
148148
switch type {
149149
case .none:
150150
return [:]
151151
case let .defined(definition):
152152
return definition.dimension
153153
case let .composite(subUnits):
154-
var dimensions: [Quantity: Int] = [:]
154+
var dimensions: [Quantity: Fraction] = [:]
155155
for (subUnit, exp) in subUnits {
156156
let subDimensions = subUnit.dimension.mapValues { value in
157157
exp * value
@@ -259,7 +259,7 @@ public struct Unit {
259259
/// Exponentiate the unit. This is equavalent to multiple `*` operations.
260260
/// - Parameter raiseTo: The exponent to raise the unit to
261261
/// - Returns: A new unit modeling the original raised to the provided power
262-
public func pow(_ raiseTo: Int) -> Unit {
262+
public func pow(_ raiseTo: Fraction) -> Unit {
263263
switch type {
264264
case .none:
265265
return .none
@@ -300,7 +300,7 @@ public struct Unit {
300300
guard subUnit.constant == 0 else { // subUnit must not have constant
301301
throw UnitError.invalidCompositeUnit(message: "Nonlinear unit prevents conversion: \(subUnit)")
302302
}
303-
totalCoefficient *= Foundation.pow(subUnit.coefficient, Double(exponent))
303+
totalCoefficient *= Foundation.pow(subUnit.coefficient, exponent.asDouble)
304304
}
305305
return number * totalCoefficient
306306
}
@@ -324,7 +324,7 @@ public struct Unit {
324324
guard subUnit.constant == 0 else { // subUnit must not have constant
325325
throw UnitError.invalidCompositeUnit(message: "Nonlinear unit prevents conversion: \(subUnit)")
326326
}
327-
totalCoefficient *= Foundation.pow(subUnit.coefficient, Double(exponent))
327+
totalCoefficient *= Foundation.pow(subUnit.coefficient, exponent.asDouble)
328328
}
329329
return number / totalCoefficient
330330
}
@@ -334,7 +334,7 @@ public struct Unit {
334334

335335
/// Returns a dictionary that represents the unique defined units and their exponents. For a
336336
/// composite unit, this is simply the `subUnits`, but for a defined unit, this is `[self: 1]`
337-
private var subUnits: [DefinedUnit: Int] {
337+
private var subUnits: [DefinedUnit: Fraction] {
338338
switch type {
339339
case .none:
340340
return [:]
@@ -349,7 +349,7 @@ public struct Unit {
349349
private enum UnitType: Sendable {
350350
case none
351351
case defined(DefinedUnit)
352-
case composite([DefinedUnit: Int])
352+
case composite([DefinedUnit: Fraction])
353353
}
354354
}
355355

0 commit comments

Comments
 (0)