Skip to content
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 @@ -140,9 +140,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 {
Copy link
Owner

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.

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 @@ 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] {
Copy link
Owner

Choose a reason for hiding this comment

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

nit: Remove explicit internal

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 @@ class Registry {
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)")
Copy link
Owner

Choose a reason for hiding this comment

The 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
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 {
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
}

public var positive: Bool {
Copy link
Owner

Choose a reason for hiding this comment

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

Since we have Comparable conformance, do we need this? Like, couldn't we just compare an instance to zero (i.e. fraction > 0)? It looks like this function is only used within description and tests.

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)))"
Copy link
Owner

Choose a reason for hiding this comment

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

I get that it isn't possible to overload the / Swift operator, but I think we could use it in the string representation here, right? As long as we require fractional exponents to be surrounded by parentheses, I think we can force valid parsing using /.

}
}
}

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.

Copy link
Owner

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.

Ah, gotcha. Yeah, sounds good to me.

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

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 over and no operator instead of providing some syntax sugar that breaks other stuff.

For a equation/string representation, I'd like to use / if possible, but if not then | seems okay.

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.
init(composedOf subUnits: [DefinedUnit: Int]) {
internal init(composedOf subUnits: [DefinedUnit: Fraction]) {
Copy link
Owner

Choose a reason for hiding this comment

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

nit: Remove internal

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],
Copy link
Owner

Choose a reason for hiding this comment

The 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 Int anyway, since whole exponents are much more common than fractional.

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
Loading