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


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закінчення, і зробив довгий проміжний більш зрозумілим

Помилка була спричинена множенням великого числа на невелике. Натомість я ділю два числа подібного розміру.

EDIT
Виправлено більше помилок. Додана перевірка на 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 настільки крутий. Ви можете знайти корисну інформацію!
Піролістичний

2
Зауважте, що для значень, близьких до круглої межі, це може незначно зазнати помилки. Наприклад, округлення 1,255 до 3 значущих цифр повинно повернути 1,26, але поверне 1,25. Це тому, що 1,255 * 100,0 - це 125,499999 ... Але цього слід очікувати при роботі з дублями
cquezel

Ого, я знаю, що це старе, але я намагаюся ним скористатися. У мене є поплавок, який я хочу відобразити до 3 значущих цифр. Якщо значення плаваючого значення 1.0, я називаю ваш метод, але він все одно повертається як 1.0, навіть якщо я додаю плаваючий знак як подвійний. Я хочу, щоб він повернувся як 1. Будь-які ідеї?
Steve W

3
Цей фрагмент Java закінчується офіційним прикладом для 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

але 12.1257 дає 12.126
піролістичний

1
Приємна відповідь Ates. Можливо, додати тригер, щоб повернути 0, якщо n==0:)
sscirrus

чи є причина робити, Math.log(n) / Math.LN10а не Math.log10(n)?
Lee

1
@Lee developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… "Це нова технологія, яка є частиною стандарту ECMAScript 2015 (ES6)." Отже, в основному, проблеми сумісності.
Ates Goral

Мені чогось не вистачає, чи ця відповідь передбачає це 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

Отже, використовуйте відповідь Піро!


1
Цей алгоритм, схоже, схильний до помилок з плаваючою комою. При реалізації з JavaScript я отримую: 0,06805 -> 0,06810000000000001 та 12.1 -> 12.100000000000001
Ates Goral

12.1 сам по собі не може бути точно представлений за допомогою плаваючої крапки - це не результат цього алгоритму.
Клавдіу,

1
Цей код в Java виробляє 12.100000000000001, і він використовує 64-розрядні подвійні, які можуть представляти 12.1 точно.
Піролістичний

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

2
для тих, хто вступає, відповідь Pyrolistic в основному є більш точною, ніж моя, так що алгоритм друку з плаваючою комою друкує '12 .1 'замість '12 .100000000000001'. його відповідь краща, навіть якщо я був технічно правильним, що ви не можете точно представити '12 .1 '.
Клаудіу

10

Хіба не "коротка і солодка" реалізація JavaScript

Number(n).toPrecision(sig)

напр

alert(Number(12345).toPrecision(3)

?

Вибачте, я тут не нахабний, просто використання функції "roundit" від Клаудіу та .toPrecision в JavaScript дає мені різні результати, але лише при округленні останньої цифри.

JavaScript:

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

.NET

roundit(8.14301,4) == 8.144

1
Number(814301).toPrecision(4) == "8.143e+5". Як правило, не те, що ви хочете, якщо ви показуєте це користувачам.
Заз

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

8

(Дуже приємне!) Рішення Pyrolistic все ще має проблему. Максимальне подвійне значення в 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 number, 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.
matt

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 Н
• Додайте 0,5
• Зріжте цифри дробу (тобто усікайте результат у ціле число)
• Поділіть на 10 Н

Для N значущих інтегральних (дробових) цифр:

• Поділіть число на 10 Н
• Додайте 0,5
• Зріжте цифри дробу (тобто усікайте результат на ціле число)
• Помножте на 10 Н

Це можна зробити на будь-якому калькуляторі, наприклад, який має оператор "INT" (цілочисельне усічення).


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

Так, я виправив розрізняти округлення до дрібного числа цифр (праворуч від десяткового дробу) по порівнянні з інтегральним числом цифр (зліва).
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);
}

1

Ось код Pyrolistics (на даний момент найкраща відповідь) у 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.