Как предотвратить ошибки округления валюты с десятичным типом переменной?

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

Баланс: 1 575,75 долл. США
Первоначальный взнос: 500,00 долл. США
Остаток: 1 075,75 долл. США
Количество платежей после первоначального взноса: 9
Сумма платежа: 119,53 долл. США (x8)
Остаток: 119,51 долл. США

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

// Method within my WinForms project
public void CalculateInstallmentPayments()
{
    decimal currentBalance = Convert.ToDecimal(txtBalanceInput.Text); // Current Balance
    decimal downPayment = Convert.ToDecimal(txtDownPayment.Text); // Down Payment
    decimal installmentCount = sliderRemainingPmtCount.Value; // Installment Count
    decimal balanceAfterDP = currentBalance - downPayment; // Balance After Down Payment
    decimal installmentAmount = (balanceAfterDP / installmentCount); // Installment Amount
    decimal remainderPayment = (balanceAfterDP - (installmentAmount * (installmentCount - 1))); // Final Payment (Remainder)

    // Using Rich Text box as a 'Console' for debugging
    rtxtNotate.Text = ($"Current Balance: {currentBalance.ToString()}\nDown Payment: {downPayment.ToString("C")}\n" +
        $"Installment Count: {installmentCount.ToString()}\nInstallment Amount: {installmentAmount.ToString("C")}\n" +
        $"Remainder: {remainderPayment.ToString("C")}\n");
}

В настоящее время это вывод:

Текущий баланс: 1575,75
Первоначальный взнос: 500,00 долларов США
Количество платежей: 9
Сумма платежей: 119,53 долларов США
Остаток: 119,53 долларов США -- это ошибка округления. Должно быть 119,51 доллара.

Я рефакторил этот код часами и чувствую, что упускаю что-то невероятно простое.


person Bob Bass    schedule 27.05.2019    source источник
comment
Это одна из немногих веских причин для использования Math.Round(), вам нужно рассчитать платеж в рассрочку до суммы, которую действительно можно заплатить. С точностью до копейки, не более.   -  person Hans Passant    schedule 28.05.2019


Ответы (4)


Вы не округляете сумму первоначального взноса до ее применения. Таким образом, вы получаете правильный результат без ошибок округления (или с очень небольшими)... для кого-то, кто платит около 119,52777777777 долларов.

Если вы округлите сумму платежа перед тем, как сохранить ее в переменной installmentAmount, вы можете получить ответ, который ищете.

person Robyn    schedule 27.05.2019
comment
Спасибо. Я понял это выше, и это был правильный ответ, но отформатировать его так, как вы, очень полезно. Спасибо. - person Bob Bass; 28.05.2019

Тип данных decimal не является типом с фиксированной запятой с двумя десятичными знаками, это число с плавающей запятой с основанием десять.

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

Но 1075.75 / 9 = 119.5277777...

Это не number, которое можно точно представить в базе 10 или в базе 2, поэтому вы получите некоторую (невероятно маленькую) ошибку округления даже с decimal.

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

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

person HugoRune    schedule 27.05.2019

Обновите расчет суммы взноса, как показано ниже.

decimal installmentAmount = Math.Round((balanceAfterDP / installmentCount),2);
person Raka    schedule 28.05.2019

Ты можешь использовать:

decimal currentBalance = 1575.75M; // Current Balance
        decimal downPayment = 500.00M; // Down Payment
        decimal installmentCount = 9; // Installment Count
        decimal balanceAfterDP = currentBalance - downPayment; // Balance After Down Payment
        decimal installmentAmount = Math.Round((balanceAfterDP / installmentCount),2, MidpointRounding.AwayFromZero); // Installment Amount
        decimal remainderPayment = (balanceAfterDP - (installmentAmount * (installmentCount - 1))); // Final Payment (Remainder)

и вы получите правильное значение: 119,51

person Hani Jissri    schedule 28.05.2019