JavaRush /Курси /Kotlin SELF /Округлення floor/ceil/round і відмінність від toInt()

Округлення floor/ceil/round і відмінність від toInt()

Kotlin SELF
Рівень 6 , Лекція 3
Відкрита

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 округляє до найближчого парного (так зване banker’s 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, то ви не самі. Це один із тих моментів, коли важливо не «вгадувати», а памʼятати правило. Програмування — це не математика.

2. 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() — до нуля.

Таблиця для закріплення

x
floor(x)
ceil(x)
round(x)
x.toInt()
3.2
3.0
4.0
3.0
3
3.8
3.0
4.0
4.0
3
-3.2
-4.0
-3.0
-3.0
-3
-3.8
-4.0
-3.0
-4.0
-3

Тут floor/ceil дотримуються математичних означень, round округляє до найближчого з правилом «половинки до парного», а toInt() робить перетворення з відкиданням дробової частини до нуля.

3. Практичні прийоми округлення

Як отримати 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() — це не «універсальне округлення», а окремий смисловий крок.

4. Типові помилки під час округлення та перетворення в 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). Тримайте коефіцієнт масштабу як частину правила, а не як випадкове число «зі стелі».

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ