Zaokrąglanie do dowolnej liczby cyfr znaczących

83

Jak zaokrąglić dowolną liczbę (nie tylko liczby całkowite> 0) do N cyfr znaczących?

Na przykład, jeśli chcę zaokrąglić do trzech cyfr znaczących, szukam formuły, która mogłaby przyjąć:

1 239 451 i zwraca 1 240 000

12.1257 i powrót 12.1

.0681 i zwraca .0681

5 i wróć 5

Naturalnie algorytm nie powinien być zakodowany na stałe tak, aby obsługiwał tylko N z 3, chociaż byłby to początek.

DougN
źródło
Wydaje się, że pytanie jest zbyt ogólne. Różne języki programowania mają do tego różne standardowe funkcje. Rzeczywiście nie należy wymyślać koła na nowo.
Johnny Wong

Odpowiedzi:

106

Oto ten sam kod w Javie bez błędu 12.100000000000001, który mają inne odpowiedzi

Usunąłem również powtarzający się kod, zmieniłem powerna typ całkowity, aby zapobiec problemom związanym z pływaniem po n - dzakończeniu, i uczyniłem długi pośredni bardziej przejrzystym

Błąd był spowodowany pomnożeniem dużej liczby przez małą. Zamiast tego dzielę dwie liczby o podobnej wielkości.

EDYCJA
Naprawiono więcej błędów. Dodano czek na 0, ponieważ spowodowałoby to NaN. Sprawiono, że funkcja faktycznie działa z liczbami ujemnymi (oryginalny kod nie obsługuje liczb ujemnych, ponieważ dziennik liczb ujemnych jest liczbą zespoloną)

public static double roundToSignificantFigures(double num, int n) {
    if(num == 0) {
        return 0;
    }

    final double d = Math.ceil(Math.log10(num < 0 ? -num: num));
    final int power = n - (int) d;

    final double magnitude = Math.pow(10, power);
    final long shifted = Math.round(num*magnitude);
    return shifted/magnitude;
}
Pirolistyczne
źródło
2
Dziękuję za przyjęcie mojej odpowiedzi. Właśnie zdałem sobie sprawę, że moja odpowiedź jest ponad rok po zadaniu pytania. To jeden z powodów, dla których przepełnienie stosu jest tak fajne. Możesz znaleźć przydatne informacje!
Pirolistyczne
2
Zauważ, że może się to nieznacznie nie udać w przypadku wartości zbliżonych do limitu rundy. Na przykład zaokrąglenie 1,255 do 3 cyfr znaczących powinno zwrócić 1,26, ale zwraca 1,25. To dlatego, że 1,255 * 100,0 to 125,499999 ... Ale tego należy się spodziewać podczas pracy z podwójnymi
cquezel
Wow, wiem, że to jest stare, ale próbuję go użyć. Mam liczbę zmiennoprzecinkową, którą chcę wyświetlić do 3 cyfr znaczących. Jeśli wartość float wynosi 1,0, wywołuję twoją metodę, ale nadal zwraca ona jako 1,0, nawet jeśli rzucam zmiennoprzecinkę jako podwójną. Chcę, żeby wrócił jako 1. Jakieś pomysły?
Steve W
3
Ten fragment kodu Java trafia do oficjalnego przykładu Androida android.googlesource.com/platform/development/+/fcf4286/samples/…
Curious Sam
1
Nie idealny. Dla num = -7999999,9999999992 i n = 2 zwraca -7999999.999999999, ale powinno być -8000000.
Duncan Calvert
16

Oto krótka i słodka implementacja JavaScript:

function sigFigs(n, sig) {
    var mult = Math.pow(10, sig - Math.floor(Math.log(n) / Math.LN10) - 1);
    return Math.round(n * mult) / mult;
}

alert(sigFigs(1234567, 3)); // Gives 1230000
alert(sigFigs(0.06805, 3)); // Gives 0.0681
alert(sigFigs(5, 3)); // Gives 5
Ates Goral
źródło
1
Dobra odpowiedź Ates. Być może dodaj wyzwalacz, który zwróci 0, jeśli n==0:)
sscirrus
czy jest powód, aby to zrobić, Math.log(n) / Math.LN10a nie Math.log10(n)?
Lee
1
@Lee developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/ ... „To nowa technologia, część standardu ECMAScript 2015 (ES6)”. Zasadniczo problemy ze zgodnością.
Ates Goral
Czy czegoś mi brakuje, czy też ta odpowiedź zakłada, że Math.floor(x) == Math.ceil(x) - 1? Ponieważ tak nie xjest, gdy jest liczbą całkowitą. Myślę, że drugim argumentem powfunkcji powinien być sig - Math.ceil(Math.log(n) / Math.LN10)(lub po prostu użyj Math.log10)
Paul
15

PODSUMOWANIE:

double roundit(double num, double N)
{
    double d = log10(num);
    double power;
    if (num > 0)
    {
        d = ceil(d);
        power = -(d-N);
    }
    else
    {
        d = floor(d); 
        power = -(d-N);
    }

    return (int)(num * pow(10.0, power) + 0.5) * pow(10.0, -power);
}

Musisz więc znaleźć miejsce dziesiętne pierwszej niezerowej cyfry, a następnie zapisać następne N-1 cyfry, a następnie zaokrąglić N-tą cyfrę na podstawie reszty.

Możemy użyć dziennika, aby zrobić pierwszy.

log 1239451 = 6.09
log 12.1257 = 1.08
log 0.0681  = -1.16

Więc dla liczb> 0, weź górną część dziennika. W przypadku liczb <0 zajmij podłogę dziennika.

Teraz mamy cyfrę d: 7 w pierwszym przypadku, 2 w drugim, -2 w trzecim.

Musimy zaokrąglić tę (d-N)cyfrę. Coś jak:

double roundedrest = num * pow(10, -(d-N));

pow(1239451, -4) = 123.9451
pow(12.1257, 1)  = 121.257
pow(0.0681, 4)   = 681

Następnie wykonaj standardowe zaokrąglenie:

roundedrest = (int)(roundedrest + 0.5);

I cofnij pow.

roundednum = pow(roundedrest, -(power))

Gdzie moc to moc obliczona powyżej.


O dokładności: odpowiedź pirolisty jest rzeczywiście bliższa rzeczywistemu wynikowi. Ale pamiętaj, że w żadnym wypadku nie możesz dokładnie reprezentować 12.1. Jeśli wydrukujesz odpowiedzi w następujący sposób:

System.out.println(new BigDecimal(n));

Odpowiedzi są następujące:

Pyro's: 12.0999999999999996447286321199499070644378662109375
Mine: 12.10000000000000142108547152020037174224853515625
Printing 12.1 directly: 12.0999999999999996447286321199499070644378662109375

A więc użyj odpowiedzi Pyro!

Claudiu
źródło
1
Ten algorytm wydaje się podatny na błędy zmiennoprzecinkowe. Po zaimplementowaniu w JavaScript otrzymuję: 0,06805 -> 0,06810000000000001 i 12,1 -> 12,100000000000001
Ates Goral
12.1 samo w sobie nie może być dokładnie odwzorowane za pomocą liczb zmiennoprzecinkowych - nie jest to wynik działania tego algorytmu.
Claudiu,
1
Ten kod w Javie daje 12.100000000000001 i używa 64-bitowych podwójnych liczb, które mogą przedstawiać dokładnie 12.1.
Pyrolistic
4
Nie ma znaczenia, czy jest 64-bitowy czy 128-bitowy. Nie możesz przedstawić ułamka 1/10 za pomocą skończonej sumy potęg 2, i tak są reprezentowane liczby zmiennoprzecinkowe
Claudiu
2
dla tych, którzy się wbijają, w zasadzie odpowiedź Pyrolistics jest bardziej precyzyjna niż moja, więc algorytm wypisywania liczb zmiennoprzecinkowych wypisuje '12 .1 'zamiast '12 .100000000000001'. jego odpowiedź jest lepsza, nawet jeśli technicznie byłem poprawny, że nie możesz dokładnie przedstawić „12.1”.
Claudiu
10

Czy to nie „krótka i słodka” implementacja JavaScript

Number(n).toPrecision(sig)

na przykład

alert(Number(12345).toPrecision(3)

?

Przepraszam, nie żartuję, po prostu użycie funkcji „roundit” z Claudiu i .toPrecision w JavaScript daje mi różne wyniki, ale tylko w zaokrągleniu ostatniej cyfry.

JavaScript:

Number(8.14301).toPrecision(4) == 8.143

.NETTO

roundit(8.14301,4) == 8.144
Justin Wignall
źródło
1
Number(814301).toPrecision(4) == "8.143e+5". Generalnie nie to, czego chcesz, jeśli pokazujesz to użytkownikom.
Zaz
Bardzo prawda Josh tak, generalnie zalecałbym .toPrecision () tylko dla liczb dziesiętnych, a zaakceptowaną odpowiedź (z edycją) należy stosować / przeglądać zgodnie z indywidualnymi wymaganiami.
Justin Wignall
8

Rozwiązanie pirolistyczne (bardzo ładne!) Wciąż zawiera problem. Maksymalna podwójna wartość w Javie jest rzędu 10 ^ 308, podczas gdy minimalna jest rzędu 10 ^ -324. Dlatego możesz wpaść w kłopoty, stosując funkcję roundToSignificantFiguresdo czegoś, co znajduje się w granicach kilku potęg dziesięciu Double.MIN_VALUE. Na przykład, kiedy dzwonisz

roundToSignificantFigures(1.234E-310, 3);

wtedy zmienna powerbędzie miała wartość 3 - (-309) = 312. W konsekwencji zmienna magnitudestanie się Infinityi odtąd wszystko będzie śmieciem. Na szczęście nie jest to problem nie do pokonania: to tylko czynnik, magnitude który przepełnia. Tak naprawdę liczy się produkt num * magnitude , który się nie przepełnia. Jednym ze sposobów rozwiązania tego problemu jest podzielenie mnożenia przez współczynnik magintudena dwa etapy:


 public static double roundToNumberOfSignificantDigits(double num, int n) {

    final double maxPowerOfTen = Math.floor(Math.log10(Double.MAX_VALUE));

    if(num == 0) {
        return 0;
    }

    final double d = Math.ceil(Math.log10(num < 0 ? -num: num));
    final int power = n - (int) d;

    double firstMagnitudeFactor = 1.0;
    double secondMagnitudeFactor = 1.0;
    if (power > maxPowerOfTen) {
        firstMagnitudeFactor = Math.pow(10.0, maxPowerOfTen);
        secondMagnitudeFactor = Math.pow(10.0, (double) power - maxPowerOfTen);
    } else {
        firstMagnitudeFactor = Math.pow(10.0, (double) power);
    }

    double toBeRounded = num * firstMagnitudeFactor;
    toBeRounded *= secondMagnitudeFactor;

    final long shifted = Math.round(toBeRounded);
    double rounded = ((double) shifted) / firstMagnitudeFactor;
    rounded /= secondMagnitudeFactor;
    return rounded;
}

Thomas Becker
źródło
6

Co powiesz na to rozwiązanie Java:

double roundToSsequantFigure (double num, int precyzyjność) {
 zwraca nowe BigDecimal (num)
            .round (nowy MathContext (precyzja, RoundingMode.HALF_EVEN))
            .doubleValue (); 
}
wolfgang grinfeld
źródło
3

Oto zmodyfikowana wersja JavaScript firmy Ates, która obsługuje liczby ujemne.

function sigFigs(n, sig) {
    if ( n === 0 )
        return 0
    var mult = Math.pow(10,
        sig - Math.floor(Math.log(n < 0 ? -n: n) / Math.LN10) - 1);
    return Math.round(n * mult) / mult;
 }
Jason Swank
źródło
2

Spóźniło się to 5 lat, ale podzielę się tym, aby inni nadal mieli ten sam problem. Podoba mi się to, ponieważ jest proste i nie ma obliczeń po stronie kodu. Aby uzyskać więcej informacji, zobacz Wbudowane metody wyświetlania liczb znaczących .

Dzieje się tak, jeśli chcesz go po prostu wydrukować.

public String toSignificantFiguresString(BigDecimal bd, int significantFigures){
    return String.format("%."+significantFigures+"G", bd);
}

To jest, jeśli chcesz go przekonwertować:

public BigDecimal toSignificantFigures(BigDecimal bd, int significantFigures){
    String s = String.format("%."+significantFigures+"G", bd);
    BigDecimal result = new BigDecimal(s);
    return result;
}

Oto przykład tego w akcji:

BigDecimal bd = toSignificantFigures(BigDecimal.valueOf(0.0681), 2);
JackDev
źródło
Spowoduje to wyświetlenie „dużych” liczb w notacji naukowej, np. 15k jako 1.5e04.
mat.
2

JavaScript:

Number( my_number.toPrecision(3) );

NumberFunkcja zmieni wyjście postaci "8.143e+5"do "814300".

Zaz
źródło
1

Czy próbowałeś po prostu zakodować to tak, jak zrobiłbyś to ręcznie?

  1. Zamień liczbę na ciąg
  2. Zaczynając od początku ciągu, policz cyfry - wiodące zera nie są znaczące, wszystko inne jest.
  3. Kiedy dojdziesz do „n-tej” cyfry, spójrz przed siebie na następną cyfrę i jeśli jest to 5 lub więcej, zaokrąglij w górę.
  4. Zastąp wszystkie końcowe cyfry zerami.
Mark Bessey
źródło
1

[Poprawiono, 2009-10-26]

Zasadniczo dla N znaczących cyfr ułamkowych :

• Pomnóż liczbę przez 10 N
• Dodaj 0,5
• Obetnij cyfry ułamkowe (tj. Skróć wynik do liczby całkowitej)
• Podziel przez 10 N

Dla N znaczących cyfr całkowitych (nieułamkowych):

• Podziel liczbę przez 10 N
• Dodaj 0,5
• Obetnij cyfry ułamkowe (tj. Skróć wynik do liczby całkowitej)
• Pomnóż przez 10 N

Możesz to zrobić na dowolnym kalkulatorze, na przykład, który ma operator „INT” (obcięcie liczby całkowitej).

David R. Tribble
źródło
Nie. Przeczytaj ponownie pytanie. 1239451 z 3 znakami przy użyciu twojego algorytmu dałoby niepoprawnie 123951
Pyrolistic
Tak, poprawiłem to, aby odróżnić zaokrąglanie do ułamkowej liczby cyfr (po prawej stronie przecinka dziesiętnego) od całkowitej liczby cyfr (po lewej).
David R Tribble
1
/**
 * Set Significant Digits.
 * @param value value
 * @param digits digits
 * @return
 */
public static BigDecimal setSignificantDigits(BigDecimal value, int digits) {
    //# Start with the leftmost non-zero digit (e.g. the "1" in 1200, or the "2" in 0.0256).
    //# Keep n digits. Replace the rest with zeros.
    //# Round up by one if appropriate.
    int p = value.precision();
    int s = value.scale();
    if (p < digits) {
        value = value.setScale(s + digits - p); //, RoundingMode.HALF_UP
    }
    value = value.movePointRight(s).movePointLeft(p - digits).setScale(0, RoundingMode.HALF_UP)
        .movePointRight(p - digits).movePointLeft(s);
    s = (s > (p - digits)) ? (s - (p - digits)) : 0;
    return value.setScale(s);
}
Valeri Shibaev
źródło
1

Oto kod Pyrolistics (obecnie najlepsza odpowiedź) w Visual Basic.NET, gdyby ktoś go potrzebował:

Public Shared Function roundToSignificantDigits(ByVal num As Double, ByVal n As Integer) As Double
    If (num = 0) Then
        Return 0
    End If

    Dim d As Double = Math.Ceiling(Math.Log10(If(num < 0, -num, num)))
    Dim power As Integer = n - CInt(d)
    Dim magnitude As Double = Math.Pow(10, power)
    Dim shifted As Double = Math.Round(num * magnitude)
    Return shifted / magnitude
End Function
Michael Zlatkovsky - Microsoft
źródło
0

Oto ten, który wymyśliłem w VB:

Function SF(n As Double, SigFigs As Integer)
    Dim l As Integer = n.ToString.Length
    n = n / 10 ^ (l - SigFigs)
    n = Math.Round(n)
    n = n * 10 ^ (l - SigFigs)
    Return n
End Function
Jakiś facet
źródło
0

return new BigDecimal(value, new MathContext(significantFigures, RoundingMode.HALF_UP)).doubleValue();

Duncan Calvert
źródło
0

Potrzebowałem tego w Go, co było nieco skomplikowane przez brak standardowej biblioteki Go math.Round()(przed go1.10). Więc musiałem to też podnieść. Oto moje tłumaczenie doskonałej odpowiedzi Pyrolisticsa :

// TODO: replace in go1.10 with math.Round()
func round(x float64) float64 {
    return float64(int64(x + 0.5))
}

// SignificantDigits rounds a float64 to digits significant digits.
// Translated from Java at https://stackoverflow.com/a/1581007/1068283
func SignificantDigits(x float64, digits int) float64 {
    if x == 0 {
        return 0
    }

    power := digits - int(math.Ceil(math.Log10(math.Abs(x))))
    magnitude := math.Pow(10, float64(power))
    shifted := round(x * magnitude)
    return shifted / magnitude
}
Michael Hampton
źródło
To ma tajemniczy głos przeciw! Ale nie mogę znaleźć w nim błędu ani problemu. Co tu się dzieje?
Michael Hampton
0

Możesz uniknąć wykonywania wszystkich tych obliczeń z potęgami 10 itd., Używając po prostu FloatToStrF.

FloatToStrF umożliwia (między innymi) wybranie dokładności (liczby cyfr znaczących) wartości wyjściowej (która będzie ciągiem znaków). Oczywiście możesz następnie zastosować do tego StrToFloat, aby uzyskać zaokrągloną wartość jako zmiennoprzecinkową.

Spójrz tutaj:

http://docs.embarcadero.com/products/rad_studio/delphiAndcpp2009/HelpUpdate2/EN/html/delphivclwin32/SysUtils_FloatToStrF@Extended@TFloatFormat@[email protected]

George Soden-Freeth
źródło
-1
public static double roundToSignificantDigits(double num, int n) {
    return Double.parseDouble(new java.util.Formatter().format("%." + (n - 1) + "e", num).toString());
}

Ten kod używa wbudowanej funkcji formatowania, która została zamieniona na funkcję zaokrąglania

Harikrishnan
źródło