Part of Github Copilot with Kotlin

Kotlin with Copilot: Given a birthday date, calculate how old a person is, in years.

Sandy LaneSandy Lane

Video: Kotlin with Copilot: Given a birthday date, calculate how old a person is, in years. by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

Kotlin: Calculate Age from a Birthday

Period.between(birthday, LocalDate.now()).years. The single-line, leap-year-aware, modern way. No date arithmetic, no off-by-one bugs.

A small puzzle: given a birthdate, return the person's age in years. The naive (today.year - birthday.year) is wrong if the birthday hasn't passed yet this year. java.time.Period does it correctly.

The Copilot prompt

import java.time.LocalDate
import java.time.Period

// Calculate age in years from a birthday
fun calculateAge(birthday: LocalDate): Int {

Copilot generates:

import java.time.LocalDate
import java.time.Period

fun calculateAge(birthday: LocalDate): Int {
  val today = LocalDate.now()
  val age = Period.between(birthday, today).years
  return age
}

fun main() {
  val birthday = LocalDate.of(1990, 5, 15)
  println("Age: ${calculateAge(birthday)} years")
}

Walkthrough

1. LocalDate.now(). Today's date. (Time zone is the system default.)

2. Period.between(start, end). Returns a Period representing the elapsed time between two dates. Has .years, .months, and .days fields.

3. .years. Just the year component. So a person born May 15, 1990, evaluated on May 14, 2024, is age = 33 (because the period is "33 years, 11 months, 30 days"). On May 15 they become 34.

That's the correct age — accounting for whether the birthday has passed this year.

The naive version is wrong

fun ageNaive(birthday: LocalDate): Int = LocalDate.now().year - birthday.year

If today is January 1, 2024 and someone was born December 31, 1990:

  • Naive: 2024 - 1990 = 34. Wrong; they're still 33 (birthday hasn't happened in 2024 yet).
  • Period.between(...) gives 33. Correct.

Period.between walks the calendar properly — knows that birthdays in December haven't happened yet in early January.

Days, months, years

val period = Period.between(birthday, LocalDate.now())
println("${period.years} years, ${period.months} months, ${period.days} days")

The three components, as separate Ints. They're already normalized — months is always 0-11, days is 0-30 (or so).

For "total days lived":

import java.time.temporal.ChronoUnit

val days = ChronoUnit.DAYS.between(birthday, LocalDate.now())
println("$days days alive")

ChronoUnit.DAYS.between ignores months/years — gives the total day count, useful for "days until X" calculations.

Other ChronoUnit options

  • ChronoUnit.DAYS — total days.
  • ChronoUnit.WEEKS — total weeks.
  • ChronoUnit.MONTHS — total months (rounds down for partial).
  • ChronoUnit.YEARS — total years (same as Period.between(...).years).

For "weeks of pregnancy" or "months since signup," ChronoUnit is the tool.

Future-proofing the API

Pass LocalDate.now() as a parameter to make the function testable:

fun calculateAge(birthday: LocalDate, today: LocalDate = LocalDate.now()): Int =
  Period.between(birthday, today).years

// In tests:
calculateAge(LocalDate.of(1990, 5, 15), LocalDate.of(2024, 5, 14))   // 33
calculateAge(LocalDate.of(1990, 5, 15), LocalDate.of(2024, 5, 15))   // 34
calculateAge(LocalDate.of(1990, 5, 15), LocalDate.of(2024, 5, 16))   // 34

Without injection, you'd need to mock the system clock or use Clock.fixed(...). The injection pattern is cleaner.

With Clock for richer time mocking

For complex time-sensitive code:

import java.time.Clock

fun calculateAge(birthday: LocalDate, clock: Clock = Clock.systemDefaultZone()): Int {
  val today = LocalDate.now(clock)
  return Period.between(birthday, today).years
}

// In tests:
val fixedClock = Clock.fixed(Instant.parse("2024-05-15T00:00:00Z"), ZoneId.of("UTC"))
calculateAge(LocalDate.of(1990, 5, 15), fixedClock)   // 34

Clock.fixed(...) is a clock that always returns the same time — perfect for tests.

Edge cases

Born today. Period.between(today, today).years == 0. Correct.

Born in the future. Period.between(future, today).years is negative. Period preserves direction.

Leap-year birthday (Feb 29). A person born Feb 29, 2000 is 24 years old on Feb 28, 2024 (period would say "23 years, 11 months, 30 days") and 24 on Mar 1, 2024. The "official" rule varies by jurisdiction; Period.between follows the calendar literally.

Time zones. LocalDate.now() uses the system zone. For the user's timezone, LocalDate.now(ZoneId.of("America/New_York")).

In Compose

@Composable
fun AgeDisplay(birthday: LocalDate) {
  val age = remember(birthday) { calculateAge(birthday) }
  Text("$age years old")
}

remember(birthday) re-runs only when the birthday changes. Saves recomputing every recomposition.

Common mistakes

Subtracting years. today.year - birthday.year — off by one if birthday hasn't passed.

Mixing Date and LocalDate. Date (legacy) is a timestamp; LocalDate is just a date. They don't mix cleanly. Use LocalDate for ages.

Mixing time zones implicitly. "Today" is locale-dependent. For a global user base, decide whose timezone you mean.

Forgetting tests with edge dates. Day-before-birthday and day-of-birthday are essential test cases. Day-after is for completeness.

Using Years.yearsBetween(...) from JodaTime. That's the old library before java.time. Modern Java/Kotlin uses java.time.Period.between.

What's next

Episode 38: Create a large array of numbers (Python). A small Python detour — list comprehensions for "an array of 1 to 10000."

Recap

Period.between(birthday, today).years for correct age. Walk the calendar, handle "birthday hasn't happened yet" automatically. Inject today: LocalDate for testability — or use Clock.fixed(...). For different units (days, months, weeks), use ChronoUnit.X.between(...). LocalDate.now(ZoneId) for explicit timezone.

Next episode: large array in Python.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.