Точная арифметика с плавающей запятой в JavaScript

Я создаю виджет числового счетчика в JavaScript, чтобы по существу имитировать числовое поле в webkit.

Когда вы меняете число, необходимо проверить, находится ли значение не только в допустимом диапазоне, но и в шаге:

<input type="number" min="0" max="100" step="1" />

Если пользователь вводит 5.5, поле усекает его до ближайшего шага ниже значения, которое в данном случае равно 5.

Для шага 2, если пользователь ввел 5.5, результатом будет 4.

Уравнение, которое я планировал использовать для этого расчета, выглядит так:

...code...
_checkStep: function (val) {
    val ret,
        diff,
        moddedDiff;
    diff = val - this._min;
    moddedDiff = diff % this._step;
    ret = val - moddedDiff;
    return ret;
},
//set elsewhere
_min,
_step,
...more code...

Хотя для целей тестирования вы можете просто использовать это:

function checkStep(val, min, step) {
    var ret,
        diff,
        moddedDiff;
    diff = val - min;
    moddedDiff = diff % step;
    ret = val - moddedDiff;
    return ret;
}

Это отлично работает для целых чисел и больших значений, однако я столкнулся с проблемами с десятичными знаками из-за того, как JavaScript обрабатывает числа с плавающей запятой.

Например:

checkStep(0.5, 0, 0.1) //returns 0.4, 0.5 is expected

При анализе каждой строки оказывается, что 0.5 % 0.1 в JavaScript возвращает 0.09999999999999998.

Что можно сделать, чтобы сделать эту функцию более точной*?


*точно в том, что он работает для приращений 0.01 и выше.


person zzzzBov    schedule 27.04.2012    source источник
comment
Не существует такой вещи, как «точная» плавающая точка. Они неизменно представляют собой максимально приближенные значения. В лучшем случае вы можете указать уровень точности, с которым вы готовы жить.   -  person Marc B    schedule 27.04.2012
comment
@MarcB, хороший момент, забыл упомянуть, что я заинтересован в том, чтобы эта работа была на сотом месте.   -  person zzzzBov    schedule 27.04.2012
comment
0,5 % 0,1 в JavaScript возвращает 0,09999999999999998. О, это только верхушка айсберга. 0.1 + 0.2 = 0.30000000000000004.   -  person T.J. Crowder    schedule 27.04.2012
comment
@ TJCrowder, да, да, я хорошо осведомлен о стандартных неточностях в арифметике с плавающей запятой, я просто не знаю, как их учитывать в этом случае.   -  person zzzzBov    schedule 27.04.2012
comment
Я нахожу ваши примеры немного запутанными. Для шага 2 и ввода 5,5 не должно ли возвращаться 6, а не 4? 5,5 ближе к 6, чем к 4. Аналогично, не должен ли шаг 0,5 и ввод 0,1 возвращать 0, поскольку 0,1 ближе к 0, чем к 0,5?   -  person Ethan Brown    schedule 27.04.2012
comment
@EthanBrown, ближайший шаг ниже, чем значение. В основном я пытался имитировать реализацию Chrome, я рассматривал округление до ближайшего шага, но это имеет дополнительные сложности, которые необходимо решить, например: что происходит, когда шаг равен 2, а значение равно 1, что направление округляется? Более простая реализация состоит в том, чтобы просто перейти к ближайшему шагу.   -  person zzzzBov    schedule 27.04.2012


Ответы (2)


Вы можете попробовать убедиться, что шаг больше 1 (неоднократно умножая на 10), затем выполните свой модуль, а затем вернитесь к оригиналу. Например:

function checkStep(val, min, step) {
  var ret,
    diff,
    moddedDiff;
  var pows = 0;
  while( step < 1 ) { // make sure step is > 1
    step *= 10;
    val *= 10;
    min *= 10;
    pows++;
  }
  diff = val - min;
  moddedDiff = diff % step;
  ret = val - moddedDiff;
  return ret / Math.pow( 10, pows );
}

Это работает для приведенных вами примеров, но я не могу гарантировать, что это сработает для всего. См. jsfiddle здесь:

http://jsfiddle.net/bCTL6/2/

person Ethan Brown    schedule 27.04.2012
comment
Я собирался обновить свой ответ некоторым обновленным кодом. По сути, я делаю то же самое, но использую рекурсивный вызов функции. Кажется, пока это работает, хотя необходимо также выполнить некоторую проверку модуля. - person zzzzBov; 28.04.2012

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

person Oleg V. Volkov    schedule 27.04.2012
comment
Забавно, что вы говорите, что нет гарантии точности, поскольку арифметика с плавающей запятой четко определена и дает одинаковые результаты в каждом браузере. Проблема в том, что это часто противоречит здравому смыслу. Я действительно надеялся получить помощь в том, чтобы сделать арифметический код с плавающей запятой более устойчивым и заставить его работать в разумных человеческих диапазонах. - person zzzzBov; 27.04.2012
comment
Ну, я имел в виду с точки зрения десятичной точки зрения, да. :) Конечно, внутри он всегда работает точно так же. - person Oleg V. Volkov; 27.04.2012