M.Hiroi's Home Page

JavaScript Programming

JavaScript Junk Scripts


Copyright (C) 2025 Makoto Hiroi
All rights reserved.

多倍長整数

> 2n
2n
> 2n * 2
Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions
> 2n * BigInt(2)
4n
> 2n ** 64n
18446744073709551616n
> 2n ** 128n
340282366920938463463374607431768211456n

●簡単な関数

リスト : 簡単な関数

// 絶対値
function abs(n) { return n > 0n ? n : -n; }

// 符号
function sign(n) {
    if (n == 0n) return 0;
    else if (n > 0n) return 1;
    else return -1;
}

// 最大値と最小値
function max(x, ...args) { return args.reduce((a, b) => b > a ? b : a, x); }
function min(x, ...args) { return args.reduce((a, b) => b < a ? b : a, x); }

// 最大公約数
function gcd(a, b) {
    while (b > 0n) [a, b] = [b, a % b];
    return a;
}

// 最小公倍数
function lcm(a, b) { return a * b / gcd(a, b); }

// 数列の生成
function iota(s, n, fn = x => x) {
    let buff = new Array(n);
    let i = 0;
    while (n-- > 0) buff[i++] = fn(s++);
    return buff;
}
> abs(12345678n)
12345678n
> abs(-12345678n)
12345678n
> abs(0n)
0n

> sign(12345678n)
1
> sign(-12345678n)
-1
> sign(0n)
0

> max(3,5,1,2,4)
5
> min(3,5,1,2,4)
1

> gcd(42n, 30n)
6n
> gcd(15n, 70n)
5n
> gcd(4n ** 64n, 2n ** 64n)
18446744073709551616n

> lcm(5n, 7n)
35n
> lcm(14n, 35n)
70n

> iota(1n, 10)
[
  1n, 2n, 3n, 4n,  5n,
  6n, 7n, 8n, 9n, 10n
]
> iota(1n, 10, x => x * x)
[
   1n,   4n,  9n, 16n,
  25n,  36n, 49n, 64n,
  81n, 100n
]
> iota(1n, 20).reduce((a, x) => lcm(a, x), 1n)
232792560n

●階乗

\( n! = \begin{cases} 1 & \mathrm{if} \ n = 0 \\ n \times (n-1)! & \mathrm{if} \ n \gt 0 \end{cases} \)

図 : 階乗の定義
リスト : 階乗

function fact(n) {
    let a = 1n;
    while (n > 0) a *= BigInt(n--);
    return a;
}
> for (let i = 0; i < 20; i++) console.log(fact(i))
1n
1n
2n
6n
24n
120n
720n
5040n
40320n
362880n
3628800n
39916800n
479001600n
6227020800n
87178291200n
1307674368000n
20922789888000n
355687428096000n
6402373705728000n
121645100408832000n
undefined

> fact(50)
30414093201713378043612608166064768844377641568960512000000000000n

●フィボナッチ数

\( fibo(n) = \begin{cases} 0 & \mathrm{if} \ n = 0 \\ 1 & \mathrm{if} \ n = 1 \\ fibo(n - 1) + fibo(n - 2) & \mathrm{if} \ n \gt 1 \end{cases} \)

0, 1, 1, 2, 3, 5, 8, 13 .... という直前の 2 項を足していく数列

図 : フィボナッチ関数の定義
リスト : フィボナッチ数 (繰り返し)

function fibo(n, a = 0n, b = 1n) {
    while (n-- > 0) [a, b] = [b, a + b];
    return a;
}
> for (let i = 50; i < 60; i++) console.log(fibo(i))
12586269025n
20365011074n
32951280099n
53316291173n
86267571272n
139583862445n
225851433717n
365435296162n
591286729879n
956722026041n
undefined

> fibo(100)
354224848179261915075n

> fibo(200)
280571172992510140037611932413038677189525n

●トリボナッチ数

次の漸化式で生成される数列をトリボナッチ数といいます。

\(\begin{array}{l} T_0 = T_1 = 0 \\ T_2 = 1 \\ T_{n+3} = T_{n+2} + T_{n+1} + T_n \end{array}\)
リスト : トリボナッチ数

function tribo(n, a = 0n, b = 0n, c = 1n) {
    while (n-- > 0) [a, b, c] = [b, c, a + b + c];
    return a;
}
> for (let i = 0; i < 20; i++) console.log(tribo(i))
0n
0n
1n
1n
2n
4n
7n
13n
24n
44n
81n
149n
274n
504n
927n
1705n
3136n
5768n
10609n
19513n
undefined

> tribo(100)
53324762928098149064722658n

●テトラナッチ数

次の漸化式で生成される数列をテトラナッチ数列といいます。

\(\begin{array}{ll} T_0 = T_1 = T_2 = 0 \\ T_3 = 1 \\ T_{n+4} = T_{n+3} + T_{n+2} + T_{n+1} + T_n \end{array}\)
リスト : テトラナッチ数

function tetra(n) {
    let [a, b, c, d] = [0n, 0n, 0n, 1n];
    while (n-- > 0) [a, b, c, d] = [b, c, d, a + b + c + d];
    return a;
}
> for (let i = 0; i < 20; i++) console.log(tetra(i))
0n
0n
0n
1n
1n
2n
4n
8n
15n
29n
56n
108n
208n
401n
773n
1490n
2872n
5536n
10671n
20569n
undefined

> tetra(100)
2505471397838180985096739296n

●組み合わせの数

組み合わせの数 \({}_n \mathrm{C}_r\) を求めるプログラムを作ります。\({}_n \mathrm{C}_r\) を求めるには、次の公式を使えば簡単です。

\( {}_n \mathrm{C}_r = \dfrac{n \times (n-1) \times (n-2) \times \cdots \times (n - r + 1)}{1 \times 2 \times 3 \times \cdots \times r} = \dfrac{n!}{r! \times (n-r)!} \)
リスト : 組み合わせの数

function combination(n, r) {
    return fact(n) / (fact(r) * fact(n - r));
}
> for (let r = 30; r <= 50; r++) console.log([r * 2, r], "=", combination(r * 2, r))
[ 60, 30 ] = 118264581564861424n
[ 62, 31 ] = 465428353255261088n
[ 64, 32 ] = 1832624140942590534n
[ 66, 33 ] = 7219428434016265740n
[ 68, 34 ] = 28453041475240576740n
[ 70, 35 ] = 112186277816662845432n
[ 72, 36 ] = 442512540276836779204n
[ 74, 37 ] = 1746130564335626209832n
[ 76, 38 ] = 6892620648693261354600n
[ 78, 39 ] = 27217014869199032015600n
[ 80, 40 ] = 107507208733336176461620n
[ 82, 41 ] = 424784580848791721628840n
[ 84, 42 ] = 1678910486211891090247320n
[ 86, 43 ] = 6637553085023755473070800n
[ 88, 44 ] = 26248505381684851188961800n
[ 90, 45 ] = 103827421287553411369671120n
[ 92, 46 ] = 410795449442059149332177040n
[ 94, 47 ] = 1625701140345170250548615520n
[ 96, 48 ] = 6435067013866298908421603100n
[ 98, 49 ] = 25477612258980856902730428600n
[ 100, 50 ] = 100891344545564193334812497256n
undefined

●多角数

点を多角形の形に並べたとき、その総数を多角数 (polygonal number) といいます。

1    3      6        10          15
●    ●      ●        ●          ●
     ●●    ●●      ●●        ●●
            ●●●    ●●●      ●●●
                     ●●●●    ●●●●
                                ●●●●●

            図 : 三角数
1   4      9        16          25
●  ●●   ●●●   ●●●●   ●●●●●
    ●●   ●●●   ●●●●   ●●●●●
           ●●●   ●●●●   ●●●●●
                    ●●●●   ●●●●●
                               ●●●●●

            図 : 四角数
1       5             12                      22
●       ●             ●                      ●
      ●    ●       ●    ●               ●      ●
                 ●            ●        ●             ●
       ●  ●         ●              ●      ●           ●
                   ●    ●  ●            ●     ●
                                       ●            ●  ●
                     ● ● ●               ●
                                         ●     ● ●  ●

                                           ● ● ● ●

            図 : 五角数

三角形に配置したものを三角数 (triangular number)、四角形に配置したものを四角数 (square number)、五角形に配置したものを五角数 (pentagonal number) といいます。三角数、四角数、五角数を上図に示します。多角数の点の増分を表に示すと、次のようになります。

 n   三角数            四角数             五角数
---+-----------------------------------------------------------
 1 |  1                 1                  1
 2 |  3 = 1+2           4 = 1+3            5 = 1+4
 3 |  6 = 1+2+3         9 = 1+3+5         12 = 1+4+7
 4 | 10 = 1+2+3+4      16 = 1+3+5+7       22 = 1+4+7+10
 5 | 15 = 1+2+3+4+5    25 = 1+3+5+7+9     35 = 1+4+7+10+13
 6 | 21 = 1+2+3+4+5+6  36 = 1+3+5+7+9+11  51 = 1+4+7+10+13+16

      ・・・・・・      ・・・・・・・     ・・・・・

 n | n(n + 1) / 2      n^2                n(3n - 1) / 2

表を見ればお分かりのように、三角数は公差 1、四角数は公差 2、五角数は公差 3、p 角数は公差 p - 2 の等差数列の和になります。初項を a, 公差を d とすると、等差数列の和 \(S_n\) は次式で求めることができます。

\( S_n = \dfrac{n(2a + (n - 1)d}{2} \)

a = 1, d = p - 2 を代入して計算すると、多角数 \(P_{p,n}\) は次式で求めることができます。

\( P_{p,n} = \dfrac{(p - 2)n^2 - (p - 4)n}{2} \)

この式を JavaScript でプログラムすると、次のようになります。

リスト : 多角数

function polygonalNumber(a, b) {
    let p = BigInt(a), n = BigInt(b);
    return ((p - 2n) * n * n - (p - 4n) * n) / 2n;
}

それでは実行してみましょう。

> for (let p = 3; p <= 8; p++) {
... console.log(iota(1, 16).map(r => polygonal_number(p, r)).toString())
... }
1,3,6,10,15,21,28,36,45,55,66,78,91,105,120,136
1,4,9,16,25,36,49,64,81,100,121,144,169,196,225,256
1,5,12,22,35,51,70,92,117,145,176,210,247,287,330,376
1,6,15,28,45,66,91,120,153,190,231,276,325,378,435,496
1,7,18,34,55,81,112,148,189,235,286,342,403,469,540,616
1,8,21,40,65,96,133,176,225,280,341,408,481,560,645,736
undefined

三角数かつ四角数である数を平方三角数 (square triangular number) といいます。JavaScript でナイーブにプログラムすると次のようになります。

リスト : 平方三角数

function square_triangular(n) {
    let i = 0, j = 0, r = [];
    let p3 = polygonal_number(3, i++);
    let p4 = polygonal_number(4, j++);
    while (p3 <= n && p4 <= n) {
        if (p3 == p4) {
            r.push(p3);
            p3 = polygonal_number(3, i++);
            p4 = polygonal_number(4, j++);
        } else if (p3 < p4) {
            p3 = polygonal_number(3, i++);
        } else {
            p4 = polygonal_number(4, j++);
        }
    }
    return r;
}

実行結果は次のようになります。

> square_triangular(100000000000n)
[
            0n,          1n,
           36n,       1225n,
        41616n,    1413721n,
     48024900n, 1631432881n,
  55420693056n
]

ところで、平方三角数 - Wikipedia に掲載されている平方三角数の公式 (一般項と漸化式) を使うと、もっと簡単にプログラムすることができます。

\( f(n) = \begin{cases} 0 & \mathrm{if} \ n = 0 \\ 1 & \mathrm{if} \ n = 1 \\ 34 \times f(n - 1) - f(n - 2) + 2 \quad & \mathrm{if} \ n \geq 2 \end{cases} \)

図 : 平方三角数の漸化式

漸化式を繰り返しでプログラムすると次のようになります。

リスト : 平方三角数 (2)

function square_triangular1(n) {
    let a = 0n, b = 1n;
    while (n-- > 0) [a, b] = [b, 34n * b - a + 2n]
    return a;
}
> for (let i = 0; i <= 20; i++) console.log(square_triangular1(i))
0n
1n
36n
1225n
41616n
1413721n
48024900n
1631432881n
55420693056n
1882672131025n
63955431761796n
2172602007770041n
73804512832419600n
2507180834294496361n
85170343853180456676n
2893284510173841030625n
98286503002057414584576n
3338847817559778254844961n
113422539294030403250144100n
3853027488179473932250054441n
130889512058808083293251706896n
undefined

●カタラン数

カタラン数 - Wikipedia によると、バランスの取れたカッコ列の総数や多角形を三角形分割したときの総数がカタラン数になるそうです。カタラン数は次に示す公式で求めることができます。

\( \mathrm{C}_n = \dfrac{{}_{2n} \mathrm{C}_n}{n + 1} = \dfrac{(2n)!}{(n+1)!n!} \)

カタラン数は組み合わせの数を求める関数 combination を使うと簡単に求めることができます。

リスト : カタラン数

function catalan(n) {
    return combination(2 * n, n) / BigInt(n + 1);
}
> for (let i = 0; i <= 20; i++) console.log(catalan(i))
1n
1n
2n
5n
14n
42n
132n
429n
1430n
4862n
16796n
58786n
208012n
742900n
2674440n
9694845n
35357670n
129644790n
477638700n
1767263190n
6564120420n
undefined

> catalan(50)
1978261657756160653623774456n

> catalan(100)
896519947090131496687170070074100632420837521538745909320n

●モンモール数

「完全順列 (derangement)」の総数を「モンモール数 (Montmort number)」といいます。モンモール数は次の漸化式で求めることができます。

\( A_n = \begin{cases} 0 & \mathrm{if} \ n = 1 \\ 1 & \mathrm{if} \ n = 2 \\ (n - 1) \times (A_{n-1} + A_{n-2}) \quad & \mathrm{if} \ n \geqq 3 \end{cases} \)
リスト : モンモール数

function montmort(n) {
    let a = 0n, b = 1n;
    for (let i = 1; i < n; i++) [a, b] = [b, BigInt(i + 1) * (a + b)]
    return a;
}
> for (let i = 1; i <= 20; i++) console.log(montmort(i))
0n
1n
2n
9n
44n
265n
1854n
14833n
133496n
1334961n
14684570n
176214841n
2290792932n
32071101049n
481066515734n
7697064251745n
130850092279664n
2355301661033953n
44750731559645106n
895014631192902121n
undefined

> montmort(50)
11188719610782480504630258070757734324011354208865721592720336801n

●ベル数

「集合を分割する方法」の総数を「ベル数 (Bell Number)」といい、次の漸化式で求めることができます。

\(\begin{array}{ll} B(0) = 1 & \\ B(1) = 1 & \\ B(n+1) = \displaystyle \sum_{k=0}^n {}_n \mathrm{C}_k \times B(k) \quad & \mathrm{if} \ n \geq 1 \end{array}\)
リスト : ベル数

// 添字付き畳み込み
function fold_with_index(f, a, xs) {
    for (let i = 0; i < xs.length; i++) a = f(i, xs[i], a);
    return a;
}

// ベル数
function bell_number(n) {
    let bs = [1n];
    for (let i = 0; i < n; i++)
        bs.push(fold_with_index((k, x, a) => combination(i, k) * x + a, 0n, bs));
    return bs.pop();
}

bellNumber は公式をそのままプログラムするだけです。累積変数 bs にベル数を格納します。\({}_n \mathrm{C}_k\) は関数 combination で求めます。

\({}_n \mathrm{C}_k \times B(k)\) の総和は関数 fold_with_index で計算します。fold_with_index は添字を関数に渡して畳み込みを行います。ラムダ式の引数 k が添字、x がリストの要素、a が累積変数です。

> for (let i = 0; i <= 20; i++) console.log(bell_number(i))
1n
1n
2n
5n
15n
52n
203n
877n
4140n
21147n
115975n
678570n
4213597n
27644437n
190899322n
1382958545n
10480142147n
82864869804n
682076806159n
5832742205057n
51724158235372n
undefined

> bell_number(50)
185724268771078270438257767181908917499221852770n

●分割数

整数 n を 1 以上の自然数の和で表すことを考えます。これを「整数の分割」といいます。整数を分割するとき、同じ自然数を何回使ってもかまいませんが、並べる順序が違うだけのものは同じ分割とします。簡単な例を示しましょう。次の図を見てください。

─┬─ 6                           : 6
  │
  ├─ 5 ─ 1                      : 5 + 1
  │
  ├─ 4 ┬ 2                      : 4 + 2
  │     │
  │     └ 1 ─ 1                 : 4 + 1 + 1
  │
  ├─ 3 ┬ 3                      : 3 + 3
  │     │
  │     ├ 2 ─ 1                 : 3 + 2 + 1
  │     │
  │     └ 1 ─ 1 ─ 1            : 3 + 1 + 1 + 1
  │
  ├─ 2 ┬ 2 ┬ 2                 : 2 + 2 + 2
  │     │   │
  │     │   └ 1 ─ 1            : 2 + 2 + 1 + 1
  │     │
  │     └ 1 ─ 1 ─ 1 ─ 1       : 2 + 1 + 1 + 1 + 1
  │
  └─ 1 ─ 1 ─ 1 ─ 1 ─ 1 ─ 1  : 1 + 1 + 1 + 1 + 1 + 1

                    図 : 整数 6 の分割

6 の場合、分割の仕方は上図のように 11 通りあります。この数を「分割数」といいます。分割の仕方を列挙する場合、整数 n から k 以下の整数を選んでいくと考えてください。まず、6 から 6 を選びます。すると、残りは 0 になるので、これ以上整数を分割することはできません。次に、6 から 5 を選びます。残りは 1 になるので、1 を選ぶしか方法はありません。

次に、4 を選びます。残りは 2 になるので、2 から 2 以下の整数を分割する方法になります。2 から 2 を選ぶと残りは 0 になるので 2 が得られます。1 を選ぶと残りは 1 になるので、1 + 1 が得られます。したがって、4 + 2, 4 + 1 + 1 となります。同様に、6 から 3 を選ぶと、残りは 3 から 3 以下の整数を選ぶ方法になります。

6 から 2 以下の整数を選ぶ方法は、残り 4 から 2 以下の整数を選ぶ方法になり、そこで 2 を選ぶと 2 から 2 以下の整数を選ぶ方法になります。1 を選ぶと 4 から 1 以下の整数を選ぶ方法になりますが、これは 1 通りしかありません。最後に 6 から 1 を選びますが、これも 1 通りしかありません。これらをすべて足し合わせると 11 通りになります。

整数 n を k 以下の整数で分割する総数を求める関数を p(n, k) とすると、p(n, k) は次のように定義することができます。

\( p(n, k) = \begin{cases} 1 & \mathrm{if} \ n = 0 \ or \ k = 1 \\ 0 & \mathrm{if} \ n \lt 0 \ or \ k \lt 1 \\ p(n - k, k) + p(n, k - 1) \quad & \mathrm{others} \end{cases} \)

たとえば、p(6, 6) は次のように計算することができます。

p(6, 6) => p(0, 6) + p(6, 5)
        => 1 + p(1, 5) + p(6, 4)
        => 1 +    1    + p(2, 4) + p(6, 3)
        => 1 + 1 + 2 + 7
        => 11

p(2, 4) => p(-2, 4) + p(2, 3)
        =>    0     + p(-1, 3) + p(2, 2)
        =>    0     +    0     + p(0, 2) + p(2, 1)
        => 0 + 0 + 1 + 1
        => 2
p(6, 3) => p(3, 3) + p(6, 2)
        => p(0, 3) + p(3, 2) + p(4, 2) + p(6, 1)
        =>    1    + p(1, 2) + p(3, 1) + p(2, 2) + p(4, 1) + 1
        =>    1    +    1    +    1    + p(0, 2) + p(2, 1) + 1 + 1
        => 1 + 1 + 1 + 1 + 1 + 1 + 1
        => 7

これをそのままプログラムすると実行時間は遅くなるのですが、動的計画法を使うと、大きな値でも高速に計算することができます。次の図を見てください。

k 
1 : [1,  1,  1,  1,  1,  1,  1] 

2 : [1,  1,  1+1=2, 1+1=2, 2+1=3, 2+1=3, 3+1=4]
 => [1,  1,  2,  2,  3,  3,  4]

3:  [1,  1,  2,  1+2=3, 1+3=4, 2+3=5, 3+4=7]
 => [1,  1,  2,  3,  4,  5,  7]

4:  [1,  1,  2,  3,  1+4=4, 1+5=6, 2+7=9]
 => [1,  1,  2,  3,  5,  6,  9

5:  [1,  1,  2,  3,  5,  1+6=7, 1+9=10]
 => [1,  1,  2,  3,  5,  7,  10]

6:  [1,  1,  2,  3,  5,  7,  10+1=11]
 => [1,  1,  2,  3,  5,  7,  11]

大きさ n + 1 の配列を用意します。配列の添字が n を表していて、p(n, 1) から順番に値を求めていきます。p(n, 1) の値は 1 ですから、配列の要素は 1 に初期化します。次に、p(n, 2) の値を求めます。定義により p(n, 2) = p(n - 2, 2) + p(n, 1) なので、2 番目以降の要素に n - 2 番目の要素を加算すれば求めることができます。あとは、k の値をひとつずつ増やして同様の計算を行えば p(n, n) の値を求めることができます。

リスト : 分割数 (動的計画法)

function partition_number(n) {
    let table = new Array(n + 1);
    table.fill(1n);
    for (let k = 2; k <= n; k++) {
        for (let m = k; m <= n; m++) table[m] += table[m - k]
    }
    return table[n];
}
> for (let i = 1; i <= 20; i++) console.log(partition_number(i))
1n
2n
3n
5n
7n
11n
15n
22n
30n
42n
56n
77n
101n
135n
176n
231n
297n
385n
490n
627n
undefined

> partition_number(100)
190569292n

> partition_number(200)
3972999029388n

> partition_number(1000)
24061467864032622473692149727991n

ところで、数がもっと大きくなると動的計画法を使ったプログラムでも遅くなります。もっと高速に求める方法があるので、興味のある方は拙作のページ Puzzle DE Programming: 「分割数」をお読みくださいませ。


有理数 (分数)

●仕様

●プログラムリスト

//
// ratio.js : 有理数 (分数)
//
// Copyright (c) 2025 Makoto Hiroi
//
// Released under the MIT license
// https://opensource.org/license/mit/
//

// 絶対値
function abs(n) { return n > 0n ? n : -n; }

// 最大公約数
function gcd(a, b) {
    while (b > 0n) [a, b] = [b, a % b];
    return a;
}

// 符号
function sign(n) {
    if (n == 0n) return 0;
    else if (n > 0n) return 1;
    else return -1;
}

// 正規化
function normalize(a, b) {
    let g = gcd(abs(a), abs(b));
    return (b < 0n) ? [(-a) / g, (-b) / g] : [a / g, b / g];
}

// 有理数の定義
class Ratio {
    constructor(a, b) {
        let [x, y] = normalize(a, b);
        this._numerator = x;
        this._denominator = y;
    }
    get numerator() { return this._numerator; }
    get denominator() { return this._denominator; }

    // 整数か?
    isInteger() {
        return this.denominator == 1n;
    }
    // BigInt に変換
    toBigInt() {
        return this.numerator / this.denominator;
    }
    // 数に変換
    toNumber() {
        return Number(this.numerator) / Number(this.denominator);
    }
    // 文字列
    toString() {
        if (this.denominator == 1n) {
            return this.numerator.toString();
        } else {
            return this.numerator.toString() + "/" + this.denominator.toString();
        }
    }
    // 算術演算
    add(other) {
        let {_numerator: anum, _denominator: aden} = this;
        let {_numerator: bnum, _denominator: bden} = other;
        return new Ratio(anum * bden + bnum * aden, aden * bden);
    }
    sub(other) {
        let {_numerator: anum, _denominator: aden} = this;
        let {_numerator: bnum, _denominator: bden} = other;
        return new Ratio(anum * bden - bnum * aden, aden * bden);
    }
    mul(other) {
        let {_numerator: anum, _denominator: aden} = this;
        let {_numerator: bnum, _denominator: bden} = other;
        return new Ratio(anum * bnum, aden * bden);
    }
    div(other) {
        let {_numerator: anum, _denominator: aden} = this;
        let {_numerator: bnum, _denominator: bden} = other;
        return new Ratio(anum * bden, aden * bnum);
    }
    inv() {
        let {_numerator: anum, _denominator: aden} = this;
        return new Ratio(aden, anum);
    }

    // 比較演算
    equals(other) {
        let {_numerator: anum, _denominator: aden} = this;
        let {_numerator: bnum, _denominator: bden} = other;
        return anum == bnum && aden == bden;
    }
    compare_ratio(other) {
        let {_numerator: anum, _denominator: aden} = this;
        let {_numerator: bnum, _denominator: bden} = other;
        return sign(anum * bden - bnum * aden);
    }
    eq(other){
        return this.compare_ratio(other) == 0;
    }
    lt(other){
        return this.compare_ratio(other) < 0;
    }
    le(other){
        return this.compare_ratio(other) <= 0;
    }
    gt(other){
        return this.compare_ratio(other) > 0;
    }
    ge(other){
        return this.compare_ratio(other) >= 0;
    }

    // 有理数 -> 循環小数 [[...],[...]]
    static repeat_decimal(rat) {
        let m = rat.numerator;
        let n = rat.denominator;
        let xs = [], ys = [];
        while (true) {
            let p = m / n, q = m % n;
            if (q == 0n) {
                ys.push(p);
                return [ys, [0n]];
            } else {
                let x = xs.indexOf(q);
                if (x >= 0) {
                    ys.push(p);
                    return [ys.slice(0, x+1), ys.slice(x+1)]
                }
            }
            xs.push(q);
            ys.push(p);
            m = q * 10n;
        }
    }
    // 循環小数 -> 有理数
    static from_repeat_decimal(xs, ys) {
        // 有限小数の部分を分数に直す
        let p0 = xs.reduce((a, x) => a * 10n + x, 0n);
        let q0 = 10n ** BigInt(xs.length - 1);
        // 循環節を分数に直す
        let p1 = ys.reduce((a, x) => a * 10n + x, 0n);
        let q1 = (10n ** BigInt(ys.length)) - 1n;
        // 有限小数 + 循環節
        return new Ratio(q1 * p0 + p1, q0 * q1);
    }
}

export default Ratio;

●簡単なテスト

//
// test_ratio.js : Ratio の簡単なテスト
//
// Copyright (c) 2025 Makoto Hiroi
//
// Released under the MIT license
// https://opensource.org/license/mit/
//
import Ratio from './ratio.js';

var a = new Ratio(1n, 2n);
var b = new Ratio(1n, 3n);
console.log('%s', a);          // 1/2
console.log('%s', b);          // 1/3

console.log('%s', a.add(b));   // 5/6
console.log('%s', a.sub(b));   // 1/6
console.log('%s', b.sub(a));   // -1/6
console.log('%s', a.mul(b));   // 1/6
console.log('%s', a.div(b));   // 3/2
console.log('%s', b.div(a));   // 2/3
console.log('%s', a.inv());    // 2

console.log(a.equals(b));                 // false
console.log(a.equals(new Ratio(1n, 2n))); // true
console.log(a == a);                      // true
console.log(a == new Ratio(1n, 2n));      // false

console.log(a.eq(b));  // false
console.log(a.eq(a));  // true
console.log(a.lt(b));  // false
console.log(a.gt(b));  // true
console.log(a.le(a));  // true
console.log(a.ge(a));  // true
console.log(b.le(a));  // true
console.log(b.ge(a));  // false

console.log(Ratio.repeat_decimal(new Ratio(1n, 7n)));  // [ 0n ], [ 1n, 4n, 2n, 8n, 5n, 7n ]
console.log(Ratio.repeat_decimal(new Ratio(1n, 8n)));  // [ 0n, 1n, 2n, 5n ], [ 0n ]
console.log(Ratio.repeat_decimal(new Ratio(1n, 13n))); // [ 0n ], [ 0n, 7n, 6n, 9n, 2n, 3n ]

console.log('%s', Ratio.from_repeat_decimal([ 0n ], [ 1n, 4n, 2n, 8n, 5n, 7n ])); // 1/7
console.log('%s', Ratio.from_repeat_decimal([ 0n, 1n, 2n, 5n ], [ 0n ]));         // 1/8
console.log('%s', Ratio.from_repeat_decimal([ 0n ], [ 0n, 7n, 6n, 9n, 2n, 3n ])); // 1/13

var c = new Ratio(355n, 113n);
console.log('%s', Ratio.from_repeat_decimal(...Ratio.repeat_decimal(c))); // 355/113
$ node test_ratio.js
1/2
1/3
5/6
1/6
-1/6
1/6
3/2
2/3
2
false
true
true
false
false
true
false
true
true
true
true
false
[ [ 0n ], [ 1n, 4n, 2n, 8n, 5n, 7n ] ]
[ [ 0n, 1n, 2n, 5n ], [ 0n ] ]
[ [ 0n ], [ 0n, 7n, 6n, 9n, 2n, 3n ] ]
1/7
1/8
1/13
355/113

Node.js の REPL でモジュール (module, ES2015) を読み込むときは、ダイナミックインポート (dynamic import, ES2020) を使います。

> const { default: Ratio } = await import("./ratio.js");
undefined
> var a = new Ratio(1n, 2n);
undefined
> var a = new Ratio(1n, 20n);
undefined
> var b = new Ratio(1n, 30n);
undefined
> console.log('%s', a.add(b))
1/12
undefined
> console.log('%s', a.sub(b))
1/60
undefined
> console.log('%s', b.sub(a))
-1/60
undefined
> console.log('%s', a.mul(b))
1/600
undefined
> console.log('%s', a.div(b))
3/2
undefined
> console.log('%s', b.div(a))
2/3
undefined
> console.log('%s', a.inv())
20
undefined
> Ratio.repeat_decimal(new Ratio(355n, 113n))
[
  [ 3n ],
  [
    1n, 4n, 1n, 5n, 9n, 2n, 9n, 2n, 0n, 3n, 5n, 3n,
    9n, 8n, 2n, 3n, 0n, 0n, 8n, 8n, 4n, 9n, 5n, 5n,
    7n, 5n, 2n, 2n, 1n, 2n, 3n, 8n, 9n, 3n, 8n, 0n,
    5n, 3n, 0n, 9n, 7n, 3n, 4n, 5n, 1n, 3n, 2n, 7n,
    4n, 3n, 3n, 6n, 2n, 8n, 3n, 1n, 8n, 5n, 8n, 4n,
    0n, 7n, 0n, 7n, 9n, 6n, 4n, 6n, 0n, 1n, 7n, 6n,
    9n, 9n, 1n, 1n, 5n, 0n, 4n, 4n, 2n, 4n, 7n, 7n,
    8n, 7n, 6n, 1n, 0n, 6n, 1n, 9n, 4n, 6n, 9n, 0n,
    2n, 6n, 5n, 4n,
    ... 12 more items
  ]
]

初版 2025 年 1 月 18 日