- 題名: 変数に格納した計算結果とイミディエイトウィンドウでの計算結果が異なる
- 日時: 2013/07/17 17:09:02
- ID: 31715
- この記事の返信元:
- (なし)
- この記事への返信:
- [31718] Re[1]: 変数に格納した計算結果とイミディエイトウィンドウでの計算結果が異なる2013/07/18 0:40:47
- ツリーを表示
>>魔界の仮面弁士様
> ありゃ。ということは、現象を再現できるコードは提示して頂けないのでしょうか…。
現象が再現される最小構成のものを作成したので、提示します。
Public Class Form1
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim cls As New Discount
cls.purchase_amount = 279350
cls.discount_rate = 0.03
cls.rounding_type = Discount.ROUNDING_TYPE_NONE
TextBox1.Text = cls.discount_amount
'TextBox1.Text の中身は、【8380】となる。
End Sub
End Class
Public Class Discount
Public Const ROUNDING_TYPE_NONE As String = "0"
Public Const ROUNDING_TYPE_TRUNCATION As String = "1"
Public Const ROUNDING_TYPE_CONCLUSION As String = "2"
Public Const ROUNDINF_TYPE_ROUNDED As String = "3"
Public Property purchase_amount As Long
Public Property discount_rate As Single
Public Property rounding_type As String
Public ReadOnly Property discount_amount As Integer
Get
Return getDiscountAmount()
End Get
End Property
Private Function getDiscountAmount() As Integer
Dim d As Double = Math.Round(_purchase_amount * _discount_rate, MidpointRounding.AwayFromZero)
Dim bin = BitConverter.GetBytes(d)
'binの中身
'(0):0
'(1):0
'(2):0
'(3):0
'(4):0
'(5):94
'(6):192
'(7):64
If _rounding_type = ROUNDING_TYPE_NONE Then
Return CInt(d)
ElseIf _rounding_type = ROUNDING_TYPE_TRUNCATION Then
'1の位切り捨て
Return CInt(Math.Truncate(d / 10) * 10)
ElseIf _rounding_type = ROUNDING_TYPE_CONCLUSION Then
'1の位切り上げ
Return CInt(Math.Ceiling(d / 10) * 10)
Else
'1の位四捨五入
Return CInt(Math.Round(d / 10, 0, MidpointRounding.AwayFromZero) * 10)
End If
End Function
End Class
SingleとDoubleは概数だから…というのは理解しましたが、
小数第一位が0の数値の、1の位までまるまってしまうのは、どうにも納得しがたいものが…
■No31722に返信(やむさんの記事) イミディエイトウィンドウで ?Math.Round(_purchase_amount * CDbl(_discount_rate), MidpointRounding.AwayFromZero) とすると同じ結果が得られるかと思います。 イミディエイトウィンドウの計算がSingleのまま計算されるのに対し 実際の処理ではMath.RoundはSingleを要求するものがないためDoubleを 要求してしまうため上記の計算が行われていると考えられます。 なので希望する処理を行いたければ Dim s as Single = _purchase_amount * _discount_rate Dim d as Double = Math.Round(s, MidpointRounding.AwayFromZero) とすればよいです。 ただこういう問題をさけるにはSingle,Doubleを使わないで十進型のDecimalを 使われた方がよいかと思います。Single,Doubleはそんな事を気にしないだいたいの 数値がわかれば良い時だけにしておいた方がよいです。
■No31722に返信(やむさんの記事)
> SingleとDoubleは概数だから…というのは理解しましたが、
Single や Double は 2 進小数であるため、.5 な値は正確に表現できます。
そのため、今回の演算結果である「8380.5」は正確に表現できますし、
それによる Math.Round の結果に差異はありません。
では、どこで今回のような差が生じたのかといえば、それは演算結果では無く、
演算途中の Single と Double の精度の違いにあります。
> 小数第一位が0の数値の、1の位までまるまってしまうのは、どうにも納得しがたいものが…
気にするべき場所はそこではありません。
バイナリ表現を踏まえた上で、もう少し具体的に説明してみましょうか。
少し長くなりますが御付き合い下さい。
> _discount_rate = 0.03
そもそも浮動小数点型は、データを2進小数で管理しています。
10÷3 の結果を、10進小数で表現するのが難しいように(3進小数なら問題なし)、
3÷10 の結果を、2進小数で表現することも難しいのです(10進小数なら問題なし)。
なので、0.3 や 0.03 といった値を正確に保持したいのであれば、
2進小数で管理される Single や Double ではなく、
10進にて管理される Decimal を使うべきである、というのが大原則です。
> TextBox1.Text = cls.discount_amount
左辺が String 型、右辺が Integer 型になってしまっていますよね。
普段から『データ型』を意識した方が良いですよ。まぁ、それはさておき。
> Dim d As Double = Math.Round(_purchase_amount * _discount_rate, MidpointRounding.AwayFromZero)
さて本題。
まず上記の「_purchase_amount * _discount_rate」ですが、VB の仕様上、
「Integer * Single」の演算結果は「Single」となります。
しかし、Math.Round は Single を受け付けないため、ここで「Double」に
変換されてから処理され、ここで問題が生じているわけです。
(既に shu さんが解説して下さっていますね)
ちなみに、「Integer * Double」や「Double * Single」の演算結果は「Double」です。
ということで、ここで Single と Double の差を見てみましょう。
たとえば、イミディエイトにて、型を明示した下記の演算を行ってみてください。
(末尾の R や # は Double 型を表し、末尾の F や ! は Single 型を意味します)
? 0.03R - 0.03F
? 0.03# - 0.03!
その答えはゼロとはならず、「0.00000000067055225261292151」という微細値で表現されます。
VB では、「Double - Single」は「Double」となる仕様の為、Single→Double変換が生じるためです。
具体的には、下記に相当する処理が途中で発生しているというワケです。
? CDbl(CSng("0.03"))
? CDbl(0.03F)
これらの結果は「0.029999999329447746」となります。
「? 279350F * 0.03F」や「? 279350R * 0.03R」は「8380.5」ですが、
「? 279350R * 0.03F」だと「8380.4998126812279」になってしまいます。
この .5000 と .4998 の違いが、五捨五入(ROUNDING_TYPE_NONE)や
四捨五入(ROUNDINF_TYPE_ROUNDED)の結果に影響を与えています。
では、そもそも何故、CDbl(0.03F) は 0.03R にならないのかというと、
それは浮動小数点数が「概数」であるため、値の間隔が異なっているからです。
…と言っても何のことか分からないと思いますので、具体例を伴って示してみます。
まず、0.03 を Single 型で扱った場合の内部バイナリを見てみると、これは
&H3CF5C28Fui となります。一緒に、その前後値も掲載しておきます。★の箇所が 0.03 のバイナリです。
&H3CF5C26Fui → 2進小数 0.00000111101011100001010001101 = 10進小数 0.02999999560415744781494140625 ≒ 0.02999995
&H3CF5C27Fui → 2進小数 0.00000111101011100001010001110 = 10進小数 0.02999999746680259704589843750 ≒ 0.02999997
★&H3CF5C28Fui → 2進小数 0.00000111101011100001010001111 = 10進小数 0.02999999932944774627685546875 ≒ 0.03000000
&H3CF5C29Fui → 2進小数 0.00000111101011100001010010000 = 10進小数 0.03000000119209289550781250000 ≒ 0.03000003
&H3CF5C2AFui → 2進小数 0.00000111101011100001010010001 = 10進小数 0.03000000305473804473876953125 ≒ 0.03000005
それぞれ、約 0.00000002 ほどの間隔がありますね。
有効桁数の関係上、この間隔は 0 に近い値ほど細かくなり、0 から遠ざかるほど荒くなります。
また、Single よりも Double の方が、より細かい間隔で表現できることになります。
このことが、先のような Single → Double 変換への問題を生じます。
先の実験で、CDbl(0.03F) の結果はイミディエイトでは「0.029999999329447746」でしたので、
0.03R《★マーク》付近のバイナリと、CDbl(0.03F)《☆マーク》付近のバイナリを、それぞれ見ていきましょう。
&H3F9EB851DFFFFFFEuL → 2進小数 0.0000011110101110000101000111011111111111111111111111111110
= 10進小数 0.0299999993294477393379615648427716223523020744323730468750
≒ 0.029999999329447739
&H3F9EB851DFFFFFFFuL → 2進小数 0.0000011110101110000101000111011111111111111111111111111111
= 10進小数 0.0299999993294477428074085167963858111761510372161865234375
≒ 0.029999999329447743
☆&H3F9EB851E0000000uL → 2進小数 0.0000011110101110000101000111100000000000000000000000000000
= 10進小数 0.0299999993294477462768554687500000000000000000000000000000
≒ 0.029999999329447746
&H3F9EB851E0000001uL → 2進小数 0.0000011110101110000101000111100000000000000000000000000001
= 10進小数 0.0299999993294477497463024207036141888238489627838134765625
≒ 0.02999999932944775
&H3F9EB851E0000002uL → 2進小数 0.0000011110101110000101000111100000000000000000000000000010
= 10進小数 0.0299999993294477532157493726572283776476979255676269531250
≒ 0.029999999329447753
--------------------
&H3F9EB851EB851EB6uL → 2進小数 0.0000011110101110000101000111101011100001010001111010110110
= 10進小数 0.0299999999999999919508830714676150819286704063415527343750
≒ 0.029999999999999992
&H3F9EB851EB851EB7uL → 2進小数 0.0000011110101110000101000111101011100001010001111010110111
= 10進小数 0.0299999999999999954203300234212292707525193691253662109375
≒ 0.029999999999999995
★&H3F9EB851EB851EB8uL → 2進小数 0.0000011110101110000101000111101011100001010001111010111000
= 10進小数 0.0299999999999999988897769753748434595763683319091796875000
≒ 0.03
&H3F9EB851EB851EB9uL → 2進小数 0.0000011110101110000101000111101011100001010001111010111001
= 10進小数 0.0300000000000000023592239273284576484002172946929931640625
≒ 0.030000000000000002
&H3F9EB851EB851EBAuL → 2進小数 0.0000011110101110000101000111101011100001010001111010111010
= 10進小数 0.0300000000000000058286708792820718372240662574768066406250
≒ 0.030000000000000006
つまり、CDbl(0.03F) というのは、CDbl("0.03") に相当する変換では無く、
CDbl("0.0299999993294477463") 相当の変換が生じているということです。
■No31726に返信(shuさんの記事)
>>★&H3CF5C28Fui → 2進小数 0.00000111101011100001010001111 = 10進小数 0.02999999932944774627685546875 ≒ 0.03000000
> ここの 『0.02999999932944774627685546875』がDoubleに変換され
> 有効桁数が増えるので
> 0.03
> ではなく
> 0.029999999329447746
> がDoubleの値として使われているのではないでしょうか?
VB.NET だけではなく、VBA でも同様でした。蛇足までに。
' ---- VBA ----
? CDbl(CSng("0.03"))
2.99999993294477E-02
? CDbl(CSng("0.3"))
0.300000011920929
一応、数値リテラルで指定した場合と、変数から指定した場合について。
Module Module1
Sub Main()
Dim f1 As Single = 0.03F ' ldc.r4 0.03
Dim f2 As Single = 0.03R ' ldc.r4 0.03
Dim f3 As Single = 0.03D ' ldc.r4 0.03
Dim r1 As Double = 0.03F ' ldc.r8 0.029999999329447746
Dim r2 As Double = 0.03R ' ldc.r8 0.03
Dim r3 As Double = 0.03D ' ldc.r8 0.03
Dim d1 As Decimal = 0.03F ' New Decimal(&H31086e8d, &H110d9, 0, 0, 16)
Dim d2 As Decimal = 0.03R ' New Decimal(3, 0, 0, 0, 2)
Dim d3 As Decimal = 0.03D ' New Decimal(3, 0, 0, 0, 2)
Console.WriteLine("数値リテラルからの直接指定")
Console.WriteLine("Float : {0}, {1}, {2}", f1, f2, f3)
Console.WriteLine("Real : {0}, {1}, {2}", r1, r2, r3)
Console.WriteLine("Decimal: {0}, {1}, {2}", d1, d2, d3)
Dim r4 As Double = f1 ' conv.r8
Dim r5 As Double = f2 ' conv.r8
Dim r6 As Double = f3 ' conv.r8
Dim d4 As Decimal = f1 ' New Decimal( Single )
Dim d5 As Decimal = f2 ' New Decimal( Single )
Dim d6 As Decimal = f3 ' New Decimal( Single )
Console.WriteLine("Single 変数からの拡大変換")
Console.WriteLine("Real : {0}, {1}, {2}", r4, r5, r6)
Console.WriteLine("Decimal: {0}, {1}, {2}", d4, d5, d6)
Console.ReadLine()
End Sub
End Module
'-----------------
数値リテラルからの直接指定
Float : 0.03, 0.03, 0.03
Real : 0.0299999993294477, 0.03, 0.03
Decimal: 0.0299999993294477, 0.03, 0.03
Single 変数からの拡大変換
Real : 0.0299999993294477, 0.0299999993294477, 0.0299999993294477
Decimal: 0.03, 0.03, 0.03
分類:[.NET]