JavaRush /Курси /Kotlin SELF /Валідація через when: in/!in, діапазони

Валідація через when: in/!in, діапазони

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

1. when як інструмент валідації

Навіщо потрібна валідація

Якщо ви коли-небудь вводили «вік = 999» або «оцінка = -12», а застосунок це «проковтнув», ви вже знайомі з болем від відсутності валідації. Валідація — це шар, який відокремлює «застосунок приймає дані» від «застосунок вірить даним». І саме тут when несподівано стає не просто розгалуженням, а маленькою таблицею правил: які значення допустимі, які — ні, а які потребують окремого повідомлення.

Уявіть, що вам потрібно перевірити число й віднести його до категорії: «0..12», «13..17», «18..120», інакше — «помилка». Якщо писати це на if, вийде ланцюжок, який читається майже як юридичний документ. Натомість when сприймається як охайна шкала.

Діапазони та in / !in

Діапазон у Kotlin — це об’єкт виду a..b, який означає «усі значення від a до b включно». Слово «включно» тут справді важливе: 0..10 містить і 0, і 10. А оператор in — це перевірка «чи належить значення діапазону». Відповідно, !in означає «не належить».

Поки що це звучить як шкільна математика, але в коді все перетворюється на дуже зрозумілі фрази: age in 0..120 читається буквально як «вік у діапазоні від 0 до 120». А тепер головне: у when можна писати гілки не лише для конкретних значень, а й для перевірок належності до діапазону.

Невеликий розігрів:


fun main() {
    val x = 7

    println(x in 1..10)   // true
    println(x !in 1..10)  // false
}

Тут немає жодної магії: in повертає Boolean, і цим Boolean зручно керувати логікою застосунку.

Валідація Int за діапазонами

Коли число потрібно не просто «прийняти/відхилити», а розкласти за категоріями, when із діапазонами виявляється майже ідеальним варіантом. Ви записуєте шкалу зверху вниз, а застосунок вибирає першу відповідну гілку. Для початківця це зручно ще й тим, що код виглядає як «правила на папері»: спочатку діти, потім підлітки, потім дорослі, а інакше — помилка.

Почнімо з мініфункції, яка класифікує вік. Зверніть увагу: це саме валідація + інтерпретація. Тобто ми не просто кажемо «погано/добре», а повертаємо осмислений результат.

fun ageCategory(age: Int): String {
    return when (age) {
        in 0..12 -> "дитина"
        in 13..17 -> "підліток"
        in 18..120 -> "дорослий"
        else -> "некоректний вік"
    }
}

fun main() {
    println(ageCategory(10))   // дитина
    println(ageCategory(17))   // підліток
    println(ageCategory(200))  // некоректний вік
}

Тут ви бачите одразу три важливі речі. По‑перше, in 0..12 — це «умова гілки» у when. По‑друге, гілки йдуть зверху вниз, і спрацьовує перша відповідна. По‑третє, else — наша страховка на випадок «усього іншого».

Ранній захист через !in

Є дуже практичний стиль валідації: спочатку ми перевіряємо, чи число взагалі схоже на правду, а вже потім ділимо його на категорії. У when це зручно робити через !in ... у найпершій гілці. Це виглядає як охоронець на вході: «із паспортом — проходьте, без паспорта — до побачення, і неважливо, який у вас стиль одягу».

Приклад із температурою (тут діапазон умовний — просто щоб побачити ідею):

fun temperatureLabel(t: Int): String {
    return when (t) {
        !in -50..60 -> "поза розумним діапазоном"
        in -50..0 -> "холодно"
        in 1..25 -> "нормально"
        else -> "спекотно"
    }
}

fun main() {
    println(temperatureLabel(10))   // нормально
    println(temperatureLabel(200))  // поза розумним діапазоном
}

Чому це зручно? Тому що ви явно відокремлюєте «погані дані» від «хороших». А потім не доводиться думати, куди «втиснути» -999 або 999 у вашу шкалу.

Діапазони для Char

У реальних задачах ви валідовуватимете не лише числа, а й формат введення. Навіть без регулярних виразів (до них ми дійдемо пізніше) можна зробити багато корисного, просто перевіряючи символи. Наприклад: «чи є символ цифрою», «чи є це латинська літера», «велика чи мала». І тут раптом з’ясовується, що діапазони працюють і для Char.

Це особливо корисно, коли ви берете «перший символ відповіді» й очікуєте щось на кшталт Y/N, A/B/C або 1/2/3.

fun charKind(ch: Char): String {
    return when (ch) {
        in '0'..'9' -> "цифра"
        in 'a'..'z' -> "мала латинська літера"
        in 'A'..'Z' -> "велика латинська літера"
        else -> "інший символ"
    }
}

fun main() {
    println(charKind('7'))  // цифра
    println(charKind('Q'))  // велика латинська літера
    println(charKind('?'))  // інший символ
}

Важливо розуміти: діапазон '0'..'9' — це не «магія про ASCII», а просто порядок символів в Unicode. Він улаштований так, що цифри йдуть підряд. Для навчальних і практичних CLI-введень цього більш ніж достатньо.

2. Групування та комбінування правил

Групування значень через кому

Іноді валідація — це не діапазони, а набір окремих допустимих значень. Наприклад: «команда може бути лише 1, 2 або 3», «місяць може бути 1..12», «для цих місяців 31 день, а для цих — 30». Якщо писати окрему гілку на кожне значення, ви швидко відчуєте себе принтером, який друкує ті самі рядки.

У when можна перелічувати кілька значень в одній гілці через кому. Це не лише економить рядки — так правило ще й читається очевидніше: «ці значення — одна група».

Приклад (дні в місяці, без високосних тонкощів — не ображайте лютий, він і так старається):

fun daysInMonth(month: Int): Int {
    return when (month) {
        1, 3, 5, 7, 8, 10, 12 -> 31
        4, 6, 9, 11 -> 30
        2 -> 28
        else -> 0
    }
}

fun main() {
    println(daysInMonth(11)) // 30
    println(daysInMonth(2))  // 28
    println(daysInMonth(99)) // 0
}

Тут else -> 0 — теж форма валідації: ми повертаємо «сигнал помилки» числом. Це неідеально (краще було б віддати окреме повідомлення), але на поточному етапі курсу — чесний і зрозумілий спосіб показати: «місяць некоректний».

Діапазони та окремі значення в одному when

Часто правило виглядає так: є загальний діапазон допустимих значень, але всередині нього є «особливі випадки». Наприклад: «бали від 0 до 100, але 100 — це окреме привітання» або «знижка за віком, але 0 — це явно помилка введення».

Суть у тому, що when дає вам змогу розташувати гілки так, щоб окремі випадки стояли вище, а широкі діапазони — нижче. Це читається майже як пріоритет правил: «якщо рівно 100 — ось так, інакше якщо в 90..99 — ось так…».

fun scoreComment(score: Int): String {
    return when (score) {
        !in 0..100 -> "некоректний результат"
        100 -> "ідеально! (і так, можна трохи пишатися)"
        in 90..99 -> "відмінно"
        in 70..89 -> "добре"
        else -> "є над чим попрацювати"
    }
}

fun main() {
    println(scoreComment(100)) // ідеально! (і так, можна трохи пишатися)
    println(scoreComment(105)) // некоректний результат
}

Зверніть увагу на порядок. Якби ми поставили in 90..100 вище, то 100 «проковтнулася» б цією гілкою, і окреме повідомлення для 100 ніколи б не спрацювало.

Мінісценарій: налаштування консольного застосунку

Тепер зберімо все в маленький цілісний фрагмент. Уявімо, що ми робимо консольний застосунок «Study Helper», який запитує в користувача «рівень складності» (1..3) і «відсоток правильних відповідей» (0..100). Наше завдання — не дати застосунку мовчки прийняти сміття й водночас видати зрозумілі повідомлення.

Ні колекції, ні складну архітектуру ми не чіпаємо. Використовуємо лише readln(), trim(), toIntOrNull() і when.

fun difficultyLabel(level: Int): String {
    return when (level) {
        1 -> "легко"
        2 -> "нормально"
        3 -> "складно"
        else -> "некоректний рівень"
    }
}

fun main() {
    print("Рівень складності (1-3): ")
    val level = readln().trim().toIntOrNull()

    val message = when (level) {
        null -> "Це не число"
        in 1..3 -> "Гаразд, обрано: ${difficultyLabel(level)}"
        else -> "Число є, але потрібно 1..3"
    }

    println(message)
}

Зверніть увагу на спокійну «драбинку». Спочатку ми перевіряємо null (не вдалося розпарсити), потім — діапазон, і лише після цього — все інше. Такий порядок майже завжди дає найлюдяніше повідомлення: користувач розуміє, що саме він зробив не так.

Перетини діапазонів і порядок гілок

when перевіряє гілки зверху вниз і бере першу відповідну. Звучить просто, але саме тут народжується один із найпідступніших багів початківця: «я написав усі правила правильно, але застосунок поводиться неправильно». Зазвичай це означає, що правила перетинаються, а ширша гілка стоїть вище за вужчу.

Подивімося на приклад, де помилка майже непомітна:

fun badGrade(score: Int): String {
    return when (score) {
        in 0..100 -> "якийсь результат"
        in 90..100 -> "відмінно"
        else -> "помилка"
    }
}

fun main() {
    println(badGrade(95)) // якийсь результат
}

Чому так? Тому що 95 уже потрапило в 0..100, а до гілки 90..100 застосунок навіть не дійшов.

Правильний варіант — переставити гілки:

fun goodGrade(score: Int): String {
    return when (score) {
        in 90..100 -> "відмінно"
        in 0..89 -> "якийсь результат"
        else -> "помилка"
    }
}

fun main() {
    println(goodGrade(95)) // відмінно
}

Якщо ви запам’ятаєте лише одну думку з цієї лекції, нехай це буде вона: у when порядок гілок — частина логіки, а не просто «гарне оформлення».

3. Шпаргалки та візуалізація

Таблиця-шпаргалка з валідації в when

Коли ви пишете валідацію, корисно тримати в голові кілька «формул». Нижче — маленька шпаргалка з найуживаніших конструкцій. Вона не замінить розуміння, але заощадить час, коли ви в сотий раз пишете перевірку «0..100».

Що хочемо перевірити Як пишемо в when (x) Як це читається
Належність діапазону
in 0..100 -> ...
«x від 0 до 100»
Не належить діапазону
!in 0..100 -> ...
«x не від 0 до 100»
Кілька конкретних значень
1, 3, 5 -> ...
«x дорівнює 1 або 3 або 5»
Окремий «особливий випадок»
100 -> ...
«якщо рівно 100 — особлива логіка (ставимо вище за діапазон)»
Усе інше
else -> ...
«будь-які інші значення»

Зауважте: Kotlin заохочує використання when саме як виразу, коли ви хочете отримати результат, а не просто друкувати в кожній гілці. Це і про читабельність, і про дисципліну в коді.

Блок‑схема конвеєра валідації

Щоб не перетворювати введення на хаос, зручно мислити валідацію як маленький конвеєр. Ми вже багато разів будували його вручну, а тепер можна зафіксувати це візуалізацією: ми рухаємося від сирого рядка до коректного значення або зрозумілої помилки.

flowchart TD
    A["readln(): сирий рядок"] --> B["trim(): прибрали пробіли"]
    B --> C["toIntOrNull(): число або null"]
    C --> D{"when (value)"}
    D -->|null| E["Повідомлення: 'Це не число'"]
    D -->|in діапазоні| F["Ок: працюємо далі"]
    D -->|else| G["Повідомлення: 'Число є, але не підходить'"]

Думка проста: when стає останнім кроком, де ви перетворюєте «просто значення» на «валідне значення або зрозумілу реакцію».

4. Типові помилки під час валідації через when

Помилка №1: діапазони, що перетинаються, де ширша гілка стоїть вище за вужчу.
Це той самий випадок, коли ви впевнені, що «все описали», але частина логіки ніколи не виконується. Якщо у вас є гілка in 0..100, то всі вужчі діапазони всередині неї мають стояти вище. Інакше вони стануть недосяжними за змістом.

Помилка №2: забута межа через «включність» ...
Початківці часто сприймають діапазон як «до, але не включно». Особливо якщо раніше бачили такі діапазони в інших мовах або в математичних позначеннях. У Kotlin 0..10 включає 10, тому помилки «на 1» трапляються регулярно. Лікується просто: промовляйте крайні значення та подумки перевіряйте їх окремо.

Помилка №3: відсутність осмисленого else або спроба обійтися без нього.
У валідації else — це не «щоб компілятор відчепився», а обов’язкова гілка «що робити з неправильними значеннями». Якщо else повертає щось беззмістовне на кшталт порожнього рядка, ви приховуєте помилку замість того, щоб повідомити про неї. Потім це «вистрілить» у найнесподіваніший момент.

Помилка №4: змішування в одній гілці обчислень, друку та зміни стану.
Коли when використовують для валідації, він особливо добрий як вираз: повернули повідомлення/статус/число, а друк зробили окремо. Якщо в гілках починається «і надрукувати, і змінити змінну, і ще раз прочитати введення», ваш when перетворюється на мінісеріал на 12 сезонів, який складно тестувати й читати.

Помилка №5: немає перевірки null після toIntOrNull().
Якщо ви розпарсили рядок через toIntOrNull(), то null — це нормальний результат, а не виняток. Часто найзрозуміліша структура — окрема гілка null -> "Це не число", а вже потім — діапазони. Так користувач отримає точне пояснення, а ви не боротиметеся з компілятором і nullable‑типами.

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