Avatar ·

Округление Double до 1 знака после запятой в Kotlin: как из 0.044999 получить 0.1?

Всем привет! Я только начал изучать Kotlin для Android, и столкнулся со странной проблемой округления чисел с плавающей точкой. У меня есть переменная типа Double: `val number = 0.044999`. Мне нужно округлить её до одного знака после запятой, чтобы получить 0.1 (то есть математически корректное округление вверх, так как следующий знак после 0.0 — это 4, но если смотреть на всё число, то 0.045 уже даёт 0.1 при округлении до одного знака). Я пробовал использовать `String.format("%.1f", number)`, но он выдаёт `0.0`, потому что format смотрит только на первый знак после запятой? Потом я попробовал `Math.round(number * 10.0) / 10.0`, но результат снова `0.0`. Я в тупике! Вот мой код: ```kotlin fun main() { val number = 0.044999 val rounded = Math.round(number * 10.0) / 10.0 println(rounded) // Выводит 0.0, а нужно 0.1 } ``` Я понимаю, что проблема в представлении чисел с плавающей точкой в двоичном виде, но как тогда правильно выполнить "честное" округление до одного десятичного знака? Мне нужно, чтобы 0.044999 превратилось в 0.1, а 0.044499 — в 0.0. Подскажите, как это сделать на Kotlin? Желательно без костылей с BigDecimal, если есть простое решение. <<>>

Почему стандартные методы не работают?

Вы абсолютно правы: проблема кроется в двоичном представлении чисел с плавающей точкой. Число 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 работает медленнее, чем примитивные типы. Если вы округляете миллионы значений в цикле, возможно, стоит рассмотреть алгоритмы с целочисленной арифметикой (например, хранить копейки вместо рублей).