/** Serializable representation of a ratio. */ export type RatioData = [numerator: number, denominator: number]; /** Representation of a ratio for performing fractional artithmetic. */ export default class Ratio { readonly #numerator: number; readonly #denominator: number; get numerator(): number { return this.#numerator; } get denominator(): number { return this.#denominator; } constructor(numerator: number, denominator: number) { if (!Number.isInteger(numerator) || !Number.isInteger(denominator)) { throw new TypeError( `Ratio must have integer parts: ${numerator} / ${denominator}`, ); } if (denominator === 0) { throw new RangeError("Ratio demnominator cannot be zero"); } this.#numerator = numerator; this.#denominator = denominator; } multiplyRatio(other: Ratio): Ratio { return new Ratio( this.numerator * other.numerator, this.denominator * other.denominator, ); } divideRatio(other: Ratio): Ratio { return new Ratio( this.numerator * other.denominator, this.denominator * other.numerator, ); } add(other: Ratio): Ratio { return new Ratio( this.numerator * other.denominator + other.numerator * this.denominator, this.denominator * other.denominator, ); } subtract(other: Ratio): Ratio { return new Ratio( this.numerator * other.denominator - other.numerator * this.denominator, this.denominator * other.denominator, ); } compare(other: Ratio): number { const left = this.numerator * other.denominator; const right = other.numerator * this.denominator; return left < right ? -1 : left > right ? 1 : 0; } toNumber(): number { return this.numerator / this.denominator; } static fromInteger(n: number): Ratio { return new Ratio(n, 1); } toData(): RatioData { return [this.numerator, this.denominator]; } static fromData(ratio: RatioData): Ratio { return new Ratio(ratio[0], ratio[1]); } }