@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
text/typescript
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))
}