JavaRush/Courses/Kotlin SELF/Validation with when: in/!in, ranges

Validation with when: in/!in, ranges

Open

1. when as a validation tool

Why validation matters

If you’ve ever entered “age = 999” or “grade = -12” and the program “swallowed” it, you already know the pain of missing validation. Validation is the layer that separates “the program accepts data” from “the program trusts data”. And this is where when unexpectedly becomes not just branching, but a small rules table: which values are allowed, which are not, and which require a separate message.

Imagine you need to check a number and assign it to a category: “0..12”, “13..17”, “18..120”, otherwise “error”. If you write this using if, you get a chain that reads like a legal document. But when reads like a neat scale.

Ranges and in / !in

A range in Kotlin is an object of the form a..b, which means “all values from a to b inclusive”. The word “inclusive” is very important: 0..10 contains both 0 and 10. And the in operator is the check “does the value belong to the range?” Accordingly, !in means “does not belong”.

So far this sounds like school math, but in code it turns into very readable phrases: age in 0..120 literally reads as “age is in the range from 0 to 120”. And now the key point: in when, you can write branches not only for specific values, but also for range membership checks.

A quick warm-up:

fun main() {
    val x = 7

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

No magic here: in returns Boolean, and that Boolean is convenient for controlling program logic.

Validating Int by ranges

When a number needs not just “accept/reject” but to be split into categories, when with ranges is close to ideal. You write the scale from top to bottom, and the program picks the first matching branch. For beginners, it’s nice because the code looks like “rules on paper”: first kids, then teens, then adults, otherwise error.

Let’s start with a mini function that classifies age. Note: this is validation + interpretation: we’re not just saying “bad/good”, we return a meaningful result.

fun ageCategory(age: Int): String {
    return when (age) {
        in 0..12 -> "child"
        in 13..17 -> "teen"
        in 18..120 -> "adult"
        else -> "invalid age"
    }
}

fun main() {
    println(ageCategory(10))   // child
    println(ageCategory(17))   // teen
    println(ageCategory(200))  // invalid age
}

Here you can immediately see three important things. First, in 0..12 is the “branch condition” in when. Second, branches are checked top to bottom, and the first matching one wins. Third, else is our safety net for “everything else”.

Early guarding with !in

There’s a very practical validation style: first we check that the number even looks plausible, and only then we split it into categories. In when this is easy to do using !in ... in the very top branch. It’s like a security guard at the entrance: “with ID — come in; without ID — goodbye, and it doesn’t matter what you’re wearing”.

An example with temperature (the range here is arbitrary, just to show the idea):

fun temperatureLabel(t: Int): String {
    return when (t) {
        !in -50..60 -> "outside a reasonable range"
        in -50..0 -> "cold"
        in 1..25 -> "normal"
        else -> "hot"
    }
}

fun main() {
    println(temperatureLabel(10))   // normal
    println(temperatureLabel(200))  // outside a reasonable range
}

Why is this convenient? Because you clearly separate “bad data” from “good data”. And then you don’t have to think about where to “stuff” -999 or 999 in your scale.

Ranges for Char

In real tasks you’ll validate not only numbers, but also input format. Even before regular expressions (which come later), you can do a lot of useful work just by checking characters. For example, “is this character a digit?”, “is it a Latin letter?”, “uppercase or lowercase?”. And suddenly you find out that ranges work for Char too.

This is especially useful when you take the “first character of an answer” and expect something like Y/N, A/B/C or 1/2/3.

fun charKind(ch: Char): String {
    return when (ch) {
        in '0'..'9' -> "digit"
        in 'a'..'z' -> "lowercase Latin letter"
        in 'A'..'Z' -> "uppercase Latin letter"
        else -> "other character"
    }
}

fun main() {
    println(charKind('7'))  // digit
    println(charKind('Q'))  // uppercase Latin letter
    println(charKind('?'))  // other character
}

It’s important to understand that the range '0'..'9' is not “ASCII magic”, but simply the order of characters in Unicode, which is arranged so that digits go consecutively. For learning and practical CLI inputs, this is more than enough.

2. Grouping and combining rules

Grouping values with commas

Sometimes validation is not ranges, but a set of specific allowed values. For example: “the command can only be 1, 2, or 3”, “the month can be 1..12”, “these months have 31 days, these have 30”. If you write a separate branch for each value, you’ll quickly feel like a printer producing the same lines.

In when, you can list multiple values in a single branch separated by commas. This doesn’t just save lines — it makes the rule more obvious: “these values are one group”.

An example (days in a month, ignoring leap-year subtleties — don’t blame February, it’s trying its best):

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
}

Here else -> 0 is also a form of validation: we return an “error signal” as a number. Not ideal (it would be better to return a message), but at this point in the course it’s a clear and honest way to show: “the month is invalid”.

Ranges and single values in one when

Often a rule looks like this: there is a general range of valid values, but inside it there are “special cases”. For example: “scores from 0 to 100, but 100 deserves a special congratulations”, or “an age-based discount, but 0 is clearly an input error”.

The point is that when lets you arrange branches so that special cases go higher, and broad ranges go lower. It reads almost like rule priority: “if exactly 100 — do this; else if in 90..99 — do that…”.

fun scoreComment(score: Int): String {
    return when (score) {
        !in 0..100 -> "invalid result"
        100 -> "perfect! (and yes, you can be a little proud)"
        in 90..99 -> "excellent"
        in 70..89 -> "good"
        else -> "room for improvement"
    }
}

fun main() {
    println(scoreComment(100)) // perfect! (and yes, you can be a little proud)
    println(scoreComment(105)) // invalid result
}

Notice the order. If we placed in 90..100 above, then 100 would be “swallowed” by that branch, and the special message for 100 would never run.

Mini scenario: console app settings

Now let’s put it all together in a small, complete snippet. Imagine we’re building a console app called “Study Helper”, which asks the user for a “difficulty level” (1..3) and a “percentage of correct answers” (0..100). Our job is to keep the program from silently accepting garbage while still producing clear messages.

We’re not touching collections or complex architecture: only readln(), trim(), toIntOrNull() and when.

fun difficultyLabel(level: Int): String {
    return when (level) {
        1 -> "easy"
        2 -> "normal"
        3 -> "hard"
        else -> "invalid level"
    }
}

fun main() {
    print("Difficulty level (1-3): ")
    val level = readln().trim().toIntOrNull()

    val message = when (level) {
        null -> "This is not a number"
        in 1..3 -> "OK, selected: ${difficultyLabel(level)}"
        else -> "It is a number, but it must be 1..3"
    }

    println(message)
}

Notice the calm “stair-step” structure. First we check null (parsing failed), then we check the range, and only then everything else. This order almost always yields the most human-friendly message: the user understands exactly what they did wrong.

Overlapping ranges and branch order

when checks branches from top to bottom and takes the first one that matches. That sounds simple, but this is exactly where one of the trickiest beginner bugs is born: “I wrote all the rules correctly, but the program behaves incorrectly”. Usually it means the rules overlap, and a broader branch is placed above a narrower one.

Let’s look at an example where the mistake is almost invisible:

fun badGrade(score: Int): String {
    return when (score) {
        in 0..100 -> "some result"
        in 90..100 -> "excellent"
        else -> "error"
    }
}

fun main() {
    println(badGrade(95)) // some result
}

Why? Because 95 already matched 0..100, so the program never even reaches the 90..100 branch.

The correct option is to swap the branches:

fun goodGrade(score: Int): String {
    return when (score) {
        in 90..100 -> "excellent"
        in 0..89 -> "some result"
        else -> "error"
    }
}

fun main() {
    println(goodGrade(95)) // excellent
}

If you remember only one idea from this lecture, let it be this: in when, branch order is part of the logic, not just “pretty formatting”.

3. Cheat sheets and visualization

Cheat-sheet table for validation in when

When you write validation, it helps to keep a couple of “formulas” in your head. Below is a small cheat sheet of the most common constructs. It won’t replace understanding, but it will save time when you write the “0..100” check for the hundredth time.

What we want to check How we write it in when (x) How it reads
Belongs to a range
in 0..100 -> ...
“x is from 0 to 100”
Does not belong to a range
!in 0..100 -> ...
“x is not from 0 to 100”
Several specific values
1, 3, 5 -> ...
“x equals 1 or 3 or 5”
A separate “special case”
100 -> ...
“if exactly 100 — special logic (place above the range)”
Everything else
else -> ...
“any other values”

Notice that Kotlin encourages using when specifically as an expression when you want to produce a result, rather than just printing inside each branch. That’s about readability and about code discipline.

Flowchart of the validation pipeline

To avoid turning input into chaos, it’s useful to think of validation as a small pipeline. We’ve already built it manually many times, but now we can lock it in visually: we go from a raw string to a correct value or a clear error.

flowchart TD
    A["readln(): raw string"] --> B["trim(): removed spaces"]
    B --> C["toIntOrNull(): number or null"]
    C --> D{"when (value)"}
    D -->|null| E["Message: 'This is not a number'"]
    D -->|in range| F["OK: continue working"]
    D -->|else| G["Message: 'It is a number, but it does not fit'"]

The idea is simple: when becomes the final step where you turn “just a value” into “a valid value or a clear reaction”.

4. Common mistakes when validating with when

Mistake No. 1: overlapping ranges where the broad branch is above the narrow one.
This is the classic case where you’re sure you “described everything”, but part of the logic never runs. If you have a branch in 0..100, then all narrower ranges inside it must be placed above it, otherwise they become unreachable in practice.

Mistake No. 2: a missed boundary due to the “inclusiveness” of ...
Beginners often think of a range as “up to, but not including”, especially if they’ve seen such ranges in other languages or in math notation. In Kotlin, 0..10 includes 10, so off-by-one errors happen regularly. The fix is simple: say the boundary values out loud and check them separately in your head.

Mistake No. 3: missing a meaningful else or trying to get by without it.
In validation, else is not “so the compiler stops complaining”, but a mandatory branch for “what to do with invalid values”. If else returns something meaningless like an empty string, you’re hiding the error instead of reporting it. Later, it will backfire at an unexpected moment.

Mistake No. 4: mixing computation, printing, and state changes in one branch.
When when is used for validation, it’s especially good as an expression: return a message/status/number, and do the printing separately. If branches start to include “print, change a variable, and read input again”, your when turns into a 12-season mini-series that’s hard to test and read.

Mistake No. 5: no null check after toIntOrNull().
If you parse a string with toIntOrNull(), then null is a normal outcome, not an exception. Often the clearest structure is a separate branch null -> "This is not a number", and only then the ranges. That way the user gets an exact explanation, and you don’t fight the compiler and nullable types.

1
Task
Kotlin SELF,  13 level,  3 lecture
Locked
Number label
Number label
1
Task
Kotlin SELF,  13 level,  3 lecture
Locked
System Temperature
System Temperature
1
Task
Kotlin SELF,  13 level,  3 lecture
Locked
Test grading
Test grading
1
Task
Kotlin SELF,  13 level,  3 lecture
Locked
Season by month
Season by month
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet