Округление до произвольного количества значащих цифр


83

Как можно округлить любое число (не только целые числа> 0) до N значащих цифр?

Например, если я хочу округлить до трех значащих цифр, я ищу формулу, которая могла бы принимать:

1,239,451 и доход 1,240,000

12.1257 и возврат 12.1

.0681 и возврат .0681

5 и возврат 5

Естественно, алгоритм не должен быть жестко запрограммирован так, чтобы обрабатывать только N из 3, хотя это было бы началом.


Кажется, вопрос слишком общий. В разных языках программирования для этого предусмотрены разные стандартные функции. На самом деле неуместно изобретать велосипед.
Джонни Вонг,

Ответы:


106

Вот тот же код на Java без ошибки 12.100000000000001, у других ответов есть

Я также удалил повторяющийся код, изменил powerего на целое число, чтобы предотвратить проблемы с плавающей точкой, когда это n - dбудет сделано, и сделал длинное промежуточное значение более понятным.

Ошибка была вызвана умножением большого числа на малое. Вместо этого я делю два числа одинакового размера.

РЕДАКТИРОВАТЬ
Исправлено больше ошибок. Добавлена ​​проверка на 0, так как это приведет к NaN. Функция действительно работает с отрицательными числами (исходный код не обрабатывает отрицательные числа, потому что лог отрицательного числа является комплексным числом)

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;
}

2
Спасибо, что приняли мой ответ. Я только что понял, что отвечу более чем через год после вопроса. Это одна из причин, почему stackoverflow такой классный. Вы можете найти полезную информацию!
Pyrolistical

2
Обратите внимание, что это может немного не работать для значений, близких к пределу округления. Например, округление 1,255 до 3 значащих цифр должно вернуть 1,26, но вернет 1,25. Это потому, что 1.255 * 100.0 - это 125.499999 ... Но этого следовало ожидать при работе с двойниками
cquezel

Вау, я знаю, что это старый, но я пытаюсь его использовать. У меня есть число с плавающей запятой, которое я хочу отобразить до трех значащих цифр. Если значение с плавающей запятой равно 1.0, я вызываю ваш метод, но он все равно возвращает значение 1.0, даже если я приведу значение с плавающей запятой как двойное. Я хочу вернуть его как 1. Есть идеи?
Steve W

3
Этот фрагмент java попадает в официальный пример Android android.googlesource.com/platform/development/+/fcf4286/samples/…
Любопытный Сэм,

1
Не идеально. Для num = -7999999.999999992 и n = 2 возвращается -7999999.999999999, но должно быть -8000000.
Дункан Калверт,

16

Вот короткая и приятная реализация 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

1
Хороший ответ, Атес. Возможно, добавим триггер для возврата 0, если n==0:)
sscirrus

есть причина делать, Math.log(n) / Math.LN10а не Math.log10(n)?
Ли

1
@Lee developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… «Это новая технология, часть стандарта ECMAScript 2015 (ES6)». Итак, в основном, проблемы совместимости.
Атес Горал

Я что-то упустил, или этот ответ предполагает это Math.floor(x) == Math.ceil(x) - 1? Потому что это не так, когда xэто целое число. Я думаю, что второй аргумент powфункции должен быть sig - Math.ceil(Math.log(n) / Math.LN10)(или просто использовать Math.log10)
Пол

15

РЕЗЮМЕ:

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);
}

Итак, вам нужно найти десятичный разряд первой ненулевой цифры, затем сохранить следующие цифры N-1, а затем округлить N-ю цифру на основе остальных.

Мы можем использовать журнал, чтобы сделать первое.

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

Итак, для чисел> 0 возьмите верхнюю часть журнала. Для чисел <0 возьмите пол журнала.

Теперь у нас есть цифра d: 7 в первом случае, 2 во втором, -2 в третьем.

Надо округлить (d-N)-ю цифру. Что-то вроде:

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

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

Затем выполните стандартное округление:

roundedrest = (int)(roundedrest + 0.5);

И отменить паузу.

roundednum = pow(roundedrest, -(power))

Где мощность - это мощность, рассчитанная выше.


По поводу точности: ответ пиролитика действительно ближе к реальному результату. Но учтите, что вы не можете точно представить 12.1. Если вы распечатаете ответы следующим образом:

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

Ответы таковы:

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

Итак, используйте ответ Pyro!


1
Этот алгоритм кажется подверженным ошибкам с плавающей запятой. При реализации с помощью JavaScript я получаю: 0,06805 -> 0,06810000000000001 и 12,1 -> 12,100000000000001
Ates Goral

12.1 сам по себе не может быть точно представлен с использованием плавающей запятой - это не результат этого алгоритма.
Claudiu

1
Этот код в Java производит 12.100000000000001, и здесь используются 64-битные числа с двойной точностью, которые могут точно представлять 12.1.
Пиролитическая

4
Неважно, 64 или 128 бит. Вы не можете представить дробь 1/10, используя конечную сумму степеней двойки, и именно так представлены числа с плавающей запятой
Клаудиу

2
для тех, кто вмешивается, в основном ответ Pyrolistical более точен, чем мой, так что алгоритм печати чисел с плавающей запятой печатает «12 .1» вместо «12 .100000000000001». его ответ лучше, даже несмотря на то, что я был технически прав, что вы не можете точно представить «12,1».
Claudiu

10

Разве не "короткая и приятная" реализация JavaScript

Number(n).toPrecision(sig)

например

alert(Number(12345).toPrecision(3)

?

Извините, я не шучу здесь, просто использование функции roundit от Claudiu и .toPrecision в JavaScript дает мне разные результаты, но только в округлении последней цифры.

JavaScript:

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

.СЕТЬ

roundit(8.14301,4) == 8.144

1
Number(814301).toPrecision(4) == "8.143e+5". Обычно это не то, что вам нужно, если вы показываете это пользователям.
Zaz

Совершенно верно, Джош, да, я бы обычно рекомендовал .toPrecision () только для десятичных чисел, и принятый ответ (с редактированием) следует использовать / проверять в соответствии с вашими индивидуальными требованиями.
Джастин Уигнал

8

Пиролитическое (очень красивое!) Решение все еще имеет проблему. Максимальное значение типа double в Java составляет порядка 10 ^ 308, а минимальное значение - порядка 10 ^ -324. Следовательно, вы можете столкнуться с проблемами при применении функции roundToSignificantFiguresк чему-то, что находится в пределах нескольких десятичных степеней Double.MIN_VALUE. Например, когда вы звоните

roundToSignificantFigures(1.234E-310, 3);

тогда переменная powerбудет иметь значение 3 - (-309) = 312. Следовательно, переменная magnitudeстанет Infinity, и с этого момента все будет мусором. К счастью, это не непреодолимая проблема: это всего лишь фактор, magnitude который переполняет. Что действительно важно, так это продукт num * magnitude , и он не переполняется. Один из способов решить эту проблему - разбить умножение на множитель magintudeна два этапа:


 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;
}


6

Как насчет этого java-решения:

double roundToSignificantFigure (double num, int precision) {
 вернуть новый BigDecimal (число)
            .round (новый MathContext (точность, RoundingMode.HALF_EVEN))
            .doubleValue (); 
}

3

Вот модифицированная версия JavaScript Ates, которая обрабатывает отрицательные числа.

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;
 }

2

Это произошло на 5 лет позже, но я поделюсь с другими, у которых все еще есть та же проблема. Мне это нравится, потому что это просто и никаких вычислений на стороне кода. Дополнительные сведения см. В разделе « Встроенные методы отображения значимых цифр» .

Это если вы просто хотите его распечатать.

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

Это если вы хотите его преобразовать:

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

Вот пример этого в действии:

BigDecimal bd = toSignificantFigures(BigDecimal.valueOf(0.0681), 2);

Это отобразит "большие" числа в экспоненциальной нотации, например 15k как 1.5e04.
матовый

2

JavaScript:

Number( my_number.toPrecision(3) );

NumberФункция изменит выходную форму "8.143e+5"к "814300".


1

Вы пробовали просто кодировать это так, как если бы вы это делали вручную?

  1. Преобразуйте число в строку
  2. Начиная с начала строки, считайте цифры - начальные нули не имеют значения, все остальное имеет значение.
  3. Когда вы дойдете до «n-й» цифры, посмотрите вперед на следующую цифру и, если она 5 или выше, округлите в большую сторону.
  4. Замените все конечные цифры нулями.

1

[Исправлено, 26.10.2009]

По сути, для N значащих дробных цифр:

• Умножьте число на 10 N
• Добавьте 0,5
• Обрежьте дробные цифры (т. Е. Усеките результат до целого числа)
• Разделите на 10 N

Для N значащих целых (не дробных) цифр:

• Разделите число на 10 N
• Добавьте 0,5
• Обрежьте дробные цифры (т. Е. Усеките результат до целого числа)
• Умножьте на 10 N

Вы можете сделать это на любом калькуляторе, например, в котором есть оператор «INT» (целочисленное усечение).


Нет. Прочтите вопрос еще раз. 1239451 с 3-мя фигами при использовании вашего алгоритма неверно даст 123951
Пиролитический

Да, я исправил это, чтобы различать округление до дробного числа цифр (справа от десятичной точки) и целого числа цифр (слева).
Дэвид Р. Триббл,

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);
}

1

Вот код Pyrolistical (в настоящее время главный ответ) в Visual Basic.NET, если он кому-то понадобится:

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

0

Это то, что я придумал в 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


0

Мне это было нужно в Go, что было немного сложно из-за отсутствия стандартной библиотеки Go math.Round()(до go1.10). Так что мне тоже пришлось это поднять. Вот мой перевод отличного ответа Пиролитика :

// 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
}

Это получил загадочный голос против! Но я не могу найти в нем ошибку или проблему. Что тут происходит?
Майкл Хэмптон

0

Вы можете избежать всех этих вычислений со степенью 10 и т. Д., Просто используя FloatToStrF.

FloatToStrF позволяет (среди прочего) выбрать точность (количество значащих цифр) в выводимом значении (которое будет строкой). Конечно, затем вы можете применить к нему StrToFloat, чтобы получить округленное значение в виде числа с плавающей запятой.

Глянь сюда:

http://docs.embarcadero.com/products/rad_studio/delphiAndcpp2009/HelpUpdate2/EN/html/delphivclwin32/SysUtils_FloatToStrF@Extended@TFloatFormat@Integer@Integer.html


-1
public static double roundToSignificantDigits(double num, int n) {
    return Double.parseDouble(new java.util.Formatter().format("%." + (n - 1) + "e", num).toString());
}

Этот код использует встроенную функцию форматирования, которая превращена в функцию округления.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.