Skip to content
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

feature: Fractional Exponents #7

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Sources/Units/Measurement/Measurement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,9 @@ public struct Measurement: Equatable, Codable {
/// Exponentiate the measurement. This is equavalent to multiple `*` operations.
/// - Parameter raiseTo: The exponent to raise the measurement to
/// - Returns: A new measurement with an exponentiated scalar value and an exponentiated unit of measure
public func pow(_ raiseTo: Int) -> Measurement {
public func pow(_ raiseTo: Fraction) -> Measurement {
return Measurement(
value: Foundation.pow(value, Double(raiseTo)),
value: Foundation.pow(value, raiseTo.asDouble),
unit: unit.pow(raiseTo)
)
}
Expand Down
6 changes: 3 additions & 3 deletions Sources/Units/Registry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ internal 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.
internal func compositeUnitsFromSymbol(symbol: String) throws -> [DefinedUnit: Int] {
internal func compositeUnitsFromSymbol(symbol: String) throws -> [DefinedUnit: Fraction] {
let symbolsAndExponents = try deserializeSymbolicEquation(symbol)

var compositeUnits = [DefinedUnit: Int]()
var compositeUnits = [DefinedUnit: Fraction]()
for (definedUnitSymbol, exponent) in symbolsAndExponents {
guard exponent != 0 else {
continue
Expand Down Expand Up @@ -70,7 +70,7 @@ internal class Registry {
internal func addUnit(
name: String,
symbol: String,
dimension: [Quantity: Int],
dimension: [Quantity: Fraction],
coefficient: Double = 1,
constant: Double = 0
) throws {
Expand Down
4 changes: 2 additions & 2 deletions Sources/Units/Unit/DefinedUnit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
struct DefinedUnit: Hashable, Sendable {
let name: String
let symbol: String
let dimension: [Quantity: Int]
let dimension: [Quantity: Fraction]
let coefficient: Double
let constant: Double

init(name: String, symbol: String, dimension: [Quantity: Int], coefficient: Double = 1, constant: Double = 0) throws {
init(name: String, symbol: String, dimension: [Quantity: Fraction], coefficient: Double = 1, constant: Double = 0) throws {
guard !symbol.isEmpty else {
throw UnitError.invalidSymbol(message: "Symbol cannot be empty")
}
Expand Down
12 changes: 6 additions & 6 deletions Sources/Units/Unit/Equations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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))"
}

Expand All @@ -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)")
}
exp = expInt
Expand Down
178 changes: 178 additions & 0 deletions Sources/Units/Unit/Fraction.swift
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 {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we provide documentation about this type? Specific areas that would be helpful to document after reading through:

  • init automatically reduces the input fraction
  • String representation uses |

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Will do. I'll make a doc pass for everything I've added.

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
Comment on lines +12 to +14

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be public?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I guess the tests do a testable import so I didn't catch that.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made some more things public

}

public var positive: Bool {
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)))"
}
}
}

extension SignedInteger {
func over<T: SignedInteger>(_ denominator: T) -> Fraction {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Were there type inference issues with using /, like 5.measured(in: .meter.pow(1/2))?

Copy link
Author

@CrownedPhoenix CrownedPhoenix Jan 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. That was what I originally tried.
It was ambiguous to the compiler whether 1 / 2 meant Int / Int or Fraction / Int or Fraction / Fraction etc because Fraction conforms to ExpressibleAsIntegerLiteral.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On that note, how do you feel about | as the operator? It wouldn't be ambiguous.

You could do 2|5 etc

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went ahead and did this - lmk if you like/dislike.

Fraction(numerator: Int(self), denominator: Int(denominator))
}
}

extension Int {
public static func |(_ lhs: Self, _ rhs: Self) -> Fraction {
Fraction(numerator: lhs, denominator: rhs)
}
}
20 changes: 10 additions & 10 deletions Sources/Units/Unit/Unit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
internal init(composedOf subUnits: [DefinedUnit: Int]) {
internal init(composedOf subUnits: [DefinedUnit: Fraction]) {
if subUnits.count == 1, let subUnit = subUnits.first, subUnit.value == 1 {
type = .defined(subUnit.key)
} else {
Expand Down Expand Up @@ -88,7 +88,7 @@ public struct Unit {
public static func define(
name: String,
symbol: String,
dimension: [Quantity: Int],
dimension: [Quantity: Fraction],
coefficient: Double = 1,
constant: Double = 0
) throws -> Unit {
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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 [:]
Expand All @@ -349,7 +349,7 @@ public struct Unit {
private enum UnitType: Sendable {
case none
case defined(DefinedUnit)
case composite([DefinedUnit: Int])
case composite([DefinedUnit: Fraction])
}
}

Expand Down
Loading