QA@IT

VB.netで精度を損なわずにSingleをDoubleに変換したい

8168 PV

VB2010を使用しています。

上位システム(計測器)から4バイトの浮動小数点データ(小数点1桁)が送信されてきます。
そのデータをSingleで取り込み、Double型に変換して別システム(制御機)に送信する必要があります。

次のプログラムを作成したところ、-22.3の値が-22.299999237060547に変換されてしまいます。
精度を保ったまま変換するには、どのようにすれば良いのでしょうか?

Dim singleValue As Single = BitConverter.ToSingle(recvBuff, 0)
Dim doubleValue As Double = Convert.ToDouble(singleValue)

一度、文字列に変換し、文字列からDouble型に変換すると
精度が損なわれないことが分かりましたが、この方法しかないのでしょうか?

Dim singleValue As Single = BitConverter.ToSingle(recvBuff, 0)
Dim doubleValue As Double = Double.Parse(singleValue.ToString())

  • コンピュータの中では 2 進数を使っていることによる丸め誤差ではないのですか? -
  • つまり、MSDN ライブラリからの抜粋ですが、"2 進数 (k と n が整数である k / (2 ^ n) の形式) ではない値は正確に格納できません。たとえば、0.5 (= 1/2) や 0.3125 (= 5/16) は正確な値を格納できますが、0.2 (= 1/5) や 0.3 (= 3/10) は近似値のみを格納できます" ということでは。 -
  • 上記 MSDN ライブラリのタイトルと URL 書き忘れました。
    Troubleshooting Data Types (Visual Basic)
    https://msdn.microsoft.com/ja-jp/library/ae382yt8.aspx
    -
  • コンピュータで浮動小数を扱うと誤差が出ることは知っていますが
    誤差が出るから、-22.299999237060547は、-22.3として扱ってくれという意見は通りません。
    というか、通りませんでした。
    それで変換方法を模索し質問をしました。
    -
  • 意見が通るか通らないかの話をしたつもりはありません。Single や Double を使う限りは "2 進数 (k と n が整数である k / (2 ^ n) の形式) ではない値は正確に格納できません" という事実は避けようがないので、どうしようもないと言っています。紹介した記事には Decimal を使う例がありますが読みましたか? -
  • 紹介して頂いた内容の記事は読みました。
    Single、Doubleには正確に0.2や0.3といった値が格納されないということも分かりました。
    でも、どうしようもないから-22.3といった値は諦めてくれとは言えないので、どのように変換するのが良いかということで、Decimalの変換方法も投稿しました。
    他に良い変換方法があれば教えてほしいです。
    -
  • 「Single、Doubleには正確に0.2や0.3といった値が格納されないということも分かりました」であれば、IEEE754 単精度の recvBuff には 10 進数 -22.3 は正確に格納できないということは理解できるでしょうか? -
  • 以下の記事(特に「入りきらない数の丸め方」のセクション)を読むとそのあたりがよく分かると思いますので読んでみてください。
    http://pc.nikkeibp.co.jp/pc21/special/gosa/eg4.shtml
    -
  • 計測器から受け取った recvBuff の中身が 0xC1B26666(10 進数で -22.299987793)だとすると、それを -22.3 に変換しろというのが要件ではないですよね? 「諦めてくれ」と言うのではなくて、一体何がしたいのかよく聞いてください。 -
  • IEEE754では、Single、Doubleに-22.3を格納できないと言うことは記事や、他の方の投稿で理解できました。VB2010でSingleで-22.3と表示されているのは、丸められて表示されているということも分かりました。
    質問の説明不足で申し訳ございませんでした、どのように質問すれば私の知識で過不足なく説明できるか分かりません。
    -
  • VB2010で有効少数1桁のSingle=-22.3となっている変数を、Doubleに変換したい。
    このとき、Double.ToString()で文字列として表示させたとき、"-22.3"と表示させたい。
    表示させるときに少数1桁を丸めて表示させるということは、他メーカのシステムのためできません。
    -
  • 表示だけの問題であれば Dim doubleValue As Double = Convert.ToDouble(singleValue) とした後、doubleValue.ToString("F1") で -22.3 と表示されます。(実際の値は -22.2999992370605 ですが) -
  • 質問する時は局所的な問題・課題だけでなく、全体的なシナリオを含めてやりたいことを書いてもらえると、もう少し的を得た回答ができると思います。(例えば Double に変換したあと -22.3 以上/未満に分けて処置を行いたいなど) -
  • 表示だけの問題なら、double.ToString(".0") で文字列にする。
    Singleから直接Doubleにすると、23ビットの情報だけ引き継ぎます。DecimalやStringを経由すると、52ビットの情報を作ります。「引き継ぐ」と「作る」の違いに注意。
    -

回答

精度は失われていません。
広い範囲を表現できるようになったので、より、精度のある数値を保存しています。
精度:プログラム系では小数点以下の桁数を表すことが多い。または、保存のために用いるビット数を指す。
vbのSingleは32ビット、Doubleは64ビット。精度はむしろ増えている。
ビットの使い方はIEEE754を参照。
古い言い方をすると「0.1を2進数で表記してみろ」。つまり、少数は1/2のn乗で表すので、10進数では割り切れる数も、2進数(コンピュータの内部表現)では割り切れない。それを精度が高い入れ物に入れたのだから、表現範囲が増えた。
Decimal使うか、表現精度を決めて使うか。つまり、-22.299でも、「小数点以下1桁まで」という精度だと、-22.3になる。

編集 履歴 (0)

コメントの文字数制限にかかるので、こっちで。

中学校あたりで、「少数点以下4桁まで使って計算し、答えは少数点以下2桁で求めよ」という問題がなかったかなぁ?
まぁ、ゆとり世代なら、なかったかもしれないけど。

計測器を使われているのですよね?その計測器の説明書は、よく読みましたか?
どこかに、「有効桁数は何桁」あるいは「有効な測定範囲」と、書いてありませんか?

我々は、目的のために必要な精度を知り、計測器がその精度を持っているか確認しなければなりません。
加えて、システムを作る場合は、必要な精度を保つために、どれくらいの精度で計算するかを決めなければなりません。
先述の通り、計算用の精度と表示用の精度は異なります。

計測器から「-22.3」というデータが送られてきている?
いいえ、「BitConverter.ToSingle(recvBuff, 0)」とされているように、
計測器が送ってきているのは 0xC1B26666(後述)という値です。
この値を単純に10進数に変換すると、-22.299987793です。
ちなみに、「-22.3」は、10進数では有限小数ですが、2進数では無限小数です。
-22.299987793を-22.3とできるのに、
-22.299999237060547を-22.3とするのが通らないのはなぜでしょうか。
しつこいですが、計算に使う精度と表示に必要な精度は異なります。
Single 型が保持する桁数は9桁ですが、有効桁数は7桁です。22.29998までしか有効ではありません。
そこで、次の7を使って丸めを行い、繰り上がって"22.3"、符号が付いて"-22.3"となります。

なお、0xC1B26665~C1B26669の範囲が-22.3になります。

そういう訳ですので、計算上は、単純に Double に代入します。
見せるときには、書式指定文字列を使って、必要な桁数のみ表示します。
https://msdn.microsoft.com/ja-jp/library/26etazsy(v=vs.110).aspx#FormatStrings

編集 履歴 (0)
  • VB2010でしか確認していませんが、0xC1B26666をSingleに格納すると-22.3となります。
    0xC1B26665は-22.2999973、0xC1B26669は-22.300005でしたが、どうしてこの範囲が-22.3になるのか理解できませんでした。
    計測器は、IEEE754の単精度で有効桁数は1桁、-99.0~900.0の範囲で値を送信してきます。
    -
  • SingleをDoubleに代入して、有効桁数が少数1桁なのだから、Math.Roundで処理をすれば良い。ということでしょうか?
    Math.Roundでの処理も考えてみます。
    -
  • Doubleに代入した後、変数.ToString(".#") としてみて下さい。-22.3と表示されます。 -
  • 有効桁数が1桁なので、-22.3と表示されるということですか?
    Singleに-22.25(0xC1B20000)の値を、Doubleに代入して、.ToString(".#")をすると-22.3と表示されます。
    私の理解が足りないのでしょうか。
    -
  • flied_onionさんの検証用コードで0xC1B26664~C1B26669の範囲が-22.3となる。の意味がやっと理解できました。
    Singleの有効桁数が7桁なので丸めて表示されると-22.3となる範囲ということですね。有効桁数の意味も理解できていないことに気づきました。
    -

有効桁数の話はすでに出てはいるようですしJittaさんが書いている通りなのですが、
検証のコードを追加してみました。
とりあえず、Singleに -22.3 をそのまま代入することはできません。
あなたが確認したという -22.3はその時点で自動的に有効桁数で丸められています。

    Sub Main()

        Dim recvBuff = New Byte() {&H66, &H66, &HB2, &HC1}
        Dim singleValue As Single = BitConverter.ToSingle(recvBuff, 0)

        Console.WriteLine(singleValue)
        Console.WriteLine(singleValue.ToString("G1")) ' 有効桁数 1桁で表示
        Console.WriteLine(singleValue.ToString("G7")) ' 有効桁数 7桁で表示
        Console.WriteLine(singleValue.ToString("G8")) ' 有効桁数 8桁で表示
        Console.WriteLine(singleValue.ToString("G9")) ' 有効桁数 9桁で表示
        Console.WriteLine(singleValue.ToString("F1")) ' 小数 1桁まで表示
        Console.ReadKey()

    End Sub

結果

-22.3
-2E+01
-22.3
-22.299999
-22.2999992
-22.3

-2E+01は-20の指数表記。
-22.2999992を有効桁数7桁で丸めると、たまたますべて繰り上がって-22.3になる。

結局のところ、計器が送ってきている「0xC1B26666」は -22.299999237060546875であり、
-22.3は既に丸められた値です。

Doubleだと既定の有効桁数が長いため、有効数字8桁目の2でたまたまそれ以上繰り上がらないので-22.29999...という数字で表示されているだけです。

あとは計器のデータをどう扱うかだけなのでそれは決めの問題だと思います。
計器に有効な小数桁が 1桁とあるならば、同等の方法で丸めた値でDoubleの値を作り次に投げるのがいいんでしょうね。

おまけで C#ですが、float(Single)やdoubleのByteデータを元に
符号、指数、仮数を求めて値を表示するコードを書いてみました。
-22.3だと思っていた値がどういう値なのか、doubleに-22.3を代入したときと同じ値なのか等の参考にしてみてください。
たぶんVS2010でも動くと思います。コンソールアプリケーションです。

ちなみに数値の後にdがついているのはdouble、fがついているのはfloat、MがついているのはDecimalであることを明示してます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1 {
    class Program {
        static void Main(string[] args) {

            var p = new Program();

            Console.WriteLine("Double -22.3");
            p.DispFloatingPointValue(-22.3d);

            Console.WriteLine("Single -22.3");
            p.DispFloatingPointValue(-22.3f);


            Console.WriteLine("Single -22.3 を Doubleに代入");
            double val = -22.3f;
            p.DispFloatingPointValue(val);

            Console.WriteLine("0xC1B26666");
            var recv = new Byte[] { 0xC1, 0xB2, 0x66, 0x66 };
            p.DispFloatingPointValue(BitConverter.ToSingle(recv.Reverse().ToArray(), 0));

            Console.WriteLine("0xC1B26665");
            recv = new Byte[] { 0xC1, 0xB2, 0x66, 0x65 };
            p.DispFloatingPointValue(BitConverter.ToSingle(recv.Reverse().ToArray(), 0));

            Console.WriteLine("0xC1B26664");
            recv = new Byte[] { 0xC1, 0xB2, 0x66, 0x64 };
            p.DispFloatingPointValue(BitConverter.ToSingle(recv.Reverse().ToArray(), 0));

            Console.WriteLine("0xC1B26669");
            recv = new Byte[] { 0xC1, 0xB2, 0x66, 0x69 };
            p.DispFloatingPointValue(BitConverter.ToSingle(recv.Reverse().ToArray(), 0));


            Console.WriteLine("0xC1B2666A");
            recv = new Byte[] { 0xC1, 0xB2, 0x66, 0x6A };
            p.DispFloatingPointValue(BitConverter.ToSingle(recv.Reverse().ToArray(), 0));

            Console.ReadKey();
        }


        private void DispFloatingPointValue(double value) {
            Console.WriteLine("* Double: {0} [{1}]", value, BitConverter.ToString(BitConverter.GetBytes(value).Reverse().ToArray()));

            var bin = SplitAsBitStrings(value);
            var ans = CalcFloatingPointValue(bin.Sign, bin.Exponent, bin.Mantissa, false);
            PrintAnswer(ans);

            Console.WriteLine("---");
        }

        private void DispFloatingPointValue(float value) {
            Console.WriteLine("* Single: {0} [{1}]", value, BitConverter.ToString(BitConverter.GetBytes(value).Reverse().ToArray()));

            var bin = SplitAsBitStrings(value);
            var ans = CalcFloatingPointValue(bin.Sign, bin.Exponent, bin.Mantissa);
            PrintAnswer(ans);

            Console.WriteLine("---");
        }

        private string BytesToBinString(byte[] arr){
            return arr.Reverse().Aggregate("", (p, next) => p + Convert.ToString(next, 2).PadLeft(8, '0'));
        }

        private BinStrings SplitAsBitStrings(double value) {

            var bytes = BitConverter.GetBytes(value);
            var bintext = BytesToBinString(bytes);

            return new BinStrings {
                Sign = bintext.Substring(0, 1),
                Exponent = bintext.Substring(1, 11),
                Mantissa = bintext.Substring(12)
            };
        }

        private BinStrings SplitAsBitStrings(float value) {
            var bytes = BitConverter.GetBytes(value);
            var bintext = BytesToBinString(bytes);

            return new BinStrings {
                Sign = bintext.Substring(0, 1),
                Exponent = bintext.Substring(1, 8),
                Mantissa = bintext.Substring(9)
            };
        }



        private void PrintAnswer(Answer ans) {
            Console.WriteLine(" {0}  仮数部 {1} × 指数部 {2} ", (ans.IsNegative ? "-1 × " : ""), ans.M, ans.E);
            Console.WriteLine(" = {0:G64}", ans.Val);
        }

        private Answer CalcFloatingPointValue(string sign, string exponent, string mantissa, bool isSingle = true) {

            // 2進を確認したい場合は以下で。
            // Console.WriteLine("{0}:{1}:{2}", sign, exponent, mantissa);

            mantissa = "1" + mantissa; // 省略された仮数部の 1.0(2進)を追加

            var p = 0M;
            var val = 1.0M;
            mantissa.ToList().ForEach(x => {
                if (x == '1') {
                    p += val;
                }
                val /= 2;
            });

            // 指数計算
            var exponentValue = Convert.ToDecimal(Math.Pow(2, Convert.ToInt64(exponent, 2) - (isSingle ? 127 : 1023 )));

            return new Answer {
                IsNegative = sign == "1",
                M = p,
                E = exponentValue,
                Val = p * exponentValue * (sign == "1" ? -1 : 1)
            };
        }

        struct BinStrings {
            public string Sign;
            public string Exponent;
            public string Mantissa;
        }

        struct Answer {
            public bool IsNegative;
            public decimal M;
            public decimal E;
            public decimal Val;
        }
    }
}
編集 履歴 (0)
  • 検証用のコードを書いて頂きありがとうございます。
    (C#の環境がすぐに用意できなく、VB2010で置き換えて動作させました。)
    -22.3という値がどういったものなのか、目で見て確認することが出来ました。
    DoubleでもSingleと同じ値が必要なのであれば、有効桁を7桁とし丸める。
    今回は、計測器の少数の有効桁数が1桁のため、少数1桁で丸める。
    -

解決済のようですが

Dim singleValue As Single = BitConverter.ToSingle(recvBuff, 0)
Dim intValue10 As Integer = CInt(singleValue * 10)  '--- 小数点以下1桁の数が保証されているので10倍すれば必ず整数になる
Dim doubleValue As Double = intValue10 / 10    '--- Doubleにするときに10で割り元の数に戻す

のようにする方法もあります。

編集 履歴 (0)
  • 小数点の有効桁数、今の場合10倍して整数にしてから変換するのですね。
    何ミリ秒以内に処理をしなければならない、というわけではないので、教えて頂いた方法も試してみます。
    ありがとうございました。
    -

Singleに比べDoubleの方が精度がよくなり表現範囲が増えたので
-22.3が-22.299999237060547になる。ということは分かりました。
ですが、別システムが送られてきた値を見ると-22.3ではなく、-22.29...のため
「小数点以下1桁まで」という制約があったとしても、-22.3ではない。と判断するようです。
(小数点以下1桁を切り捨てている感じ?)

Decimal型に変換するとSingleの-22.3はDoubleの-22.3に変換できました。
文字列か、Decimalかのどちからに変換して対応することにします。

Dim singleValue As Single = BitConverter.ToSingle(recvBuff, 0)
Dim decimalValue As Decimal = Convert.ToDecimal(singleValue)
Dim doubleValue As Double = Convert.ToDouble(decimalValue)

編集 履歴 (0)
  • 文字列に変換するか、Decimalに変換するかで変換速度を調べると文字列よりDecimalに変換してからの方が5倍ほど速かったのでDecimalに変換して対応することにします。
    投稿したプログラムを1億回×2回実行した結果です。
    -
ウォッチ

この質問への回答やコメントをメールでお知らせします。