JavaScriptで有理数・複素数(ガウス有理数)を扱うための備忘録

> この記事は、[「Qiita全国学生対抗戦 Advent Calendar 2024」](https://qiita.com/advent-calendar/2024/student) シリーズ3 12月3日の記事です。

[綾坂こと](https://x.com/AXT_AyaKoto)です。趣味でJavaScript狂人をやっている大学1年生です。
JavaScriptで3次方程式を解かなきゃいけなくなったので、複素数を扱うためのコードを書きました。
この記事はその実装で詰まったところとその解決方法の備忘録です。

完成品は"Zahlen.js"という名前で公開しています。実装だるいな〜って人は使ってください。

https://github.com/AXT-AyaKoto/Zahlen.js

:::note warn
- 数学ⅡB以上未履修人間が雰囲気でやってます
    - 数学的に間違ってることをやってたら教えて下さい、とくに複素数の扱い
- 自然数を$\mathbb{N}$、非負整数を$\mathbb{Z}_{\ge0}$、整数を$\mathbb{Z}$、有理数を$\mathbb{Q}$、ガウス有理数を$\mathbb{Q}\mathrm{(i)}$で表すことがあります
    - ガウス有理数は実部と虚部がともに有理数な複素数
- 「ガウス有理数」のことを「複素数」って言ったりします
    - めんどいので
:::

---

## クラスの作成

まあ$\mathbb{Z}\in\mathbb{Q}\in\mathbb{Q}\mathrm{(i)}$だと思うのでガウス有理数型を作ってそれを継承すればいいでしょう

- 実部分母・実部分子・虚部分母・虚部分子の4つのプロパティを持つガウス有理数クラスを作ります
    - 各プロパティはbigintで持っておきましょう、numberだと絶対値$2^{53}$以上で正確性が吹っ飛びます
    - 実部と虚部はそれぞれ既約分数に揃えておきます
    - 符号は分子側に揃えておくことにします
- 継承は[`super()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/super)を使います

既約分数に揃えるためには分母と分子を最大公約数で割ればいいです。最大公約数を求める方法は[ユークリッドの互除法](https://w.wiki/4sVj)。

```js:ユークリッドの互除法の実装方針
const gcd = (a, b) => {
    if (b === 0n) return a; // bが0ならaを返せばOK
    if (a < b) return gcd(b, a); // a ≧ bにそろえておく
    return gcd(b, a % b); // bとaをbで割ったあまりをgcd()自身にわたす
}
```

JavaScript標準のMathはbigint非対応のため、`abs()`と`sign()`を自力で作っておく必要があります。
まあ比較演算子でやるだけなので説明する必要はないでしょう

## number・stringへの変換

numberへの変換は[`.valueOf()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/valueOf)、stringへの変換は[`.toString()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/toString)を実装すればあとはいい感じにやってくれます

```js:numberへの変換はvalueOf()でできる
const Example = class Example {
    constructor(a, b) {
        this.a = a;
        this.b = b;
    }
    valueOf() {
        return this.a / this.b;
    }
}
console.log(Number(new Example(3, 2))); // Expected Log Output : <number> 1.5
```

## numberから有理数への変換

JavaScriptのnumberはIEEE 754 倍精度浮動小数点数なので、それをそのまま変換してあげるのがいいでしょう。
浮動小数点数がどういう形式で表しているかは[Wikipedia](https://w.wiki/CC8t)みるのが早いです

- numberを内部表現のビット列に変換するのは[DataView](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/DataView)を使うのが楽っぽそうです
    - [https://scrapbox.io/AyaExpTech/JavaScriptのnumberを内部表現のビット列に変換する.js](https://scrapbox.io/AyaExpTech/JavaScriptのnumberを内部表現のビット列に変換する.js)
- まああとはやるだけといってもいいと思います
    - [https://scrapbox.io/AyaExpTech/IEEE754倍精度浮動小数点数のビット列を分数に変換する.js](https://scrapbox.io/AyaExpTech/IEEE754倍精度浮動小数点数のビット列を分数に変換する.js)

## 数学関数の実装

ECMAScript標準のnumberは`Math`でいろんな計算ができるので、そこにあるやつはなるべく実装したいですね

### 数学定数

`number`を有理数に変換できるので`Math`の定数を借りれますね！やったーー！

### 端数処理

$\mathbb{Q}\mathrm{(i)}$をどうするか迷いましたが、実部と虚部に対してそれぞれ端数処理をすることにしました。
実部と虚部をそれぞれ有理数で表せば有理数範囲の実装をするだけでOK。

有理数範囲の実装は`trunc()`をとっかかりにするのが楽でしょう、bigintに対して`/`を使うと`trunc()`と同じ挙動になるので。
`trunc()`ができればあとは0と正と負で場合分けしたらいけます

| 関数 | 0の場合 | 正の場合 | 負の場合 |
|:----:|:----:|:----:|:----:|
| `ceil(x)`  | 0 | $\textrm{trunc}(x) + 1$ | $\textrm{trunc}(x)$ |
| `floor(x)` | 0 | $\textrm{trunc}(x)$ | $\textrm{trunc}(x) - 1$ |
| `round(x)` | 0 | $\lfloor{x+0.5}\rfloor$ | $\lceil{x-0.5}\rceil$ |

### 絶対値・符号

bigintの`abs()`と`sign()`を実装したはずなので、まあやるだけですね
クラス作成のタイミングで「符号は分子側に揃えておくことにする」といったのはここを楽にするためでもあります

複素数範囲の`abs()`は三平方の定理を使う必要があります
どうせあとで有理数の`hypot()`を実装すると思うのでそれの存在を仮定するのが楽でしょう

### 四則演算+α

`add()`, `sub()`, `mul()`, `div()`は[KIT数学ナビゲーション](https://w3e.kanazawa-it.ac.jp/math/category/fukusosuu/henkan-tex.cgi?target=/math/category/fukusosuu/fukusosuu-no-sisokuenzan.html&pcview=2)をガン見してそのまま実装します、ありがとう金沢工業大学。
だからといって有理数や整数を↑でやるのは"鶏を割くに焉んぞ牛刀を用いん"案件なのでそれは個別実装。こっちは義務教育範囲のはずです

有理数(実数)範囲の`mod()`は正負で場合分けするのが楽だと思います

- 任意の実数x, yについて$x \textrm{ mod } y$は$x$が正かどうかで分岐させれば以下のようにできます
    - $x - y \times \textrm{trunc}(\frac{x}{y}) (x\ge{0})$
    - $\textrm{sign}(x)\times(|x| \textrm{ mod } |y|) (x\lt{0})$

複素数範囲の`mod()`ってなんだよと思いましたがmathlogの記事でmodの拡張をやっている方を見つけたのでそれを参考にしました。
[mathlog『mod演算を複素数に拡張する』(by おれの名は。)](https://mathlog.info/articles/ImSGjYNIrCfld3cYDo0I)
$\textrm{mod}(x,m)=\frac{m}{2\pi i}\log(e^{\frac{2\pi }{m}ix})$らしいです、なんか色々未実装なものを使っていますがあとでどうにかなります

### 比較演算

JavaScriptは演算子オーバーロードができないので比較演算子も関数で実装する必要があります。
`eq()`($=$), `ne()`($\ne$), `ge()`($\ge$), `gt()`($\gt$), `le()`($\le$), `lt()`($\lt$)の6つを実装すべき。

`eq()`は「実部分母・実部分子・虚部分母・虚部分子」の4プロパティをそれぞれ比較すればOKです。
~~それだけで済ませることができるようにするための既約分数にする処理~~
`ne()`は`eq()`の否定。

不等号系をどうするかですが、複素数範囲は絶対値で比較することにして`Zahlen.Q`範囲のみ考えます。
`lt()`は$\frac{\texttt{x分子}}{\texttt{x分母}}\lt \frac{\texttt{y分子}}{\texttt{y分母}} \iff \texttt{x分子} \times \texttt{y分母} \lt \texttt{y分子} \times \texttt{x分母}$になるはずです。
残り3つは適当に実装できます。「`ge()`は`lt()`の否定、`gt()`は`ge() || eq()`、`le()`は`lt() || eq()`」など。

> ["複素数全体に通常の大小関係を入れることはできない"](https://ja.wikipedia.org/wiki/%E8%A4%87%E7%B4%A0%E6%95%B0)とのことなので不等号な4つは複素数を弾くようにしたほうがいいのかも。エラー出すならなんかは返したほうがいいかな〜と思って無理やり比較させましたが

最大・最小は一つずつ比較すればいいでしょう(サボり)

### 指数・対数・三角・逆三角・双曲線・逆双曲線

有理数範囲はnumberに変換してMathを借りましょう、めんどいので
強いて言及するなら「`atan2`もあとで使うので実装しましょう」くらいだと思います

以降、複素数範囲に対応することだけ考えます
Wikipediaをガン見したらそこに答えが載ってました、ありがとうWikipedia
~~(自力で計算しようとして4日無駄にしたのは内緒)~~

- [複素指数関数](https://w.wiki/CGAW)は $e^z = \exp(z) = \exp(a+bi) = e^a(\cos b + i \sin b)$ です
    - $e^a$ も $\sin b$ も $\cos b$ も 実数範囲なので実装を仮定していいです
- [複素対数関数](https://ja.wikipedia.org/wiki/複素対数函数#対数の主値)は $\textrm{Log}z = \log_e|z|+i\textrm{Arg}z$です
    - 後述する「偏角の主値Arg(`arg()`)」の存在を仮定します
    - `log2()`と`log10()`は底の変換公式でいいと思います
- [複素三角関数](https://ja.wikipedia.org/wiki/%E4%B8%89%E8%A7%92%E9%96%A2%E6%95%B0#%E8%A4%87%E7%B4%A0%E9%96%A2%E6%95%B0%E3%81%A8%E3%81%97%E3%81%A6)は $\sin{z}=\frac{e^{iz}-e^{-iz}}{2i}$, $\cos{z}=\frac{e^{iz}+e^{-iz}}{2i}$です
    - $\tan{z}$ は $\frac{\sin{z}}{\cos{z}}$ でいいです
- [複素逆三角関数](https://ja.wikipedia.org/wiki/%E9%80%86%E4%B8%89%E8%A7%92%E9%96%A2%E6%95%B0#%E5%AF%BE%E6%95%B0%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9F%E5%BD%A2)は $\textrm{asin}x = -i \log_e{(ix+\sqrt{1-x^2})}$, $\textrm{acos}x = \frac{\pi}{2}-\textrm{asin}x$, $\textrm{atan}x = \frac{i}{2}[\log(1-ix) - \log(1+ix)]$です
- [複素双曲線関数](https://ja.wikipedia.org/wiki/%E5%8F%8C%E6%9B%B2%E7%B7%9A%E9%96%A2%E6%95%B0#%E5%AE%9A%E7%BE%A9)は$\sinh{z}=\frac{e^{z}-e^{-z}}{2i}$, $\cosh{z}=\frac{e^{z}+e^{-z}}{2i}$です
    - $\tanh{z}$ は $\frac{\sinh{z}}{\cosh{z}}$ でいいです
    - 実数範囲の定義がそのまま複素数範囲でも使える
- [複素逆双曲線関数](https://ja.wikipedia.org/wiki/%E9%80%86%E5%8F%8C%E6%9B%B2%E7%B7%9A%E9%96%A2%E6%95%B0#%E5%AF%BE%E6%95%B0%E8%A1%A8%E7%8F%BE)は $\textrm{asinh}z = \log_e{(z+\sqrt{z^2+1})}$, $\textrm{acosh}z = \log_e{(z+\sqrt{z+1}\sqrt{z-1})}$, $\textrm{atanh}z = \frac{1}{2}\log_e{\frac{1+z}{1-z}}$です
    - めっちゃきれい

### 冪乗・冪根

`pow()`の存在を仮定すれば、`sqrt()`・`cbrt()`・`hypot()`はやるだけになりますね！

`pow()`は $\textrm{pv }z^a = e^{(a \textrm{Log}z)}$で複素数範囲まで行けるらしいです。
……が、簡単に実装できるところは個別で実装してあげたほうが速いと思います。
$x^{-a} = \frac{1}{x^a}$、$(\frac{n}{d})^a = \frac{n^a}{d^a}$を使えば「有理数の整数乗」までは容易に実装可能なはず。

### 複素数関連

Pythonのcomplex型を参考にいくつかメソッドを追加しておくといいかな〜と思ったのでそれもやりました

- ラジアン ←→ 角度
    - [前やった](https://scrapbox.io/AyaExpTech/ラジアン(弧度)と角度の相互変換.js)
- 偏角
    - [Wikipedia(ja)『複素数の偏角』](https://ja.wikipedia.org/wiki/%E8%A4%87%E7%B4%A0%E6%95%B0%E3%81%AE%E5%81%8F%E8%A7%92)を見る
    - 偏角の主値は`atan2(imag, real)`でOK
        - `atan2`の範囲は $\pi \lt \textrm{atan2}(y, x) \le \pi$ で、`arg`の主値の範囲と同じ
    - `arg`と`phase`の両方で使えるようにしておくといいかも
- 複素数平面 ←→ 極形式表現
    - [Pythonの`cmath.polar()`](https://docs.python.org/ja/3/library/cmath.html#cmath.polar)と[同`rect()`](https://docs.python.org/ja/3/library/cmath.html#cmath.rect)に相当するものをつくると便利かも
    - polar(x) は (abs(x), phase(x)) に等しいです ← はい
    - rect(r, phi) は complex(r * math.cos(phi), r * math.sin(phi)) に等しいです ← はい
        - 私は名前を`orthogonal()`にしました

---

以上備忘録でした。
再掲になりますが書いたコードは公開してるので実装だるいな〜って人は使ってください。

https://github.com/AXT-AyaKoto/Zahlen.js