Skip to content

Commit 03652e7

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

8 files changed

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

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)