UNPKG

@typester/big-rational

Version:

BigRational is a ~1kB arbitrary precision rational number library powered by the native BigInt type.

205 lines (171 loc) 5.41 kB
const re = (() => { const inner = '[0-9]+(?:[.][0-9]*)?' const nonNegativeDecimal = `(${inner})` const decimal = `(-?${inner})` const ws = '\\s*' const make = (parts: string[]) => new RegExp(['^', ...parts, '$'].join(ws)) return [ make([decimal]), make([decimal, '/', nonNegativeDecimal]), make([decimal, nonNegativeDecimal, '/', nonNegativeDecimal]), ] as const })() export class BigRational { #numerator: bigint #denominator: bigint // Creates a new BigRational without simplifying the fraction or checking that // the denominator is positive. private constructor(numerator: bigint, denominator: bigint) { this.#numerator = numerator this.#denominator = denominator } static from(numerator: bigint, denominator: bigint): BigRational { if (denominator === 0n) throw new Error('denominator cannot be zero') // Fast case. if (denominator === 1n) return new BigRational(numerator, 1n) // The denominator must always be positive. if (denominator < 0) { numerator = -numerator denominator = -denominator } const gcd_ = gcd(abs(numerator), denominator) return new BigRational(numerator / gcd_, denominator / gcd_) } static fromString(string: string): BigRational { const match0 = string.match(re[0]) if (match0) { return parseOne(match0[1]!) } const match1 = string.match(re[1]) if (match1) { return parseOne(match1[1]!).divide(parseOne(match1[2]!)) } const match2 = string.match(re[2]) if (match2) { const whole = parseOne(match2[1]!) const fraction = parseOne(match2[2]!).divide(parseOne(match2[3]!)) return whole.#numerator >= 0n ? whole.add(fraction) : whole.subtract(fraction) } throw new Error(`Invalid BigRational: ${string}`) } numerator(): bigint { return this.#numerator } denominator(): bigint { return this.#denominator } add(other: BigRational): BigRational { return BigRational.from( this.#numerator * other.#denominator + other.#numerator * this.#denominator, this.#denominator * other.#denominator, ) } subtract(other: BigRational): BigRational { return BigRational.from( this.#numerator * other.#denominator - other.#numerator * this.#denominator, this.#denominator * other.#denominator, ) } multiply(other: BigRational): BigRational { return BigRational.from( this.#numerator * other.#numerator, this.#denominator * other.#denominator, ) } divide(other: BigRational): BigRational { return BigRational.from( this.#numerator * other.#denominator, this.#denominator * other.#numerator, ) } compare(other: BigRational): number { const a = this.#numerator * other.#denominator const b = other.#numerator * this.#denominator return a === b ? 0 : a > b ? 1 : -1 } power(n: bigint): BigRational { return BigRational.from(this.#numerator ** n, this.#denominator ** n) } modulo(other: BigRational): BigRational { return this.subtract( other.multiply(new BigRational(this.divide(other).floor(), 1n)), ) } // Round towards negative infinity. floor(): bigint { return ( (this.#numerator >= 0 ? this.#numerator : this.#numerator - this.#denominator + 1n) / this.#denominator ) } // Round towards positive infinity. ceil(): bigint { return ( (this.#numerator >= 0 ? this.#numerator + this.#denominator - 1n : this.#numerator) / this.#denominator ) } // Round towards zero. truncate(): bigint { return this.#numerator / this.#denominator } negate(): BigRational { return new BigRational(-this.#numerator, this.#denominator) } inverse(): BigRational { if (this.#numerator === 0n) throw new Error('cannot take inverse of zero') return this.#numerator < 0n ? new BigRational(-this.#denominator, -this.#numerator) : new BigRational(this.#denominator, this.#numerator) } abs(): BigRational { return this.#numerator < 0n ? this.negate() : this } toNumberApproximate() { const whole = this.#numerator / this.#denominator const numerator = this.#numerator % this.#denominator return Number(whole) + Number(numerator) / Number(this.#denominator) } toFractionString() { return this.#denominator === 1n ? `${this.#numerator}` : `${this.#numerator}/${this.#denominator}` } toMixedString() { const whole = this.#numerator / this.#denominator let numerator = this.#numerator % this.#denominator if (whole < 0) numerator = -numerator return numerator === 0n ? `${whole}` : whole === 0n ? `${numerator}/${this.#denominator}` : `${whole} ${numerator}/${this.#denominator}` } toString() { return this.toFractionString() } } const abs = (a: bigint): bigint => (a >= 0n ? a : -a) const gcd = (a: bigint, b: bigint): bigint => { while (b !== 0n) { const t = b b = a % b a = t } return a } const parseOne = (string: string) => { const indexOfDot = string.indexOf('.') if (indexOfDot === -1) { return BigRational.from(BigInt(string), 1n) } const before = string.slice(0, indexOfDot) const after = string.slice(indexOfDot + 1) return BigRational.from(BigInt(before + after), 10n ** BigInt(after.length)) }