浮動小数点数を最小桁まで文字列化
浮動小数点数を丸めずに文字列化したかった。
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