浮動小数点数を最小桁まで文字列化

浮動小数点数を丸めずに文字列化したかった。

class IEEE754
{
    private FloatingPointDefinition mode;


    private readonly FloatingPointDefinition DefFloat
        = new FloatingPointDefinition()
        {
            ExpLength = 8,
            FracLength = 23,
        };
    private readonly FloatingPointDefinition DefDouble
        = new FloatingPointDefinition()
        {
            ExpLength = 11,
            FracLength = 52,
        };

    private string decimalStrSmall;//小数部文字列
    private string decimalStrBig;//整数部文字列
    public int Digit { get; private set; }//10進指数

    public static bool EnableDenormal { get; set; }//指数部==0のとき非正規化数を扱うか?
    public static bool EnableInfinity { get; set; }//指数部が最大値の時Inf,NaNを扱うか?

    public double Value { get; private set; }//元になった浮動小数点値
    public ulong Code { get; private set; }//バイナリ値

    private bool Sign { get { return (Bit.Check(this.Code, mode.BitSize - 1)); } }
    private string SignStr { get { return this.Sign ? "-" : "+"; } }
    private int ExpWithBias
    {
        get
        {
            return (int)((this.Code >> mode.FracLength) & Bit.FillTo(mode.ExpLength));
        }
    }
    public int ExpWithoutBias
    {
        get
        {
            return
                ((EnableDenormal && ExpWithBias == 0) ? 1 : ExpWithBias)
                - (int)Bit.FillTo(mode.ExpLength - 1);
        }
    }

    private ulong Frac//仮数部のバイナリ
    {
        get
        {
            return (this.Code & Bit.FillTo(mode.FracLength))
                + ((EnableDenormal && ExpWithBias == 0) ? 0 : (1UL << mode.FracLength));
        }
    }
    static IEEE754()
    {
        EnableDenormal = true;
        EnableInfinity = true;
    }

    public IEEE754(float value)
    {
        mode = DefFloat;
        SetValue(value, Float2Bin(value) & mode.MaxBits);
    }
    public IEEE754(uint code)
    {
        mode = DefFloat;
        SetValue(Bin2Float(code), code);
    }
    public IEEE754(double value)
    {
        mode = DefDouble;
        SetValue(value, Double2Bin(value) & mode.MaxBits);
    }
    public IEEE754(ulong code)
    {
        mode = DefDouble;
        SetValue(Bin2Double(code), code);
    }
    private void SetValue(double value, ulong code)
    {
        this.Value = value;
        this.Code = code;

        DecodeDecimal();

    }



    private uint Float2Bin(float f)
    {
        return BitConverter.ToUInt32(BitConverter.GetBytes(f), 0);
    }

    private float Bin2Float(ulong i)
    {
        return BitConverter.ToSingle(BitConverter.GetBytes((uint)(i & 0xFFFFFFFF)), 0);
    }
    private ulong Double2Bin(double f)
    {
        return BitConverter.ToUInt64(BitConverter.GetBytes(f), 0);
    }

    private double Bin2Double(ulong i)
    {
        return BitConverter.ToDouble(BitConverter.GetBytes(i), 0);
    }

    public string GetBinCode()
    {
        var sb = new StringBuilder(mode.BitSize);
        for (int i = mode.BitSize - 1; i >= 0; i--)
        {
            sb.Append(Bit.Check(this.Code, i) ? "1" : "0");
        }
        return sb.ToString();
    }


    /// <summary>
    /// 整数部,小数部それぞれを10進整数文字列に変換
    /// 数値 XXX.xxx を 0.XXXxxxe+n の形に変換し,
    /// decimalStrBig = "XXX"
    /// decimalStrSmall = "xxx"
    /// Digit = n
    /// と設定する
    /// </summary>
    private void DecodeDecimal()
    {
        BigInteger bigPart;
        BigInteger smallPart;

        ulong smallBin;

        int minFracExp = mode.FracLength - ExpWithoutBias;//最小桁の指数
        int minDigit = 0;


        if (minFracExp <= 0)//整数
        {
            bigPart = (new BigInteger(Frac)) << (-minFracExp);
            smallBin = 0;
        }
        else if (ExpWithoutBias < 0)//value < 1
        {
            bigPart = 0;
            smallBin = Frac;
        }
        else//(0 < minFracExp <= FracLength)
        {
            bigPart = Frac >> minFracExp;
            smallBin = Frac & Bit.FillTo(minFracExp);
        }


        if (smallBin > 0)
        {
            for (int i = 0; i <= mode.FracLength; i++)
            {
                if ((smallBin & 1) == 1)
                {
                    minDigit = i;
                    break;
                }
                smallBin >>= 1;
            }

            //小数部を10^n倍した整数
            smallPart = BigInteger.Pow(5, minFracExp - minDigit) * smallBin;

            decimalStrSmall = smallPart.ToString();
            Digit = -minFracExp + minDigit + decimalStrSmall.Length;
            //Digitには元の数値の小数部のみを取り出して 0.xxxe+n と表記した場合の指数nが入る
        }
        else
        {
            smallPart = 0;
            decimalStrSmall = "0";
            Digit = 0;
        }

        decimalStrBig = bigPart.ToString();

        if (bigPart > 0)//整数部があるなら小数部を10^(-1)の桁まで0埋め
        {
            if (Digit < 0)
            {
                decimalStrSmall
                    = decimalStrSmall.PadLeft(decimalStrSmall.Length - Digit, '0');
                Digit = 0;
            }
            Digit += decimalStrBig.Length;
        }
        else
        {
            decimalStrBig = "";
        }
    }


    public override string ToString()
    {
        return this.ToString(null);
    }
    public string ToString(int? decimalPoint)
    {

        //指数部が最大値ならInfまたはNaN
        if (EnableInfinity
            && ExpWithBias == ((1U << mode.ExpLength) - 1))
        {
            string status;
            if ((this.Code & Bit.FillTo(mode.FracLength)) == 0)
            {
                status = "Inf";
            }
            else if (Bit.Check(Frac, mode.FracLength - 1))
            {
                status = "qNaN";
            }
            else
            {
                status = "sNaN";
            }
            return this.SignStr + status;
        }



        int point = 0;

        if (decimalPoint.HasValue)
        {
            point = decimalPoint.Value;
        }
        else
        {
            point = decimalStrBig.Length;//point>=0
            if (point == 0 && Digit < 0)//整数部==0かつ小数部が0始まり
            {
                point = Digit;
            }
        }

        int dispDigit = Digit - point;
        var numStr = decimalStrBig + decimalStrSmall;


        if (point >= numStr.Length)//小数点位置が文字列桁数より大きい
        {
            numStr = numStr.PadRight(point, '0') + ".0";
        }
        else if (point > 0)
        {
            numStr = numStr.Insert(point, ".");
        }
        else
        {
            numStr = "0.".PadRight(2 - point, '0') + numStr;
        }

        return this.SignStr + numStr
            + ((decimalPoint.HasValue || dispDigit != 0)
            ? ("e" + (dispDigit < 0 ? "" : "+") + dispDigit.ToString())
            : "");
    }
}

floatとかdoubleの引数のバイナリ値を読み込んで、定義されたフォーマットに従って指数部・仮数部を解釈。
整数部と小数部をそれぞれ文字列化してから最後にドッキング。
このときにBigIntegerを使ってる。もうちょいスマートにできないもんか。


浮動小数点数フォーマットの定義はこんな感じのクラスで表す。
ExpLengthに指数部の長さ、FracLengthに仮数部の長さをビット単位で入れておく。

//浮動小数点数フォーマットの定義
class FloatingPointDefinition
{
    public int ExpLength { get; set; }//指数部のbit長
    public int FracLength { get; set; }//仮数部のbit長//must be <64

    public int BitSize { get { return ExpLength + FracLength + 1; } }//全bit長
    public ulong MaxBits { get { return Bit.FillTo(BitSize); } }//ビットマスク
}


あとビット操作関連は別クラスにしておいた。

class Bit
{
    /// <summary>
    /// nビット目が立っているか調べる
    /// </summary>
    /// <param name="x">調査対象</param>
    /// <param name="n">LSBからのオフセット</param>
    /// <returns>立っていたらtrue</returns>
    public static bool Check(int x, int n)
    {
        return !((x & (0x01 << n)) == 0);
    }
    public static bool Check(ulong x, int n)
    {
        return !((x & (0x01UL << n)) == 0);
    }

    /// <summary>
    /// 指定bitまで埋めたバイナリを取得
    /// </summary>
    /// <param name="length">ビット長n</param>
    /// <returns>2^n-1</returns>
    public static ulong FillTo(int length)
    {
        return (length >= 64) ? ~0UL : ((1UL << length) - 1);
    }
}


使うときはコンストラクタにfloatかdoubleの値を渡す。

var d = new IEEE754(Math.PI);
var f = new IEEE754((float)Math.PI);

Console.WriteLine(d.ToString());
Console.WriteLine(f.ToString());


結果はこんな感じ。

+3.141592653589793115997963468544185161590576171875
+3.1415927410125732421875

改めてfloatの精度が低いのがわかる。


整数部の長さを指定することもできる。

Console.WriteLine(f.ToString(0));
Console.WriteLine(f.ToString(-1));
Console.WriteLine(f.ToString(2));

結果

+0.31415927410125732421875e+1
+0.031415927410125732421875e+2
+31.415927410125732421875e-1


あとはバイナリコードを取得できるようにしてみたり。

Console.WriteLine(f.GetBinCode());

結果

01000000010010010000111111011011