(NET) NET (2003 год)

Round - Округление это не просто!

Поскольку я занимался написанием банковских систем вопросы точности расчетов и количесва значащих цифр в подсчетах имеют для меня первостепенное значение. В этом топике я решил чуть-чуть подъитожить эту тему.

В качеcтве начала для этого разговора об округлениях я рекомендую ознакомиться вот с этой микрософтовской статьей, в которой собственно и обозначены существующие ТИПЫ ОКРУГЛЕНИЙ:

Теперь, когда становится ясно, что вопрос округления не так прост, как кажется на первый взгляд, я приведу фонкцию БАНКОВСКОГО округления для SQL с сайта sqlservercentral:

00001: create FUNCTION RoundBanker
00002: ( @Amt numeric(38,16)
00003: , @RoundToDecimal tinyint
00004: )
00005: RETURNS numeric(38,16)
00006: AS
00007: BEGIN
00008: declare @RoundedAmt numeric(38,16)
00009: , @WholeAmt integer
00010: , @Decimal tinyint
00011: , @Ten numeric(38,16)
00012: set @Ten = 10.0
00013: set @WholeAmt = ROUND(@Amt,0, 1 )
00014: set @RoundedAmt = @Amt - @WholeAmt
00015: set @Decimal = 16
00016: While @Decimal > @RoundToDecimal
00017: BEGIN
00018: set @Decimal = @Decimal - 1
00019: if 5 = ( ROUND(@RoundedAmt * POWER( @Ten, @Decimal + 1 ) ,0,1) - (ROUND(@RoundedAmt * POWER( @Ten, @Decimal ) ,0,1) * 10) )
00020: and 0 = cast( ( ROUND(@RoundedAmt * POWER( @Ten, @Decimal ) ,0,1) - (ROUND(@RoundedAmt * POWER( @Ten, @Decimal - 1 ) ,0,1) * 10) ) AS INTEGER ) % 2
00021: SET @RoundedAmt = ROUND(@RoundedAmt,@Decimal, 1 )
00022: ELSE
00023: SET @RoundedAmt = ROUND(@RoundedAmt,@Decimal, 0 )
00024: END
00025: RETURN ( @RoundedAmt + @WholeAmt )
00026: END
00027: GO

В целом же, на уровне SQL существуют следующие типы округлений:

00001: declare @i int, @S1 decimal (10,4) , @S2 decimal (10,4),  @S3 decimal (10,4), @S4 decimal (10,4), @S5 decimal (10,4), @S6 decimal (10,4)
00002: select @i=0, @S1=0, @S2=0,  @S3=0, @S4=0 , @S5=0 , @S6=0
00003: While @i<100 Begin
00004: select @i=@i+1,  
00005:        @S1=@S1+     Floor      (cast('0.'+ cast(@i as varchar) as decimal(10,4))  ),
00006:        @S2=@S2+     Left       (cast('0.'+ cast(@i as varchar) as decimal(10,4)),3),
00007:        @S3=@S3+                (cast('0.'+ cast(@i as varchar) as decimal(10,4))  ),
00008:        @S4=@S4+     Round      (cast('0.'+ cast(@i as varchar) as decimal(10,4)),1),
00009:        @S5=@S5+ dbo.RoundBanker(cast('0.'+ cast(@i as varchar) as decimal(10,4)),1),
00010:        @S6=@S6+     Ceiling    (cast('0.'+ cast(@i as varchar) as decimal(10,4))  )
00011: 
00012: End
00013: select @S1 as 'Floor()',@S2 as 'Left(,3)', @S3 as 'Без округления', @S4 as 'Round(,1)', @S5 as 'RoundBanker(,1)',  @S6 as 'Ceiling()' 
Которые дают следующие результаты:
Floor()  Left(,3) Без округления Round(,1) RoundBanker(,1)  Ceiling() 
-------  -------- -------------- --------------- --------- ---------
0.0000   49.6000  53.6500        54.1000   53.7000          100.0000

После того, как вы осознаете разницу в этих цифрах (особенно при подсчете одной и той же суммы денег) вы поймете важность такой казалось бы малозначительной темы как округление...Но продолжим...
В шестом бейсике существуют следующие типа округлений:
00001: Private Sub Form_Load()
00002: 'Бейсик не позволяет напрямую создать тип DECIMAL - только VARIANT с подтипом CDEC, ну или так:
00003: Dim S1 As Currency, S2 As Currency, S3 As Currency, S4 As Currency, S5 As Currency
00004: For i = 0 To 99
00005:     S1 = S1 + Excel.WorksheetFunction.RoundDown(CDec("0." & CStr(i)), 1)
00006:     S2 = S2 + CDec("0." & CStr(i))
00007:     S3 = S3 + Round(CDec("0." & CStr(i)), 1)
00008:     S4 = S4 + Excel.WorksheetFunction.Round(CDec("0." & CStr(i)), 1)
00009:     S5 = S5 + Excel.WorksheetFunction.RoundUp(CDec("0." & CStr(i)), 1)
00010: Next
00011: Debug.Print "Excel.RoundDown", "Без округления", "Round", "Excel.Round", "Excel.RoundUP"
00012: Debug.Print S1, S2, S3, S4, S5
00013: End Sub
Которые дают следующие результаты:
Excel.RoundDown   Без округления    Round      Excel.Round   Excel.RoundUP
49.5              53.55             53.6       54            57.6 

Как видите, в результаты округлений в бейсике практически не совпададают с округлениями на уровне SQL... Хотя подсчитывается та же самая сумма вклада!
Именно поэтому в .NET существуют десятки различных механизмов округлений. Вот механизмы для типа DECIMAL:
00001:     Sub Main()
00002:         Dim S1 As Decimal, S2 As Decimal, S3 As Decimal, S4 As Decimal, S5 As Decimal, S6 As Decimal, S7 As Decimal
00003:         Dim S8 As Decimal, S9 As Decimal, S10 As Decimal, S11 As Decimal, S12 As Decimal, S13 As Decimal, S14 As Decimal
00004:         For i = 0 To 99
00005:             S1 += CDec("0." & i)
00006: 
00007:             S2 += Math.Round(CDec("0." & i), 1)
00008:             S3 += Math.Round(CDec("0." & i), 1, MidpointRounding.AwayFromZero)
00009:             S4 += Math.Round(CDec("0." & i), 1, MidpointRounding.ToEven)
00010:             'предыдущие три способа окрушления можно получить и через Decimal.Round
00011:             S5 += Decimal.ToOACurrency(CDec("0." & i))
00012:             '
00013:             S6 += Math.Truncate(CDec("0." & i))
00014:             S7 += Math.Floor(CDec("0." & i))
00015:             S8 += Math.Ceiling(CDec("0." & i))
00016:             '
00017:             S9 += CDec(System.Data.SqlTypes.SqlDecimal.Round(CDec("0." & i), 1))
00018:             S10 += CDec(System.Data.SqlTypes.SqlDecimal.Ceiling(CDec("0." & i)))
00019:             S11 += CDec(System.Data.SqlTypes.SqlDecimal.Floor(CDec("0." & i)))
00020:             '
00021:             'В .NET2 есть также множество других функций округления, например в  System.Xml.Xsl.Runtime.XsltFunctions()
00022:         Next
00023:         Console.WriteLine("Без округления: " & CStr(S1))
00024:         Console.WriteLine("Math.Round: " & CStr(S2))
00025:         Console.WriteLine("Math.Round (,,MidpointRounding.AwayFromZero):  " & CStr(S3))
00026:         Console.WriteLine("Math.Round (,,MidpointRounding.ToEven): " & CStr(S4))
00027:         Console.WriteLine("Decimal.ToOACurrency: " & CStr(S5))
00028:         Console.WriteLine("Math.Truncate: " & CStr(S6))
00029:         Console.WriteLine("Math.Floor: " & CStr(S7))
00030:         Console.WriteLine("Math.Ceiling: " & CStr(S8))
00031:         Console.WriteLine("SqlTypes.SqlDecimal.Round: " & CStr(S9))
00032:         Console.WriteLine("SqlTypes.SqlDecimal.Ceiling: " & CStr(S10))
00033:         Console.WriteLine("SqlTypes.SqlDecimal.Floor: " & CStr(S11))
00034:         Console.ReadLine()
00035:     End Sub
Которые дают следующие результаты:
Без округления: Math.Round:  Math.Round (,,MidpointRounding.AwayFromZero): Math.Round (,,MidpointRounding.ToEven): Decimal.ToOACurrency:
53.55	        53.6        54.0                                          53.6                                    535500
 
Math.Truncate:  Math.Floor:  Math.Ceiling:      SqlTypes.SqlDecimal.Round:     SqlTypes.SqlDecimal.Ceiling:    SqlTypes.SqlDecimal.Floor:     
0               0            99                 54.00                          99                              0

Не правда ли, впечатляющие различия !!! А ведь это та же сумма, что подсчитывалась и в SQL и на шестерке!!! Есть даже такая банковская шутка - если у клиента крепкая крыша - то округляем как RoundUP, если крыша дохлая, то округляем RoundDown, а если клиент вообще лох - то округляем его вклад до нуля !!! Ну взлянув на различия в способах округления - можно осознать, что в этой шутке только небольшая доля шутки...

Именно в силу столь различных результатов округлений .NET просто ЗАТОЧЕН для написания СОБСТВЕННЫХ типов округлений. Помимо переопределения функций округления и сравнения (как любых других стандарных функций общего плана) в пространстве имен DECIMAL есть куча методов типа Decimal.op_GreaterThanOrEqual, позволяющих САМОМУ переопределить в коде как именно поступать с пятеркой (или иной цифрой) в последнем знаке.

Кроме того, в NET2 можно вызвать и методы рабочей страницы EXCEL - только почему-то Враппер COM-NET поддерживает только числа с плавающей запятой (но не с DECIMAL). Впрочем есть метод - на тесте он приведен - получить напрямую представление DECIMAL в NET и разобрать его самому. Кроме того, как видите выше - есть методы из пространства имен SqlTypes, однако по странному стечению обстоятельств реультаты они дают ИНЫЕ, чем в SQL.

У меня также вызывает удивление тот факт, что, несмотря на утверждения вышеуказанной микрософтовской статьи, результаты округлений в VB6 - SQL - NET практически не совпадают. А ведь есть еще и VBscript и Jscript! Кроме того, в пространстве Windows.Forms есть куча совершенно ИНЫХ методов округлений (обычно используемых для графики). Кроме того, свое округление есть в XSLT-преобразованиях, как на уровне SQL, так и на уровне System.XML


Кроме того для получения правильных результатов, надо еще учитывать количество ЗНАЧАЩИХ знаков. Ну собственно как и при любых арифметических операциях вообще. О чем речь? А о том, что если два числа известны с некоторой точностью, например 28,65 +/- 0,01 и 28,66 +/- 0,01 то при их умножении и делении точность повышается, в данном случае станет 0,005, а вот при вычитании двух вышеприведенных чисел результат будет РАВЕН НУЛЮ. Как это ни парадоксально может показаться кому-то. Поэтому вопросы округления следует рассматривать в комплексе с вопросами ТОЧНОСТИ результата.


Думаю, после прочтения этой моей заметки вы проникнетесь уважением к слову ОКРУГЛЕНИЕ...



Comments ( )
<00>  <01>  <02>  <03>  <04>  <05>  <06>  <07>  <08>  <09>  <10>  <11>  <12>  <13>  <14>  <15>  <16>  <17>  <18>  <19>  <20>  <21>  <22>  <23
Link to this page: //www.vb-net.com/windows/round/index.htm
<SITEMAP>  <MVC>  <ASP>  <NET>  <DATA>  <KIOSK>  <FLEX>  <SQL>  <NOTES>  <LINUX>  <MONO>  <FREEWARE>  <DOCS>  <ENG>  <CHAT ME>  <ABOUT ME>  < THANKS ME>