Dlaczego Math.Round (2.5) zwraca 2 zamiast 3?

415

W języku C # wynikiem Math.Round(2.5)jest 2.

To ma być 3, prawda? Dlaczego jest to 2 zamiast w C #?

jeffu
źródło
5
To właściwie funkcja. Zobacz <a href=" msdn.microsoft.com/en-us/library/… Dokumentacja MSDN</a> . Ten rodzaj zaokrąglania jest znany jako zaokrąglanie bankiera. Jeśli chodzi o obejście, istnieje <a href = " msdn. microsoft.com/en-us/library/... przeciążenie </a>, które pozwala dzwoniącemu na określenie sposobu zaokrąglania.
Joe
1
Najwyraźniej metoda okrągłe, gdy zostanie poproszona o zaokrąglenie liczby dokładnie między dwiema liczbami całkowitymi, zwraca parzystą liczbę całkowitą. Zatem Math.Round (3.5) zwraca 4. Zobacz ten artykuł
Matthew Jones
20
Math.Round(2.5, 0, MidpointRounding.AwayFromZero);
Robert Durgin
SQL Server zaokrągla w ten sposób; ciekawe wyniki testu, gdy istnieje test Ci jednostki ti sprawdzający zaokrąglenie wykonane w T-SQL.
idstam
7
@amed to nie jest błąd. Tak działa binarne zmiennoprzecinkowe. 1.005nie może być dokładnie reprezentowany podwójnie. To pewnie 1.00499.... Jeśli użyjesz Decimaltego problemu, zniknie. Istnienie przeciążenia Math.Round, które wymaga podwójnej liczby cyfr dziesiętnych, jest wątpliwym wyborem IMO, ponieważ rzadko działa w znaczący sposób.
CodesInChaos

Odpowiedzi:

560

Po pierwsze, i tak nie byłby to błąd C # - byłby to błąd .NET. C # to język - nie decyduje o sposobie Math.Roundimplementacji.

Po drugie, nie - jeśli przeczytasz dokumenty , zobaczysz, że domyślne zaokrąglenie to „zaokrąglenie do parzystego” (zaokrąglenie bankiera):

Zwracana wartość
Typ: System.Double
Liczba całkowita najbliższa a. Jeśli ułamkowa część a znajduje się w połowie drogi między dwiema liczbami całkowitymi, z których jedna jest parzysta, a druga nieparzysta, zwracana jest liczba parzysta. Zauważ, że ta metoda zwraca Doublezamiast typu całkowego.

Uwagi
Zachowanie tej metody jest zgodne z normą IEEE 754, sekcja 4. Ten rodzaj zaokrąglania jest czasem nazywany zaokrąglaniem do najbliższego lub zaokrąglaniem bankierskim. Minimalizuje błędy zaokrąglania wynikające z konsekwentnego zaokrąglania wartości punktu środkowego w jednym kierunku.

Możesz określić, jak Math.Roundzaokrąglić punkty środkowe, używając przeciążenia, które przyjmuje MidpointRoundingwartość. Jest jedno przeciążenie MidpointRoundingodpowiadające każdemu z przeciążeń, które go nie ma:

To, czy to ustawienie domyślne zostało dobrze wybrane, czy nie, to inna sprawa. ( MidpointRoundingzostał wprowadzony tylko w .NET 2.0. Do tego czasu nie jestem pewien, czy istniał jakiś łatwy sposób na wdrożenie pożądanego zachowania bez robienia tego sam.) W szczególności historia pokazała, że ​​nie jest to oczekiwane zachowanie - i w większości przypadków jest to grzech główny w projekcie API. Rozumiem, dlaczego zaokrąglanie za pomocą bankiera jest przydatne ... ale dla wielu wciąż jest zaskoczeniem.

Być może zainteresuje Cię najbliższa enum Java (enum RoundingMode), która oferuje jeszcze więcej opcji. (Nie dotyczy to tylko punktów środkowych).

Jon Skeet
źródło
4
nie wiem, czy to błąd, myślę, że to był projekt, ponieważ .5 jest tak blisko najbliższej najniższej liczby całkowitej, jak i najbliższej najwyższej liczby całkowitej.
Stan R.,
3
Pamiętam to zachowanie w VB przed zastosowaniem .NET.
John Fiala
7
Rzeczywiście, IEEE Standard 754, sekcja 4, jak stanowi dokumentacja.
Jon Skeet
2
Już dawno temu to mnie poparzyło i pomyślałem, że to też zwykła obłęd. Na szczęście dodali sposób na określenie zaokrąglenia, którego wszyscy nauczyliśmy się w szkole podstawowej; MidPointRounding.
Shea
26
+1 za „to nie jest oczekiwane zachowanie [...] to
główny
215

Nazywa się to zaokrąglaniem do parzystego (lub zaokrąglania bankowego), co jest prawidłową strategią zaokrąglania w celu minimalizacji narastających błędów sum (MidpointRounding.ToEven). Teoria jest taka, że ​​jeśli zawsze zaokrąglasz liczbę 0,5 w tym samym kierunku, błędy będą narastały szybciej (zaokrąglanie do parzystej ma to zminimalizować) (a) .

Skorzystaj z tych linków, aby uzyskać opisy MSDN:

  • Math.Floor, który zaokrągla w dół w kierunku ujemnej nieskończoności.
  • Math.Ceiling, który zaokrągla w górę w kierunku dodatniej nieskończoności.
  • Math.Truncate, który zaokrągla w górę lub w dół w kierunku zera.
  • Math.Round, który zaokrągla do najbliższej liczby całkowitej lub określonej liczby miejsc dziesiętnych. Możesz określić zachowanie, jeśli jest dokładnie w jednakowej odległości między dwiema możliwościami, takimi jak zaokrąglanie, tak aby ostatnia cyfra była parzysta („ Round(2.5,MidpointRounding.ToEven)” staje się 2) lub tak, że jest dalej od zera („ Round(2.5,MidpointRounding.AwayFromZero)” staje się 3).

Poniższy schemat i tabela mogą pomóc:

-3        -2        -1         0         1         2         3
 +--|------+---------+----|----+--|------+----|----+-------|-+
    a                     b       c           d            e

                       a=-2.7  b=-0.5  c=0.3  d=1.5  e=2.8
                       ======  ======  =====  =====  =====
Floor                    -3      -1      0      1      2
Ceiling                  -2       0      1      2      3
Truncate                 -2       0      0      1      2
Round(ToEven)            -3       0      0      2      3
Round(AwayFromZero)      -3      -1      0      2      3

Zauważ, że Roundjest o wiele potężniejszy niż się wydaje, po prostu dlatego, że może zaokrąglać do określonej liczby miejsc po przecinku. Wszystkie pozostałe zawsze zaokrąglają do dziesiętnych. Na przykład:

n = 3.145;
a = System.Math.Round (n, 2, MidpointRounding.ToEven);       // 3.14
b = System.Math.Round (n, 2, MidpointRounding.AwayFromZero); // 3.15

W przypadku innych funkcji musisz użyć funkcji zwielokrotniania / dzielenia, aby osiągnąć ten sam efekt:

c = System.Math.Truncate (n * 100) / 100;                    // 3.14
d = System.Math.Ceiling (n * 100) / 100;                     // 3.15

(a) Oczywiście, teoria ta zależy od tego, że twoje dane mają dość równomierny rozkład wartości na parzyste połowy (0,5, 2,5, 4,5, ...) i nieparzyste połowy (1,5, 3,5, ...).

Jeśli wszystkie „pół-wartości” są równe (na przykład), błędy będą kumulować się tak szybko, jakbyś zawsze zaokrąglał w górę.

paxdiablo
źródło
3
Znany również jako Banker's
Rounding
Dobre wytłumaczenie! Chciałem zobaczyć na własne oczy, w jaki sposób kumuluje się błąd, i napisałem skrypt, który pokazuje, że wartości zaokrąglone za pomocą zaokrąglenia bankowego na dłuższą metę mają swoje sumy i średnie wartości znacznie bliższe wartościom pierwotnym. github.com/AmadeusW/RoundingDemo (zdjęcia dostępnych działek)
Amadeusz Wieczorek
Krótko po tym: czy etykać (= 2.8) nie powinno być dalej, niż 2tyka?
superjos,
Prosty sposób na zapamiętanie, przy założeniu, że dziesiąte miejsce to 5: - wszystkie miejsca i dziesiąte są nieparzyste = zaokrąglić w górę - jedno miejsce i dziesiąte miejsce są mieszane = zaokrąglone w dół * Zero nie jest nieparzyste * Odwrócone dla liczb ujemnych
Arkham Angel
@ArkhamAngel, to wydaje się trudniejsze do zapamiętania niż po prostu „zrób nawet ostatnią cyfrę” :-)
paxdiablo
42

Z MSDN zwraca Math.Round (double a) :

Liczba całkowita najbliższa a. Jeśli ułamkowa część a znajduje się w połowie drogi między dwiema liczbami całkowitymi, z których jedna jest parzysta, a druga nieparzysta, zwracana jest liczba parzysta.

... i tak 2,5, będące w połowie między 2 a 3, jest zaokrąglane w dół do liczby parzystej (2). nazywa się to zaokrąglaniem za pomocą Bankera (lub zaokrąglaniem do parzystości) i jest powszechnie stosowanym standardem zaokrąglania.

Ten sam artykuł MSDN:

Zachowanie tej metody jest zgodne z normą IEEE 754, sekcja 4. Ten rodzaj zaokrąglania jest czasami nazywany zaokrąglaniem do najbliższego lub zaokrąglania bankierskiego. Minimalizuje błędy zaokrąglania wynikające z konsekwentnego zaokrąglania wartości punktu środkowego w jednym kierunku.

Możesz określić inne zachowanie zaokrąglania, wywołując przeciążenia Math.Round, które przyjmują MidpointRoundingtryb.

Michael Petrotta
źródło
37

Powinieneś sprawdzić MSDN pod kątem Math.Round:

Zachowanie tej metody jest zgodne z normą IEEE 754, sekcja 4. Ten rodzaj zaokrąglania jest czasami nazywany zaokrąglaniem do najbliższego lub zaokrąglania bankierskiego.

Możesz określić zachowanie Math.Roundprzy użyciu przeciążenia:

Math.Round(2.5, 0, MidpointRounding.AwayFromZero); // gives 3

Math.Round(2.5, 0, MidpointRounding.ToEven); // gives 2
Dirk Vollmar
źródło
31

Charakter zaokrąglania

Rozważ zadanie zaokrąglenia liczby zawierającej ułamek do, powiedzmy, liczby całkowitej. Proces zaokrąglania w takich okolicznościach polega na określeniu, która liczba całkowita najlepiej odpowiada zaokrąglanej liczbie.

W przypadku zaokrąglania wspólnego lub „arytmetycznego” jasne jest, że 2.1, 2.2, 2.3 i 2.4 zaokrąglają do 2,0; oraz 2.6, 2.7, 2.8 i 2.9 do 3.0.

Pozostawia to 2.5, co nie jest bliższe 2,0 niż 3,0. Od Ciebie zależy, czy wybierzesz między 2,0 a 3,0, oba będą równie ważne.

Dla liczb ujemnych -2,1, -2,2, -2,3 i -2,4 staną się na -2,0; a -2,6, 2,7, 2,8 i 2,9 staną się -3,0 w zaokrągleniu arytmetycznym.

Dla -2,5 potrzebny jest wybór między -2,0 a -3,0.

Inne formy zaokrąglania

„Zaokrąglanie w górę” przyjmuje dowolną liczbę z miejscami dziesiętnymi i czyni ją kolejną „liczbą całkowitą”. Zatem nie tylko rundy 2.5 i 2.6 do 3.0, ale także 2.1 i 2.2.

Zaokrąglanie w górę przesuwa liczby dodatnie i ujemne od zera. Na przykład. 2,5 do 3,0 i -2,5 do -3,0.

„Zaokrąglanie w dół” obcina liczby, odcinając niechciane cyfry. To powoduje przesuwanie liczb w kierunku zera. Na przykład. 2,5 do 2,0 i -2,5 do -2,0

W „zaokrąglaniu bankiera” - w swojej najczęstszej postaci - zaokrąglane .5 jest zaokrąglane w górę lub w dół, aby wynik zaokrąglania był zawsze liczbą parzystą. Tak więc 2,5 zaokrągla do 2,0, 3,5 do 4,0, 4,5 do 4,0, 5,5 do 6,0 i tak dalej.

„Alternatywne zaokrąglanie” naprzemiennie wykonuje proces dla dowolnego .5 pomiędzy zaokrąglaniem w dół i zaokrąglaniem w górę.

„Losowe zaokrąglanie” zaokrągla a .5 w górę lub w dół całkowicie losowo.

Symetria i asymetria

Mówi się, że funkcja zaokrąglania jest „symetryczna”, jeśli albo zaokrągla wszystkie liczby od zera, albo zaokrągla wszystkie liczby w kierunku zera.

Funkcja jest „asymetryczna”, jeśli zaokrągla liczby dodatnie w kierunku zera, a liczby ujemne od zera. Np. Od 2,5 do 2,0; i -2,5 do -3,0.

Również asymetryczna jest funkcja, która zaokrągla liczby dodatnie od zera i liczby ujemne w kierunku zera. Na przykład. Od 2,5 do 3,0; i -2,5 do -2,0.

Przez większość czasu ludzie myślą o zaokrąglaniu symetrycznym, w którym -2,5 będzie zaokrąglane w kierunku -3,0, a 3,5 będzie zaokrąglane w kierunku 4,0. (w C #Round(AwayFromZero))

Patrick Peters
źródło
28

Domyślne MidpointRounding.ToEvenlub zaokrąglanie przez Bankierów ( 2,5 staje się 2, 4,5 staje się 4 itd. ) Uprzedziło mnie wcześniej pisaniem raportów do księgowości, więc napiszę kilka słów o tym, czego się dowiedziałem wcześniej i patrząc na to ten post.

Kim są ci bankierzy, którzy zaokrąglają liczby parzyste (być może brytyjscy bankierzy!)?

Z wikipedii

Geneza terminu zaokrąglania przez bankierów jest bardziej niejasna. Jeśli ta metoda zaokrąglania była kiedykolwiek standardem w bankowości, dowody okazały się niezwykle trudne do znalezienia. Przeciwnie, sekcja 2 raportu Komisji Europejskiej Wprowadzenie euro i zaokrąglanie kwot walutowych sugeruje, że wcześniej nie było standardowego podejścia do zaokrąglania w bankowości; i określa, że ​​kwoty „w połowie drogi” należy zaokrąglić w górę.

Wydaje się to bardzo dziwnym sposobem zaokrąglania, szczególnie w przypadku bankowości, chyba że banki używają do przyjmowania dużej liczby depozytów o równych kwotach. Wpłać 2,4 miliona funtów, ale nazwiemy to 2 miliony funtów.

Standard IEEE 754 pochodzi z 1985 roku i oferuje oba sposoby zaokrąglania, ale bankier jest zalecany przez standard. W tym artykule na Wikipedii znajduje się długa lista sposobów zaokrąglania języków (popraw mnie, jeśli któreś z poniższych stwierdzeń jest błędne) i większość z nich nie używa bankierów, ale zaokrąglania, którego uczysz w szkole:

  • C / C ++ round () z matematyki. H zaokrągla od zera (nie zaokrąglanie przez bankiera)
  • Java Math.Round zaokrągla wartość od zera (wyrównuje wynik, dodaje 0,5, rzutuje na liczbę całkowitą). W BigDecimal istnieje alternatywa
  • Perl używa metody podobnej do C.
  • JavaScript jest taki sam jak Java's Math.Round.
Chris S.
źródło
Dzięki za informację. Nigdy nie zdawałem sobie z tego sprawy. Twój przykład o milionach wyśmiewa to trochę, ale nawet jeśli zaokrąglisz centy, konieczność zapłaty odsetek na 10 milionach rachunków bankowych będzie kosztować bank dużo, jeśli wszystkie pół centy zostaną zaokrąglone w górę, lub będzie kosztować klientów, jeśli wszyscy pół centa są zaokrąglane w dół. Mogę sobie wyobrazić, że jest to uzgodniony standard. Nie jestem jednak pewien, czy bankierzy tak naprawdę go używają. Większość klientów nie zauważy zaokrąglania w dół, przynosząc przy tym dużo pieniędzy, ale mogę sobie wyobrazić, że jest to wymagane przez prawo, jeśli mieszkasz w kraju, w którym obowiązują przepisy przyjazne dla klientów
Harald Coppoolse
15

Z MSDN:

Domyślnie Math.Round używa MidpointRounding.ToEven. Większość ludzi nie jest zaznajomiona z „zaokrąglaniem do parzystości” jako alternatywą, „zaokrąglania od zera” częściej uczy się w szkole. .NET domyślnie przyjmuje wartość „Zaokrąglanie do parzystej”, ponieważ jest statystycznie lepszy, ponieważ nie dzieli tendencji do „zaokrąglania od zera” do zaokrąglania w górę nieco częściej niż zaokrągla w dół (zakładając, że zaokrąglane liczby są dodatnie. )

http://msdn.microsoft.com/en-us/library/system.math.round.aspx

Cristian Donoso
źródło
3

Ponieważ Silverlight nie obsługuje opcji MidpointRounding, musisz napisać własną. Coś jak:

public double RoundCorrect(double d, int decimals)
{
    double multiplier = Math.Pow(10, decimals);

    if (d < 0)
        multiplier *= -1;

    return Math.Floor((d * multiplier) + 0.5) / multiplier;

}

Aby zapoznać się z przykładami użycia tego rozszerzenia jako rozszerzenia, zobacz post: .NET i Silverlight Rounding

JBrooks
źródło
3

Miałem ten problem, gdy mój serwer SQL zaokrągla w górę 0,5 do 1, podczas gdy moja aplikacja C # nie. Widzisz dwa różne wyniki.

Oto implementacja z int / long. W ten sposób Java się kręci.

int roundedNumber = (int)Math.Floor(d + 0.5);

Jest to prawdopodobnie najbardziej wydajna metoda, o której możesz pomyśleć.

Jeśli chcesz zachować podwójność i używać precyzji dziesiętnej, to tak naprawdę wystarczy użyć wykładników 10 na podstawie liczby miejsc dziesiętnych.

public double getRounding(double number, int decimalPoints)
{
    double decimalPowerOfTen = Math.Pow(10, decimalPoints);
    return Math.Floor(number * decimalPowerOfTen + 0.5)/ decimalPowerOfTen;
}

Możesz wprowadzić ujemną liczbę dziesiętną dla kropek dziesiętnych, a także słowo w porządku.

getRounding(239, -2) = 200
ShortFuse
źródło
1

Prosty sposób to:

Math.Ceiling(decimal.Parse(yourNumber + ""));
Nalan Madheswaran
źródło
0

Ten post zawiera odpowiedź, której szukasz:

http://weblogs.asp.net/sfurman/archive/2003/03/07/3537.aspx

Zasadniczo tak mówi:

Zwracana wartość

Liczba najbliższa wartości z dokładnością równą cyfrom. Jeśli wartość znajduje się w połowie odległości między dwiema liczbami, z których jedna jest parzysta, a druga nieparzysta, wówczas zwracana jest liczba parzysta. Jeśli dokładność wartości jest mniejsza niż cyfry, wówczas wartość jest zwracana bez zmian.

Zachowanie tej metody jest zgodne z normą IEEE 754, sekcja 4. Ten rodzaj zaokrąglania jest czasami nazywany zaokrąglaniem do najbliższego lub zaokrąglania bankierskiego. Jeśli cyfry wynoszą zero, ten rodzaj zaokrąglania jest czasami nazywany zaokrąglaniem w kierunku zera.

Vaccano
źródło
0

Silverlight nie obsługuje opcji MidpointRounding. Oto metoda rozszerzenia dla Silverlight, która dodaje wyliczenie MidpointRounding:

public enum MidpointRounding
{
    ToEven,
    AwayFromZero
}

public static class DecimalExtensions
{
    public static decimal Round(this decimal d, MidpointRounding mode)
    {
        return d.Round(0, mode);
    }

    /// <summary>
    /// Rounds using arithmetic (5 rounds up) symmetrical (up is away from zero) rounding
    /// </summary>
    /// <param name="d">A Decimal number to be rounded.</param>
    /// <param name="decimals">The number of significant fractional digits (precision) in the return value.</param>
    /// <returns>The number nearest d with precision equal to decimals. If d is halfway between two numbers, then the nearest whole number away from zero is returned.</returns>
    public static decimal Round(this decimal d, int decimals, MidpointRounding mode)
    {
        if ( mode == MidpointRounding.ToEven )
        {
            return decimal.Round(d, decimals);
        }
        else
        {
            decimal factor = Convert.ToDecimal(Math.Pow(10, decimals));
            int sign = Math.Sign(d);
            return Decimal.Truncate(d * factor + 0.5m * sign) / factor;
        }
    }
}

Źródło: http://anderly.com/2009/08/08/silverlight-midpoint-rounding-solution/

jascur2
źródło
-1

za pomocą niestandardowego zaokrąglenia

public int Round(double value)
{
    double decimalpoints = Math.Abs(value - Math.Floor(value));
    if (decimalpoints > 0.5)
        return (int)Math.Round(value);
    else
        return (int)Math.Floor(value);
}
anand360
źródło
>.5daje takie samo zachowanie jak Math.Round. Pytanie brzmi, co się dzieje, gdy dokładnie jest część dziesiętna 0.5. Math.Round pozwala określić żądany algorytm zaokrąglania
Panagiotis Kanavos
-2

Jest to brzydkie jak całe piekło, ale zawsze powoduje prawidłowe zaokrąglanie arytmetyczne.

public double ArithRound(double number,int places){

  string numberFormat = "###.";

  numberFormat = numberFormat.PadRight(numberFormat.Length + places, '#');

  return double.Parse(number.ToString(numberFormat));

}
James Montagne
źródło
5
Podobnie jest z dzwonieniem Math.Roundi określaniem, w jaki sposób ma on się odbywać.
konfigurator
-2

Oto jak musiałem to obejść:

Public Function Round(number As Double, dec As Integer) As Double
    Dim decimalPowerOfTen = Math.Pow(10, dec)
    If CInt(number * decimalPowerOfTen) = Math.Round(number * decimalPowerOfTen, 2) Then
        Return Math.Round(number, 2, MidpointRounding.AwayFromZero)
    Else
        Return CInt(number * decimalPowerOfTen + 0.5) / 100
    End If
End Function

Próba z 1,905 z 2 miejscami po przecinku da 1,91 zgodnie z oczekiwaniami, ale Math.Round(1.905,2,MidpointRounding.AwayFromZero)daje 1,90! Metoda Math.Round jest absolutnie niespójna i nie nadaje się do większości podstawowych problemów, które mogą napotkać programiści. Muszę sprawdzić, czy (int) 1.905 * decimalPowerOfTen = Math.Round(number * decimalPowerOfTen, 2)nie chcę zaokrąglać w górę tego, co powinno być zaokrąglane w dół.

MikeMuffinMan
źródło
Math.Round(1.905,2,MidpointRounding.AwayFromZero)powraca1.91
Panagiotis Kanavos,