tagCANDY CGI VBレスキュー(花ちゃん) の Visual Basic 2010 用 掲示板(VB.NET 掲示板) [ツリー表示へ]   [Home]
一括表示(VB.NET VB2005)
タイトル数字の比較について
記事No11781
投稿日: 2017/01/19(Thu) 10:09
投稿者SUZUKI
VB6.0からVB2005に変換しているSUZUKIです
もう聞く事も無くなったと思っていたらまた理解出来ない現象で出くわしました
今年もよろしくお願いしたいのですが。。

  Dim K As Single
        K = 2.3
        If K = 2.3 Then
            MsgBox("Kは2.3です")
        Else
            MsgBox("Kは2.3でありません")
        End If
VB6.0
  ボタンを作り実行すると Kは2.3です と出ます
VB2005
  同じくボタンを作り実行すると Kは2.3ではありません と出ます
 
 なぜ違うのか理解出来ません
 どう対処したらよいのでしょうか?
 こんな基本的な事どこかにあると思うのですが、どう検索したらよいか。。。。。


[ツリー表示へ]
タイトルRe: 数字の比較について
記事No11782
投稿日: 2017/01/19(Thu) 10:40
投稿者Hongliang
// VB6の事情は知りませんが。以下はVB.NETについての記述です。

まず、2.3は、SingleやDoubleで使用される2進浮動小数点数としては無限小数としてしか表現できないので、性格に2.3を表現することは不可能です。
また、ただの小数点を含むリテラル数値は、Doubleであるとみなされます。
2.3をSingle型変数に代入した場合、無限小数の下の方の桁が丸められて格納されることになりますので、それを丸める前のDoubleと比較した場合、厳密には同じ値ではなくなります。

Double型と、Single型(をDouble型に変換した値)のビット表現を、以下のコードで確認してみてください。
Dim d As Double = 2.3
Dim s As Single = 2.3
Debug.Print("{0}", Convert.ToString(BitConverter.DoubleToInt64Bits(d), 2))
Debug.Print("{0}", Convert.ToString(BitConverter.DoubleToInt64Bits(s), 2))

浮動小数点数型は諸々誤差の出やすい型ですので、単純に = で比較すると問題が出やすいです。

[ツリー表示へ]
タイトルRe^2: 数字の比較について
記事No11783
投稿日: 2017/01/19(Thu) 11:09
投稿者SUZUKI
Hongliangさん 早速の回答ありがとうございます
計算しなくても誤差が出るというのは驚きというか困りものです
vb6.0は何か親切?機能が働いているという事でしょうか
ちなみにkをdouble型に変更して見ると
=側に流れます
doubleでも誤差が出るとなると根本的な対策でも
無いような気がします。。。。。。
質問してよいでしょうか

数字2.3はdouble型でしょうか?
なぜ=側に流れるのでしょうか?
=を問題にするならdecimal型でなければならないという事になりますか?

[ツリー表示へ]
タイトルRe^3: 数字の比較について
記事No11784
投稿日: 2017/01/19(Thu) 13:26
投稿者SUZUKI
下記のプログラムを作成しました
結果は  
MsgBox("Kは2.3でありません")
MsgBox("nは2.333333333333333です")
MsgBox("mは2.3です")
2.3はDouble型ですね
ただ k = 2.3 でビルドエラーにならないのが不思議ではあります
自分なりの結論としては
vb6.0でSingle型で定義したのはvb2005では少なくともDouble型に
しないとうまく動かないという結論となりました



  Option Strict On
      
       Dim k As Single
        k = 2.3
        '=======
        If k = 2.3 Then
            MsgBox("Kは2.3です")
        Else
            MsgBox("Kは2.3でありません")
        End If
        Dim n As Double
        n = 2.333333333333333
        '========
        If n = 2.333333333333333 Then
            MsgBox("nは2.333333333333333です")
        Else
            MsgBox("nは2.333333333333333でありません")
        End If
        Dim m As Decimal
        m = CDec(2.3)
        '===================
        If m = 2.3 Then
            MsgBox("mは2.3です")
        Else
            MsgBox("mは2.3でありません")
        End If

  

[ツリー表示へ]
タイトルRe^4: 数字の比較について
記事No11785
投稿日: 2017/01/19(Thu) 17:42
投稿者shu
        Dim a = 2.3!
        Dim b = 2.3

        Console.WriteLine(a = 2.3)
        Console.WriteLine(a = 2.3!)
        Console.WriteLine(b = 2.3)
        Console.WriteLine(b = 2.3!)


結果:
False
True
True
False

[ツリー表示へ]
タイトルRe^4: 数字の比較について
記事No11788
投稿日: 2017/01/19(Thu) 22:00
投稿者魔界の仮面弁士
> ただ k = 2.3 でビルドエラーにならないのが不思議ではあります

値としては範囲内なので、エラーにはなりません。
同様の理由で、
  'UInt64 の値を Int32 に代入
  Dim i As Integer = 123UL
なども OK です。


ただし、これは「定数値」や「リテラル」に限られます。
変数やプロパティ値だった場合、たとえば
  Dim a As UInteger = 123UL
  Dim b As Integer = a
あるいは
  Dim x As Double = Double.MaxValue
  Dim y As Single = x
などだと、Option Strict On で弾かれます。


ということで、実は
  ' MaxValue はプロパティではなく「定数」
  Dim x As Single = Double.MaxValue
も、Option Strict に関わらずコンパイル可能です。
それどころか、VB6 と違って実行時例外(オーバーフローエラー)にさえなりません。


> =を問題にするならdecimal型でなければならないという事になりますか?
なりません。そもそも Decimal は、有効桁数は広いものの、扱える範囲は狭いですし。
(たとえば CDec(Single.Epsilon) は、Decimal.Zero に丸められてしまいます)


今回の件について言えば、Single 型で統一していれば問題は出なかったはずです。

型変換を伴うのであれば、たとえ Decimal を併用したとしても、
No11786, 11787 で述べたような、縮小変換/拡大変換による差異は生じ得ます。

すべての小数を Decimal で統一するにしても、たとえば「^ 演算子」ですとか、
「PointF 型」「Math.Pow メソッド」などなど、Decimal が使えないケースは存在します。

重要なのは、データ型を意識して使い分けると言うことですね。
良い機会ですし、曖昧なままにせず、それぞれの型の特性を正しく理解しておくことをお奨めします。



> vb6.0でSingle型で定義したのはvb2005では少なくともDouble型に
> しないとうまく動かないという結論となりました

折角なので、VB6 側の動作についても説明しておきましょうか。

――まずはリテラル表記について:

ご存知かも知れませんが、VB6 では、2.3R や 2.3F といった数値リテラルは使えません。
しかし、型文字(Type Characters)を使うことで区別して記載できます。

VB6 の「2.3」や「2.3#」は Double 型ですし、
VB6 の Single 型リテラルは「2.3!」です。

もちろん「2.3E0」などの指数表現も使えます(VB6 でも VB.NET でも)。
ただし「2.3D0」形式は、VB.NET ではサポートされなくなっています。
(VB.NET で使うと、コンパイルエラー BC30827 が発生します)


ということで、最初の質問にあった Dim K As Single に対する
比較処理についても、 No11786 に書いたのと同じ理由により、
本来は VB6 と言えども
  If K = 2.3! Then
のように書くべきでしょう。



――次に、浮動小数点数の扱いについて:

浮動小数点型(Single / Double)の精度は、VB6 と VB.NET とで違いはありません。
非数値がサポートされるようになったり、文字列表現が多少変更されたりといった差異はありますが。

では、どうして VB6 と VB.NET とで
  If K = 2.3 Then
の結果が異なっていたのかと言えば、VB6 では
  If CDbl(K) = 2.3R Then 'これは False
の判定ではなく、
  If K = CSng(2.3R) Then 'これは True
の方の比較処理が行われていたためです。


VB6 ヘルプ『比較演算子』の項より引用:

> 単精度浮動小数点数型 (Single) の値を倍精度浮動小数点数型 (Double) の値と比較すると、
> 倍精度浮動小数点数点型の値は単精度浮動小数点数型の値の精度に丸められます。


上記には、通貨型(Currency) や通貨型(Decimal) についても述べられていますので、
併せてチェックしてみてください。VB6 の Decimal 型は、あまり馴染みが無いかもしれませんが。



こうした仕様変更は他にもあります。たとえば
  answer = 10.25 Mod 3.5
が、VB6 だと Long 演算で「2」になるのに対し、
VB.NET だと Double のまま「3.25」が返されるとか、
  answer = 1.0 / 0.0
が、VB6 だと 0 除算エラーになるの対し、
VB.NET だとエラーにならず、+∞ になるとか…。

[ツリー表示へ]
タイトルRe: 数字の比較について
記事No11786
投稿日: 2017/01/19(Thu) 21:17
投稿者魔界の仮面弁士
そもそも、小数リテラルの記述に問題があります。

「2.3」「2.3#」「2.3R」はいずれも Double 型のリテラルです。
「2.3!」「2.3F」なら Single 型のリテラルとなります。
「2.3@」「2.3D」なら Decimal 型のリテラルです。

ですから本来であれば、最初のコードは
  Dim K As Single
  K = 2.3F
  If K = 2.3F Then
    MsgBox("Kは2.3です")
  Else
    MsgBox("Kは2.3でありません")
  End If
と書くべきだったということです。これなら正しく比較されます。


> If K = 2.3 Then

この場合、K は Single 型、2.3 は Double 型として処理されます。

型は大きい方に丸められますので、結果として
 If K = CSng(2.3R) Then 'これは True
ではなく、
 If CDbl(K) = 2.3R Then 'これは False
として比較されます。



> 計算しなくても誤差が出るというのは驚きというか困りものです

既に指摘されていますが、Single や Double は二進小数です。
(VB6 の通貨型や十進型、VB.NET の Decimal 型は別の管理方法です)


「三分の一」を十進小数で表すと、無限小数になってしまい、誤差が生じますよね。
でも、三進小数なら誤差なく表現できるのです。

同様に「十分の一」という値は、十進小数では問題なく扱えますが、
二進小数では有限桁の小数では表現しきれません。

変数で無限の桁を扱うわけにもいきませんし、Single と Double では
扱える桁数(有効数字の精度)も異なりますので、今回のような差が生じます。

[ツリー表示へ]
タイトルRe^2: 数字の比較について
記事No11787
投稿日: 2017/01/19(Thu) 21:22
投稿者魔界の仮面弁士
> > If K = 2.3 Then
>
> 型は大きい方に丸められますので、結果として
>  If K = CSng(2.3R) Then 'これは True
> ではなく、
>  If CDbl(K) = 2.3R Then 'これは False
> として比較されます。


上記の部分について、もう少し具体的に書いてみます。


まず、浮動小数点数の内部表現を見てみましょう。

 Dim b1() As Byte = BitConverter.GetBytes(2.3)
 Dim K As Single = 2.3
 Dim b2() As Byte = BitConverter.GetBytes(K)

b1 は Double のバイナリなので 8 バイトです。
b2 は Single のバイナリなので 4 バイトです。


b1 のバイナリは 16進数で 66-66-66-66-66-66-02-40 です。
これは 2 進小数「10.010011001100110011001100110011001100110011001100110」を表しており、
10進数では『2.2999999999999998223643160599749535322189331054687500』にあたります。

一方、b2 のバイナリは 16進数で 33-33-13-40 です。
これは 2 進小数「10.0100110011001100110011」を表しており、
10進数では『2.29999995231628417968750』にあたります。


もしも
> If K = CSng(2.3R) Then
が行われていたとしたらどうなるでしょう。

右辺の CSng(2.3R) においては、2.3R すなわち、
『2.2999999999999998223643160599749535322189331054687500』
という値が、Single の精度に【縮小変換】されることになります。

この変換処理により、右辺は 33-33-13-40 のバイナリ、
すなわち『2.29999995231628417968750』になります。

左辺値も『2.29999995231628417968750』なので、同値として扱われます。



しかし実際には、
>  If CDbl(K) = 2.3R Then
に相当する処理が行われていました。

左辺の CDbl(K) により、『2.29999995231628417968750』が【拡大変換】で Double になりますが、
この結果は 00-00-00-60-66-66-02-40 のバイナリとなります。

これは、「10.010011001100110011001100000000000000000000000000000」な 2 進小数を意味しており、
10進小数『2.2999999523162841796875000000000000000000000000000000』に相当する数値です。

右辺値は「10.010011001100110011001100110011001100110011001100110」な 2 進小数、
すなわち『2.2999999999999998223643160599749535322189331054687500』な値なのですから、
両者は不一致とみなされるわけですね。

[ツリー表示へ]
タイトルRe^3: 数字の比較について(ありがとうございます)
記事No11790
投稿日: 2017/01/20(Fri) 09:38
投稿者SUZUKI
shuさん、魔界の仮面弁士さん回答ありがとうございます
また、詳細にわたって説明していただき申し訳ありません
じっくり読ませていただきます

vb6.0では
1.0  とすると 1# になりますが
2.3# とすると 2.3 になるので
リテラル表現がおろそかになっていました
これからは、 単なる数字でも
型を意識して行こうと思います

>すべての小数を Decimal で統一するにしても、たとえば「^ 演算子」ですとか、
>「PointF 型」「Math.Pow メソッド」などなど、Decimal が使えないケースは存在します。

よかった。^演算子が使えないですか、統一は止めときます

[ツリー表示へ]
タイトルRe^4: 数字の比較について(ありがとうございます)
記事No11791
投稿日: 2017/01/20(Fri) 19:35
投稿者魔界の仮面弁士
> よかった。^演算子が使えないですか

^演算の結果は常に Double 固定ですし、各項も事前に Double 化されます。
(VB.NET の ^演算子は、コンパイル時に Math.Pow での呼び出しへと変わります)


VB6/VBA も、第一項、第二項、演算結果すべてが Double 型で処理される仕様です。
――厳密に言えば、Variant型(内部形式 vbDouble もしくは vbNull)にもなりえますが。


とはいえ、Integer ^ Integer などの場合は、結果が整数であることが明確なので、
結果が Double だったとしても、そのまま結果を CInt や CLng するだけで十分かと。


もし、Decimal ^ Integer での演算を求めるなら、For ループで乗除算を繰り返すための
 Function Power(base As Decimal, exponent As Integer) As Decimal
な拡張メソッドを用意してやれば対処できます。



> 統一は止めときます

No11788 にも書きましたが、統一はするべきです。VB6 でも VB.NET でも。

ただ、すべてを一つのデータ型で補うのは現実的には無理なので、
要所要所で明示的に型変換することになりますね。

各データ型の特性を把握した上で、丸め誤差、桁落ち、情報落ちを
防ぐようなコーディングを心がけましょう。



VB.NET 型名   VBA の型名   サイズ     符号桁   有効桁数   小数部の長さ
-----------   ----------   -------    ------   --------   --------------------
Double        Double        64 bit    1 bit    1+52 bit   11 bit (-1022〜1023)
Single        Single        32 bit    1 bit    1+23 bit    8 bit (-126〜127)
Decimal       (Deciaml)    128 bit    8 bit      96 bit    8 bit (0〜28)
-             Currency      64 bit    1 bit      63 bit    0 bit (4)
ULong         -             64 bit    0 bit      64 bit    0 bit (なし)
Long          LongLong      64 bit    1 bit      63 bit    0 bit (なし)
UInteger      -             32 bit    0 bit      32 bit    0 bit (なし)
Integer       Long          32 bit    1 bit      31 bit    0 bit (なし)
UShort        -             16 bit    0 bit      16 bit    0 bit (なし)
Short         Integer       16 bit    1 bit      15 bit    0 bit (なし)
Byte          Byte           8 bit    0 bit       8 bit    0 bit (なし)
SByte         -              8 bit    1 bit       7 bit    0 bit (なし)
UIntPtr       -            32 / 64    0 bit     32 / 64    0 bit (なし)
IntPtr        LongPtr      32 / 64    1 bit     31 / 63    0 bit (なし)

[ツリー表示へ]
タイトル報告
記事No11792
投稿日: 2017/01/23(Mon) 11:47
投稿者SUZUKI
  魔界の仮面弁士さん 色々ありがとうございます
 少しソースが長いですが単純な計算式なので
 ご容赦願います

       Dim a As Double
        Dim b As Double
        Dim c As Double
        Dim d As Single
        Dim f As Double
        Dim g As Single
        Dim H As Single
        Dim J As Single
        Dim K As Single

        b = 2018.8
        c = 27.03
        d = 0.311
        f = 553
        H = 0.777
        g = 1
        J = 2
        K = 1

        a = b * c * H * d * g * (J / (K * f)) ^ 0.5

        MsgBox(a)

        Dim a1 As Double
        Dim b1 As Double
        Dim c1 As Double
        Dim d1 As Double
        Dim f1 As Double
        Dim g1 As Double
        Dim H1 As Double
        Dim J1 As Double
        Dim K1 As Double

        b1 = 2018.8
        c1 = 27.03
        d1 = 0.311
        f1 = 553
        H1 = 0.777
        g1 = 1
        J1 = 2
        K1 = 1

        a1 = b1 * c1 * H1 * d1 * g1 * (J1 / (K1 * f1)) ^ 0.5

        MsgBox(a1)

        Dim a2 As Decimal
        Dim b2 As Decimal
        Dim c2 As Decimal
        Dim d2 As Decimal
        Dim f2 As Decimal
        Dim g2 As Decimal
        Dim H2 As Decimal
        Dim J2 As Decimal
        Dim K2 As Decimal
        b2 = 2018.8
        c2 = 27.03
        d2 = 0.311
        f2 = 553
        H2 = 0.777
        g2 = 1
        J2 = 2
        K2 = 1

        a2 = b2 * c2 * H2 * d2 * g2 * (J2 / (K2 * f2)) ^ 0.5
        MsgBox(a2)

  上記で a は 現行のロジックです
   a1 は double 型の統一したもの
  a2 は decimal 型に統一したらどうなるか見ました

     a= 792.99973337178
     a1= 792.999989866276
     a2= 792.999989866276
    ここで 言いたいのは実は値をエクセルで検証していたのですが
  エクセルの値=792.999989866276

    自分はエクセルとvbの計算は合わないものと思っていました
  これが合うようになったのが実に感動ものです
 
  ありがとうございます  

[ツリー表示へ]
タイトルRe: 数字の比較について
記事No11793
投稿日: 2017/01/23(Mon) 14:54
投稿者魔界の仮面弁士
No.11792 への返信ですが、スレッドが深くなってきたので、
元質問の No11781 に繋げます。


> これが合うようになったのが実に感動ものです
> ありがとうございます

元質問は解決したようですが、もう少し補足させてください。


> 自分はエクセルとvbの計算は合わないものと思っていました
> これが合うようになったのが実に感動ものです

Excel の演算精度は、基本的には VB の Double 型と同じです。
https://support.microsoft.com/ja-jp/kb/78113/



> a2 は decimal 型に統一したらどうなるか見ました

いえ、これではまだ統一されていません。

先に回答したとおり、「Decimal ^ Decimal」の時点で Double 精度に落ちていますし、
「Decimal * Double」の演算は Double 型になってしまっていますので、
これはやはり、演算精度は Double のままとなってしまいます。


ひとまず、
 a2 = b2 * c2 * H2 * d2 * g2 * (J2 / (K2 * f2)) ^ 0.5
ではなく、暫定的に
 a2 = b2 * c2 * H2 * d2 * g2 * CDec((J2 / (K2 * f2)) ^ 0.5)
とすることで、演算結果の桁数は増えますが、見た目上精度が上がったように
見えるだけで、途中で Double 型が混じっている時点で、
Double 型を超える精度の結果が得られると言うわけでもありません。

平方根の演算部を、ニュートン法を用いた演算に差し替えるなどして、
Decimal 型を維持できるような処理が必要でしょう。

たとえば Excel VBA や VB6 なら、こんな方法があります。
[getBigSquareRoot]
http://www.vbaexpress.com/kb/getarticle.php?kb_id=887


>  a= 792.99973337178
> a1= 792.999989866276
> a2= 792.999989866276

VB6 でも、a と a1 は同様の結果になるでしょう。
a1 は Single 型、a2 や a3 は Double 型の精度ですね。

ちなみに a2 については、Variant 型を通じて演算することで
VB6 でも Decimal 演算を利用できます。
「 (J / (K * f)) ^ 0.5」が Double になってしまう点は変わりませんが…。


' VB6 の Decimal 演算
Dim a3, b3, c3, d3, f3, g3, H3, J3, K3
b3 = CDec("2018.8")
c3 = CDec("27.03")
d3 = CDec("0.311")
f3 = CDec("553")
H3 = CDec("0.777")
g3 = CDec("1")
J3 = CDec("2")
K3 = CDec("1")
'a3 = b3 * c3 * H3 * d3 * g3 * (J3 / (K3 * f3)) ^ 0.5
'a3 = b3 * c3 * H3 * d3 * g3 * CDec((J3 / (K3 * f3)) ^ 0.5)
'Call MsgBox(a3)



> エクセルの値=792.999989866276

とりあえず小数点以下 1000 桁まで求めてみました。

792.
99998 98662 75502 50219 88355 08659 99282 76083 41929 29331
45504 75192 88153 61645 79045 24809 03166 66313 06469 92862
40882 13367 36051 44300 05657 90842 24989 49252 38830 44803
30998 69961 91578 93238 27365 78310 15993 36082 14845 48677
75605 33965 97881 22750 00588 16356 67810 48442 39092 87815
93328 38654 80597 64153 07679 77261 53570 36281 37877 87682
31249 42717 83267 22791 59147 58105 01020 83200 16008 65155
35561 08568 70965 70057 26518 18795 69492 79896 39775 68793
07083 39204 42861 69416 24960 02805 75975 08171 89668 42391
39426 97149 39058 52156 56371 59953 81178 12076 30474 53677
44469 93453 82167 39304 49148 25715 27853 56867 52969 85833
93051 32840 85677 92793 40213 59407 57197 04683 45350 73337
08726 87805 10781 83023 17996 23860 03241 79108 58340 89929
62079 92284 33126 66774 03448 74046 70640 89447 83173 54567
07318 95161 51415 43476 61799 68639 54255 16090 35670 53632
61432 01943 46129 20599 44610 62012 87214 23860 35875 50456
53739 26704 66056 01936 81565 11765 23438 93339 33140 68792
32592 31586 47336 48987 61708 93958 29674 81588 04422 47022
22649 87403 32807 47686 10898 92530 57663 76117 85191 08672
33904 87068 32648 11566 33904 65108 02733 72607 73937 41756(以下略)


a の結果と比べると、6桁目(整数部3桁+小数部3桁目)までは正しいですが、それ以降の桁に差異があります。
a1 の結果と比べると、14桁目(整数部3桁+小数部11桁目)までは正しいですが、それ以降の桁に差異があります。

これは a が Single 相当、a1 が Double 型相当の演算であるためです。

No11791 の末尾にも書きましたが、
Single 型の有効桁数は 1+23 bit 相当で、
Double 型の有効桁数は 1+52bit 相当です。

そして、 Log10( 2 ^ 23 ) ≒ 9.9236899… なわけです。

2進数で 1+23 桁ということは、10進数に直すと、6.92〜7.22 桁相当の有効桁数。
2進数で 1+52 桁ということは、10進数に直すと、15.65〜15.95 桁相当の有効桁数。
先の a や a1 の精度とほぼ合致していますね。


小数値を Integer 型に格納しようとすれば、実際の値とは ±0.5 程度の誤差が発生しますが、
Double 型も同様です。有効桁数を超える精度の数値は正確に保持できません。

これは Decimal 型であっても同じこと。

仮に、暗黙的型変換による丸めを排除し、X ^ 0.5 を Decimal として
処理できたとしても、保証できる精度は有効桁数までです。


そしてあくまでも「近似値」である以上、最初のコードのように
 If K = 2.3 Then
という判定にしてしまうと、不一致と看做される可能性が生じます。


そこで、誤差を許容できる範囲を定めて、
 If Math.Abs(K - 2.3) < epsilon Then
のように ± の範囲で判定するといった手法がしばしば用いられます。

どの程度の精度を求めるかは、開発するアプリケーションの仕様にもよりますが
上記だとたとえば、epsilon 値は 1E-6 〜 1E-7 ぐらいですかね。

[許容範囲を決めて値を比較する]
http://dobon.net/vb/dotnet/beginner/floatingpointerror.html#section6

[浮動小数点数の等値比較]
http://qiita.com/fujieda/items/90d5465c887f2607e21a

[ツリー表示へ]