Kotlin with Copilot: Given a birthday date, calculate how old a person is, in years.
Video: Kotlin with Copilot: Given a birthday date, calculate how old a person is, in years. by Taught by Celeste AI - AI Coding Coach
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 asPeriod.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.