Есть ли надежный способ в JavaScript получить количество десятичных разрядов произвольного числа? - PullRequest
30 голосов
/ 02 марта 2012

Важно отметить, что я не ищу функцию округления. Я ищу функцию, которая возвращает количество десятичных знаков в упрощенном десятичном представлении произвольного числа. То есть имеем следующее:

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

Моим первым инстинктом было использование строковых представлений: разделить на '.', затем на '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. Кто-нибудь из вас знает надежный способ получить истинную точность числа после запятой?

Назначение этой функции, если кто-нибудь спросит, предназначено для использования в другой функции, которая преобразует число с плавающей точкой в ​​упрощенную дробь (то есть возвращает сравнительно простое целое число и ненулевой натуральный знаменатель). Единственная недостающая часть в этой внешней функции - это надежный способ определить количество десятичных разрядов в плавающей запятой, чтобы я мог умножить его на соответствующую степень 10. Надеюсь, я переосмыслил это.

Ответы [ 6 ]

17 голосов
/ 03 марта 2012

Историческая справка: поток комментариев ниже может ссылаться на первую и вторую реализации. Я поменял заказ в сентябре 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, примененный к типу номера

  1. В противном случае пусть n, k и s будут целыми числами, так что k ≥ 1, 10k − 1 ≤ s <10k, числовое значение для s × 10n − k равно m, а k настолько мало, насколько это возможно. Обратите внимание, что k - это количество цифр в десятичном представлении s, что s не делится на 10, и что младшая значащая цифра s не обязательно определяется этими критериями однозначно. </li>
  2. Если k ≤ n ≤ 21, вернуть строку, состоящую из k цифр десятичного представления s (по порядку, без начальных нулей), за которыми следует n − k вхождений символа ‘0’.
  3. Если 0
  4. Если −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 (!)
12 голосов
/ 02 декабря 2013

Я использую решение, основанное на том факте, что если вы умножите число с плавающей запятой на правильную степень 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));
3 голосов
/ 03 марта 2012

это работает для чисел меньше e-17:

function decimalPlaces(n){
    var a;
    return (a=(n.toString().charAt(0)=='-'?n-1:n+1).toString().replace(/^-?[0-9]+\.?([0-9]+)$/,'$1').length)>=1?a:0;
}
2 голосов
/ 29 июня 2017

2017 Обновление

Вот упрощенная версия, основанная на ответе Эдвина. Он имеет набор тестов и возвращает правильное количество десятичных знаков для угловых случаев, включая NaN, Infinity, обозначения экспонент и числа с проблемными представлениями их последовательных дробей, например, 0,0029 или 0,0408. Это охватывает подавляющее большинство финансовых приложений, где 0.0408 с 4 десятичными знаками (не 6) важнее, чем 3.14e-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 ).

Однако, это пятый угловой случай, который я обнаружил после 0,0029, 0,0408, 0,1584 и 4,3573. После каждого мне приходилось уменьшать точность на один десятичный знак. Я не знаю, есть ли другие числа с менее чем десятичными знаками, для которых эта функция может возвратить неправильное число десятичных знаков. Чтобы быть в безопасности, ищите библиотеку произвольной точности .

Обратите внимание, что преобразование в строку и разбиение на . является решением только до 7 десятичных знаков. String(0.0000007) === "7e-7". Или, может быть, даже меньше? Представление с плавающей точкой не является интуитивно понятным.

1 голос
/ 03 марта 2012

Мало того, что числа квантованы перед хранением;кроме того, некоторые числа, хранящиеся в двоичном формате, имеют неоправданно длинные десятичные представления, поэтому их десятичные представления усекаются.

JavaScript представляет числа с использованием IEEE-754 двойной точности (64 бита)формат.Насколько я понимаю, это дает вам точность 53 бита, или от пятнадцати до шестнадцати десятичных цифр.

Так что для любого числа с большим количеством цифр вы просто получите приближение.Есть несколько библиотек для обработки больших чисел с большей точностью, в том числе упомянутые в этой теме .

0 голосов
/ 26 декабря 2013

На основании ответа 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, когда нет десятичных разрядов. Насколько я могу сказать, это работает без ошибок.

...