apple / swift-numerics

Advanced mathematical types and functions for Swift

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Rational Number Module

kieranb662 opened this issue · comments

Rational Number Data Type

This is an example proposal for a rational number module. A good example use of a rational number
would for when a user wishes to simplify symbolic expressions without needing to worry about
floating point errors. I believe this type would be most useful as an internal currency for symbolic
calculations, where performance is sacrificed for accuracy. This type would also allow for the
division of integers without explicit calculations needing to be performed. Some issues will arise
such as the reduction of fractions to simplest form. Creating a performant algorithm for finding
common factors will be a must. Another important issue will be the conversion of both terminal and
repeating decimal numbers into simplified fractions.

The Rational type would conform to the Real protocol and could be used for both
the real and imaginary components of a Complex value.

Simple Example

struct Rational {
    
    let numerator: Int
    let denominator: Int
    
    // todo - fix this so that infinities are handled properly
    var decimal: Double {
        denominator != 0 ? Double(numerator)/Double(denominator) : .infinity
    }
    
    public init(_ numerator: Int, _ denominator: Int) {
        self.numerator = numerator
        self.denominator = denominator
    }
    
    /// Need to test and refactor this to be more robust.
    var isNegative: Bool {
        if numerator >= 0 && denominator > 0 {
            return false
        } else if numerator <= 0 && denominator < 0 {
            return false
        } else {
            return true
        }
    }
}


// MARK: - Equatable Conformance
extension Rational: Equatable {
    // todo: come back and adjust this because using the decimal is a bit of a naive
    // approach to checking equivalence.
    static func == (lhs: Rational, rhs: Rational) -> Bool {
        return lhs.decimal == rhs.decimal
    }
}


// MARK: - Comparable Conformance
extension Rational: Comparable {
    // todo: Come back and fix for same reason as Equatable.
    static func < (lhs: Rational, rhs: Rational) -> Bool {
        return lhs.decimal < rhs.decimal
    }
}


// MARK: - Int and Rational Math

/// Compute the value of a rational number to the power of an integer.
/// The three main cases are handled
/// * power greater than 0
/// * power less than 0
/// * power equal to 0
/// - important- Relies on a for loop to multiply the numerator and denominator by its self (power-1) times. This could be very slow for large powers.
func pow(base: Rational, power: Int) -> Rational {
    if power == 0 {
        return 1
    } else {
        var num = base.numerator
        var denom = base.denominator
        for _ in 1...abs(power)-1 {
            num *= base.numerator
            denom *= base.denominator
        }
        
        return power > 0 ? RationalNumber(num, denom) : RationalNumber(denom, num)
    }
}

// MARK: AdditiveArithmetic Conformance
extension Rational: AdditiveArithmetic {
    static var zero: Rational = 0
    
    static func += (lhs: inout Rational, rhs: Rational) {
        lhs = lhs+rhs
    }
    
    static func -= (lhs: inout Rational, rhs: Rational) {
        lhs = lhs-rhs
    }
    
}

// MARK: Numeric Conformance 
extension Rational: Numeric {
    var magnitude: Rational {
        return RationalNumber(abs(numerator), abs(denominator))
    }
    
    init?<T>(exactly source: T) where T : BinaryInteger {
        self.numerator = Int(source)
        self.denominator = 1
    }
    
    static func *= (lhs: inout Rational, rhs: Rational) {
        lhs = lhs*rhs
    }
}




// MARK: - (Rational, Rational) Math
extension Rational {
    static func *(lhs: Rational, rhs: Rational) -> Rational {
        return Rational(lhs.numerator*rhs.numerator,
                              lhs.denominator*rhs.denominator)
    }
    
    static func /(lhs: Rational, rhs: Rational) -> Rational {
        return Rational(lhs.numerator*rhs.denominator,
                              lhs.denominator*rhs.numerator)
    }
    
    static func +(lhs: Rational, rhs: Rational) -> Rational {
        return Rational(lhs.numerator*rhs.denominator + lhs.denominator*rhs.numerator,
                              lhs.denominator*rhs.denominator)
    }
    
    static func -(lhs: Rational, rhs: Rational) -> Rational {
        return Rational(lhs.numerator*rhs.denominator - lhs.denominator*rhs.numerator,
                              lhs.denominator*rhs.denominator)
    }
    
    static func ^(lhs: Rational, rhs: Int) -> Rational {
        return pow(base: lhs, power: rhs)
    }
    
    static prefix func -(input: Rational) -> Rational {
        return Rational(-input.numerator, input.denominator)
    }
}


// MARK: - ExpressibleByIntegerLiteral Conformance
extension Rational: ExpressibleByIntegerLiteral {
    init(integerLiteral value: Int) {
        numerator = value
        denominator = 1
    }
}

// MARK: - Sequence Extension
extension Sequence where Element: Numeric {
    func product() -> Element {
        return reduce(1, *)
    }
}

Tests

final class RationalTests: XCTestCase {
    
    // MARK: - Math operations
    
    func testScalarMultiplication() {
        let scalar: Rational = 3
        let rational: Rational = Rational(2, 3)
        
        let standard: Rational = Rational(6, 3)
        
        XCTAssertEqual((scalar*rational).numerator, standard.numerator)
        XCTAssertEqual((scalar*rational).denominator, standard.denominator)
    }
    
    func testMultiplication() {
        let left = Rational(5, 2)
        let right = Rational(3, 4)
        
        let standard = Rational(15, 8)
        
        XCTAssertEqual((left*right).numerator, standard.numerator)
        XCTAssertEqual((left*right).denominator, standard.denominator)
    }
    
    func testDivision() {
        let left = Rational(5, 2)
        let right = Rational(3, 4)
        
        let standard = Rational(20, 6)
        
        XCTAssertEqual((left/right).numerator, standard.numerator)
        XCTAssertEqual((left/right).denominator, standard.denominator)
    }
    
    func testAddition() {
        let left = Rational(5, 2)
        let right = Rational(3, 4)
        
        let standard = Rational(26, 8)
        
        XCTAssertEqual((left+right).numerator, standard.numerator)
        XCTAssertEqual((left+right).denominator, standard.denominator)
    }
    
    func testSubtraction() {
        let left = Rational(5, 2)
        let right = Rational(3, 4)
        
        let standard = Rational(14, 8)
        
        XCTAssertEqual((left-right).numerator, standard.numerator)
        XCTAssertEqual((left-right).denominator, standard.denominator)
    }
    
    func testExponentiation() {
        let left = Rational(5, 2)
        let power: Int = 3
        
        let standard = Rational(125, 8)
        
        let output = pow(base: left, power: power)
        
        XCTAssertEqual(output.numerator, standard.numerator)
        XCTAssertEqual(output.denominator, standard.denominator)
    }
    
    func testNegativeExponentiation() {
        let left = Rational(5, 2)
        let power: Int = -3
        
        let standard = Rational(8, 125)
        
        let output = pow(base: left, power: power)
        
        XCTAssertEqual(output.numerator, standard.numerator)
        XCTAssertEqual(output.denominator, standard.denominator)
    }
    
    func testZeroExponentiation() {
        let left = Rational(5, 2)
        let power: Int = 0
        
        let standard = Rational(1, 1)
        
        let output = pow(base: left, power: power)
        
        XCTAssertEqual(output.numerator, standard.numerator)
        XCTAssertEqual(output.denominator, standard.denominator)
    }
    
    func testExponentiationOperator() {
        let left = Rational(5, 2)
        let power: Int = 3
        
        let standard = Rational(125, 8)
        
        let output = left^power
        
        XCTAssertEqual(output.numerator, standard.numerator)
        XCTAssertEqual(output.denominator, standard.denominator)
    }
    
    // MARK: - Equivalence and Comparison
    
    func testEquivalence() {
        let left = Rational(4, 2)
        let right = Rational(8, 4)
        
        XCTAssertEqual(left, right)
    }
    
    // MARK: - AdditiveArithmetic
    
    func testInOutAddition() {
        var left = Rational(4, 2)
        let right = Rational(3, 1)
        
        let standard = Rational(10, 2)
        
        left += right
        
        XCTAssertEqual(left, standard)
    }
    
    func testReduce() {
        let numbers = [Rational(2, 2), Rational(3, 3), Rational(4, 4)]
        
        let standard = Rational(24, 24)
        
        XCTAssertEqual(numbers.product(), standard)
    }
}
  

Hi @kieranb662, thanks!

Can you give some examples of the use cases you have in mind for this module? Broadly speaking, my experience is that fixed-width rational numbers (i.e rational numbers that use a fixed-size integer type for the numerator and denominator) are not very useful, because most non-trivial computations will overflow the denominator after a few operations, and computations that don't suffer from that problem involve numbers that all share a common unit scaling, so scaled-integer or floating-point would always be a better option.

I'm not opposed to adding a rational type, but to get this started I'd like to see less implementation and more justification.

Thank you so much for your response @stephentyrone. I see what you mean about the use cases and limitations. I will write up a a list of use cases with their justifications and limitations(I may need some help noticing them).

I'll be back in a jiffy!