Это происходит из-за того, как числа с плавающей запятой представлены в компьютере: они на самом деле в base-2, а не в base-10 (немного упрощенно, но достаточно хорошо). Как следствие, когда вы вводите 0.545
, компьютер фактически записывает это как 0.5499999999...
- очень близко к 0.545
, но немного меньше. И поскольку он меньше 0.545
, неудивительно, что он округляется до 0.54
.
Если вам действительно нужно иметь точные числа с базовыми 10, вам следует использовать Decimal
вместо Float
или Double
. Этот пакет специально заботится о представлении чисел с плавающей точкой в base-10 без потерь.
x :: Decimal
x = 0.545
show x
> "0.545"
Предостережение заключается в том, что printf
не поддерживает Decimal
, поэтому вам придется отображать его с помощьюокругление по roundTo
и преобразование в строку по show
. Еще одно предостережение, что roundTo
выполняет «округление банкира» - если последняя цифра равна пяти, она округляется до ближайшей даже цифры, поэтому нам нужно было бы противодействовать этому как особому случаю (я не могнайдите готовую к использованию функцию, которая округляется по арифметическим правилам):
displayDecimal :: Decimal -> String
displayDecimal x = show (rounded + compensate)
where rounded = roundTo 2 x
compensate = if (x - rounded) == 0.005 then 0.01 else 0
displayDecimal 0.545
> "0.55"
displayDecimal 0.5450000000001
> "0.55"
displayDecimal 0.544
> "0.54"
displayDecimal 0.5449999999999
> "0.54"
Однако, если вы просто хотите, чтобы это работало для чисел с тремя десятичными знаками , вы можете уйтипросто добавив очень маленькое значение перед округлением, как 0.00001
. Это значение достаточно мало, чтобы не испортить ваши действительные числа, но достаточно велико, чтобы компенсировать расхождение между базой-2 и базой-10:
displayRounded :: Double -> String
displayRounded x = printf "%.2f" (x + 0.00001)
displayRounded 0.544
> "0.54"
displayRounded 0.545
> "0.55"