swift-numerics icon indicating copy to clipboard operation
swift-numerics copied to clipboard

Rational Number Module

Open kieranb662 opened this issue 4 years ago • 2 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)
    }
}
  

kieranb662 avatar Nov 08 '19 14:11 kieranb662

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.

stephentyrone avatar Nov 08 '19 14:11 stephentyrone

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!

kieranb662 avatar Nov 08 '19 15:11 kieranb662