【JavaScript】小数値を分数(分子と分母)に変換する方法

更新日:2023/07/28

小数を含む数値を分数(分子と分母)に変換するJavaScriptコードを作成したので紹介します。

 

完成したコード

まずは今回作成したコードを掲載します。

// 小数値を分数(分子と分母)に変換する関数
const decimalToFraction = (number,significant_digits=6) =>{
    const digits = getdecimalPrecision( number , significant_digits );
    if( digits === false ) return false;

    const sign = Math.sign(number) < 0 ? ["-",-1] : ["",1];

    let denominator = 10 ** digits; // 分母
    let numerator = Math.abs( Math.round(number * denominator) ) ; // 分子

    if( denominator !== 1 ){
            // 約分
        let gcd;
        while( (gcd = greatestCommonDivisor2( denominator , numerator )) !== 1 ){

            denominator /= gcd;
            numerator /= gcd;
        }
    }
    return { sign,denominator , numerator };
}
// 最大公約数を求める https://note.affi-sapo-sv.com/js-greatest-common-divisor.php
const greatestCommonDivisor2 = ( value1 , value2 )=>{
    let r , a = value1 , b = value2;

    do{
        r = a % b;
        a = b;b = r;
    }while( r !== 0 );

    return a;
};
// 小数部の桁数を求める https://note.affi-sapo-sv.com/js-get-fractional-digits.php
const getdecimalPrecision = (number,significant_digits=6) =>{
            // 数値文字列かどうかのチェック
    if( typeof number !== "number" ) return false;

    const f = number.toFixed(significant_digits).split(".")[1];
    if( f === undefined || f.length === 0) return 0;

        // 後方の"0"を取り除いた桁数取得
    return f.length - ["1",...f].reverse().findIndex( e=>e!="0");

}

コードは3つの関数を定義しています。
最初の decimalToFraction() が今回作成したコードで、小数を分数に変換する関数です。
残りの関数は、他のページで紹介しているものをコピペしたものです。

decimalToFraction()は二つの引数を受け付けます。
一つ目の引数は変換元の数値です。
型は数値型のみです。

二つ目の引数は、小数点以下の許容桁数です。
これは省略可能で、規定値は6です。

関数を実行すると、次のオブジェクトを返します。

{ 
 sign : [ 符号文字列 , -1 または 1 ] の配列 
 denominator : 分母
 numerator : 分子
}

実際に使ってみます。

[1.5 , 2.3 , 123.456 , 0.065 , -0.333].forEach(e=>{
    const r = decimalToFraction(e);

    console.log( 
        `${e} => ${r.sign[0]} ${r.numerator } / ${ r.denominator} => ${ r.sign[1] * r.numerator / r.denominator }`
        );
});
// 結果:
// 1.5 =>  3 / 2 => 1.5
// 2.3 =>  23 / 10 => 2.3
// 123.456 =>  15432 / 125 => 123.456
// 0.065 =>  13 / 200 => 0.065
// -0.333 => - 333 / 1000 => -0.333

関数の結果を検証するため、分子を分母で割って検算をしています。
結果を見ると、上手く変換できていますね。

 

解説

数値から分数への変換は、次の手順でおこないます。

  1. 数値の小数以下の桁数を求める
  2. 10を(1)で求めた値で累乗する
  3. 分子 を 数値 × (2)の値 、 分母 を (2)の値とする
  4. 分子と分母の最大公約数を求める
  5. 公倍数が1なら終了
  6. 分子と分母を公倍数で割る
  7. (4)へ

例えば、1.45なら次のような流れになります。

1.45 → 145 /100(最大公約数5) → 29 / 20(最大公約数1)

分子が29、分母が10ですね。

手順はループしていますが、最大公約数の意味的には1回で終わる(はず)です。
念のため、ループしています。

この流れでキーになるのが、小数以下の桁数と最大公約数の算出です。

これらは他のページで解説しています。

確認してみてくださいね。

 

入力値が数値文字列の場合

今回作成した関数は数値型のみ受け付けています。
そのため数値文字列を扱うときは、数値型に変換する必要があります。

しかし小数を含む数値文字列を数値型に変換するとき、誤差が出る可能性があります。
(※数値リテラルを数値型に変換する際にも同じ誤差がでています)
そこで、数値文字列の時点で整数表記にしてから数値型に変換したほうが誤差が少なくなります。

const decimalStringToFraction = (numberString) =>{

    const digits = getStringFractionalDigits( numberString );
    if( digits === false ) return false;

        // 文字列の時点で整数化して数値化
    const number = Number( numberString.trim().replace( "." , "" ) );

    const sign = Math.sign(number) < 0 ? ["-",-1] : ["",1];

    let denominator = 10 ** digits; // 分母
    let numerator = Math.abs( number ) ; // 分子

    if( denominator !== 1 ){
            // 約分
        let gcd;
        while( (gcd = greatestCommonDivisor2( denominator , numerator )) !== 1 ){

            denominator /= gcd;
            numerator /= gcd;
        }
    }
    return { sign,denominator , numerator };
}
// 数値文字列の小数部の桁数を求める https://note.affi-sapo-sv.com/js-get-fractional-digits.php
const getStringFractionalDigits = numberString =>{
        // 数値文字列かどうかのチェック
    if( typeof numberString !== "string" || isNaN( Number(numberString) )) return false;

        // 小数部を取得
    const f = numberString.trim().split(".")[1];
    if( f === undefined || f.length === 0) return 0;
    
        // 後方の"0"を取り除いた桁数取得
    return f.length - ["1",...f].reverse().findIndex( e=>e!="0");
}

前項と同じ値の数値文字列で確認してみます。

["1.5" , "2.3" , "123.456" , "0.065" , "-0.333"].forEach(e=>{
    const r = decimalStringToFraction(e);

    console.log( 
        `${e} => ${r.sign[0]} ${r.numerator } / ${ r.denominator} => ${ r.sign[1] * r.numerator / r.denominator }`
        );
});
// 結果:
// 1.5 =>  3 / 2 => 1.5
// 2.3 =>  23 / 10 => 2.3
// 123.456 =>  15432 / 125 => 123.456
// 0.065 =>  13 / 200 => 0.065
// -0.333 => - 333 / 1000 => -0.333

結果が同じになりましたね。
今度は、差が出そうな値で比較してみます。

const e = "1000000000000.001";
let r = decimalToFraction( Number(e) );  // 数値型に変換して実行
console.log( 
        `${e} => ${r.sign[0]} ${r.numerator } / ${ r.denominator} => ${ r.sign[1] * r.numerator / r.denominator }`
        );
 // 結果: 1000000000000.001 =>  15625000000000016 / 15625 => 1000000000000.001

r = decimalStringToFraction( e );  // 数値文字列のまま実行
console.log( 
        `${e} => ${r.sign[0]} ${r.numerator } / ${ r.denominator} => ${ r.sign[1] * r.numerator / r.denominator }`
        );
 // 結果: 1000000000000.001 =>  1000000000000001 / 1000 => 1000000000000.001

数値型に変換してから分数化すると、少しおかしな結果が出ました。
これは、1000000000000.001を整数化するために1000を掛けたとき、誤差がでているのが原因です。
数値文字列は、この誤差を排除できたので想定した結果を得ることができました。

console.log( 1000000000000.001 * 1000 );  // 数値を整数化
 // 結果: 10000000000000002 ← 誤差が出た
console.log( Number( "1000000000000001") ); // 整数文字列を数値化
 // 結果: 10000000000000001 ← 誤差が出ない

ただし、これ以上桁が大きくなると整数文字列でも誤差が出ます。

console.log( Number( "10000000000000001" ) )
 // 結果: 10000000000000000 ←  誤差が出た

何らかの制限が必要かもしれないですね。

更新日:2023/07/28

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。