Skip to content

Commit 53a7872

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

8 files changed

Lines changed: 405 additions & 23 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: 6 additions & 6 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 {
@@ -76,7 +76,7 @@ func serializeSymbolicEquation<T>(
7676
}
7777
let symbol = object[keyPath: symbolPath]
7878
var expStr = ""
79-
if abs(exp) > 1 {
79+
if abs(exp) != 0, abs(exp) != 1 {
8080
expStr = "\(expSymbol)\(abs(exp))"
8181
}
8282

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

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)