1. Wprowadzenie
W realnym życiu rzadko się zdarza, żeby wypłacać kasę z dokładnością do stu trylionowych euro, albo mierzyć długość pokoju do setnych milimetra. W programowaniu jest podobnie: wyniki pracy z double albo float często są “zbyt ułamkowe”.
Klasyczny problem:
double result = 10.0 / 3.0;
Console.WriteLine(result); // 3.3333333333333335
A jeśli chcemy wyświetlić tę liczbę, na przykład tylko z dwoma miejscami po przecinku (żeby nie “straszyć” użytkownika)? Do tego właśnie jest zaokrąglanie.
2. Podstawowe metody zaokrąglania w C#
C# daje kilka sposobów na zaokrąglenie liczby zmiennoprzecinkowej. I tu zaczyna się zabawa: zaokrąglanie może być różne! Zobaczmy główne opcje i ogarnijmy, czym się różnią.
1) Funkcja Math.Round — klasyczne zaokrąglanie
Najpopularniejszy sposób — metoda Math.Round. Zaokrągla liczbę do najbliższej całkowitej (albo do żądanej liczby miejsc po przecinku).
Math.Round(liczba[, ilosc_miejsc[, MidpointRounding]])
- liczba — co zaokrąglamy.
- ilosc_miejsc — ile zostawić miejsc po przecinku (domyślnie 0, czyli do całkowitej).
- MidpointRounding — metoda zaokrąglania połówek (o tym zaraz pogadamy).
Przykłady:
double x = 2.71828;
Console.WriteLine(Math.Round(x)); // 3
Console.WriteLine(Math.Round(x, 2)); // 2.72
Console.WriteLine(Math.Round(x, 3)); // 2.718
Wizualizacja:
| Wartość wejściowa | Math.Round(x) | Math.Round(x, 2) |
|---|---|---|
| 2.3 | 2 | 2.3 |
| 2.5 | 2 | 2.5 |
| 2.7 | 3 | 2.7 |
| 2.71828 | 3 | 2.72 |
Typ wyniku:
Wszystkie funkcje w tym wykładzie zwracają typ double, więc jeśli chcesz przypisać wynik zaokrąglenia, możesz dostać błąd:
double result = 10.0 / 5.0; // Wynik 2.0
int x = Math.Round(x); // Błąd! Nie można tak po prostu przypisać double do int
Jeśli chcesz przypisać double do zmiennej typu int, musisz jawnie rzutować typ:
double result = 10.0 / 5.0; // Wynik 2.0
int x = (int) Math.Round(x); // Wszystko gra!
2) Zaokrąglanie w górę lub w dół
C# obsługuje nie tylko “klasyczne” zaokrąglanie, ale też wymuszone zaokrąglanie w górę albo w dół.
Zaokrąglanie w dół (Math.Floor)
Metoda Math.Floor po prostu “ucina” część ułamkową (w stronę minus nieskończoności). Czyli zawsze zaokrągla w dół!
Console.WriteLine(Math.Floor(2.99)); // 2
Console.WriteLine(Math.Floor(-2.99)); // -3
Zaokrąglanie w górę (Math.Ceiling)
Metoda Math.Ceiling — przeciwieństwo Floor: zawsze zaokrągla w górę (w stronę plus nieskończoności).
Console.WriteLine(Math.Ceiling(2.01)); // 3
Console.WriteLine(Math.Ceiling(-2.01)); // -2
Schemat porównawczy:
| Funkcja | 2.3 | -2.3 |
|---|---|---|
| Math.Round | 2 | -2 |
| Math.Floor | 2 | -3 |
| Math.Ceiling | 3 | -2 |
3) Truncate: po prostu uciąć część ułamkową
Metoda Math.Truncate — “spokojny chirurg”. Po prostu usuwa część ułamkową, nie przejmując się matematyczną poprawnością zaokrąglania. Jeśli x jest dodatnie — wynik jak w Floor, jeśli ujemne — jak w Ceiling.
Console.WriteLine(Math.Truncate(2.99)); // 2
Console.WriteLine(Math.Truncate(-2.99)); // -2
3. Jak działa Math.Round dla “połówek”? (MidpointRounding)
Wyobraź sobie: masz 2.5. Trzeba zaokrąglić — ale gdzie? W sumie “po równo” między 2 a 3. W matematyce są dwa popularne podejścia:
- Zaokrąglanie w górę (“do najbliższej większej”)
- Zaokrąglanie do najbliższej parzystej (Banker's rounding)
C# domyślnie używa drugiego podejścia — zaokrąglania do najbliższej parzystej (“banker's rounding”, czyli “zaokrąglanie w stronę parzystej”).
Console.WriteLine(Math.Round(2.5)); // 2
Console.WriteLine(Math.Round(3.5)); // 4
Dlaczego? Bo z dwóch równo oddalonych całkowitych wygrywa parzysta. To po to, żeby przy dużej liczbie zaokrągleń nie było systematycznego błędu w górę albo w dół. To mega ważne np. przy liczeniu dużych sum kasy w banku (stąd nazwa).
Jeśli chcesz zawsze zaokrąglać w górę, możesz jawnie wskazać tryb:
// Zaokrąglić zawsze "od zera"
Console.WriteLine(Math.Round(2.5, 0, MidpointRounding.AwayFromZero)); // 3
Console.WriteLine(Math.Round(-2.5, 0, MidpointRounding.AwayFromZero)); // -3
Tabela trybów MidpointRounding:
| Wartość wejściowa | Round (Domyślnie) | AwayFromZero | ToZero / ToEven |
|---|---|---|---|
| 2.5 | 2 | 3 | 2 |
| 3.5 | 4 | 4 | 4 |
| -2.5 | -2 | -3 | -2 |
4. Zaokrąglanie do żądanej liczby miejsc: nie zapominaj o "ósemkach" i "trójkach"!
Czasem trzeba zaokrąglić nie do całkowitej, a np. do dwóch miejsc po przecinku (np. do wyświetlania kasy albo procentów).
double price = 149.9999;
double roundedPrice = Math.Round(price, 2);
Console.WriteLine(roundedPrice); // 150
A jeśli chcemy po prostu “uciąć” nadmiarowe miejsca, ale nie zaokrąglać? Na przykład z 123.4567 zrobić 123.45.
Można to zrobić małym trikiem:
double num = 123.4567;
double result = Math.Floor(num * 100) / 100;
Console.WriteLine(result); // 123.45
Ręcznie “przesuwamy” przecinek, ucinamy część ułamkową i cofamy przecinek.
5. Formatowanie wyniku wyjścia vs zaokrąglanie
Formatowanie wyjścia ({x:F2}) — to nie zawsze "prawdziwe" zaokrąglanie, a tylko “maska”, jak liczba wygląda na ekranie. W pamięci zostaje długi “ogon” double. Jeśli chcesz naprawdę zaokrąglić wartość i ją zapisać, użyj Math.Round.
Przykład, gdzie różnica może być kluczowa:
double value = 2.555;
Console.WriteLine($"{value:F2}"); // 2.56
Console.WriteLine(Math.Round(value, 2)); // 2.56
double stored = Math.Round(value, 2);
Console.WriteLine(stored); // 2.56
// Ale jeśli tylko wyświetlisz bez zaokrąglania, tylko z formatem...
Console.WriteLine($"{value:F2}"); // 2.56, ale w pamięci — 2.555
6. Typowe błędy i niuanse
- Nie używaj zaokrąglania do dokładnych obliczeń finansowych z double! Do kasy w .NET jest specjalny typ decimal — on “nie szaleje” z przecinkiem (więcej — w jednym z kolejnych wykładów).
- Smutne “wiszące ósemki”: przez binarne przechowywanie nie wszystkie liczby zaokrąglają się “logicznie” na oko. Na przykład 0.1 + 0.2 może wcale nie być 0.3.
- Nie myl Math.Floor i Math.Round! Pierwszy zawsze w dół, drugi — jak matematyka każe.
- Nieograniczone narastanie błędu: jeśli często zaokrąglasz wynik obliczeń po drodze (np. w pętli), końcowa wartość może niespodziewanie “odpłynąć”. Zwykle lepiej zaokrąglać tylko końcowy wynik, a nie wszystkie pośrednie kroki.
7. Finałowa tabela: która metoda co daje?
| Wartość wejściowa | Math.Round(x) | Math.Floor(x) | Math.Ceiling(x) | Math.Truncate(x) |
|---|---|---|---|---|
| 3.2 | 3 | 3 | 4 | 3 |
| 3.5 | 4 | 3 | 4 | 3 |
| -3.2 | -3 | -4 | -3 | -3 |
| -3.5 | -4 | -4 | -3 | -3 |
8. Ciekawostki
- Kiedyś NASA straciła sondę kosmiczną przez nieścisłe zaokrąglanie współrzędnych i różnicę między jednostkami metrycznymi/imperialnymi. Morał: zaokrąglaj z głową!
- Banki już dawno przeszły na “banker's rounding”, żeby nie tracić groszy — kiedyś po “staremu” wszyscy zaokrąglali w górę i klienci tracili ułamki centa przy każdym przelewie.
- W C# jest nawet metoda Math.Sign, która zwraca znak liczby — czasem przydatne przed zaokrąglaniem, żeby samemu wybrać, gdzie “ucinać”.
GO TO FULL VERSION