1. Базовые функции: floor, ceil, round
Если вы только начали программировать, кажется, что округление — это что-то «для бухгалтеров» или «для Excel». На практике оно появляется намного раньше: как только вы начали делить числа и получать дроби, вы почти сразу захотите либо красиво вывести результат, либо применить правило вроде «округлять вверх», либо посчитать количество целых единиц, которые покрывают дробный объём.
Представьте типичный бытовой сценарий. У вас есть длина комнаты 3.2 метра, а вы покупаете плинтус по 1 метру. Вам нужно 4 штуки, а не 3 (то есть округление вверх). Или вы считаете скидку и хотите показать цену с двумя знаками после запятой. Или вы вычисляете «сколько страниц осталось», и дробные страницы никого не радуют. В программировании это всё происходит постоянно — и именно поэтому в стандартной библиотеке Kotlin есть целый набор функций округления в пакете kotlin.math.
floor(x): округление вниз из математики
Когда говорят «округлить вниз», новички часто слышат «просто отбрось дробь». Это опасная путаница. floor(x) округляет число к меньшему целому (то есть «вниз по числовой прямой», в сторону минус бесконечности). По определению, floor(x) — это округление к целому в сторону отрицательной бесконечности.
Почему это важно? Потому что на положительных числах «округление вниз» и «отбрасывание дроби» выглядят одинаково, а на отрицательных — внезапно расходятся. И именно отрицательные числа чаще всего «вскрывают» ошибки в вашем понимании.
Мини-пример: floor на положительных и отрицательных
import kotlin.math.floor
fun main() {
println(floor(3.14159)) // 3.0
println(floor(10.0)) // 10.0
println(floor(-1.1)) // -2.0
}
Обратите внимание: floor(-1.1) даёт -2.0, потому что -2 меньше, чем -1.1, а -1 — больше. Это и есть «вниз по оси».
ceil(x): округление вверх из математики
Теперь симметричная операция. ceil(x) округляет число к большему целому (то есть «вверх по числовой прямой», к плюс бесконечности). Формально это округление в сторону положительной бесконечности.
Очень полезная функция, когда вы считаете количество целых единиц, чтобы «хватило наверняка»: сколько коробок нужно, сколько автобусов нужно, сколько полных метров материала нужно купить. Если floor — это «сколько целых точно помещается», то ceil — «сколько целых нужно, чтобы покрыть всё».
Мини-пример: ceil на положительных и отрицательных
import kotlin.math.ceil
fun main() {
println(ceil(3.14159)) // 4.0
println(ceil(10.0)) // 10.0
println(ceil(-1.1)) // -1.0
}
Заметьте, как ведёт себя отрицательное: ceil(-1.1) = -1.0, потому что -1 больше, чем -1.1.
round(x): округление до ближайшего
Самое «привычное» округление — «до ближайшего целого». В Kotlin есть функция round(x), которая округляет к ближайшему целому, но у неё есть важный нюанс: если число ровно посередине (например, 2.5), Kotlin округляет к ближайшему чётному (так называемое bankers rounding).
Звучит немного как «математика решила поиграть в справедливость», но логика в этом есть: при больших объёмах данных такой способ уменьшает накопление систематической ошибки (не «всегда вверх» и не «всегда вниз»).
Мини-пример: round и «половинки»
import kotlin.math.round
fun main() {
println(round(3.49)) // 3.0
println(round(3.51)) // 4.0
println(round(3.5)) // 4.0 (между 3 и 4 -> к чётному 4)
println(round(2.5)) // 2.0 (между 2 и 3 -> к чётному 2)
}
Если вы ожидали, что 2.5 станет 3.0, то вы не одиноки. Это один из тех моментов, когда важно не «угадывать», а помнить правило. Программирование — это не математика.
3. toInt() и почему это не округление
floor/ceil/round возвращают Double
Иногда кажется, что если мы округлили число до целого, то и тип должен стать Int. Но функции floor, ceil, round работают в мире вещественных чисел и возвращают Double (или Float для Float-версий). Это удобно: вы можете продолжать вычислять сложные выражения как Double и только в конце, если нужно, переводить результат в Int.
Здесь полезно запомнить простую «ментальную модель»: округление — это математическая операция, а toInt() — это преобразование типа. И это разные сущности, даже если иногда дают одинаковые цифры.
toInt() отбрасывает дробь к нулю
Вот главный герой сегодняшней ловушки. Метод toInt() у Double делает не «математическое округление», а преобразование: дробная часть отбрасывается в сторону нуля.
То есть 3.99.toInt() станет 3, а -3.99.toInt() станет -3. Не -4. И это очень частый источник «почему у меня поехали расчёты при отрицательных числах».
Мини-пример: toInt() на плюсах и минусах
fun main() {
println(3.99.toInt()) // 3
println(-3.99.toInt()) // -3
println(3.0.toInt()) // 3
}
Если вы сейчас подумали «ну да, обрезает дробь», то уточнение такое: обрезает к нулю, а не вниз по оси. Это и есть ключевой момент.
Нюанс: NaN и очень большие числа
toInt() ещё и защищает вас от некоторых крайностей. Если значение NaN, то вернётся 0. Если число слишком большое или слишком маленькое для Int, то результат превратится в Int.MAX_VALUE или Int.MIN_VALUE.
Мы не будем сегодня углубляться в эти случаи, но вам полезно знать, что toInt() — не просто «отрезать дробь», а ещё и «влезть в диапазон Int».
Где именно toInt() расходится с floor/ceil/round
Если вы возьмёте положительное число 7.9, то:
- floor(7.9) = 7.0
- 7.9.toInt() = 7
И легко ошибиться, решив, что это одно и то же.
А теперь возьмём отрицательное -7.9:
- floor(-7.9) = -8.0
- (-7.9).toInt() = -7
Разница принципиальная: floor идёт к минус бесконечности, toInt() — к нулю.
Таблица для закрепления
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Здесь floor/ceil следуют математическим определениям, round округляет к ближайшему с «половинками к чётному», а toInt() делает преобразование с отбрасыванием дроби к нулю.
4. Практические приёмы округления
Как получить Int по нужному правилу
Часто вам нужен именно Int: например, количество предметов, количество страниц, количество упаковок. Но округлять вы хотите по правилу floor/ceil/round, а не «к нулю».
Тогда используем простой приём: сначала округляем как Double, затем переводим в Int. Да, в два шага. Да, это нормально. Kotlin не обидится.
Пример: округлить вверх и получить Int
import kotlin.math.ceil
fun main() {
val metersNeeded = 3.2
val pieces = ceil(metersNeeded).toInt()
println(pieces) // 4
}
Пример: округлить вниз и получить Int
import kotlin.math.floor
fun main() {
val fullBoxes = 9.99
val boxes = floor(fullBoxes).toInt()
println(boxes) // 9
}
Пример: округлить до ближайшего и получить Int
import kotlin.math.round
fun main() {
val rating = 4.6
val rounded = round(rating).toInt()
println(rounded) // 5
}
Округление до N знаков после запятой
Очень часто возникает задача — округлить до 2 знаков (цены, проценты). Но round(x) округляет до целого. Как быть? Используем школьный трюк, который на практике встречается постоянно: умножаем на 10N, округляем до целого, потом делим обратно.
Важно помнить: мы всё ещё в мире Double, и из-за точности иногда можно увидеть не идеально красивую дробь. Но как базовый приём — это отличный старт.
Пример: округление до 2 знаков
import kotlin.math.round
fun main() {
val x = 12.34567
val rounded2 = round(x * 100.0) / 100.0
println(rounded2) // 12.35
}
Смысл такой: x * 100 сдвигает запятую на 2 знака вправо, round делает число целым, / 100 возвращает запятую назад.
Мини-сценарий: калькулятор цены доставки
Сейчас мы напишем маленький (но хороший) проект, который связывает всё изученное. Представим, что у нас есть простая консольная программа, которая считает стоимость заказа, налог и доставку. Мы не читаем данные с консоли, поэтому используем константы в коде — нам важно именно округление.
Допустим:
- цена товаров: subtotal
- налог 7.5%: taxRate
- доставка считается «за каждый начатый килограмм» (то есть округление вверх)
- выводим итог с 2 знаками после запятой
Шаг 1: считаем налог и округляем итог для вывода
import kotlin.math.round
fun main() {
val subtotal = 39.90
val taxRate = 0.075
val tax = subtotal * taxRate
val total = subtotal + tax
val totalPretty = round(total * 100.0) / 100.0
println("Итого: $totalPretty") // Итого: 42.89
}
Здесь важный момент: мы округлили только для вывода. В реальных задачах правила могут быть строже (когда и как округлять), но сегодня нам достаточно понять механику.
Шаг 2: считаем доставку по правилу «за каждый начатый килограмм»
Теперь добавим вес и цену за килограмм. Если вес 2.1, платить нужно как за 3 (округление вверх).
import kotlin.math.ceil
fun main() {
val weightKg = 2.1
val pricePerKg = 3.0
val billedKg = ceil(weightKg).toInt()
val shipping = billedKg * pricePerKg
println("Вес к оплате: $billedKg кг") // Вес к оплате: 3 кг
println("Доставка: $shipping") // Доставка: 9.0
}
Обратите внимание: ceil(weightKg) возвращает Double, поэтому toInt() — отдельным шагом.
Шаг 3: почему нельзя писать weightKg.toInt() вместо ceil(weightKg)
И вот здесь — момент истины. На положительных весах всё может выглядеть «почти нормально», но смысл будет неверный: 2.1.toInt() даст 2, то есть вы недосчитаете доставку.
fun main() {
val weightKg = 2.1
println(weightKg.toInt()) // 2 (это НЕ "за каждый начатый", это "отрезать к нулю")
}
toInt() — это не «округление вверх», и даже не «математическое округление вниз». Это преобразование с отбрасыванием дробной части к нулю.
Схема выбора округления
Когда вы в следующий раз будете выбирать между floor, ceil, round и toInt(), полезно не перебирать в голове «кажется, это то», а задать себе один вопрос: какое правило у задачи?
flowchart TD
A["Есть Double x"] --> B{"Это для вывода человеку?"}
B -->|Да| C["Округление до N знаков: round(x*10^N)/10^N"]
B -->|Нет| D{"Нужно целое количество единиц?"}
D -->|Да, чтобы ХВАТИЛО| E["ceil(x) -> toInt()"]
D -->|Да, сколько помещается точно| F["floor(x) -> toInt()"]
D -->|Нет, просто привести тип| G["x.toInt() (отрезает к нулю)"]
Эта схема специально напоминает: toInt() — это не «универсальное округление», а отдельный смысловой шаг.
5. Типичные ошибки при округлении и преобразовании в Int
Ошибка №1: считать, что toInt() — это округление.
На первых порах очень хочется думать: «перевёл в Int — значит округлил». На самом деле Double.toInt() отбрасывает дробную часть в сторону нуля. Из-за этого логика «округлить вниз» на отрицательных значениях ломается, а логика «округлить вверх» ломается вообще всегда.
Ошибка №2: путать “округление вниз” и “в сторону нуля”.
floor(x) округляет вниз по числовой прямой, то есть к отрицательной бесконечности. А toInt() и truncate(x) (если вы его встретите) работают к нулю. На положительных числах разницы почти не видно, поэтому ошибка прячется и «вылезает» потом в самый неудобный момент.
Ошибка №3: ожидать, что round(2.5) даст 3.0.
В Kotlin round(x) использует правило “ties to even” — половинки округляются к ближайшему чётному. Если вы пишете алгоритм, где поведение на .5 важно (например, баллы, рейтинги), обязательно проверьте, что именно ожидается по правилам задачи.
Ошибка №4: забыть, что ceil/floor/round возвращают Double, и пытаться «скормить» его туда, где ждут Int.
В Kotlin это обычно не скомпилируется (и это хорошо). Но новички иногда пытаются «продавить» идею: мол, раз 3.0, значит это же целое. Нет: тип всё ещё Double. Правильный путь — сначала округление (ceil/floor/round), потом явное преобразование (toInt()).
Ошибка №5: округлять “до 2 знаков” без масштаба или с неправильным масштабом.
Трюк round(x * 100.0) / 100.0 работает ровно потому, что 100 соответствует двум знакам. Если вы случайно умножили на 10, а делите на 100, получится странная «магия» (и это будет честно ваша магия, не Kotlin). Держите коэффициент масштаба как часть правила, а не как случайное число из воздуха.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ