Как исправить неточную математику Javascript с помощью math.round()?

#javascript #math #rounding

#javascript #математика #округление

Вопрос:

Я опубликовал этот код пару дней назад, но обсуждение, казалось, приобрело философский характер по поводу слабостей Javascript (не говоря уже о моих собственных очевидных недостатках как «программиста»), а затем прекратилось, так и не прояснив решение. Я надеюсь, что кто-нибудь поможет это исправить.

Из-за явления, по-видимому, известного как «математика с плавающей запятой», Javascript возвращает арифметически неточный ответ (.00000004 или аналогичный ниже правильного ответа) в приведенном ниже коде калькулятора. Мне посоветовали округлить ответ, «вызвав math.round() для переменной», что, я думаю, отлично подошло бы для моих целей, только мое JS kungfu было слишком слабым, и синтаксис для этого в контексте до сих пор ускользал от меня.

Где / как мне выполнить этот вызов? До сих пор все мои попытки не увенчались успехом, даже когда я был уверен, что каждая из них не будет. Я был бы очень признателен за ответ, который учитывает мои низкоуровневые знания предмета. Это, должно быть, верняк для кого-то там.

 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Untitled Document</title>

<script language="javascript">

<!-- Begin Trip Tickets Savings Calc script
function  doMath4() {
    var one = parseInt(document.theForm4.elements[0].value);
    var two = parseInt(document.theForm4.elements[1].value);
    var selection = document.getElementsByName("zonett")[0].value;

    if(selection == "z4"){
        var prodZ4tt = (((one  *   two) * 4.25) *12) - (((one  *   two) * 3.75) *12);
        alert("Your yearly savings if you buy Trip Tickets is $"     prodZ4tt     ".");
    }
    else if(selection == "z3"){
        var prodZ3tt = (((one  *   two) * 3.75) *12) - (((one  *   two) * 3.35) *12);
        alert("Your yearly savings if you buy Trip Tickets is $"     prodZ3tt     ".");
    }
    else if(selection == "z2"){
        var prodZ2tt = (((one  *   two) * 3) *12) - (((one  *   two) * 2.8) *12);
        alert("Your yearly savings if you buy Trip Tickets is $"     prodZ2tt     ".");
    }
    else if(selection == "z1"){
        var prodZ1tt = (((one  *   two) * 2.5) *12) - (((one  *   two) * 2.3) *12);
        alert("Your yearly savings if you buy Trip Tickets is $"     prodZ1tt     ".");
    }
    else if(selection == "Base"){
        var prodBasett = (((one  *   two) * 1.5) *12) - (((one  *   two) * 1.5) *12);
        alert("Your yearly savings if you buy Trip Tickets is $"     prodBasett     ".");
    }
}

// End Trip Tickets Savings Calc script -->

</script>
</head>

<body>


<form name="theForm4" class="calcform">
<h2>You Do the Math: Commuter Express Trip Tickets Vs. Cash</h2>
<div class="calcform-content">
    <div class="formrow-calc">
      <div class="calcform-col1">
        <p>Days you commute on Commuter Express monthly:</p>
      </div>
      <div class="calcform-col2">
        <input type="text">
      </div>
      <div class="calcform-col3">amp;nbsp;</div>
    </div>
    <div class="clear"></div>


    <div class="formrow-calc">
      <div class="calcform-col1">
        <p>Daily boardings on Commuter Express Bus:</p>

        <table border="0" cellspacing="0" cellpadding="0" class="fareexampletable">
            <tr>
              <td colspan="2" class="savingsleft"><p class="ifyouride">EXAMPLE:</p></td>
              </tr>
            <tr>
              <td class="savingsleft"><p><strong>Go to work:</strong></p></td>
              <td class="savingsright"><p>1 time</p></td>
            </tr>
            <tr>
              <td class="savingsleft"><p><strong>Come home from work:</strong></p></td>
              <td class="additionline savingsright"><p> 1 time</p></td>
            </tr>
            <tr>
              <td class="savingsleft"><p><strong>Total:</strong></p></td>
              <td class="savingsright"><p>2 times</p></td>
            </tr>
          </table>
      </div>
      <div class="calcform-col2">
        <input type="text">
      </div>
      <div class="calcform-col3">amp;nbsp;</div>
    </div>
    <div class="clear"></div>


    <div class="formrow-calc savings-zone">
      <div class="calcform-col1">
        <p>Choose Zone:</p>
      </div>
      <div class="calcform-col2">
        <select name="zonett">
          <option value="Base">Base</option>
          <option value="z1">Zone 1</option>
          <option value="z2">Zone 2</option>
          <option value="z3">Zone 3</option>
          <option value="z4">Zone 4</option>
        </select>
      </div>
    </div>



    <div class="formrow-calc">


          <div class="calcform-col4-ce">


 <button type="submit" onclick="doMath4()" class="btn-submit"><div class="btn-submit"><img src="img/btn_savings.png" alt="Show My Yearly Savings" /></div></button> 


    </div>
    </div>
    <div class="clear">


</div>



</div>
</form>

</body>
</html>
  

Комментарии:

1. Где вы обнаруживаете неточность?

2. @ 65Fbef05 Только при вводе-выводе выбираются зоны 1-3

3. Решение простое: не используйте значения с плавающей запятой в финансовых вычислениях. Используйте центы для всего или соответственно несколько, чтобы вам не приходилось работать с числами с плавающей запятой…

4. @Felix скажи, а? Просто, вы говорите? Я явно здесь не в своей тарелке, но я вижу центовые значения в этих, по общему признанию, неудобных, но все еще функциональных формулах. Решение может быть простым, но я до сих пор его не видел.

5. Я имею в виду, что вы должны работать только с целыми числами и только для представления создавать значения с плавающей запятой. Итак, вместо 3.12 * 12 у вас есть 312 * 1200 и для представления вы снова делите это на 100 .

Ответ №1:

В общем случае вы хотите округлять в последнюю очередь, чтобы поддерживать математическую точность вплоть до момента отображения данных.

Вот как я бы реализовал вашу маленькую библиотеку:

 var SavingsLib = {
    round : function(val, digits) {
        var pow = Math.pow(10, digits);
        return Math.round(pow * val) / pow;
    },

    padCurrency : function (val) {
        var str = val.toString();
        var parts = str.split(".");
        return parts.length > 1 amp;amp; parts[1].length == 1 ? str   "0" : str;
    },

    processForm : function() {
        var daysPerMonth = parseInt(document.theForm4.elements[0].value);
        var boardingsPerDay = parseInt(document.theForm4.elements[1].value);
        var zone = document.getElementsByName("zonett")[0].value;

        var savings = 0;
        if(zone == "z4") savings = this.calculateSavings(daysPerMonth, boardingsPerDay, 4.25, 3.75);
        else if(zone == "z3") savings = this.calculateSavings(daysPerMonth, boardingsPerDay, 3.75, 3.35);
        else if(zone == "z2") savings = this.calculateSavings(daysPerMonth, boardingsPerDay, 3, 2.8);
        else if(zone == "z1") savings = this.calculateSavings(daysPerMonth, boardingsPerDay, 2.5, 2.3);
        else if(zone == "Base") savings = this.calculateSavings(daysPerMonth, boardingsPerDay, 1.5, 1.5);

        this.showMessage(savings);

        return false; // Don't submit form
    },

    calculateSavings : function(daysPerMonth, boardingsPerDay, price1, price2) {
        var amount1 = (((daysPerMonth * boardingsPerDay) * price1) * 12);
        var amount2 = (((daysPerMonth * boardingsPerDay) * price2) * 12);

        return this.round(amount1 - amount2, 2);
    },

    showMessage : function(savings) {
        alert("Your yearly savings if you buy Trip Tickets is $"   this.padCurrency(savings));
    }
}
  

Мои изменения:

  • Инкапсулированные функции в объект для пространства имен
  • Извлек математику в один метод, чтобы вы могли выполнить один вызов раунда
  • Извлек отображение сообщения в метод, чтобы вы могли легко изменять его
  • Добавлена функция округления в цифры (Так 1.4500001 становится 1.45 вместо 1 )
  • Добавлена функция pad currency (так 1.4 становится 1.40 )

Комментарии:

1. Спасибо за подробности. Конечно, я реализовал это неправильно, потому что это не сработало. Это для замены содержимого функции doMath4()? Как в: функция doMath4() { … ваш код здесь … }

2. Нет, на самом деле, вы хотите вызвать SavingsLib.processForm(); с кнопки, которую вы хотите предупредить о значении экономии.

Ответ №2:

Вы можете написать kludge с помощью Math.round , но это приведет к потере центов (не уверен, применимо ли это в вашем примере, похоже, что могло бы). Возможно, вам захочется исправить:

 alert((102.000000004).toFixed(2)); // alerts "102.00"
  

Это не округляет в IE, но из того, что я могу сказать, это не будет иметь значения в вашем случае.

(документы: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Number/toFixed , http://www.codingforums.com/showthread.php?t=102421 , http://msdn.microsoft.com/en-us/library/sstyff0z.aspx )

Редактировать: предполагалось поместить это в комментарии, но markdown там не работает? Все еще привыкаю к SO…

В любом случае, вы также можете использовать kludge с Math.round, если предпочитаете:

 alert(Math.round(val * 100) / 100);
  

Который все еще может отображать глупые проблемы с плавающей запятой, для которых вы могли бы использовать другой .toFixed (и теперь вы на 100% уверены, что нет оставшихся десятичных знаков, которые вы округляете неправильно):

 alert((Math.round(val * 100) / 100).toFixed(2));
  

Редактировать: для интеграции:

Добавьте это в начало блока вашего скрипта:

 function roundNicely(val) {
     return (Math.round(val * 100) / 100).toFixed(2);
}
  

Затем в ваших оповещениях перенесите значение prodBasett в вызов функции: roundNicely(prodBasett) (и аналогично для других оповещений)

Имеет ли это смысл?

Комментарии:

1. @Gijs, спасибо, но, к сожалению, мне нужна кроссбраузерная совместимость (вы хотите сказать, что IE на самом деле вычисляет математику правильно?), и мне нужно получить два знака после запятой там.

2. @user: Вы проверили результаты в IE?

3. @Felix — на данный момент на Mac, и мой КОМПЬЮТЕР в настоящее время не работает. Я проверю, когда эта ситуация разрешится.

4. Насколько я понимаю, это будет отлично работать в IE. Просто если бы у вас было, скажем, 240.005 долларов, в итоге получилось бы 240.00 вместо 240.01, как вы ожидали бы, в IE. Однако, глядя на цифры в вашем приложении, это не имеет значения. У вас есть только числа с 2 или меньшим количеством знаков после запятой, и вы умножаете, поэтому не будет 3-го знака после запятой (во всяком случае, никаких допустимых).

5. @Gijs Я люблю kludges, и я очарован вашим ответом, но как мне включить это в свой код синтаксически? Это правильное слово?

Ответ №3:

Это очень просто:

 var prodZ4tt = Math.round(((one  *   two) * 4.25) *12) - (((one  *   two) * 3.75) *12);
  

Хотя понятно, почему вы можете запутаться в синтаксисе, поскольку JavaScript не известен своим интуитивным способом выполнения действий. 😉

Примечание: При этом число округляется до ближайшего целого числа, оно не «округляется в большую сторону».

Комментарии:

1. Спасибо, как за ответ, так и за сочувствие. Во-первых, как нам округлить это значение до ближайшего 2-го десятичного знака (чтобы в нем отображались доллары и центы)?

Ответ №4:

Для тех случаев, когда вам нужна точность, вам должна понадобиться математическая «Библиотека произвольной точности»… вы могли бы найти хорошую статью здесь.

http://blog.thatscaptaintoyou.com/introducing-big-js-arbitrary-precision-math-for-javascript/

В Javascript изначально есть round, ceil и floor для преобразования чисел с плавающей точкой в целые числа.

 var fl = 349.12783691847;
Math.round(fl) // 349
Math.ceil(f1) // 350
Math.floor(fl) // 349
  

при выполнении математических операций рекомендуется использовать дистрибутивность и правильно использовать круглые скобки.

 Math.round(((((a   b) * (c - d)) % e) * f));
  

Будьте осторожны с «NaN» (http://en.wikipedia.org/wiki/NaN ) и не числовые типы.

хорошего дня