JavaRush/Courses/Kotlin SELF/Getting to Know Dates

Getting to Know Dates

Open

1. Introduction

Almost everyone starts the same way: “well, the date comes in as text — so I’ll store it as text”. And it’s true: the user enters 2026-01-06, in a file it will also be a string, in JSON it’s also a string… so why not keep it that way in code? At this point many programs smile, nod and… quietly prepare surprises for you in production.

The problem is that a string is just a sequence of characters. The Kotlin compiler sees only text in a String. It doesn’t know it’s a “date”, doesn’t check bounds, can’t add “plus one day”, and doesn’t guarantee correct “earlier/later” comparisons. As a result, you get a situation where the data looks like dates, but behaves like plain text (because it is plain text).

Let’s go over three big classes of problems:

  1. string comparison is not the same as date comparison;
  2. a string doesn’t guarantee date validity;
  3. formats can be ambiguous, and the “same” notation can mean different dates.

2. Comparing strings: why a date becomes a “word”

When we compare strings, Kotlin compares them lexicographically: roughly like words in a dictionary — left to right, character by character. This behavior is built into String and, more generally, into the comparison order for objects that have a “natural order”. For strings, that order is lexicographic.

And this is where the “why did my accounting decide that February comes before January” kind of magic begins.

Example: two “nice-looking” formats, one bug

It looks like a date, but it’s compared as text:

fun main() {
    val a = "01.02.2026"
    val b = "12.01.2026"

    println(a < b) // true (but on the calendar 01.02.2026 is later than 12.01.2026)
}

Why true? Because the string "01.02.2026" starts with '0', and "12.01.2026" starts with '1'. For strings, that’s enough: '0' < '1', so the whole string is considered smaller.

If you sort a list of events in a project using such strings, you’ll get “beautifully sorted chaos”.

“But I use ISO format — everything is fine there!”

Yes, there is a format that accidentally plays well with lexicographic comparison — yyyy-MM-dd (year-month-day). It often behaves “correctly” when comparing strings — but that doesn’t make it a date type.

fun main() {
    val iso1 = "2026-01-06"
    val iso2 = "2026-12-31"

    println(iso1 < iso2) // true (and here it matches the calendar)
}

Why does it match? Because the more significant parts (year, then month, then day) are on the left. But it’s still text comparison. It only takes the slightest deviation from the format for the “magic” to break.

Micro-trap: “without leading zeros” everything breaks

A very common situation: someone decided to store the month as 1 instead of 01. Visually: “what’s the difference, a human will understand”. But sorting won’t.

fun main() {
    val d1 = "2026-1-10"
    val d2 = "2026-01-11"

    println(d1 < d2) // false (but on the calendar January 10 is earlier than January 11)
}

Lexicographically, "2026-1-10" is not compared the same way as "2026-01-10". And if you think “we will always write it correctly”, I’ve got news for you: one day a user will do it. Or your colleague. Or you yourself on a Friday evening.

3. Validity: a string allows “2026-13-40” and doesn’t blush

After comparison usually comes the second hit: a string doesn’t guarantee the date even exists.

fun main() {
    val s = "2026-13-40"
    println(s) // 2026-13-40 (looks confident, lives boldly, but does not exist)
}

A string doesn’t know that the month must be from 1 to 12, that February doesn’t always have 29 days, that 31 June is fantasy. This is not a “Kotlin bug”. This is correct: String doesn’t have to be a calendar.

“Then I’ll validate it myself with split!”

Sounds energetic. In practice you quickly discover that a “date” is not three numbers, but three numbers plus a bunch of rules.

Here’s an example of a very simplified (and intentionally incomplete) ISO format check:

fun isIsoDateRough(s: String): Boolean {
    val parts = s.split("-")
    if (parts.size != 3) return false

    val year = parts[0].toIntOrNull() ?: return false
    val month = parts[1].toIntOrNull() ?: return false
    val day = parts[2].toIntOrNull() ?: return false

    return year >= 1 && month in 1..12 && day in 1..31
}

This function is useful as a learning step, but it lets through a lot of “impossible” dates: 2026-02-31, 2026-04-31, 2026-11-00, and so on. To do it truly correctly, you would have to implement calendar rules, leap years, the number of days in each month… and at some point you’ll realize you’re reinventing the calendar. That’s usually a bad deal.

Why a “regex” isn’t a solution either

You can write Regex("""\d{4}-\d{2}-\d{2}"""), and it will guarantee that the string looks like 2026-13-40. That is, the regex checks “does it look like a date”, but not “is it a date”.

It’s like validating a passport by the pattern “two letters and six digits”: it looks similar, but it doesn’t mean it exists.

4. Format: “01.02.2026” — is it February 1 or January 2?

The third big problem is format ambiguity. A person (and a program) must agree on what the parts of the date mean.

When you see 01.02.2026, it can be:

  • 1 February 2026 (format dd.MM.yyyy, popular in Europe),
  • 2 January 2026 (format MM.dd.yyyy, commonly used in everyday life in the US).

And the most unpleasant part: both dates are valid, so you won’t catch the mistake with simple checks.

fun main() {
    val s = "01.02.2026"
    println(s) // one string, but at least two meanings
}

A good habit: separate storage from display

Even if for now you have to store a date as a string (for example, at an early project stage), it’s useful to agree that:

  • inside the program, the date is stored in one strict format (often ISO yyyy-MM-dd),
  • for the user, you display it in a familiar form (for example, dd.MM.yyyy),
  • user input is normalized (or at least validated) at the boundary.

This is not the final solution (we’re still not using date types), but it’s already a strong step toward sanity.

5. Mini-case: expenses with dates that “won’t sort”

Let’s imagine we have a simple console app “Expense Tracker”. We already know how to store a list of expenses in a MutableList, we can add items and print reports. And now the logical desire: add the purchase date.

A beginner version of the data model often looks like this:

data class Expense(
    val title: String,
    val amount: Int,
    val date: String // "as entered by the user"
)

And then you want to do “show expenses by date” or “sort by date”.

Sorting by string: works… until it doesn’t

data class Expense(val title: String, val amount: Int, val date: String)

fun main() {
    val expenses = listOf(
        Expense("Coffee", 250, "12.01.2026"),
        Expense("Transit", 70, "02.02.2026"),
        Expense("Pizza", 900, "05.01.2026"),
    )

    val sorted = expenses.sortedBy { it.date }
    println(sorted.joinToString("\n"))
}

With high probability you’ll get an order that looks “random” from a calendar perspective. Because sortedBy sorts by what you give it — by a string — and strings are compared lexicographically.

“Fine, we’ll store the date in an ISO string”

That’s better than dd.MM.yyyy, because ISO more often sorts correctly as text. Let’s “improve” the model but still keep a string:

data class Expense(val title: String, val amount: Int, val isoDate: String)

fun main() {
    val expenses = listOf(
        Expense("Coffee", 250, "2026-01-12"),
        Expense("Transit", 70, "2026-02-02"),
        Expense("Pizza", 900, "2026-01-05"),
    )

    val sorted = expenses.sortedBy { it.isoDate }
    println(sorted.map { it.isoDate + " " + it.title }.joinToString("\n"))
    // 2026-01-05 Pizza
    // 2026-01-12 Coffee
    // 2026-02-02 Transit
}

This already looks like a win. But it’s important to be honest with yourself: we simply chose a format that conveniently “masks” the comparison problem. Validity is still not guaranteed, arithmetic like “+ 1 day” is still manual, and “event time” (if needed) will immediately bring new pain.

6. Why a string is a bad internal model

In this section it’s important to slow down a bit and formulate a thought that will follow you throughout your career: a program’s internal types should reflect the meaning of the data. Input and output are almost always strings, but inside the program you should have meaningful entities.

Strings are convenient in three places:

  • user input (readln() always gives a String);
  • screen output (a human needs text);
  • storage/exchange (files, JSON, network protocols).

But as soon as you start doing “logic” — comparisons, sorting, calculations, correctness checks — the string becomes a trap.

A small table: what’s wrong with a date string

Task If the date is a
String
What goes wrong
Compare “earlier/later”
a < b
comparison is lexicographic; depends on the format
Check correctness
split
,
Regex
easy to miss impossible dates
Add “+ 1 day” manually you need calendar rules; months have different lengths
Support different formats “if there’s a dot it’s one thing, if there’s a dash it’s another” the code grows and becomes fragile
Handle a format error exceptions/
null
easy to “crash” the program without careful handling

How a bug is usually born: “the string leaked into logic”

To cement this, it helps to see it as a data flow. Almost always the error appears when the string (packaging) becomes internal logic.

flowchart TD
    A[User entered the date as a string] --> B[We stored the String 'as is']
    B --> C[Sort / compare / calculate]
    C --> D[We get a bug: wrong order or an impossible date]
    D --> E[We start patching with hacks: split, regex, if-else]
    E --> C

A vicious cycle: the more hacks, the more complex the code and the more new bugs appear.

7. What to do now, if date types still don’t exist

Important: today we are not introducing date/time types yet. But we can already make a couple of disciplining decisions so the project doesn’t turn into a “hellish calendar parser” of five thousand lines.

Centralize format rules: one normalization function

Let’s make a small utility: it takes a string, tries to convert it to ISO yyyy-MM-dd, and if it can’t, it returns null. This is not perfect calendar validation, but it’s already a good practice: rules in one place, a clear contract.

fun normalizeIsoDateOrNull(input: String): String? {
    val s = input.trim()
    val parts = s.split("-")
    if (parts.size != 3) return null

    val year = parts[0].toIntOrNull() ?: return null
    val month = parts[1].toIntOrNull() ?: return null
    val day = parts[2].toIntOrNull() ?: return null

    if (month !in 1..12) return null
    if (day !in 1..31) return null

    val mm = month.toString().padStart(2, '0')
    val dd = day.toString().padStart(2, '0')
    return "${year}-$mm-$dd"
}

Yes, it still simplifies with “day up to 31”. But the idea is useful: normalize the format and return null instead of crashing.

Use “read → prepare → validate → save”

You’ve already used this pattern for numbers. It works for a date string too:

fun main() {
    print("Enter purchase date (yyyy-M-d): ")
    val raw = readln()

    val iso = normalizeIsoDateOrNull(raw)
    if (iso == null) {
        println("Invalid date: '$raw'") // clear message
        return
    }

    println("Saving date in ISO format: $iso") // for example: 2026-01-06
}

We’re not fixing the calendar “100%”, but we stop storing “anything at all”.

Calendar date and point in time are different entities

In everyday speech, “date/time” is one phrase. In programming, it’s at least two different entities:

  • calendar date: “January 6, 2026” (without time of day);
  • point in time: “2026-01-06 10:15:30” (and sometimes also with a time zone).

And if you store everything as a string, you mix these meanings. Then you wonder why an “event at 10:00” suddenly became “at a different time” on another computer, or why “start of day” can’t be computed correctly by simply adding seconds.

Today our goal is not to solve this completely, but to see why a string as an internal model is doomed. In the next lectures we’ll approach the solution using date and time types, where the compiler will start helping you instead of silently watching the fire.

8. Typical mistakes when working with a date as a string

Mistake No. 1: comparing dates with <, >, sorted() by the string “as entered by the user”.
This almost always produces a lexicographic order that depends on the format and breaks easily on leading zeros, different separators, and mixed styles. Even if it “seems to work” on test data, it’s fragile luck, not a guarantee.

Mistake No. 2: treating an ISO string as a “full-fledged date” and building arithmetic on it.
The ISO format is indeed convenient for storage and exchange, and it often “sorts correctly” as text. But you still can’t safely do “+ 1 day” or “difference between dates” without writing half a calendar by hand.

Mistake No. 3: validating a date with a regex or split and thinking everything is correct now.
Checking the form (\d{4}-\d{2}-\d{2}) doesn’t check the meaning. It’s very easy to accept 2026-13-40 or 2026-02-31 as a “normal date” if you only check the number of digits and separators.

Mistake No. 4: mixing formats in one project without an explicit contract.
When part of the code expects dd.MM.yyyy, part expects yyyy-MM-dd, and part expects “whatever came in”, the program starts “working sometimes”. This is the worst mode: it doesn’t fail fast, it quietly corrupts data and reports.

Mistake No. 5: smearing formatting across the whole codebase instead of one or two functions.
If in ten places you write "$day.$month.$year" and in five places you parse via split("."), then at the first requirements change (for example, you want dd/MM/yyyy) you’ll have a fun safari through the project. Centralized formatting and normalization functions are a simple habit that saves hours of life.

Mistake No. 6: not handling “bad input” and assuming the user will always enter it correctly.
Any parsing and any validation can end with an error. This is not a “rare case”; it’s a normal execution branch. If you don’t design for it in your logic (via null, runCatching, try/catch), the program either crashes or saves garbage, and both options are equally sad.

1
Task
Kotlin SELF,  51 level,  0 lecture
Locked
Tricky dates
Tricky dates
1
Task
Kotlin SELF,  51 level,  0 lecture
Locked
Aligned Calendar
Aligned Calendar
1
Task
Kotlin SELF,  51 level,  0 lecture
Locked
A Date Look-Alike
A Date Look-Alike
1
Task
Kotlin SELF,  51 level,  0 lecture
Locked
Tourist date
Tourist date
Комментарии
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны sign in
This page has no comments yet