Skip to content

Commit ee0e29a

Browse files
feat(math): add fraction conversions, GCD, LCM
1 parent 63b90f7 commit ee0e29a

File tree

3 files changed

+107
-0
lines changed

3 files changed

+107
-0
lines changed

packages/math/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@
104104
"./fit": {
105105
"default": "./fit.js"
106106
},
107+
"./fraction": {
108+
"default": "./fraction.js"
109+
},
107110
"./int": {
108111
"default": "./int.js"
109112
},

packages/math/src/fraction.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import type { FnN2 } from "@thi.ng/api";
2+
3+
/**
4+
* Iteratively computes Greatest Common Divisor of given `a` and `b`.
5+
*
6+
* @remarks
7+
* Reference:
8+
*
9+
* - https://en.wikipedia.org/wiki/Greatest_common_divisor
10+
*
11+
* @param a
12+
* @param b
13+
*/
14+
export const gcd: FnN2 = (a, b) => {
15+
a = Math.abs(a);
16+
b = Math.abs(b);
17+
while (b !== 0) {
18+
const tmp = b;
19+
b = a % b;
20+
a = tmp;
21+
}
22+
return a;
23+
};
24+
25+
/**
26+
* Computes Least Common Multiple for given `a` and `b`. Returns zero if either
27+
* input is zero.
28+
*
29+
* @remarks
30+
* Reference:
31+
*
32+
* - https://en.wikipedia.org/wiki/Least_common_multiple
33+
*
34+
* @param a
35+
* @param b
36+
*/
37+
export const lcm: FnN2 = (a, b) => {
38+
if (!Number.isFinite(a) || !Number.isFinite(b))
39+
throw new Error("both inputs must be finite");
40+
return a && b ? Math.abs(a * b) / gcd(a, b) : 0;
41+
};
42+
43+
/**
44+
* Converts given `x` to a fraction with optional `maxDenom`inator, using
45+
* continued fractions for best possible precision.
46+
*
47+
* @remarks
48+
* Reference:
49+
*
50+
* - https://en.wikipedia.org/wiki/Continued_fraction
51+
*
52+
* @example
53+
* ```ts tangle:../export/as-fraction.ts
54+
* import { asFraction } from "@thi.ng/math";
55+
*
56+
* console.log(Math.PI, asFraction(Math.PI));
57+
* // 3.141592653589793 [1146408, 364913]
58+
*
59+
* // keep denominator <= 1000
60+
* console.log(Math.PI, asFraction(Math.PI, 1000));
61+
* // 3.141592653589793 [ 355, 113 ]
62+
* ```
63+
*
64+
* @param x
65+
* @param maxDenom
66+
*/
67+
export const asFraction = (x: number, maxDenom = 1e6) => {
68+
if (!Number.isFinite(x)) throw new Error("input must be a finite");
69+
if (Number.isInteger(x)) return [x, 1];
70+
71+
const sign = Math.sign(x);
72+
x = Math.abs(x);
73+
74+
let n0 = 0;
75+
let n1 = 1;
76+
let d0 = 1;
77+
let d1 = 0;
78+
79+
while (true) {
80+
const i = Math.floor(x);
81+
const d2 = i * d1 + d0;
82+
if (d2 > maxDenom) break;
83+
84+
const n2 = i * n1 + n0;
85+
n0 = n1;
86+
n1 = n2;
87+
d0 = d1;
88+
d1 = d2;
89+
90+
const rem = x - i;
91+
if (rem < Number.EPSILON) break;
92+
x = 1 / rem;
93+
}
94+
95+
return [sign * n1, d1];
96+
};
97+
98+
/**
99+
* Reverse op of {@link asFraction}. Converts a fraction tuple to a JS number.
100+
*
101+
* @param fraction
102+
*/
103+
export const asFloat = ([a, b]: [number, number]) => a / b;

packages/math/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export * from "./eqdelta.js";
77
export * from "./extrema.js";
88
export * from "./easing.js";
99
export * from "./fit.js";
10+
export * from "./fraction.js";
1011
export * from "./int.js";
1112
export * from "./interval.js";
1213
export * from "./libc.js";

0 commit comments

Comments
 (0)