-
Notifications
You must be signed in to change notification settings - Fork 2
feature: Fractional Exponents #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,10 +29,10 @@ class Registry { | |
|
|
||
| /// Returns a list of defined units and their exponents, given a composite unit symbol. It is expected that the caller has | ||
| /// verified that this is a composite unit. | ||
| func compositeUnitsFromSymbol(symbol: String) throws -> [DefinedUnit: Int] { | ||
| internal func compositeUnitsFromSymbol(symbol: String) throws -> [DefinedUnit: Fraction] { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Remove explicit |
||
| let symbolsAndExponents = try deserializeSymbolicEquation(symbol) | ||
|
|
||
| var compositeUnits = [DefinedUnit: Int]() | ||
| var compositeUnits = [DefinedUnit: Fraction]() | ||
| for (definedUnitSymbol, exponent) in symbolsAndExponents { | ||
| guard exponent != 0 else { | ||
| continue | ||
|
|
@@ -70,7 +70,7 @@ class Registry { | |
| func addUnit( | ||
| name: String, | ||
| symbol: String, | ||
| dimension: [Quantity: Int], | ||
| dimension: [Quantity: Fraction], | ||
| coefficient: Double = 1, | ||
| constant: Double = 0 | ||
| ) throws { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,7 +14,7 @@ | |
| /// - spaceAroundOperators: Whether to include space characters before and after multiplication and division characters. | ||
| /// - Returns: A string that represents the equation of the object symbols and their respective exponentiation. | ||
| func serializeSymbolicEquation<T>( | ||
| of dict: [T: Int], | ||
| of dict: [T: Fraction], | ||
| symbolPath: KeyPath<T, String>, | ||
| spaceAroundOperators: Bool = false | ||
| ) -> String { | ||
|
|
@@ -76,7 +76,7 @@ func serializeSymbolicEquation<T>( | |
| } | ||
| let symbol = object[keyPath: symbolPath] | ||
| var expStr = "" | ||
| if abs(exp) > 1 { | ||
| if abs(exp) != 0, abs(exp) != 1 { | ||
| expStr = "\(expSymbol)\(abs(exp))" | ||
| } | ||
|
|
||
|
|
@@ -93,19 +93,19 @@ func serializeSymbolicEquation<T>( | |
| /// - Returns: A dictionary containing object symbols and exponents | ||
| func deserializeSymbolicEquation( | ||
| _ equation: String | ||
| ) throws -> [String: Int] { | ||
| ) throws -> [String: Fraction] { | ||
| let expSymbol = OperatorSymbols.exp.rawValue | ||
| let multSymbol = OperatorSymbols.mult.rawValue | ||
| let divSymbol = OperatorSymbols.div.rawValue | ||
|
|
||
| var result = [String: Int]() | ||
| var result = [String: Fraction]() | ||
| for multChunks in equation.split(separator: multSymbol, omittingEmptySubsequences: false) { | ||
| for (index, divChunks) in multChunks.split(separator: divSymbol, omittingEmptySubsequences: false).enumerated() { | ||
| let symbolChunks = divChunks.split(separator: expSymbol, omittingEmptySubsequences: false) | ||
| let subSymbol = String(symbolChunks[0]).trimmingCharacters(in: .whitespaces) | ||
| var exp = 1 | ||
| var exp: Fraction = 1 | ||
| if symbolChunks.count == 2 { | ||
| guard let expInt = Int(String(symbolChunks[1])) else { | ||
| guard let expInt = Fraction(String(symbolChunks[1])) else { | ||
| throw UnitError.invalidSymbol(message: "Symbol '^' must be followed by an integer: \(equation)") | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Can we update this error message to specify that it has to be an integer or fraction? |
||
| } | ||
| exp = expInt | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
|
|
||
| /// Represents a reduced fractional number. | ||
| /// An invariant exists such that it is not possible to create a ``Fraction`` | ||
| /// that is not represented in its most reduced form. | ||
| public struct Fraction: Hashable, Equatable, Sendable { | ||
NeedleInAJayStack marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| public let numerator: Int | ||
| public let denominator: Int | ||
|
|
||
| /// Combines the provided `numerator` and `denominator` into a reduced ``Fraction``. | ||
| /// - Warning: Attempts to create a ``Fraction`` with a zero denominator will fatally error. | ||
| public init(numerator: Int, denominator: Int) { | ||
| let gcd = Self.gcd(numerator, denominator) | ||
| self.numerator = numerator / gcd | ||
| self.denominator = denominator / gcd | ||
NeedleInAJayStack marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| public var positive: Bool { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we have |
||
| switch (numerator, denominator) { | ||
| // 0/0 is not positive in this logic | ||
| case let (n, d) where n >= 0 && d > 0: true | ||
|
|
||
| // Seems like this case can't happen because | ||
| // all Fractions are reduced. | ||
| case let (n, d) where n < 0 && d < 0: true | ||
|
|
||
| default: false | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private extension Fraction { | ||
| static func gcd(_ a: Int, _ b: Int) -> Int { | ||
| // See: https://en.wikipedia.org/wiki/Euclidean_algorithm | ||
| var latestRemainder = max(a, b) | ||
| var previousRemainder = min(a, b) | ||
|
|
||
| while latestRemainder != 0 { | ||
| let tmp = latestRemainder | ||
| latestRemainder = previousRemainder % latestRemainder | ||
| previousRemainder = tmp | ||
| } | ||
| return previousRemainder | ||
| } | ||
| } | ||
|
|
||
|
|
||
| extension Fraction { | ||
| public static func * (lhs: Self, rhs: Self) -> Self { | ||
| Self(numerator: lhs.numerator * rhs.numerator, denominator: lhs.denominator * rhs.denominator) | ||
| } | ||
|
|
||
| public static func / (lhs: Self, rhs: Self) -> Self { | ||
| Self(numerator: lhs.numerator * rhs.denominator, denominator: lhs.denominator * rhs.numerator) | ||
| } | ||
|
|
||
| public static func + (lhs: Self, rhs: Self) -> Self { | ||
| Self(numerator: (lhs.numerator * rhs.denominator) + (rhs.numerator * lhs.denominator), denominator: lhs.denominator * rhs.denominator) | ||
| } | ||
|
|
||
| public static func - (lhs: Self, rhs: Self) -> Self { | ||
| Self(numerator: (lhs.numerator * rhs.denominator) - (rhs.numerator * lhs.denominator), denominator: lhs.denominator * rhs.denominator) | ||
| } | ||
| } | ||
| extension Fraction { | ||
| public static func * (lhs: Self, rhs: Int) -> Self { | ||
| lhs * Self(integerLiteral: rhs) | ||
| } | ||
|
|
||
| public static func / (lhs: Self, rhs: Int) -> Self { | ||
| lhs / Self(integerLiteral: rhs) | ||
| } | ||
|
|
||
| public static func * (lhs: Int, rhs: Self) -> Self { | ||
| Self(integerLiteral: lhs) * rhs | ||
| } | ||
|
|
||
| public static func / (lhs: Int, rhs: Self) -> Self { | ||
| Self(integerLiteral: lhs) / rhs | ||
| } | ||
|
|
||
| public static func + (lhs: Self, rhs: Int) -> Self { | ||
| lhs + Self(integerLiteral: rhs) | ||
| } | ||
|
|
||
| public static func - (lhs: Self, rhs: Int) -> Self { | ||
| lhs - Self(integerLiteral: rhs) | ||
| } | ||
|
|
||
| public static func + (lhs: Int, rhs: Self) -> Self { | ||
| Self(integerLiteral: lhs) + rhs | ||
| } | ||
|
|
||
| public static func - (lhs: Int, rhs: Self) -> Self { | ||
| Self(integerLiteral: lhs) - rhs | ||
| } | ||
| } | ||
|
|
||
| extension Fraction: ExpressibleByIntegerLiteral { | ||
| public typealias IntegerLiteralType = Int | ||
|
|
||
| public init(integerLiteral value: Int) { | ||
| self.init(numerator: value, denominator: 1) | ||
| } | ||
| } | ||
|
|
||
| extension Fraction: SignedNumeric { | ||
|
|
||
| public init?<T>(exactly source: T) where T : BinaryInteger { | ||
| self.init(integerLiteral: Int(source)) | ||
| } | ||
|
|
||
| public static func *= (lhs: inout Fraction, rhs: Fraction) { | ||
| lhs = lhs * rhs | ||
| } | ||
|
|
||
| public var magnitude: Fraction { | ||
| Self(numerator: abs(numerator), denominator: abs(denominator)) | ||
| } | ||
|
|
||
| public typealias Magnitude = Self | ||
|
|
||
| } | ||
|
|
||
| extension Fraction { | ||
| public var asDouble: Double { | ||
| Double(numerator) / Double(denominator) | ||
| } | ||
| } | ||
|
|
||
| extension Fraction: Comparable { | ||
| public static func < (lhs: Fraction, rhs: Fraction) -> Bool { | ||
| lhs.numerator * rhs.denominator < rhs.numerator * lhs.denominator | ||
| } | ||
| } | ||
|
|
||
| extension Fraction: LosslessStringConvertible { | ||
| /// The format for string conversion is: `(<integer>|<integer>)` or `<integer>` | ||
| public init?(_ description: String) { | ||
| if | ||
| description.first == "(", | ||
| description.last == ")" | ||
| { | ||
| let parts = description.dropFirst().dropLast().split(separator: "|").compactMap({ Int(String($0)) }) | ||
| guard | ||
| parts.count == 2, | ||
| let numerator = parts.first, | ||
| let denominator = parts.last | ||
| else { | ||
| return nil | ||
| } | ||
| self.init(numerator: numerator, denominator: denominator) | ||
| } else if let number = Int(description) { | ||
| self.init(integerLiteral: number) | ||
| } else { | ||
| return nil | ||
| } | ||
| } | ||
|
|
||
| public var description: String { | ||
| if denominator == 1 { | ||
| "\(!positive && numerator != 0 ? "-" : "")\(abs(numerator))" | ||
| } else { | ||
| "(\(positive ? "" : "-")\(abs(numerator))|\(abs(denominator)))" | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I get that it isn't possible to overload the |
||
| } | ||
| } | ||
| } | ||
|
|
||
| extension SignedInteger { | ||
| func over<T: SignedInteger>(_ denominator: T) -> Fraction { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Were there type inference issues with using
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah. That was what I originally tried.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On that note, how do you feel about You could do
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I went ahead and did this - lmk if you like/dislike.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ah, gotcha. Yeah, sounds good to me.
For the Swift API, I don't really like it. It causes inheriting packages to require type declarations on any integer bitwise OR which can be pretty annoying (we can see this in the new FractionTests, where you had to do some gymnastics to get type declarations in there). Honestly, I'd prefer just using For a equation/string representation, I'd like to use |
||
| Fraction(numerator: Int(self), denominator: Int(denominator)) | ||
| } | ||
| } | ||
|
|
||
| extension Int { | ||
| public static func |(_ lhs: Self, _ rhs: Self) -> Fraction { | ||
| Fraction(numerator: lhs, denominator: rhs) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -55,7 +55,7 @@ public struct Unit { | |
| /// Create a new from the sub-unit dictionary. | ||
| /// - Parameter subUnits: A dictionary of defined units and exponents. If this dictionary has only a single unit with an exponent of one, | ||
| /// we return that defined unit directly. | ||
| init(composedOf subUnits: [DefinedUnit: Int]) { | ||
| internal init(composedOf subUnits: [DefinedUnit: Fraction]) { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Remove |
||
| if subUnits.count == 1, let subUnit = subUnits.first, subUnit.value == 1 { | ||
| type = .defined(subUnit.key) | ||
| } else { | ||
|
|
@@ -88,7 +88,7 @@ public struct Unit { | |
| public static func define( | ||
| name: String, | ||
| symbol: String, | ||
| dimension: [Quantity: Int], | ||
| dimension: [Quantity: Fraction], | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same deal on all the public API changes in this file - it'd be great to make new functions instead of changing existing ones so that this isn't a breaking change. I'd expect most users to use |
||
| coefficient: Double = 1, | ||
| constant: Double = 0 | ||
| ) throws -> Unit { | ||
|
|
@@ -123,7 +123,7 @@ public struct Unit { | |
| public static func register( | ||
| name: String, | ||
| symbol: String, | ||
| dimension: [Quantity: Int], | ||
| dimension: [Quantity: Fraction], | ||
| coefficient: Double = 1, | ||
| constant: Double = 0 | ||
| ) throws -> Unit { | ||
|
|
@@ -144,14 +144,14 @@ public struct Unit { | |
| } | ||
|
|
||
| /// The dimension of the unit in terms of base quanties | ||
| public var dimension: [Quantity: Int] { | ||
| public var dimension: [Quantity: Fraction] { | ||
| switch type { | ||
| case .none: | ||
| return [:] | ||
| case let .defined(definition): | ||
| return definition.dimension | ||
| case let .composite(subUnits): | ||
| var dimensions: [Quantity: Int] = [:] | ||
| var dimensions: [Quantity: Fraction] = [:] | ||
| for (subUnit, exp) in subUnits { | ||
| let subDimensions = subUnit.dimension.mapValues { value in | ||
| exp * value | ||
|
|
@@ -259,7 +259,7 @@ public struct Unit { | |
| /// Exponentiate the unit. This is equavalent to multiple `*` operations. | ||
| /// - Parameter raiseTo: The exponent to raise the unit to | ||
| /// - Returns: A new unit modeling the original raised to the provided power | ||
| public func pow(_ raiseTo: Int) -> Unit { | ||
| public func pow(_ raiseTo: Fraction) -> Unit { | ||
| switch type { | ||
| case .none: | ||
| return .none | ||
|
|
@@ -300,7 +300,7 @@ public struct Unit { | |
| guard subUnit.constant == 0 else { // subUnit must not have constant | ||
| throw UnitError.invalidCompositeUnit(message: "Nonlinear unit prevents conversion: \(subUnit)") | ||
| } | ||
| totalCoefficient *= Foundation.pow(subUnit.coefficient, Double(exponent)) | ||
| totalCoefficient *= Foundation.pow(subUnit.coefficient, exponent.asDouble) | ||
| } | ||
| return number * totalCoefficient | ||
| } | ||
|
|
@@ -324,7 +324,7 @@ public struct Unit { | |
| guard subUnit.constant == 0 else { // subUnit must not have constant | ||
| throw UnitError.invalidCompositeUnit(message: "Nonlinear unit prevents conversion: \(subUnit)") | ||
| } | ||
| totalCoefficient *= Foundation.pow(subUnit.coefficient, Double(exponent)) | ||
| totalCoefficient *= Foundation.pow(subUnit.coefficient, exponent.asDouble) | ||
| } | ||
| return number / totalCoefficient | ||
| } | ||
|
|
@@ -334,7 +334,7 @@ public struct Unit { | |
|
|
||
| /// Returns a dictionary that represents the unique defined units and their exponents. For a | ||
| /// composite unit, this is simply the `subUnits`, but for a defined unit, this is `[self: 1]` | ||
| private var subUnits: [DefinedUnit: Int] { | ||
| private var subUnits: [DefinedUnit: Fraction] { | ||
| switch type { | ||
| case .none: | ||
| return [:] | ||
|
|
@@ -349,7 +349,7 @@ public struct Unit { | |
| private enum UnitType: Sendable { | ||
| case none | ||
| case defined(DefinedUnit) | ||
| case composite([DefinedUnit: Int]) | ||
| case composite([DefinedUnit: Fraction]) | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To avoid a breaking change, could we just add a new function that takes
Fraction, but keep the old one too? I think this would also resolve the current compilation failures.