Calcular una edad, mostrar «miércoles 16 de septiembre», saber qué hora es en Tijuana cuando en la Ciudad de México son las 10: las fechas parecen sencillas hasta que dejan de serlo. Kotlin usa java.time, la API de fechas de la JVM. Aquí verás qué tipo usar en cada caso, cómo formatear en español y las trampas que más errores causan.
También te puede interesar
Qué tipo usar
Kotlin en la JVM usa java.time, la API de fechas de Java 8 en adelante: clara, inmutable (cada operación devuelve un objeto nuevo) y con un tipo para cada caso. Elegir bien el tipo resuelve la mitad de los problemas:
- TipoGuardaPara
LocalDateaño, mes y díacumpleaños, fechas de pago, días festivosLocalTimehora del díahorarios de aperturaLocalDateTimefecha y hora, sin zonauna cita en «la hora de aquí»ZonedDateTimefecha, hora y zona horariareuniones entre ciudades, cambios de horarioInstantun instante exacto en UTCmarcas de tiempo, registros, bases de datosDuration/Periodun lapso en horas o en días, meses y añosdiferencias y sumas
Todas las fechas de esta página son fijas, para que veas exactamente las mismas salidas que nosotros; en tu programa, la fecha de hoy es LocalDate.now().
Crear fechas y moverse por el calendario
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
fun main() {
val fecha = LocalDate.of(2026, 11, 9)
val hora = LocalTime.of(18, 30)
val cita = LocalDateTime.of(fecha, hora)
println(fecha)
println(hora)
println(cita)
println("Día de la semana: ${fecha.dayOfWeek}, día ${fecha.dayOfYear} del año")
println("¿2028 es bisiesto? ${LocalDate.of(2028, 1, 1).isLeapYear}")
println("Noviembre tiene ${fecha.lengthOfMonth()} días")
println("En 30 días: ${fecha.plusDays(30)}")
println("Un mes antes del 31 de marzo: ${LocalDate.of(2026, 3, 31).minusMonths(1)}")
println(fecha.isBefore(LocalDate.of(2026, 12, 12)))
}salida2026-11-09 18:30 2026-11-09T18:30 Día de la semana: MONDAY, día 313 del año ¿2028 es bisiesto? true Noviembre tiene 30 días En 30 días: 2026-12-09 Un mes antes del 31 de marzo: 2026-02-28 true
Los meses van del 1 al 12 (no del 0 al 11, como en el viejo java.util.Date) y las sumas respetan el calendario: un mes antes del 31 de marzo es el 28 de febrero, porque no existe el 31. dayOfWeek es un enum con los nombres en inglés; para mostrarlo en español, usa un formato, como en la sección siguiente.
Formatos en español de México
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Locale
val mx: Locale = Locale.of("es", "MX")
fun main() {
val fecha = LocalDate.of(2026, 9, 16)
val momento = LocalDateTime.of(2026, 9, 16, 9, 5)
println(fecha.format(DateTimeFormatter.ofPattern("dd/MM/yyyy")))
println(fecha.format(DateTimeFormatter.ofPattern("EEEE d 'de' MMMM 'de' yyyy", mx)))
println(momento.format(DateTimeFormatter.ofPattern("d MMM yyyy, HH:mm", mx)))
println(fecha.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).withLocale(mx)))
val leida = LocalDate.parse("25/12/2026", DateTimeFormatter.ofPattern("dd/MM/yyyy"))
println(leida)
}salida16/09/2026 miércoles 16 de septiembre de 2026 16 sep 2026, 09:05 16 de septiembre de 2026 2026-12-25
Con un Locale de español de México, EEEE da el nombre del día y MMMM el del mes, en minúsculas como se escriben en español. Los textos entre comillas simples ('de') se copian tal cual. Para leer una fecha escrita a la mexicana, parse con el mismo patrón.
Las letras que se confunden
En los patrones, la diferencia entre mayúscula y minúscula cambia el significado. Estas son las confusiones más comunes, con el 28 de diciembre de 2026 a las 9:05:
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.Locale
fun main() {
val mx = Locale.of("es", "MX")
val momento = LocalDateTime.of(2026, 12, 28, 9, 5)
println("yyyy: " + momento.format(DateTimeFormatter.ofPattern("dd/MM/yyyy", mx)))
println("YYYY: " + momento.format(DateTimeFormatter.ofPattern("dd/MM/YYYY", mx)))
println("mm: " + momento.format(DateTimeFormatter.ofPattern("dd/mm/yyyy", mx)))
println("hh: " + momento.withHour(21).format(DateTimeFormatter.ofPattern("hh:mm", mx)))
println("HH: " + momento.withHour(21).format(DateTimeFormatter.ofPattern("HH:mm", mx)))
}salidayyyy: 28/12/2026 YYYY: 28/12/2027 mm: 28/05/2026 hh: 09:05 HH: 21:05
YYYYno es el año: es el año de la semana. La semana del 28 de diciembre de 2026 ya es la primera de 2027 (con las semanas que empiezan en domingo, como en México), y la fecha sale con el año equivocado. Solo pasa los últimos días de diciembre, así que el error se esconde en las pruebas. Usa siempreyyyy.mmson minutos; el mes esMM.hhes la hora de 1 a 12 (sin a. m. o p. m. no sabes cuál es); para 24 horas,HH.
El aviso de Locale(«es», «MX»)
En código antiguo verás Locale("es", "MX"). Funciona, pero ese constructor está marcado como obsoleto desde Java 19 y kotlinc lo avisa:
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.Locale
fun main() {
val mx = Locale("es", "MX")
println(LocalDate.of(2026, 9, 16).format(DateTimeFormatter.ofPattern("MMMM", mx)))
}kotlincdeprecado.kt:6:14: warning: 'constructor(p0: String!, p1: String!): Locale' is deprecated. Deprecated in Java.
val mx = Locale("es", "MX")
^^^^^^La forma actual es Locale.of("es", "MX"), que es la que usa esta página. En Android, que va con versiones de Java más antiguas, sigue siendo habitual el constructor (o Locale.forLanguageTag("es-MX")).
Diferencias: Period, Duration y ChronoUnit
import java.time.Duration
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.Period
import java.time.temporal.ChronoUnit
fun main() {
val nacimiento = LocalDate.of(1995, 4, 21)
val hoy = LocalDate.of(2026, 11, 9)
val edad = Period.between(nacimiento, hoy)
println("Edad: ${edad.years} años, ${edad.months} meses y ${edad.days} días")
println("Días vividos: ${ChronoUnit.DAYS.between(nacimiento, hoy)}")
println("Días para Navidad: ${ChronoUnit.DAYS.between(hoy, LocalDate.of(2026, 12, 25))}")
val entrada = LocalDateTime.of(2026, 11, 9, 8, 50)
val salida = LocalDateTime.of(2026, 11, 9, 17, 35)
val jornada = Duration.between(entrada, salida)
println("Jornada: ${jornada.toHours()} h ${jornada.toMinutesPart()} min ($jornada)")
println("Más 90 minutos: ${salida.plus(Duration.ofMinutes(90))}")
}salidaEdad: 31 años, 6 meses y 19 días Días vividos: 11525 Días para Navidad: 46 Jornada: 8 h 45 min (PT8H45M) Más 90 minutos: 2026-11-09T19:05
Period cuenta en años, meses y días del calendario (lo que quieres para una edad) y Duration en horas, minutos y segundos exactos (lo que quieres para una jornada o un cronómetro). Para un solo número total, como «días que faltan», ChronoUnit.DAYS.between. PT8H45M es como se escribe una duración en ISO 8601.
Zonas horarias en México
México eliminó el horario de verano en 2022, salvo en la franja fronteriza del norte, que lo sigue cambiando a la par de Estados Unidos. Quintana Roo tiene su propia zona todo el año. Por eso la diferencia entre la Ciudad de México y Tijuana no es fija:
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
val formato: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm xxx")
fun mostrar(en: ZonedDateTime) {
for (zona in listOf("America/Mexico_City", "America/Tijuana", "America/Cancun", "Europe/Madrid", "UTC")) {
val ahi = en.withZoneSameInstant(ZoneId.of(zona))
println(" ${zona.padEnd(20)} ${ahi.format(formato)}")
}
}
fun main() {
val cdmx = ZoneId.of("America/Mexico_City")
println("Reunión el 9 de noviembre a las 10:00 en CDMX:")
mostrar(ZonedDateTime.of(LocalDateTime.of(2026, 11, 9, 10, 0), cdmx))
println("La misma hora el 1 de julio:")
mostrar(ZonedDateTime.of(LocalDateTime.of(2026, 7, 1, 10, 0), cdmx))
println("Como instante: " + ZonedDateTime.of(LocalDateTime.of(2026, 11, 9, 10, 0), cdmx).toInstant())
}salidaReunión el 9 de noviembre a las 10:00 en CDMX: America/Mexico_City 2026-11-09 10:00 -06:00 America/Tijuana 2026-11-09 08:00 -08:00 America/Cancun 2026-11-09 11:00 -05:00 Europe/Madrid 2026-11-09 17:00 +01:00 UTC 2026-11-09 16:00 +00:00 La misma hora el 1 de julio: America/Mexico_City 2026-07-01 10:00 -06:00 America/Tijuana 2026-07-01 09:00 -07:00 America/Cancun 2026-07-01 11:00 -05:00 Europe/Madrid 2026-07-01 18:00 +02:00 UTC 2026-07-01 16:00 +00:00 Como instante: 2026-11-09T16:00:00Z
No calcules estas diferencias a mano: usa siempre el identificador de la zona (America/Mexico_City, America/Tijuana, America/Cancun…) y deja que java.time aplique las reglas. Esas reglas vienen en la base de datos de zonas horarias que trae el JDK y que se actualiza con él: si un país cambia de horario, mantén tu JDK al día. Para guardar un momento en una base de datos o un registro, usa Instant (en UTC) y convierte a la zona del usuario solo al mostrarlo.
El reloj, como parámetro
El código que llama a LocalDate.now() por dentro es difícil de probar: el resultado cambia cada día. java.time resuelve esto con Clock: todas las funciones now() aceptan uno, y Clock.fixed crea un reloj parado en el instante que elijas. En producción pasas Clock.systemDefaultZone() y en las pruebas, uno fijo:
import java.time.Clock
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
fun saludo(reloj: Clock): String {
val hora = java.time.LocalTime.now(reloj).hour
return when (hora) {
in 6..11 -> "Buenos días"
in 12..18 -> "Buenas tardes"
else -> "Buenas noches"
}
}
fun diasParaVencer(vence: LocalDate, reloj: Clock) =
java.time.temporal.ChronoUnit.DAYS.between(LocalDate.now(reloj), vence)
fun main() {
val cdmx = ZoneId.of("America/Mexico_City")
val manana = Clock.fixed(Instant.parse("2026-11-09T15:00:00Z"), cdmx)
val noche = Clock.fixed(Instant.parse("2026-11-10T04:30:00Z"), cdmx)
println("${saludo(manana)} (son las 9:00 en CDMX)")
println("${saludo(noche)} (son las 22:30 en CDMX)")
println("Faltan ${diasParaVencer(LocalDate.of(2026, 11, 30), manana)} días para el pago")
}salidaBuenos días (son las 9:00 en CDMX) Buenas noches (son las 22:30 en CDMX) Faltan 21 días para el pago
Fechas que no existen y textos que no se entienden
import java.time.LocalDate
fun main() {
println(LocalDate.of(2026, 2, 30))
}salidaException in thread "main" java.time.DateTimeException: Invalid date 'FEBRUARY 30'
at InvalidaKt.main(invalida.kt:4)java.time no «ajusta» una fecha imposible: lanza DateTimeException. Al leer texto, el error es DateTimeParseException, y su mensaje dice qué parte falló:
import java.time.LocalDate
import java.time.format.DateTimeParseException
fun main() {
for (texto in listOf("2026-11-09", "2026-13-01", "9/11/2026")) {
try {
println("$texto -> ${LocalDate.parse(texto)}")
} catch (e: DateTimeParseException) {
println("$texto -> ${e.message}")
}
}
}salida2026-11-09 -> 2026-11-09 2026-13-01 -> Text '2026-13-01' could not be parsed: Invalid value for MonthOfYear (valid values 1 - 12): 13 9/11/2026 -> Text '9/11/2026' could not be parsed at index 0
Sin patrón, LocalDate.parse solo entiende el formato ISO (2026-11-09). Para «9/11/2026» hay que pasarle un DateTimeFormatter con d/M/yyyy.
Si vienes de Java: idéntico
Es la misma API que en Java, sin cambios; mira fechas en Java. Si haces Kotlin Multiplatform, donde no hay java.time, existe kotlinx-datetime, de JetBrains, con tipos parecidos (LocalDate, Instant, TimeZone).
Todo el código de esta página se compiló con Kotlin 2.4.21 y se ejecutó en la JVM de Temurin 25.0.5 en un contenedor limpio y sin red antes de publicarla; las salidas y los mensajes del compilador están copiados de esa ejecución.