From 043c93a50f59a1d9709707bba40a1e6207da46c6 Mon Sep 17 00:00:00 2001 From: Vlad Pronsky Date: Wed, 31 May 2023 13:33:44 +0300 Subject: [PATCH] library & tests --- src/main.test.ts | 187 +++++++++++++++++++++++++++++++++++++++ src/main.ts | 226 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 413 insertions(+) create mode 100644 src/main.test.ts create mode 100644 src/main.ts diff --git a/src/main.test.ts b/src/main.test.ts new file mode 100644 index 0000000..c8dc443 --- /dev/null +++ b/src/main.test.ts @@ -0,0 +1,187 @@ +import { test } from "uvu" +import { equal, throws } from "uvu/assert" +import { Fraction, fraq, gcd } from "../src/main" + +test("should calc gcd", () => { + equal(gcd(1, 1), 1) + equal(gcd(2, 1), 1) + equal(gcd(3, 0), 3) + equal(gcd(3, 5), 1) + equal(gcd(6, 10), 2) + equal(gcd(15, 25), 5) + equal(gcd(24, 36), 12) + equal(gcd(33, 99), 33) + equal(gcd(100, 200), 100) + equal(gcd(121, 242), 121) + equal(gcd(999, 1000), 1) + equal(gcd(1111, 2222), 1111) + equal(gcd(12345, 6789), 3) + equal(gcd(270, 192), 6) + + equal(gcd(0, 1), 1) + equal(gcd(0, 2), 2) + equal(gcd(2, 0), 2) + equal(gcd(9007199254740881, 9007199254740997), 1) + throws(() => gcd(3.1415, 1), /TypeError/i) +}) + +test("should create fraction", () => { + equal(new Fraction(1, 1).toPair(), [1, 1]) + equal(new Fraction(1, 2).toPair(), [1, 2]) + equal(new Fraction(-1, 2).toPair(), [-1, 2]) + equal(new Fraction(1, -2).toPair(), [-1, 2]) + equal(new Fraction(-1, -2).toPair(), [1, 2]) + equal(new Fraction(4, 2).toPair(), [2, 1]) // reduced + equal(new Fraction(4, 2, false).toPair(), [4, 2]) + equal(new Fraction(0, 1).toPair(), [0, 1]) + equal(new Fraction(0, 2).toPair(), [0, 1]) + + equal(new Fraction(2).toPair(), [2, 1]) + throws(() => new Fraction(1, 0), /ZeroDivisionError/i) + throws(() => new Fraction(3.1415, 1), /TypeError/i) + + equal(fraq([1, 2]).toPair(), [1, 2]) + equal(fraq(1, 2).toPair(), [1, 2]) + equal(fraq(2).toPair(), [2, 1]) + equal(fraq(new Fraction(2)).toPair(), [2, 1]) + + equal(fraq(0.5).toPair(), [1, 2]) + equal(fraq(1.5).toPair(), [3, 2]) + equal(fraq(-0.5).toPair(), [-1, 2]) + equal(fraq(-1.5).toPair(), [-3, 2]) +}) + +test("should abs fraction", () => { + equal(new Fraction(1, 1).abs().toPair(), [1, 1]) + equal(new Fraction(1, 2).abs().toPair(), [1, 2]) + equal(new Fraction(-1, 2).abs().toPair(), [1, 2]) + equal(new Fraction(1, -2).abs().toPair(), [1, 2]) + equal(new Fraction(-1, -2).abs().toPair(), [1, 2]) +}) + +test("should add fraction", () => { + equal(new Fraction(1, 1).add([1, 1]).toPair(), [2, 1]) + equal(new Fraction(1, 1).add([1, 2]).toPair(), [3, 2]) + equal(new Fraction(1, 2).add([1, 1]).toPair(), [3, 2]) + equal(new Fraction(1, 2).add([1, 2]).toPair(), [1, 1]) + equal(new Fraction(1, 2).add([1, 3]).toPair(), [5, 6]) + + equal(new Fraction(-1, 2).add([1, 2]).toPair(), [0, 1]) + equal(new Fraction(1, 2).add([-1, 2]).toPair(), [0, 1]) + equal(new Fraction(1, 2).add([1, -2]).toPair(), [0, 1]) + equal(new Fraction(1, -2).add([1, 2]).toPair(), [0, 1]) +}) + +test("should sub fraction", () => { + equal(new Fraction(1, 1).sub([1, 1]).toPair(), [0, 1]) + equal(new Fraction(1, 1).sub([1, 2]).toPair(), [1, 2]) + equal(new Fraction(1, 2).sub([1, 1]).toPair(), [-1, 2]) + equal(new Fraction(1, 2).sub([1, 2]).toPair(), [0, 1]) + equal(new Fraction(1, 2).sub([1, 3]).toPair(), [1, 6]) + equal(new Fraction(1, 3).sub([1, 2]).toPair(), [-1, 6]) + equal(new Fraction(1, 3).sub([1, 3]).toPair(), [0, 1]) + equal(new Fraction(1, 3).sub([1, 4]).toPair(), [1, 12]) + equal(new Fraction(1, 4).sub([1, 3]).toPair(), [-1, 12]) + + equal(new Fraction(-1, 2).sub([1, 2]).toPair(), [-1, 1]) + equal(new Fraction(1, 2).sub([-1, 2]).toPair(), [1, 1]) + equal(new Fraction(1, 2).sub([1, -2]).toPair(), [1, 1]) +}) + +test("should mul fraction", () => { + equal(new Fraction(1, 2).mul([3, 4]).toPair(), [3, 8]) + equal(new Fraction(1, 2).mul([-3, 4]).toPair(), [-3, 8]) + equal(new Fraction(-1, 2).mul([-3, 4]).toPair(), [3, 8]) + equal(new Fraction(1, 2).mul(-3).toPair(), [-3, 2]) + equal(new Fraction(-1, 2).mul(3).toPair(), [-3, 2]) + equal(new Fraction(1, 2).mul(0).toPair(), [0, 1]) + equal(new Fraction(-1, 2).mul(0).toPair(), [0, 1]) + equal(new Fraction(2, 3).mul([2, 3]).toPair(), [4, 9]) + equal(new Fraction(3, 4).mul([4, 3]).toPair(), [1, 1]) + equal(new Fraction(3, 2).mul([2, 5]).toPair(), [3, 5]) + equal(new Fraction(2, 5).mul([3, 2]).toPair(), [3, 5]) + equal(new Fraction(-3, 4).mul([-2, 5]).toPair(), [3, 10]) + equal(new Fraction(3, -4).mul([2, 5]).toPair(), [-3, 10]) + equal(new Fraction(-2, -3).mul([-1, -4]).toPair(), [1, 6]) + equal(new Fraction(1, -4).mul([-2, 3]).toPair(), [1, 6]) + equal(new Fraction(-1, 3).mul([4, 5]).toPair(), [-4, 15]) + equal(new Fraction(-2, 3).mul([5, 7]).toPair(), [-10, 21]) +}) + +test("should div fraction", () => { + equal(new Fraction(1, 2).div([3, 4]).toPair(), [2, 3]) + equal(new Fraction(1, 2).div([-3, 4]).toPair(), [-2, 3]) + equal(new Fraction(-1, 2).div([-3, 4]).toPair(), [2, 3]) + equal(new Fraction(1, 2).div(-3).toPair(), [-1, 6]) + equal(new Fraction(-1, 2).div(3).toPair(), [-1, 6]) + equal(new Fraction(2, 3).div([2, 3]).toPair(), [1, 1]) + equal(new Fraction(3, 4).div([4, 3]).toPair(), [9, 16]) + equal(new Fraction(3, 2).div([2, 5]).toPair(), [15, 4]) + equal(new Fraction(2, 5).div([3, 2]).toPair(), [4, 15]) + equal(new Fraction(-3, 4).div([-2, 5]).toPair(), [15, 8]) + equal(new Fraction(3, -4).div([2, 5]).toPair(), [-15, 8]) + equal(new Fraction(-2, -3).div([-1, -4]).toPair(), [8, 3]) + equal(new Fraction(1, -4).div([-2, 3]).toPair(), [3, 8]) + equal(new Fraction(-1, 3).div([4, 5]).toPair(), [-5, 12]) + equal(new Fraction(-2, 3).div([5, 7]).toPair(), [-14, 15]) + + throws(() => new Fraction(1, 0).div(0), /ZeroDivisionError/i) +}) + +test("should compare fraction", () => { + equal(new Fraction(1, 2).gt([1, 3]), true) + equal(new Fraction(1, 2).gt([1, 2]), false) + equal(new Fraction(1, 2).gt([1, 1]), false) + + equal(new Fraction(1, 2).gte([1, 3]), true) + equal(new Fraction(1, 2).gte([1, 2]), true) + equal(new Fraction(1, 2).gte([1, 1]), false) + + equal(new Fraction(1, 2).lt([1, 3]), false) + equal(new Fraction(1, 2).lt([1, 2]), false) + equal(new Fraction(1, 2).lt([1, 1]), true) + + equal(new Fraction(1, 2).lte([1, 3]), false) + equal(new Fraction(1, 2).lte([1, 2]), true) + equal(new Fraction(1, 2).lte([1, 1]), true) +}) + +test("should limit denominator", () => { + equal(new Fraction(1, 2).limit().toPair(), [1, 2]) + equal(new Fraction(33, 100).limit().toPair(), [33, 100]) + equal(new Fraction(333, 1000).limit().toPair(), [333, 1000]) + equal(new Fraction(3_333, 10_000).limit().toPair(), [3333, 10_000]) + equal(new Fraction(33_333, 100_000).limit().toPair(), [1, 3]) +}) + +test("other", () => { + equal(new Fraction(1, 2).toString(), "1/2") + equal(new Fraction(3, 2).toString(), "3/2") + equal(new Fraction(1, -2).toString(), "-1/2") + equal(new Fraction(-1, 2).toString(), "-1/2") + equal(new Fraction(-1, -2).toString(), "1/2") + equal(new Fraction(1, 1).toString(), "1") + equal(new Fraction(2, 1).toString(), "2") + equal(new Fraction(-1, 1).toString(), "-1") + equal(new Fraction(1, -1).toString(), "-1") + equal(new Fraction(-1, -1).toString(), "1") + equal(new Fraction(0, 1).toString(), "0") + equal(new Fraction(0, -1).toString(), "0") + equal(new Fraction(6, 2).toString(), "3") + throws(() => new Fraction(0, 0).toString(), /ZeroDivisionError/i) + + equal(new Fraction(1, 2).toParts(), { s: 1, c: 0, n: 1, d: 2 }) + equal(new Fraction(3, 2).toParts(), { s: 1, c: 1, n: 1, d: 2 }) + equal(new Fraction(1, -2).toParts(), { s: -1, c: 0, n: 1, d: 2 }) + equal(new Fraction(-1, 2).toParts(), { s: -1, c: 0, n: 1, d: 2 }) + equal(new Fraction(-1, -2).toParts(), { s: 1, c: 0, n: 1, d: 2 }) + equal(new Fraction(1, 1).toParts(), { s: 1, c: 0, n: 1, d: 1 }) + equal(new Fraction(2, 1).toParts(), { s: 1, c: 0, n: 2, d: 1 }) /// ??? + equal(new Fraction(-1, 1).toParts(), { s: -1, c: 0, n: 1, d: 1 }) + equal(new Fraction(1, -1).toParts(), { s: -1, c: 0, n: 1, d: 1 }) + equal(new Fraction(-1, -1).toParts(), { s: 1, c: 0, n: 1, d: 1 }) + equal(new Fraction(0, 1).toParts(), { s: 1, c: 0, n: 0, d: 1 }) + equal(new Fraction(0, -1).toParts(), { s: 1, c: 0, n: 0, d: 1 }) +}) + +test.run() diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..f68454b --- /dev/null +++ b/src/main.ts @@ -0,0 +1,226 @@ +/** + * Most of this file is rewritten from the Python Standard Library + * Regards to Sjoerd Mullender & Jeffrey Yasskin + * https://github.com/python/cpython/blob/3.11/Lib/fractions.py + */ + +export type Fraq = Fraction | [number, number] | number +type Parts = { s: 1 | -1; c: Number; n: Number; d: Number } + +export const gcd = (a: number, b: number): number => { + if (!Number.isInteger(a) || !Number.isInteger(b)) { + throw new Error("TypeError") + } + + if (a === 0) return b + if (b === 0) return a + + a = Math.abs(a) + b = Math.abs(b) + + while (b > 0) { + const k = a % b + a = b + b = k + } + + return a +} + +export const fraq = (...args: [Fraq] | [number, number]): Fraction => { + return Fraction.make(...args) +} + +export class Fraction { + public n: number + public d: number + + constructor(n: number, d: number = 1, reduce = true) { + if (d === 0) throw new Error("ZeroDivisionError") + + let a = Math.abs(n) * (Math.sign(n) * Math.sign(d)) + let b = n === 0 ? 1 : Math.abs(d) + let g = reduce ? gcd(a, b) : 1 + + this.n = a / g + this.d = b / g + } + + static make(...args: [Fraq] | [number, number]) { + if (args.length === 2) return new Fraction(...args) + + const val = args[0] + if (val instanceof Fraction) return val + + if (typeof val === "number") { + if (Number.isInteger(val)) return new Fraction(val, 1) + + const [c, r] = val.toString().split(".").map(Number) + const d = Math.pow(10, r.toString().length) + const n = (Math.abs(c) * d + r) * Math.sign(val) + return new Fraction(n, d) + } + + return new Fraction(...val) + } + + toString() { + return this.d === 1 ? `${this.n}` : `${this.n}/${this.d}` + } + + toNumber(): number { + return this.n / this.d + } + + toPair(): [number, number] { + return [this.n, this.d] + } + + toParts(): Parts { + const s = this.n < 0 ? -1 : 1 + const c = Math.floor(Math.abs(this.n) / this.d) + const n = Math.abs(this.n) % this.d + const d = this.d + return n === 0 ? { s, c: 0, n: c, d } : { s, c, n, d } + } + + limit(max: number = 10_000): Fraction { + max = Math.max(1, Math.ceil(max)) + if (this.d <= max) return new Fraction(this.n, this.d) + + let [n, d] = [this.n, this.d] + let [p0, q0, p1, q1] = [0, 1, 1, 0] + + while (true) { + const a = Math.floor(n / d) + const q2 = q0 + a * q1 + if (q2 > max) break + ;[p0, q0, p1, q1] = [p1, q1, p0 + a * p1, q2] + ;[n, d] = [d, n - a * d] + } + + const k = Math.floor((max - q0) / q1) + const b1 = new Fraction(p0 + k * p1, q0 + k * q1, false) + const b2 = new Fraction(p1, q1, false) + + const t1 = b2.sub(this).abs() + const t2 = b1.sub(this).abs() + return t1.lte(t2) ? b2 : b1 + } + + // math + + abs(): Fraction { + return new Fraction(Math.abs(this.n), Math.abs(this.d)) + } + + add(b: Fraq): Fraction { + const that = fraq(b) + + let [na, da] = [this.n, this.d] + let [nb, db] = [that.n, that.d] + const g = gcd(da, db) + if (g === 1) return new Fraction(na * db + da * nb, da * db) + + const s = Math.floor(da / g) + const t = na * Math.floor(db / g) + nb * s + const g2 = gcd(t, g) + if (g2 === 1) return new Fraction(t, s * db) + + return new Fraction(Math.floor(t / g2), s * Math.floor(db / g2)) + } + + sub(b: Fraq): Fraction { + const that = fraq(b) + + let [na, da] = [this.n, this.d] + let [nb, db] = [that.n, that.d] + const g = gcd(da, db) + if (g === 1) return new Fraction(na * db - da * nb, da * db) + + const s = Math.floor(da / g) + const t = na * Math.floor(db / g) - nb * s + const g2 = gcd(t, g) + if (g2 === 1) return new Fraction(t, s * db) + + return new Fraction(Math.floor(t / g2), s * Math.floor(db / g2)) + } + + mul(b: Fraq): Fraction { + const that = fraq(b) + + let [na, da] = [this.n, this.d] + let [nb, db] = [that.n, that.d] + const g1 = gcd(na, db) + if (g1 > 1) { + na = Math.floor(na / g1) + db = Math.floor(db / g1) + } + + const g2 = gcd(nb, da) + if (g2 > 1) { + nb = Math.floor(nb / g2) + da = Math.floor(da / g2) + } + + return new Fraction(na * nb, db * da) + } + + div(b: Fraq): Fraction { + const that = fraq(b) + + let [na, da] = [this.n, this.d] + let [nb, db] = [that.n, that.d] + + const g1 = gcd(na, nb) + if (g1 > 1) { + na = Math.floor(na / g1) + nb = Math.floor(nb / g1) + } + + const g2 = gcd(db, da) + if (g2 > 1) { + da = Math.floor(da / g2) + db = Math.floor(db / g2) + } + + let [n, d] = [na * db, nb * da] + if (d < 0) { + n = -n + d = -d + } + + return new Fraction(n, d) + } + + // comparison + + eq(b: Fraq) { + const that = fraq(b) + return this.n * that.d === this.d * that.n + } + + // a < b + lt(b: Fraq) { + const that = fraq(b) + return this.n * that.d < this.d * that.n + } + + // a <= b + lte(b: Fraq) { + const that = fraq(b) + return this.n * that.d <= this.d * that.n + } + + // a > b + gt(b: Fraq) { + const that = fraq(b) + return this.n * that.d > this.d * that.n + } + + // a >= b + gte(b: Fraq) { + const that = fraq(b) + return this.n * that.d >= this.d * that.n + } +}