Skip to content

Commit

Permalink
feat(lib): support rationalizing decimal arguments to Rational
Browse files Browse the repository at this point in the history
  • Loading branch information
rektdeckard committed Oct 19, 2023
1 parent cbd53fe commit aa930fe
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 60 deletions.
78 changes: 56 additions & 22 deletions src/math/rational.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,20 @@ export class Rational
#d: number;

constructor(numerator: number, denominator: number = 1) {
assertInteger(numerator, denominator);
if (denominator === 0) {
throw new RangeError("Cannot divide by zero");
}

this.#n = numerator;
this.#d = denominator;
if (!Number.isInteger(numerator) || !Number.isInteger(denominator)) {
const n = new Rational(...Rational.#rationalize(numerator));
const self = n.div(...Rational.#rationalize(denominator));

this.#n = self.numerator;
this.#d = self.denominator;
} else {
this.#n = numerator;
this.#d = denominator;
}

this.#simplify();
}
Expand All @@ -77,9 +87,7 @@ export class Rational
? base
: typeof base === "string"
? Rational.parse(base)
: typeof opt === "number"
? (assertInteger(base, opt), new Rational(base, opt))
: (assertInteger(base), new Rational(base, opt));
: new Rational(base, opt);
}

static parse(fraction: string): Rational {
Expand All @@ -89,18 +97,53 @@ export class Rational
if (second !== undefined) {
const denominator = Number(dString);
const numerator = Number(first) * denominator + Number(second);

assertInteger(numerator, denominator);
return new Rational(numerator, denominator);
} else {
const denominator = Number(dString);
const numerator = Number(first);

assertInteger(numerator, denominator);
return new Rational(numerator, denominator);
}
}

static #rationalize(
n: number,
precision: number = 0.0000001
): [number, number] {
// TODO: implement with Stern-Brocot tree?
// https://en.wikipedia.org/wiki/Stern%E2%80%93Brocot_tree

// Continued fraction method
// https://en.wikipedia.org/wiki/Continued_fraction
if (Number.isInteger(n)) return [n, 1];

function integerDecimal(n: number): [number, number] {
return [Math.trunc(n), n % 1];
}

const components = [];
let x = n;
while (true) {
const [integer, decimal] = integerDecimal(x);
components.unshift(integer);

if (decimal < precision) {
break;
}

if (decimal !== 0) {
x = 1 / decimal;
}
}

const base = components.pop()!;
let r = new Rational(0);
for (const c of components) {
r = new Rational(c).add(r).recip();
}
r = r.add(base);
return [r.numerator, r.denominator];
}

#simplify() {
const factor = gcf(this.numerator, this.denominator);
if (factor !== 1) {
Expand Down Expand Up @@ -143,10 +186,7 @@ export class Rational
add(...addend: RationalLike): Rational {
const other = Rational.from(...addend);
if (other.denominator === this.denominator) {
return new Rational(
this.numerator + other.numerator,
this.denominator
);
return new Rational(this.numerator + other.numerator, this.denominator);
}

const { denominator, numerator, otherNumerator } = this.#factor(other);
Expand All @@ -156,10 +196,7 @@ export class Rational
sub(...subtrahend: RationalLike): Rational {
const other = Rational.from(...subtrahend);
if (other.denominator === this.denominator) {
return new Rational(
this.numerator - other.numerator,
this.denominator
);
return new Rational(this.numerator - other.numerator, this.denominator);
}

const { denominator, numerator, otherNumerator } = this.#factor(other);
Expand Down Expand Up @@ -200,10 +237,7 @@ export class Rational
}

abs(): Rational {
return new Rational(
Math.abs(this.numerator),
Math.abs(this.denominator)
);
return new Rational(Math.abs(this.numerator), Math.abs(this.denominator));
}

eq(...other: RationalLike): boolean {
Expand Down
60 changes: 22 additions & 38 deletions test/math/rational.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,6 @@ import { Rational, FRACTION_SLASH } from "../../src/math";

describe("Rational", () => {
describe("constructor", () => {
it("throws with non-integral arguments", () => {
expect(() => new Rational(2, 3.5)).toThrowError(
"Arguments must be integers"
);
});

it("constructs a rational", () => {
const r = new Rational(2, 9);
expect(r.numerator).toBe(2);
Expand All @@ -19,6 +13,14 @@ describe("Rational", () => {
const r = new Rational(8, -24);
expect([r.numerator, r.denominator]).toStrictEqual([-1, 3]);
});

it("constructs from decimals", () => {
const r = new Rational(4.462365591397849);
expect([r.numerator, r.denominator]).toStrictEqual([415, 93]);

const s = new Rational(5 / 6548646);
expect([s.numerator, s.denominator]).toStrictEqual([5, 6548646]);
});
});

describe("parse", () => {
Expand Down Expand Up @@ -50,10 +52,9 @@ describe("Rational", () => {
expect(fiveAndOneSeventh.denominator).toBe(7);
});

it("throws with decimals", () => {
expect(() => Rational.parse("9.3 / 5")).toThrowError(
"Arguments must be integers"
);
it("parses decimals", () => {
const r = Rational.parse("9.3 / 5");
expect([r.numerator, r.denominator]).toStrictEqual([93, 50]);
});
});

Expand All @@ -76,10 +77,9 @@ describe("Rational", () => {
expect(r.denominator).toBe(11);
});

it("throws with decimals", () => {
expect(() => Rational.from("1 / 4.7")).toThrowError(
"Arguments must be integers"
);
it("parses decimals", () => {
const r = Rational.from("1 / 4.7");
expect([r.numerator, r.denominator]).toStrictEqual([10, 47]);
});
});

Expand All @@ -102,30 +102,22 @@ describe("Rational", () => {
});

it("adds similar fractions", () => {
const fourths = Rational.from("1 / 4").add(
Rational.from("2 / 4")
);
const fourths = Rational.from("1 / 4").add(Rational.from("2 / 4"));
expect([fourths.numerator, fourths.denominator]).toStrictEqual([3, 4]);

const thirteenths = new Rational(5, 13).add(
new Rational(1, 13)
);
const thirteenths = new Rational(5, 13).add(new Rational(1, 13));
expect([thirteenths.numerator, thirteenths.denominator]).toStrictEqual([
6, 13,
]);
});

it("adds dissimilar fractions", () => {
const thirtyFifths = new Rational(6, 7).add(
new Rational(1, 5)
);
const thirtyFifths = new Rational(6, 7).add(new Rational(1, 5));
expect([thirtyFifths.numerator, thirtyFifths.denominator]).toStrictEqual([
37, 35,
]);

const twentyEigths = new Rational(2, 7).add(
new Rational(1, 4)
);
const twentyEigths = new Rational(2, 7).add(new Rational(1, 4));
expect([twentyEigths.numerator, twentyEigths.denominator]).toStrictEqual([
15, 28,
]);
Expand All @@ -139,30 +131,22 @@ describe("Rational", () => {
});

it("subtracts similar fractions", () => {
const fourths = Rational.from("1 / 4").sub(
Rational.from("2 / 4")
);
const fourths = Rational.from("1 / 4").sub(Rational.from("2 / 4"));
expect([fourths.numerator, fourths.denominator]).toStrictEqual([-1, 4]);

const thirteenths = new Rational(5, 13).sub(
new Rational(1, 13)
);
const thirteenths = new Rational(5, 13).sub(new Rational(1, 13));
expect([thirteenths.numerator, thirteenths.denominator]).toStrictEqual([
4, 13,
]);
});

it("subtracts dissimilar fractions", () => {
const thirtyFifths = new Rational(6, 7).sub(
new Rational(1, 5)
);
const thirtyFifths = new Rational(6, 7).sub(new Rational(1, 5));
expect([thirtyFifths.numerator, thirtyFifths.denominator]).toStrictEqual([
23, 35,
]);

const twentyEigths = new Rational(2, 7).sub(
new Rational(1, 4)
);
const twentyEigths = new Rational(2, 7).sub(new Rational(1, 4));
expect([twentyEigths.numerator, twentyEigths.denominator]).toStrictEqual([
1, 28,
]);
Expand Down

0 comments on commit aa930fe

Please sign in to comment.