Есть ли надежный способ в JavaScript получить количество десятичных знаков произвольного числа?
важно отметить, что я не ищу функцию округления. Я ищу функцию, которая возвращает количество десятичных знаков в упрощенном десятичном представлении произвольного числа. То есть, мы имеем следующее:
decimalPlaces(5555.0); //=> 0
decimalPlaces(5555); //=> 0
decimalPlaces(555.5); //=> 1
decimalPlaces(555.50); //=> 1
decimalPlaces(0.0000005); //=> 7
decimalPlaces(5e-7); //=> 7
decimalPlaces(0.00000055); //=> 8
decimalPlaces(5.5e-7); //=> 8
моим первым побуждением было использовать строковые представления: split on '.'
, затем на 'e-'
, и сделайте математику, например (пример подробный):
function decimalPlaces(number) {
var parts = number.toString().split('.', 2),
integerPart = parts[0],
decimalPart = parts[1],
exponentPart;
if (integerPart.charAt(0) === '-') {
integerPart = integerPart.substring(1);
}
if (decimalPart !== undefined) {
parts = decimalPart.split('e-', 2);
decimalPart = parts[0];
}
else {
parts = integerPart.split('e-', 2);
integerPart = parts[0];
}
exponentPart = parts[1];
if (exponentPart !== undefined) {
return integerPart.length +
(decimalPart !== undefined ? decimalPart.length : 0) - 1 +
parseInt(exponentPart);
}
else {
return decimalPart !== undefined ? decimalPart.length : 0;
}
}
для моих примеров выше эта функция работает. Тем не менее, я не удовлетворен, пока я не проверил все возможные значения, поэтому я вырвался Number.MIN_VALUE
.
Number.MIN_VALUE; //=> 5e-324
decimalPlaces(Number.MIN_VALUE); //=> 324
Number.MIN_VALUE * 100; //=> 4.94e-322
decimalPlaces(Number.MIN_VALUE * 100); //=> 324
это выглядело разумным сначала, но затем на двойном дубле я понял, что 5e-324 * 10
должно быть 5e-323
! И тут меня осенило: я имею дело с эффектами квантования очень небольшом количестве. Кроме того, некоторые числа, хранящиеся в двоичном формате, имеют неоправданно длинные десятичные представления, поэтому их десятичные представления усекаются. Это неудачно для меня, потому что это означает, что я не могу получить их истинную десятичную точность, используя их строковые представления.
Итак, я пришел к вам, сообщество StackOverflow. Кто-нибудь из вас знает надежный способ получить истинную точность числа после десятичной запятой?
цель этой функции, если кто-нибудь спросит, заключается в использовании в другой функции, которая преобразует float в упрощенную дробь (то есть возвращает относительно целочисленный числитель coprime и ненулевой естественный знаменатель). Единственная недостающая часть в этой внешней функции-надежный способ определить количество десятичных знаков в поплавке, чтобы я мог умножить его на соответствующую степень 10. Надеюсь, я слишком много об этом думаю.
6 ответов
историческое Примечание: поток комментариев ниже может ссылаться на первую и вторую реализации. Я поменял заказ в сентябре 2017 года, так как ведущий с багги-реализацией вызвал путаницу.
если вы хотите что-то, что карты "0.1e-100"
до 101, то вы можете попробовать что-то вроде
function decimalPlaces(n) {
// Make sure it is a number and use the builtin number -> string.
var s = "" + (+n);
// Pull out the fraction and the exponent.
var match = /(?:\.(\d+))?(?:[eE]([+\-]?\d+))?$/.exec(s);
// NaN or Infinity or integer.
// We arbitrarily decide that Infinity is integral.
if (!match) { return 0; }
// Count the number of digits in the fraction and subtract the
// exponent to simulate moving the decimal point left by exponent places.
// 1.234e+2 has 1 fraction digit and '234'.length - 2 == 1
// 1.234e-2 has 5 fraction digit and '234'.length - -2 == 5
return Math.max(
0, // lower limit.
(match[1] == '0' ? 0 : (match[1] || '').length) // fraction length
- (match[2] || 0)); // exponent
}
согласно спецификации, любое решение на основе встроенного числа - > преобразование строк может быть точным только до 21 места за пределами экспоненты.
9.8.1 ToString применяется к типу числа
- в противном случае пусть N, k и s-целые числа, такие, что k ≥ 1, 10k-1 ≤ s
- если k ≤ n ≤ 21, верните строку, состоящую из k цифр десятичного представления s (по порядку, без начальных нулей), за которыми следуют N-k вхождений символа ‘0’.
- Если 0
- если -6
историческое Примечание: реализация ниже проблематична. Я оставляю его здесь как контекст для потока комментариев.
на основании определения Number.prototype.toFixed
, похоже, что должно работать следующее, но из-за представления IEEE-754 двойных значений некоторые числа будут давать ложные результаты. Например, decimalPlaces(0.123)
вернутся 20
.
function decimalPlaces(number) {
// toFixed produces a fixed representation accurate to 20 decimal places
// without an exponent.
// The ^-?\d*\. strips off any sign, integer portion, and decimal point
// leaving only the decimal fraction.
// The 0+$ strips off any trailing zeroes.
return ((+number).toFixed(20)).replace(/^-?\d*\.?|0+$/g, '').length;
}
// The OP's examples:
console.log(decimalPlaces(5555.0)); // 0
console.log(decimalPlaces(5555)); // 0
console.log(decimalPlaces(555.5)); // 1
console.log(decimalPlaces(555.50)); // 1
console.log(decimalPlaces(0.0000005)); // 7
console.log(decimalPlaces(5e-7)); // 7
console.log(decimalPlaces(0.00000055)); // 8
console.log(decimalPlaces(5e-8)); // 8
console.log(decimalPlaces(0.123)); // 20 (!)
Ну, я использую решение, основанное на том, что если вы умножите число с плавающей запятой на правильную степень 10, вы получите целое число.
например, если вы умножите 3.14 * 10 ^ 2, вы получите 314 (целое число). Показатель представляет собой число десятичных знаков, которое имеет число с плавающей запятой.
Итак, я подумал, что если я постепенно умножу плавающую точку на увеличение степеней 10, вы в конечном итоге придете к решение.
let decimalPlaces = function () {
function isInt(n) {
return typeof n === 'number' &&
parseFloat(n) == parseInt(n, 10) && !isNaN(n);
}
return function (n) {
const a = Math.abs(n);
let c = a, count = 1;
while (!isInt(c) && isFinite(c)) {
c = a * Math.pow(10, count++);
}
return count - 1;
};
}();
for (const x of [
0.0028, 0.0029, 0.0408,
0, 1.0, 1.00, 0.123, 1e-3,
3.14, 2.e-3, 2.e-14, -3.14e-21,
5555.0, 5555, 555.5, 555.50, 0.0000005, 5e-7, 0.00000055, 5e-8,
0.000006, 0.0000007,
0.123, 0.121, 0.1215
]) console.log(x, '->', decimalPlaces(x));
это работает для чисел меньше e-17
:
function decimalPlaces(n){
var a;
return (a=(n.toString().charAt(0)=='-'?n-1:n+1).toString().replace(/^-?[0-9]+\.?([0-9]+)$/,'').length)>=1?a:0;
}
2017 обновления
вот упрощенная версия, основанная на ответе Эдвина. Он имеет набор тестов и возвращает правильное количество десятичных дробей для угловых случаев, включая NaN, бесконечность, экспоненциальные обозначения и числа с проблемными представлениями их последовательных дробей, таких как 0.0029 или 0.0408. Это охватывает подавляющее большинство финансовых приложений, где 0.0408
наличие 4 десятичных знаков (не 6) более важно, чем 3.14 e-21, имеющий 23.
function decimalPlaces(n) {
function hasFraction(n) {
return Math.abs(Math.round(n) - n) > 1e-10;
}
let count = 0;
// multiply by increasing powers of 10 until the fractional part is ~ 0
while (hasFraction(n * (10 ** count)) && isFinite(10 ** count))
count++;
return count;
}
for (const x of [
0.0028, 0.0029, 0.0408, 0.1584, 4.3573, // corner cases against Edwin's answer
11.6894,
0, 1.0, 1.00, 0.123, 1e-3, -1e2, -1e-2, -0.1,
NaN, 1E500, Infinity, Math.PI, 1/3,
3.14, 2.e-3, 2.e-14,
1e-9, // 9
1e-10, // should be 10, but is below the precision limit
-3.14e-13, // 15
3.e-13, // 13
3.e-14, // should be 14, but is below the precision limit
123.12345678901234567890, // 14, the precision limit
5555.0, 5555, 555.5, 555.50, 0.0000005, 5e-7, 0.00000055, 5e-8,
0.000006, 0.0000007,
0.123, 0.121, 0.1215
]) console.log(x, '->', decimalPlaces(x));
компромисс заключается в том, что метод не более 10 гарантированных дроби. Он может вернуть больше десятичных знаков правильно, но не полагайтесь на это. Числа меньше 1e-10 можно считать нулевыми, и функция будет возвращать 0. Это конкретное значение было выбрано для правильного решения углового случая 11.6894, для которого простой метод умножения на степени 10 терпит неудачу (он возвращает 5 вместо 4).
однако, это 5-й угловой случай, который я обнаружил, после 0.0029, 0.0408, 0.1584 и 4.3573. После каждого я должен был уменьшить точность на один десятичный знак. Я не знаю, есть ли другие числа с менее чем 10 десятичными знаками, для которых эта функция может возвращать неправильное число десятичных знаков. Чтобы быть в безопасности, ищите произвольной точности библиотека.
обратите внимание, что преобразование в строку и разделение на .
- это только решение для до 7 десятичных знаков. String(0.0000007) === "7e-7"
. Или даже меньше? Представление с плавающей запятой не интуитивно.
не только числа квантуются перед хранением; кроме того, некоторые числа, хранящиеся в двоичном формате, имеют неоправданно длинные десятичные представления, поэтому их десятичные представления усекаются.
JavaScript представляет числа с помощью IEEE-754 формат двойн-точности (64 бита). Насколько я понимаю, это дает вам точность 53 бита или пятнадцать-шестнадцать десятичных цифр.
поэтому для любого числа с большим количеством цифр вы просто получаете приближение. Есть некоторые библиотеки, чтобы обрабатывать большие числа с большей точностью, в том числе упомянутые в этой теме.
на основе ответа gion_13 я придумал следующее:
function decimalPlaces(n){
let result= /^-?[0-9]+\.([0-9]+)$/.exec(n);
return result === null ? 0 : result[1].length;
}
for (const x of [
0, 1.0, 1.00, 0.123, 1e-3, 3.14, 2.e-3, -3.14e-21,
5555.0, 5555, 555.5, 555.50, 0.0000005, 5e-7, 0.00000055, 5e-8,
0.000006, 0.0000007,
0.123, 0.121, 0.1215
]) console.log(x, '->', decimalPlaces(x));
Он исправляет возвращаемый 1, когда нет десятичных знаков. Насколько я могу судить, это работает без ошибок.