JavaScriptで有理数を扱いたい.js
モチベーション
Q:なぜ金融系では未だにCOBOLが使われるんですか?
A:お手元にExcelがありましたら任意のセルに「=4.8-4.7-0.1」って入れてみてください。
まあCOBOLだから……とかそういうわけではないんですが、正確に扱いたいときはあるよね
じゃあ我らがJavaScriptだと数値がどう扱われるかというと
JavaScript の数値 (Number) 型は IEEE 754 の倍精度 64ビットバイナリー形式であり、 Java や C# の double のようなものです。
すべての数値がdoubleで扱われるので、同じような誤差が出ます
一応BigIntもありますが、整数しか扱えないので小数の計算はできませんね……
ということで、有理数型をつくりましょうかました
使い方
インスタンスの生成
code:example.js
new RationalNumber(3, 5); // 0.6
new RationalNumber(2, 10); // 既約分数じゃなくてもOK
new RationalNumber(2, 0); // ERROR 0除算はエラー code:example.js
new RationalNumber(3, 5).valueOf() // Expected Eval Value : <Number> 0.6
new RationalNumber(-2, 16).toString() // Expected Eval Value : <String> "-1/8"
前置記法(ポーランド記法)
code:example.js
RationalNumber.Add(new RationalNumber(3, 5), new RationalNumber(1, 4)).toString() // Expected Eval Value : <String> "17/20"
RationalNumber.Sub(new RationalNumber(3, 5), new RationalNumber(1, 4)).toString() // Expected Eval Value : <String> "7/20"
RationalNumber.Mul(new RationalNumber(3, 5), new RationalNumber(1, 4)).toString() // Expected Eval Value : <String> "3/20"
RationalNumber.Div(new RationalNumber(3, 5), new RationalNumber(1, 4)).toString() // Expected Eval Value : <String> "12/5"
中置記法
code:example.js
new RationalNumber(3, 5).plus(new RationalNumber(1, 4)).toString() // Expected Eval Value : <String> "17/20"
new RationalNumber(3, 5).minus(new RationalNumber(1, 4)).toString() // Expected Eval Value : <String> "7/20"
new RationalNumber(3, 5).times(new RationalNumber(1, 4)).toString() // Expected Eval Value : <String> "3/20"
new RationalNumber(3, 5).div(new RationalNumber(1, 4)).toString() // Expected Eval Value : <String> "12/5"
コード
code:script.js
globalThis.RationalNumber = class RationalNumber {
/* ======== constructor ======== */
/**
* 有理数を生成する
* @param {!(number|bigint)} top - 分子
* @param {!(number|bigint)} bottom - 分母(≠0)
*/
constructor(top, bottom) {
/** @type {bigint} - 分子の入力をBigIntにしたもの */
const topBigInt = BigInt(top);
/** @type {bigint} - 分母の入力をBigIntにしたもの */
const bottomBigInt = BigInt(bottom);
/** もし分母が0ならエラーを投げる */
if (bottomBigInt === 0n) throw new Error('AXT RationalNumber not allowed to divide by 0'); /** @type {bigint} - 符号 */
const sign = BigInt(this.#Math.sign(topBigInt) * this.#Math.sign(bottomBigInt));
/** @type {bigint} - 分子の絶対値 */
const topBigIntAbs = this.#Math.abs(topBigInt);
/** @type {bigint} - 分母の絶対値 */
const bottomBigIntAbs = this.#Math.abs(bottomBigInt);
/** @type {bigint} - 最大公約数 */
const gcd = this.#getGCD(topBigIntAbs, bottomBigIntAbs);
/** @type {bigint} - 約分後の分子 */
const topAfterReduce = topBigIntAbs / gcd;
/** @type {bigint} - 約分後の分母 */
const bottomAfterReduce = bottomBigIntAbs / gcd;
/** @description - インスタンスのプロパティを設定 */
/** @type {bigint} - 分母 */
this.top = topAfterReduce;
/** @type {bigint} - 分子 */
this.bottom = bottomAfterReduce;
/** @type {bigint} - 符号 */
this.sign = sign;
}
/* ======== Private Class Field ======== */
/**
* 既約分数にするために、2数の最大公約数を求める
* @param {bigint} a - 最大公約数を求めたい数
* @param {bigint} b - 最大公約数を求めたい数
* @returns {bigint} 最大公約数 (ただし、bが0の場合はa)
*/
if (b === 0n) return a;
const r = a % b;
return r === 0n ? b : this.#getGCD(b, r);
}
/**
* NumberだったらMath.XXXX()で使える各種関数のうち、RationalNumberで使うもののBigInt移植実装
* @type {Object}
*/
/**
* BigIntの絶対値を求める
* @param {bigint} bigInt - 絶対値を求めたいBigInt
* @returns {bigint} 絶対値
*/
abs: function (bigInt) {
return bigInt < 0n ? -bigInt : bigInt;
},
/**
* BigIntの符号を求める
* @param {bigint} bigInt - 符号を求めたいBigInt
* @returns {bigint} 符号 (1n: 正, -1n: 負, 0n: 0)
*/
sign: function (bigInt) {
return bigInt === 0n ? 0n : (bigInt < 0n) ? -1n : 1n;
},
};
/* ======== 各種Primitiveへの変換 ======== */
/**
* 文字列に変換する
* @returns {string} 有理数を表す文字列
*/
toString() {
return ${this.sign < 0n ? '-' : ''}${this.top}${this.bottom !== 1n ? /${this.bottom} : ''};
}
/**
* 数値に変換する
* @returns {number} 有理数を浮動小数点数に変換したもの
*/
valueOf() {
return Number(this.sign) * Number(this.top) / Number(this.bottom);
}
/* ======== 前置記法(ポーランド記法)による演算 (staticメソッド) ======== */
/**
* 有理数の加算
* @param {RationalNumber} a - 加算する有理数
* @param {RationalNumber} b - 加算する有理数
* @returns {RationalNumber} 加算結果
*/
static Add(a, b) {
return new RationalNumber(a.top * b.bottom * a.sign + b.top * a.bottom * b.sign, a.bottom * b.bottom);
}
/**
* 有理数の減算
* @param {RationalNumber} a - 減算される有理数
* @param {RationalNumber} b - 減算する有理数
* @returns {RationalNumber} 減算結果
*/
static Sub(a, b) {
return this.Add(a, new RationalNumber(-b.top, b.bottom));
}
/**
* 有理数の乗算
* @param {RationalNumber} a - 乗算する有理数
* @param {RationalNumber} b - 乗算する有理数
* @returns {RationalNumber} 乗算結果
*/
static Mul(a, b) {
return new RationalNumber(a.top * b.top * a.sign * b.sign, a.bottom * b.bottom);
}
/**
* 有理数の除算
* @param {RationalNumber} a - 除算される有理数
* @param {RationalNumber} b - 除算する有理数
* @returns {RationalNumber} 除算結果
*/
static Div(a, b) {
return this.Mul(a, new RationalNumber(b.bottom, b.top));
}
/* ======== 中置記法による演算 (プロトタイプメソッドチェーン) ======== */
/**
* 有理数の加算
* @param {RationalNumber} b - 加算する有理数
* @returns {RationalNumber} 加算結果
*/
plus(b) {
return RationalNumber.Add(this, b);
}
/**
* 有理数の減算
* @param {RationalNumber} b - 減算する有理数
* @returns {RationalNumber} 減算結果
*/
minus(b) {
return RationalNumber.Sub(this, b);
}
/**
* 有理数の乗算
* @param {RationalNumber} b - 乗算する有理数
* @returns {RationalNumber} 乗算結果
*/
times(b) {
return RationalNumber.Mul(this, b);
}
/**
* 有理数の除算
* @param {RationalNumber} b - 除算する有理数
* @returns {RationalNumber} 除算結果
*/
div(b) {
return RationalNumber.Div(this, b);
}
}