Округление Double до 1 знака после запятой в Kotlin: как из 0.044999 получить 0.1?
Почему стандартные методы не работают?
Вы абсолютно правы: проблема кроется в двоичном представлении чисел с плавающей точкой. Число 0.044999 в Double хранится не точно, а как значение, близкое к 0.044999000000000004.... При умножении на 10 мы получаем 0.44999..., и Math.round() честно округляет это до 0, а не до 1. Деление даёт 0.0. String.format работает аналогично — он отбрасывает остальные знаки, не учитывая, что само число чуть больше, чем кажется.
Правильное решение: BigDecimal с режимом округления HALF_UP
Единственный надёжный способ получить математически корректное округление в Kotlin — использовать BigDecimal. Он работает с десятичной арифметикой и позволяет явно задать режим округления.
import java.math.BigDecimal
import java.math.RoundingMode
fun Double.roundTo1Decimal(): Double {
return BigDecimal(this)
.setScale(1, RoundingMode.HALF_UP)
.toDouble()
}
fun main() {
val number = 0.044999
println(number.roundTo1Decimal()) // 0.1
val number2 = 0.044499
println(number2.roundTo1Decimal()) // 0.0
val number3 = 0.045001
println(number3.roundTo1Decimal()) // 0.1
}
Разберём по шагам:
BigDecimal(this)— создаёт десятичное представление числа. Важно: конструктор принимает Double, что может внести крошечную погрешность, но в большинстве практических задач это допустимо..setScale(1, RoundingMode.HALF_UP)— задаёт точность в 1 знак после запятой и режим округления "половина вверх" (как нас учили в школе: 0.045 → 0.1)..toDouble()— преобразует результат обратно в Double.
Важные нюансы
Если вы работаете с финансовыми расчётами, никогда не конвертируйте обратно в Double — храните и выводите BigDecimal. Для UI (например, отображения в TextView) используйте String.format после округления BigDecimal:
val rounded = BigDecimal(0.044999)
.setScale(1, RoundingMode.HALF_UP)
.toPlainString() // "0.1"
Также есть альтернативный подход — использовать kotlin.math.roundToInt() с дополнительным смещением, но он менее надёжен из-за погрешностей Double. Например, для 0.044999 можно сделать так:
fun Double.roundTo1DecimalHack(): Double {
return (this * 10.0 + 1e-10).roundToInt() / 10.0
}
Но 1e-10 — это "магическое число", которое может не сработать для других значений. Поэтому BigDecimal — единственное правильное решение для production-кода.
Альтернативный подход: округление через строку
Если вам не хочется подключать BigDecimal (хотя он входит в стандартную библиотеку Java/Kotlin), можно использовать трюк с преобразованием в строку и обратно:
fun Double.roundTo1DecimalString(): Double {
return this.toBigDecimal()
.setScale(1, RoundingMode.HALF_UP)
.toDouble()
}
Обратите внимание: this.toBigDecimal() — это extension-функция из Kotlin, которая делает то же самое, что и конструктор BigDecimal(this), но более читаемо. Код становится короче:
fun main() {
val result = 0.044999.toBigDecimal()
.setScale(1, RoundingMode.HALF_UP)
.toDouble()
println(result) // 0.1
}
Когда этот способ плох
Если вы работаете с очень большими числами (например, миллиарды с дробной частью), преобразование Double → BigDecimal → Double может привести к потере точности. В таких случаях лучше оставить результат в BigDecimal и выводить его как строку через .toPlainString() или String.format.
Также имейте в виду: BigDecimal работает медленнее, чем примитивные типы. Если вы округляете миллионы значений в цикле, возможно, стоит рассмотреть алгоритмы с целочисленной арифметикой (например, хранить копейки вместо рублей).
Ответов пока нет.
Войдите чтобы оставить ответ