summaryrefslogtreecommitdiff
path: root/packages/web/src/math/Ratio.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/web/src/math/Ratio.ts')
-rw-r--r--packages/web/src/math/Ratio.ts105
1 files changed, 105 insertions, 0 deletions
diff --git a/packages/web/src/math/Ratio.ts b/packages/web/src/math/Ratio.ts
new file mode 100644
index 0000000..e2a1fbf
--- /dev/null
+++ b/packages/web/src/math/Ratio.ts
@@ -0,0 +1,105 @@
+import { gcd } from ".";
+
+/** 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");
+ }
+
+ const divisor = gcd(numerator, denominator);
+
+ this.#numerator = numerator / divisor;
+ this.#denominator = denominator / divisor;
+ }
+
+ 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;
+ }
+
+ equals(other: Ratio): boolean {
+ return this.compare(other) === 0;
+ }
+
+ toNumber(): number {
+ return this.numerator / this.denominator;
+ }
+
+ toString(): string {
+ return `${this.numerator}/${this.denominator}`;
+ }
+
+ [Symbol.for("nodejs.util.inspect.custom")](): string {
+ return `Ratio { ${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]);
+ }
+
+ static min(...ratios: Ratio[]): Ratio {
+ return ratios.reduce((a, b) => (a.compare(b) <= 0 ? a : b));
+ }
+
+ static max(...ratios: Ratio[]): Ratio {
+ return ratios.reduce((a, b) => (a.compare(b) >= 0 ? a : b));
+ }
+}