0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

浮動小数点データから短い10進数表記を求める

Last updated at Posted at 2025-01-28

コンピューターで小数点以下を扱う数値計算は、CPU に演算機能が搭載されている IEEE 754 浮動小数点がよく使われています。ここでは様々な開発言語で使用されている IEEE 754 倍精度浮動小数点型(F64と略記)のうち、正の数を前提とします。

算出した F64 データの全精度で表記するとき、16進浮動小数点表記ができる言語は長くても20数文字です。16進表記できない言語では10進数に頼ることになります。すると、2進数での小数点以下の桁数が10進数の桁数になるので小さい値ほど長くなります。非正規化数の正の最小値 $2^{-1074}$ を厳密値にすると下1074桁になります。

全桁(下1074桁)
0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004940656458412465441765687928682213723650598026143247644255856825006755072702087518652998363616359923797965646954457177309266567103559397963987747960107818781263007131903114045278458171678489821036887186360569987307230500063874091535649843873124733972731696151400317153853980741262385655911710266585566867681870395603106249319452715914924553293054565444011274801297099995419319894090804165633245247571478690147267801593552386115501348035264934720193790268107107491703332226844753335720832431936092382893458368060106011506169809753078342277318329247904982524730776375927247874656084778203734469699533647017972677717585125660551199131504891101451037862738167250955837389733598993664809941164205702637090279242767544565229087538682506419718265533447265625
指数表現形式(下750桁)
4.940656458412465441765687928682213723650598026143247644255856825006755072702087518652998363616359923797965646954457177309266567103559397963987747960107818781263007131903114045278458171678489821036887186360569987307230500063874091535649843873124733972731696151400317153853980741262385655911710266585566867681870395603106249319452715914924553293054565444011274801297099995419319894090804165633245247571478690147267801593552386115501348035264934720193790268107107491703332226844753335720832431936092382893458368060106011506169809753078342277318329247904982524730776375927247874656084778203734469699533647017972677717585125660551199131504891101451037862738167250955837389733598993664809941164205702637090279242767544565229087538682506419718265533447265625e-324

整数と小数で状況が異なる

小数点以下を含む10進のを F64 に変換したとき、10進数の整数部は2進数に変換しても余程大きな数でない限り同じ整数値ですが、10進小数は2進数表現ができないものが多数あるため一対一対応になりません。

整数部の変換

整数部を2進数から10進数への変換の桁数比は概ね $10:3$ で、逆変換も同様です。

2進数 10進数
$11111\ 00111$ $999$
$11111\ 11111$ $1,023$
$11110\ 10000\ 10001\ 11111$ $999,999$
$11111\ 11111\ 11111\ 11111$ $1,048,575$
$11101\ 11001\ 10101\ 10010\ 01111\ 11111$ $999,999,999$
$11111\ 11111\ 11111\ 11111\ 11111\ 11111$ $1,073,741,823$
... ...
\begin{eqnarray}
\log_{10}{2} & = & 0.30102999566398 \cdots \\
\log_{2}{10} & = & 3.32192809488736 \cdots \\
\end{eqnarray}

小数部の変換

2進数から10進数への変換

小数点部の変換は整数と異なり、2進数から10進数への変換すると桁数比は $1:1$ です。

2進数 10進数
$0.1$ $0.5$
$0.01$ $0.25$
$0.001$ $0.125$
$0.0001$ $0.0625$
$0.00001$ $0.03125$
... ...

小数点以下の桁数を $n$ として、小数部の2進数値を $N = 0.\ b_1 b_2 b_3 \cdots b_{n-1}\ 1_n$ から整数 $B$

B = 2^n \ N = \left( b_1 b_2 b_3 \cdots b_{n-1}\ 1_n \right)_{(2)}

とします。式を弄ると

N = \frac{ B }{ 2^n } = \frac{ B }{ 2^n } \frac{ 5^n }{ 5^n } = \frac{ D = B \times 5^n }{ 10^n }

$B$ が2進数表現、$D$ が10進数表現に対応していて、分母の指数から同じ桁数であることが分かります。さらに $D$ は $5^n$ の倍数になります。

10進数から2進数への変換

小数点を除いた10進表記を $D$、小数点以下の桁数を $n$ とすると

N = \frac{ D }{ 10^n }

の演算が実行されるでしょう。結果、10進数から2進数への変換では循環小数になるものが多発します。例えば10進数の $0.2$ を2進数にすると

0.0011\ 0011\ 0011\ 0011 \cdots

になります。10進数の $1 \div 3 = 0.3333\cdots$ と同様に $2 \div 10 = 1 \div 5$ の2進数表現では循環小数になります。以下、2進数での計算。

10進数[ 2] = 2進数[  10]
10進数[10] = 2進数[1010]

         0.0011 0011
      ---------------
 1010 ) 10.000
         1 010
        ------
           1100
           1010
           ----
             10 000
              1 010
             ------
                1100
                1010
                ----
                  10

F64 への変換は、手元の環境では

Python
>>> (2/10).hex()
'0x1.999999999999ap-3'
16進表記: 0x1.999999999999ap-3
厳密値: 0.200000000000000011102230246251565404236316680908203125

になります。ゼロ方向丸めの環境では

16進表記: 0x1.9999999999999p-3
厳密値: 0.1999999999999999833466546306226518936455249786376953125

となるかもしれません。$0.2$ は2つの数値の間にあります。

0x1.9999999999999p-3 = 0.1999999999999999833466546306226518936455249786376953125
0x1.999999999999ap-3 = 0.200000000000000011102230246251565404236316680908203125

F64 の10進数近似

F64 では負符号を省くと、指数部 $E$ (11ビット)と仮数部 $T$ (52ビット)から

\begin{eqnarray}
n & = & E - 1023 \\
N & = & 2^n \times (0 + \frac{ T }{ 2^{52} }) \ \cdots\ (E = 0) \\
N & = & 2^n \times (1 + \frac{ T }{ 2^{52} }) \ \cdots\ (E \ne 0) \\
\end{eqnarray}

の値になります。$T$ が $52$ ビットなので、さらに下位桁の範囲で $N$ には幅(許容誤差)があると考えることができます。次の場合

  1. 算出した F64 値を10進数表記
  2. 表記された10進数を F64 値として使用

F64 では同値となる範囲で短い10進数表記を探ることができます。しかし、浮動小数点演算プロセッサの丸めや10進数文字列からの F64 値変換処理次第で許容誤差が変わるので、開発環境に合った許容誤差を模索する必要があります。

サンプルプログラム

Python
import math
import struct


def altfmtf(value, exact=None, inf='inf', nan='nan'):
    """
    float 型の絶対値10進文字列を得る

    value: 数値
    exact: 厳密値
    inf  : 無限大の文字列
    nan  : 非数の文字列
    """

    LOG_2 = 0.30103  # log10(2) ≒ 0.30103

    B_MAN = 52
    B_EXP = 11

    M_MAN = (1 << B_MAN) - 1
    M_EXP = (1 << B_EXP) - 1

    O_EXP = M_EXP >> 1

    P_MAN = 0
    P_EXP = P_MAN + B_MAN

    B_FX1 = O_EXP + B_MAN - 1

    FX_POW5 = pow(5, B_FX1)
    FX_POW10 = pow(10, B_FX1)

    value = float(value)
    avalue = abs(value)
    fp_data = struct.unpack('<Q', struct.pack('<d', value))[0]
    fp_exp = (fp_data >> P_EXP) & M_EXP
    fp_man = fp_data & M_MAN

    if fp_exp == 0 and fp_man == 0:
        return '0.0'
    if fp_exp == M_EXP:
        return (nan if fp_man else inf)
    if exact and fp_exp > B_FX1:
        return str(int(avalue))

    fp_bin = fp_man | (bool(fp_exp) << 52)
    fx_pos = max(0, fp_exp - 1)

    dn_val = (fp_bin << fx_pos) * FX_POW5
    dn_dlt = (1 << fx_pos) * FX_POW5
    dn_lcn = int((dn_dlt.bit_length() - 1) * LOG_2)
    dn_mod = pow(10, dn_lcn)

    def cl_check(val):  # 再現確認
        try:
            return avalue == val / FX_POW10
        except:
            return False

    # 10進数の下位桁から順に '0' にしつつ再現する値を検索
    cl_fix = [dn_val]
    cl_mod = dn_mod
    while not exact:
        cl_1 = (v - v % cl_mod for v in cl_fix)          # 切り捨て値の表
        cl_2 = sum(([t, t + cl_mod] for t in cl_1), [])  # 切り捨て・切り上げ値の表
        cl_z = list(filter(cl_check, cl_2))              # 再現する値の表
        if not cl_z:
            break
        cl_fix = cl_z
        cl_mod *= 10
    cl_val = sorted(cl_fix, key=lambda v: abs(v - dn_val))[0]  # 元値に最も近い10進数を選択

    s_tmp = str(cl_val)
    s_val = '0' * max(0, 1 + B_FX1 - len(s_tmp)) + s_tmp
    s_int = s_val[:-B_FX1]
    s_dec = s_val[-B_FX1:].rstrip('0')
    return f'{s_int}.{s_dec}'.rstrip('.')


def cvtexp(nstr, inf='inf', nan='nan'):
    """通常10進数を指数形式へ変換"""

    if nstr in (inf, nan):
        return nstr

    s_int, s_dec = (nstr.split('.') + [''])[:2]

    if s_int == '0':
        e_dec = len(s_dec)
        s_dec = s_dec.lstrip('0')
        s_num = f'{s_dec[:1]}.{s_dec[1:]}'.rstrip('.')
        return f'{s_num}e-{e_dec - len(s_dec) + 1:02}'

    e_int = len(s_int)
    s_num = (s_int + s_dec).rstrip('0')
    s_num = f'{s_num[:1]}.{s_num[1:]}'.rstrip('.')
    return f'{s_num}e+{e_int - 1:02}'


def ftopm(value):
    return '-' if value < 0 else ''


def ftoas(value):
    fv = altfmtf(value)
    ev = cvtexp(fv)
    return ftopm(value) + (fv if len(fv) <= len(ev) else ev)

def ftoax(value):
    return ftopm(value) + altfmtf(value, exact=True)


def test(value):
    sv = ftoas(value)
    xv = ftoax(value)
    print(f'16進 : {value.hex()}')
    print(f'厳密 : {xv}')
    print(f'repr : {value}')
    print(f'ftoas: {sv}')
    return
実行結果
Python 3.9.6 (default, Nov 11 2024, 03:15:39)
[Clang 16.0.0 (clang-1600.0.26.6)] on darwin
>>> test(0.2)
16 : 0x1.999999999999ap-3
厳密 : 0.200000000000000011102230246251565404236316680908203125
repr : 0.2
ftoas: 0.2
>>> test(float.fromhex('0x1.9999999999999p-3'))
16 : 0x1.9999999999999p-3
厳密 : 0.1999999999999999833466546306226518936455249786376953125
repr : 0.19999999999999998
ftoas: 0.19999999999999998
>>> test(0.1)
16 : 0x1.999999999999ap-4
厳密 : 0.1000000000000000055511151231257827021181583404541015625
repr : 0.1
ftoas: 0.1
>>> test(float.fromhex('0x1.9999999999999p-4'))
16 : 0x1.9999999999999p-4
厳密 : 0.09999999999999999167332731531132594682276248931884765625
repr : 0.09999999999999999
ftoas: 0.09999999999999999
>>> test(0.12345678901234567)
16 : 0x1.f9add3746f65ep-4
厳密 : 0.1234567890123456634920984242853592149913311004638671875
repr : 0.12345678901234566
ftoas: 0.12345678901234566
>>> test(0.123456789012345678)
16 : 0x1.f9add3746f65fp-4
厳密 : 0.12345678901234567736988623209981597028672695159912109375
repr : 0.12345678901234568
ftoas: 0.12345678901234568
>>> test(1e100)
16 : 0x1.249ad2594c37dp+332
厳密 : 10000000000000000159028911097599180468360808563945281389781327557747838772170381060813469985856815104
repr : 1e+100
ftoas: 1e+100
>>> (1e100).hex()
'0x1.249ad2594c37dp+332'
node.jsの場合
Welcome to Node.js v22.13.1
> 0.2
0.2
> 0.1
0.1
> 0.12345678901234567
0.12345678901234566
> 0.123456789012345678
0.12345678901234568
> 1e100
1e+100

実行環境は Mac Mini (Intel), macOS 15.2 です。

Python float型の既定の文字列化(repr/strなど)や JavaScript Number 型の既定文字列化も同様の処理が行われているようです。環境違いでも最下位ビット分の誤差が想定できるので、それが許容されるなら、そのまま使ってもよさそうです。(随分前に調べたことを思い出しましたが、すっかり忘れてました...)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?