Fractions Revisited

This post is a follow-up on myprevious post about fractions from some years ago, where I discuss the creation of the Fraction type. (I meant to publish this much sooner, but life got in the way.)

As I started working more with Fraction, I found that I needed to be able to more easily mix Fractions with the base numeric types during calculations, as well as making it easy to instantiate fractions from numeric literals. I also found it inconvenient to only have failable initializers, since often I can guarantee I will only pass in valid values to the initializers. So I added non-failable alternatives that are guaranteed to instantiate (or, crash if you pass in invalid values).

This post picks up from where the previous post on Fractions left of, so I advise you to read that first, if you haven’t already, and then come back here.

Guaranteed Initializers

If you know what you’re doing, i.e. you have taken care to eliminate the possibility of providing invalid input, you can now use guaranteed initializers when instantiating Fractions. This eliminates the frequent need for unwrapping and/or forced-unwrapping when creating fractions.

public init(verifiedNumerator: Int) {
    precondition(verifiedNumerator > Int.min, "Illegal numerator value: Int.min is not allowed")

    self.numerator = verifiedNumerator
    self.denominator = 1
}

public init(verifiedNumerator: Int, verifiedDenominator: Int, wholes: Int = 0) {
    precondition(verifiedNumerator > Int.min, "Illegal numerator value: Int.min is not allowed")
    precondition(verifiedDenominator != 0, "0 is an illegal value for the denominator")
    
    self.numerator = verifiedNumerator + (verifiedDenominator * wholes)
    self.denominator = verifiedDenominator
}

The preconditions ensure you will crash early during development if you pass in an illegal numerator value. Thus reducing the chance of programmer error in production code. In the second initializer I have also added the option to pass in an amount of whole numbers, as a convenience, if you want to turn e.g. 2•2/3 into a Fraction. This wholes option was also added to the existing initializers.

Instantiating Fractions from Literals

While the Fraction type already had initializers, I regularly found it inconvenient to have to instantiate fractions through those wordy initializers:

let f = Fraction(numerator: 3, denominator: 1)

Therefore Fraction now conforms to ExpressibleByIntegerLiteral and ExpressibleByFloatLiteral, allowing the creation of Fraction instances from Ints and the various float types. So you can now write things like:

let wholeFraction: Fraction = 3
let wholeFraction2 = wholeFraction * 2

let fractionalFraction: Fraction = 3.9
let fractionalFraction2 = try? fractionalFraction / 3.3

Fraction now sports an adjustable static var to guide the precision when creating fractions from floating point values, which defaults to a precision of four digits: static var significantFloatingPointDigits = 4. You may adjust this value as needed.

The test suite has been accordingly updated.

You can find the full source code here.

Published on 12 September, 2023