Skip to content

Instantly share code, notes, and snippets.

@sounisi5011
Last active December 20, 2023 04:04
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sounisi5011/a5039aedd1c378971d966fa55a61f473 to your computer and use it in GitHub Desktop.
Save sounisi5011/a5039aedd1c378971d966fa55a61f473 to your computer and use it in GitHub Desktop.
指数表記にしかならないようなJavaScriptの数値を整数表記や小数表記の文字列にする関数

例えばJavaScriptで「100000000000000000000000000000000」みたいなデカイ数値や「0.00000001」みたいな小さい数値を扱っていると、表示した時に1e+321e-8みたいな指数表記になってしまう。

console.log(100000000000000000000000000000000);
// 1e+32

console.log(0.00000001);
// 1e-8

そして恐るべきことに、この指数表記を少数表記に変換する関数が標準では存在しない。 文字列に型変換しても、指数表記になってしまう。

console.log(String(100000000000000000000000000000000));
// 1e+32

console.log(String(0.00000001));
// 1e-8

さらに、ユーザーランドの実装も意外と見つからない。ググっても思うものが出てこない。 なので、書きました:

/*
 * 巨大数の場合
 */

/* 普通に数値を表示すると指数表記になる */
console.log(100000000000000000000000000000000);
// 1e+32

/* 文字列に型変換した場合。この場合も指数表記になってしまう */
console.log(String(100000000000000000000000000000000));
// 1e+32

/* 今回作った関数`Num2FracStr`を使用した場合。ちゃんと整数表記になる */
console.log(Num2FracStr(100000000000000000000000000000000));
// 100000000000000000000000000000000

/* 数値を指数表記で指定した場合。もちろん整数表記になる */
console.log(Num2FracStr(1e+32));
// 100000000000000000000000000000000

/*
 * 微少数の場合
 */

/* 普通に数値を表示した場合。やっぱり指数表記になる */
console.log(0.00000001);
// 1e-8

/* 文字列に型変換しても解決しないのは上と同様 */
console.log(String(0.00000001));
// 1e-8

/* `Num2FracStr`を使用した場合。ちゃんと小数表記になる */
console.log(Num2FracStr(0.00000001));
// 0.00000001

/* この場合も、もちろん少数表記になる */
console.log(Num2FracStr(1e-8));
// 0.00000001

/*
 * 普通の数値を文字列化する用途にも使える。
 * Note: この用途で`Num2FracStr`を使うつもりなら、`String()`で直接型変換しよう。`String()`のほうが何百億倍もマシ。
 */

/* “生命、宇宙、そして万物についての究極の疑問の答え”(42)を変換すると、普通に文字列に変わる。 */
console.log(Num2FracStr(42));
// 42

/* 絶対零度を変換してみる。もちろん、普通に文字列化される。 */
console.log(Num2FracStr(-459.67));
// -459.67

/* NaNも数値の一種なので、変換できる */
console.log(Num2FracStr(NaN));
// NaN

/* 無限も当然変換できる */
console.log(Num2FracStr(Infinity));
// Infinity

/*
 * 数値だけではなく、文字列も指定できる。
 * ただし、文字列の場合は厳格な書式判定が行われるため、"NaN"や"Infinity"のような文字列は拒否される。
 */

/* さっきのデカイ数を文字列で指定してみる */
console.log(Num2FracStr("1e+32"));
// 100000000000000000000000000000000

/* さっきの小さい数を文字列で指定してみる */
console.log(Num2FracStr("1e-8"));
// 0.00000001

/* 普通の数値も指定可能。この場合はそのまま出力される */
console.log(Num2FracStr("18446744073709552000"));
// 18446744073709552000

/* 余計なゼロはちゃんと取り除かれる */
console.log(Num2FracStr("0000003.141592653589790000"));
// 3.14159265358979

/* 指数表記のゼロも取り除かれる */
console.log(Num2FracStr("00000002718281828459e-00012"));
// 2.718281828459

/* 文字列の場合、NaNは指定できない */
console.log(Num2FracStr("NaN"));
// Uncaught Error: Invalid Number: "NaN"

/* 同様に、Infinityも指定できない */
console.log(Num2FracStr("Infinity"));
// Uncaught Error: Invalid Number: "Infinity"

/* 漢数字も指定できない */
console.log(Num2FracStr("九十ニ"));
// Uncaught Error: Invalid Number: "九十ニ"

/* 全角数値を通すほど甘くはない */
console.log(Num2FracStr("2017"));
// Uncaught Error: Invalid Number: "2017"

/* 数値ですらないんだから当然アウト */
console.log(Num2FracStr("にっぽん"));
// Uncaught Error: Invalid Number: "にっぽん"

/*
 * 文字列処理なので、数値だと桁あふれで消えてしまうような大きい数すら表示できる。
 * ただし、文字列で指定しないとうまくいかない。
 */

/* 普通に数値を表示すると指数表記になる */
console.log(1.4142135623730950488016887242096980785696718753769480731766797379907324784621070388503875e-17);
// 1.4142135623730952e-17

/* 文字列に型変換してみてもやっぱりダメ */
console.log(String(1.4142135623730950488016887242096980785696718753769480731766797379907324784621070388503875e-17));
// 1.4142135623730952e-17

/* 数値で指定した場合。一応少数表記にはなるが、途中からケタが消滅してしまっている。上をよく見ると分かる通り、実は数値の時点で既にケタが消えている */
console.log(Num2FracStr(1.4142135623730950488016887242096980785696718753769480731766797379907324784621070388503875e-17));
// 0.000000000000000014142135623730952

/* 文字列で指定した場合。ちゃんとケタが消えずに少数表記になる */
console.log(Num2FracStr("1.4142135623730950488016887242096980785696718753769480731766797379907324784621070388503875e-17"));
// 0.000000000000000014142135623730950488016887242096980785696718753769480731766797379907324784621070388503875

文字列操作でゴリ押ししてる感じのやつなので、パフォーマンスは保証しません。 また、ECMAScript 2017で書いているため、最新のブラウザでないと動きません。 Google Chrome 62で動作確認はしていますが、他のブラウザでの動作は保証しません。

/**
* 数値を整数・少数表記に変換する。
* 内部的には、指数表記の文字列をパースし、小数表記に変換している。
*
* @param {number|string} number 変換したい数値、または数値形式の文字列。
* 数値型であればNaNやInfinityも指定できるが、そのまま文字列化して返される。
* @return {string} 小数表記の数値文字列
* @throws 適切な形式の数値、または文字列が与えられなかった場合に発生する。
*
* Note: この関数は、JavaScriptで正確な数値演算を行うために使う**べきではない**。
* この関数でなければ変換できない数値は、JavaScriptの内部データの時点で誤差が発生しており、正確な演算は期待できない。
* また、この関数によって変換された数値が厳密に正しい事も保証しない。
* この関数は、JavaScriptで生成した数値を「見やすく表示する」ためにのみ使用するべきである。
* Note: この関数の設計が正しければ(つまり、バグが無ければ)、エラーが発生するのは誤った形式の文字列を与えられた場合のみとなる。
* 逆に言えば、数値のプリミティブ型が与えられた場合は、いかなる場合でもエラーは発生しないはずである。
* もし、数値が与えられた場合にもエラーが発生してしまった場合は、この関数のバグを修正する必要がある。
*/
const Num2FracStr = number => {
/*
* 引数の値を文字列化
*/
const numStr = String(number);
/*
* 正規表現でマッチング
*/
const match = numStr.match(/^([+-]?)0*([1-9][0-9]*|)(?:\.([0-9]*[1-9]|)0*)?(?:[eE]([+-]?[0-9]+))?$/);
/*
* 引数の型が適切な形式ではない場合…
*/
if (!match) {
if (typeof number == "number") {
/*
* 引数の型が数値であれば、文字列化した値をそのまま返す
*/
return numStr;
} else {
/*
* 引数の型が数値でなければ、エラーにする
*/
throw new Error(`Invalid Number: "${numStr}"`);
}
}
/** @type {string} 数の符号 */
const sign = (match[1] === "-" ? "-" : "");
/** @type {string} 仮数部の整数部 */
const mantissa_int = match[2];
/** @type {string} 仮数部の少数部 */
const mantissa_frac = (match[3] ? match[3] : "");
/** @type {number} 指数部 */
const exponent = Number(match[4]);
let returnValue = "";
if (exponent) {
/*
* exponentがundefinedではなく(正規表現で指数部がマッチしていて)、
* かつ、0ではない場合、指数表記として処理を開始する
*
* Note: 指数部が0の場合、ここで処理する意味は無いので少数表記として処理する。
* よって、指数部が0以外の場合にここで処理する。
* Note: undefinedは数値化されるとNaNになり、false相当となる。
* 一方、0の場合もfalse相当となる。
* ので、↑の条件文はコレで合っている。
*/
/** @type {string} */
const mantissa_str = mantissa_int + mantissa_frac;
/** @type {number} */
const mantissa_len = mantissa_str.length;
if (0 < mantissa_len) {
/** @type {number} */
const mantissa_int_len = mantissa_int.length + exponent;
/*
12.145e+7 121450000 ; mantissa_str: "12145" mantissa_int_len: 9 ; 小数部が存在しない数値
12.145e+6 12145000 ; mantissa_str: "12145" mantissa_int_len: 8 ; 小数部が存在しない数値
12.145e+5 1214500 ; mantissa_str: "12145" mantissa_int_len: 7 ; 小数部が存在しない数値
12.145e+4 121450 ; mantissa_str: "12145" mantissa_int_len: 6 ; 小数部が存在しない数値
12.145e+3 12145 ; mantissa_str: "12145" mantissa_int_len: 5 ; 小数部が存在しない数値
12.145e+2 1214.5 ; mantissa_str: "12145" mantissa_int_len: 4 ; 小数部が存在し、かつ、1より大きい数値
12.145e+1 121.45 ; mantissa_str: "12145" mantissa_int_len: 3 ; 小数部が存在し、かつ、1より大きい数値
12.145e0 12.145 ; mantissa_str: "12145" mantissa_int_len: 2 ; 小数部が存在し、かつ、1より大きい数値
12.145e-1 1.2145 ; mantissa_str: "12145" mantissa_int_len: 1 ; 小数部が存在し、かつ、1より大きい数値
12.145e-2 0.12145 ; mantissa_str: "12145" mantissa_int_len: 0 ; 小数部が存在し、かつ、1未満の数値
12.145e-3 0.012145 ; mantissa_str: "12145" mantissa_int_len: -1 ; 小数部が存在し、かつ、1未満の数値
12.145e-4 0.0012145 ; mantissa_str: "12145" mantissa_int_len: -2 ; 小数部が存在し、かつ、1未満の数値
12.145e-5 0.00012145 ; mantissa_str: "12145" mantissa_int_len: -3 ; 小数部が存在し、かつ、1未満の数値
12.145e-6 0.000012145 ; mantissa_str: "12145" mantissa_int_len: -4 ; 小数部が存在し、かつ、1未満の数値
12.145e-7 0.0000012145 ; mantissa_str: "12145" mantissa_int_len: -5 ; 小数部が存在し、かつ、1未満の数値
*/
if (mantissa_len <= mantissa_int_len) {
/*
* 小数部が存在しない数値(ex: 0, 12, 176, 1214500)の場合の処理
*/
returnValue = mantissa_str.padEnd(mantissa_int_len, "0");
} else if (0 < mantissa_int_len) {
/*
* 小数部が存在し、かつ、1より大きい数値(ex: 1.26, 1.0009, 121.45)の場合の処理
*/
returnValue = mantissa_str.slice(0, mantissa_int_len) + "." + mantissa_str.slice(mantissa_int_len);
} else {
/*
* 小数部が存在し、かつ、1未満の数値(ex: 0.26, 0.20098, 0.0012145)の場合の処理
*/
returnValue = "0." + "0".repeat(-mantissa_int_len) + mantissa_str;
}
}
} else if (mantissa_frac) {
/*
* 少数表記の場合
*/
returnValue = (mantissa_int || "0") + "." + mantissa_frac;
} else if (mantissa_int) {
/*
* 整数表記の場合
*/
returnValue = mantissa_int;
}
return (returnValue) ? sign + (
returnValue
/* 先頭の余計なゼロを削除 */
.replace(/^(?:0(?!\.|$))+/, "")
/* 末尾の余計なゼロを削除 */
.replace(/(?:\.0+|(\.[0-9]*[1-9])0+)$/, "$1")
) : "0";
};
/*
* テストコード
* 何も表示されなければ問題なし
*/
{
const padZero = (...list) => {
return list
.reduce((list, item) => list.concat((String(item) !== "0") ? [item, `0${item}`, `${item}0`, `0${item}0`] : [item, `0${item}`]), []);
};
const createExponent = (...list) => {
return list
.reduce((list, item) => list.concat(padZero(item)), [])
.reduce((list, item) => list.concat(`e${item}`, `E${item}`, `e+${item}`, `E+${item}`, `e-${item}`, `E-${item}`), []);
};
const testNumberSet = new Set;
for (const sign of ["", "+", "-"]) {
for (const mantissa_int of padZero(12, 1, 0)) {
for (const mantissa_frac of ["", ...padZero(145, 1, 0).map(f => `.${f}`)]) {
for (const exponent of ["", ...createExponent(...Array(10).keys())]) {
testNumberSet.add(sign + mantissa_int + mantissa_frac + exponent);
}
}
}
}
const table = [];
for (const testNumber of testNumberSet) {
const numStr = String(Number(testNumber));
const fracStr = Num2FracStr(testNumber);
const eq = numStr === fracStr;
if (!/e/i.test(numStr))
if (!eq)
table.push({
testNumber,
"String(Number(testNumber))": numStr,
"Num2FracStr(testNumber)": fracStr,
"String(Number(testNumber)) === Num2FracStr(testNumber)": eq,
});
}
console.assert(table.length === 0, `${table.length} test failed`);
if (table.length !== 0 ) {
console.groupCollapsed("Test result");
console.table(table);
console.groupEnd();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment