Округление до произвольного числа значащих цифр в JavaScript не работает
Я пробовал ниже пример кода
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;
}
Но эта функция не работает для таких входных данных, как sigFigs(24730790,3), возвращает 24699999.999999996, а sigFigs(4.7152e-26,3) возвращает: 4.7200000000000004e-26
Если у кого-то есть рабочий пример, пожалуйста, поделитесь. Благодарю.
6 ответов
Вы можете попробовать встроенный метод javascript-
Number( my_number.toPrecision(3) )
Для вашего случая попробуйте
Number( 24730790.0.toPrecision(5) )
Для вашего справочного и рабочего примера вы можете увидеть ссылку
Прежде всего, спасибо всем, без этих фрагментов было бы непросто.
Моя добавленная стоимость - это следующий фрагмент (полную реализацию см. Ниже)
parseFloat(number.toPrecision(precision))
Обратите внимание: если число, например, 10000, а точность 2, то number.toPrecision(precision)
будет '1.0e+4', но parseFloat
понимает экспоненциальную запись.
Также стоит сказать, что алгоритм, использующий Math.pow
и логарифмы, указанные выше, при запуске на тестовом примере formatNumber(5, 123456789)
давал успех на Mac (узел v12), но рос и ошибка в Windows (узел v10). Это было странно, поэтому мы пришли к решению выше.
В конце концов, я нашел это как окончательную реализацию, воспользовавшись всеми отзывами, представленными в этом посте. Предполагая, что у нас есть файл formatNumber.js со следующим содержимым
/**
* Format number to significant digits.
*
* @param {Number} precision
* @param {Number} number
*
* @return {String} formattedValue
*/
export default function formatNumber (precision, number) {
if (typeof number === 'undefined' || number === null) return ''
if (number === 0) return '0'
const roundedValue = round(precision, number)
const floorValue = Math.floor(roundedValue)
const isInteger = Math.abs(floorValue - roundedValue) < Number.EPSILON
const numberOfFloorDigits = String(floorValue).length
const numberOfDigits = String(roundedValue).length
if (numberOfFloorDigits > precision) {
return String(floorValue)
} else {
const padding = isInteger ? precision - numberOfFloorDigits : precision - numberOfDigits + 1
if (padding > 0) {
if (isInteger) {
return `${String(floorValue)}.${'0'.repeat(padding)}`
} else {
return `${String(roundedValue)}${'0'.repeat(padding)}`
}
} else {
return String(roundedValue)
}
}
}
function round (precision, number) {
return parseFloat(number.toPrecision(precision))
}
Если вы используете ленту для тестов, вот несколько основных тестов
import test from 'tape'
import formatNumber from '..path/to/formatNumber.js'
test('formatNumber', (t) => {
t.equal(formatNumber(4, undefined), '', 'undefined number returns an empty string')
t.equal(formatNumber(4, null), '', 'null number return an empty string')
t.equal(formatNumber(4, 0), '0')
t.equal(formatNumber(4, 1.23456789), '1.235')
t.equal(formatNumber(4, 1.23), '1.230')
t.equal(formatNumber(4, 123456789), '123500000')
t.equal(formatNumber(4, 1234567.890123), '1235000')
t.equal(formatNumber(4, 123.4567890123), '123.5')
t.equal(formatNumber(4, 12), '12.00')
t.equal(formatNumber(4, 1.2), '1.200')
t.equal(formatNumber(4, 1.234567890123), '1.235')
t.equal(formatNumber(4, 0.001234567890), '0.001235')
t.equal(formatNumber(5, 123456789), '123460000')
t.end()
})
Использование чистой математики (т.е. без использования строк)
Вы можете округлить значение до любого произвольного количества значащих цифр, просто сначала вычислив наибольшую степень десяти, содержащуюся в числе, а затем использовать ее для расчета точности, до которой следует округлять число.
/**
* Rounds a value to a given number of significant figures
* @param value The number to round
* @param significanFigures The number of significant figures
* @returns The value rounded to the given number of significant figures
*/
const round = (value, significantFigures) => {
const exponent = Math.floor(Math.log10(value))
const nIntegers = exponent + 1
const precision = 10 ** (nIntegers - significantFigures)
return Math.round(value / precision) * precision
}
В приведенном выше коде просто означает ближайшее кратное, до которого мы хотим округлить. Например, если мы хотим округлить до ближайшего числа, кратного 100, точность равна 100. Если мы хотим округлить до ближайшего числа, кратного 0,1, то есть до одного десятичного знака, точность равна 0,1.
The
exponent
это просто показатель наибольшей степени 10, содержащейся в значении. Продолжайте читать ниже, если вам интересно узнать, откуда это взялось.
nIntegers
— количество целых чисел (цифр слева от десятичной точки) в значении.
Некоторые примеры
> round(173.25, 1)
200
> round(173.25, 2)
170
> round(173.25, 3)
173
> round(173.25, 4)
173.3
> round(173.25, 5)
173.25
Интуитивное и познавательное объяснение
Как правило, мы можем округлить любое число до заданной точности, сначала «перемещая десятичный знак» в одном направлении (деление), затем округляя число до ближайшего целого числа и, наконец, перемещая десятичный знак обратно в исходное положение (умножение)..
const rounded = Math.round(value / precision) * precision
Например, чтобы округлить значение до ближайшего числа, кратного 10 , точность устанавливается равной 10 , и мы получаем
> Math.round(173.25 / 10) * 10
170
Аналогично, если мы хотим округлить значение до одного десятичного знака, т. е. найти ближайшее кратное 0,1 , точность устанавливается равной 0,1.
> Math.round(173.25 / 0.1) * 0.1
173.3
Здесь
precision
просто означает «ближайшее кратное, до которого мы хотим округлить».
Так как же нам использовать эти знания для округления значения до любого заданного количества значащих цифр?
Проблема, которую нам предстоит решить, состоит в том, чтобы определить точность, до которой следует округлять, учитывая количество значащих цифр. Допустим, мы хотим округлить значение 12345,67 до трех значащих цифр. Как определить, что в этом случае точность должна быть 100?
Точность должна быть 100 , потому что
Math.round(12345.67 / 100) * 100
дает 12300 , т.е. округляется до трех значащих цифр.
На самом деле это очень легко решить.
По сути, нам нужно 1) определить, сколько цифр находится в левой части десятичного знака, а затем 2) использовать это, чтобы определить, на сколько шагов нужно «переместить десятичный знак» перед округлением числа.
Начнем с подсчета количества цифр в целой части числа (то есть 5 цифр) и вычтем количество значащих цифр, до которых мы хотим округлить (3 цифры). Результат 5 - 3 = 2 — это количество шагов, на которое мы должны переместить десятичный знак влево (если результат был отрицательным, мы переместили бы десятичный знак вправо).
Чтобы переместить десятичный знак на два шага влево, нам нужно использовать точность 10^2 = 100. Другими словами, результат дает нам степень, в которую следует возвести 10 , чтобы получить точность.
n_integer = "number of digits in the integer part of the number" = 5
n_significant = "the number of significant figures we want to round" = 3
precision = 10 ** (n_integer - n_significant)
Вот и все!
Но, эй, подождите минутку! На самом деле вы не показали нам, как это кодировать! Кроме того, вы сказали, что мы не хотим использовать строки. Как посчитать количество цифр в целой части числа, не преобразуя его в строку? Ну, нам не обязательно использовать для этого строки. Математика приходит на помощь!
Мы знаем, что действительное значение v можно выразить как степень десяти (используя десять, поскольку мы работаем в десятичной системе). То есть v = 10^a . Если теперь мы возьмем только целую часть a , назовем ее a' , новое значение v' = 10^a' будет самой большой степенью 10 , содержащейся в v . Количество цифр в v такое же, как и в v' , то есть a' + 1 . Таким образом, мы показали, что n_integer = a' + 1 , где a' =floor(log10(v)).
В коде это выглядит так
const exponent = Math.floor(Math.log10(v)) // a'
const nIntegers = exponent + 1
И, как мы уже говорили ранее, точность
const precision = 10 ** (nInteger - nSignificant)
И, наконец, округление
return Math.round(value / precision) * precision
Пример
Допустим, мы хотим округлить значение v = 12345,67 до 1 значащей цифры. Чтобы приведенный выше код работал, точность должна быть Precision = 10000 = 10^(n_integers - 1).Если бы мы хотели округлить до 6 значащих цифр, точность должна была бы быть Precision = 0,1 = 10^(n_integers - 6) .
Как правило, точность должна быть Precision = 10^(n_integers - n_significant).
Забавный побочный эффект
Используя этот код и знания, вы можете округлить число до ближайшего кратного любого значения, а не только до старых и скучных степеней 10 (т. е. {..., 1000, 100, 10, 1, 0,1, 0,01, ...} ). Нет, при этом можно, например, округлить до ближайшего числа, кратного, скажем, 0,3 .
> Math.round(4 / 0.3) * 0.3
3.9
0,3 * 13 = 3,9 , что кратно 0,3 , ближайшему к 4.
Как насчет автоматического преобразования типов, которое заботится об экспоненциальной записи?
f = (x, n) => +x.toPrecision(n)
Тестирование:
> f (0.123456789, 6)
0.123457
> f (123456789, 6)
123457000
> f (-123456789, 6)
-123457000
> f (-0.123456789, 6)
-0.123457
> f (-0.123456789, 2)
-0.12
> f (123456789, 2)
120000000
И он возвращает число, а не строку.
К сожалению, встроенный метод даст вам глупые результаты, когда число> 10, например, в экспоненциальной нотации и т. Д.
Я создал функцию, которая должна решить проблему (возможно, не самый элегантный способ ее написания, но вот оно):
function(value, precision) {
if (value < 10) {
value = parseFloat(value).toPrecision(precision)
} else {
value = parseInt(value)
let significantValue = value
for (let i = value.toString().length; i > precision; i--) {
significantValue = Math.round(significantValue / 10)
}
for (let i = 0; significantValue.toString().length < value.toString().length; i++ ) {
significantValue = significantValue * 10
}
value = significantValue
}
return value
}
Если вы предпочитаете использовать экспоненциальную нотацию для более высоких чисел, не стесняйтесь использовать метод toPrecision().
Если вы хотите указать значащие цифры слева от десятичной точки и заменить посторонние заполнители на T B M K соответственно
// example to 3 sigDigs (significant digits)
//54321 = 54.3M
//12300000 = 12.3M
const moneyFormat = (num, sigDigs) => {
var s = num.toString();
let nn = "";
for (let i = 0; i <= s.length; i++) {
if (s[i] !== undefined) {
if (i < sigDigs) nn += s[i];
else nn += "0";
}
}
nn = nn
.toString()
.replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,")
.replace(",000,000,000", "B")
.replace(",000,000", "M")
.replace(",000", "k");
if (
nn[nn.length - 4] === "," &&
nn[nn.length - 2] === "0" &&
nn[nn.length - 1] === "0"
) {
let numLetter = "K";
if (parseInt(num) > 999999999999) numLetter = "T";
else if (parseInt(num) > 999999999) numLetter = "B";
else if (parseInt(num) > 999999) numLetter = "M";
console.log("numLetter: " + numLetter);
nn = nn.toString();
let nn2 = ""; // new number 2
for (let i = 0; i < nn.length - 4; i++) {
nn2 += nn[i];
}
nn2 += "." + nn[nn.length - 3] + numLetter;
nn = nn2;
}
return nn;
};