1. Introducao
Quando comecamos a trabalhar com data e hora, de repente aparece uma verdade desagradavel: string e conveniente enquanto voce so a imprime no console. Mas assim que voce quiser comparar datas, ordenar eventos ou validar a entrada, a string vira areia movedica. Por isso precisamos de disciplina: armazenar data/hora separadamente como tipo, e usar a string apenas para entrada e saida.
Parsing e a transformacao de uma string em um tipo (LocalDate, LocalDateTime, Instant). Formatacao e a operacao inversa: transformar um tipo em string. Essas operacoes sao como entrada e saida do metro: se voce trocar, acaba no lugar errado — e ainda com a sensacao de que alguem roubou seu bom senso.
Para esta aula, e importante manter a ideia: nos nao “armazenamos a data como string”; nos armazenamos a data como data, e deixamos a string apenas como uma “embrulho” para o ser humano.
2. ISO-8601 e parse()
Se programadores do mundo inteiro se reunissem e combinassem um unico formato de data, provavelmente seria o ISO‑8601. Ele nao parece muito “humano”, mas e bem “para maquina”: 2026-01-14 e lido sem ambiguidade, e ordenado de forma previsivel, sem obrigar voce a adivinhar se o meio e mes ou dia. Em kotlinx.datetime, muitos tipos conseguem fazer parsing exatamente de strings ISO via parse().
Vamos combinar logo uma regra pratica: para troca e armazenamento usamos ISO (o que parse() entende), e para exibir ao usuario podemos fazer nosso proprio formato (por exemplo, dd.MM.yyyy). Essa separacao e como a diferenca entre o “idioma interno” do programa e o “idioma da interface”.
Um pequeno exemplo com LocalDate:
import kotlinx.datetime.LocalDate
fun main() {
val d = LocalDate.parse("2026-01-14")
println(d) // 2026-01-14
}
Aqui, println(d) chama toString() (por baixo dos panos), e toString() para LocalDate normalmente retorna a forma ISO.
E aqui vai um exemplo com Instant:
import kotlinx.datetime.Instant
fun main() {
val t = Instant.parse("2026-01-14T12:30:00Z")
println(t) // 2026-01-14T12:30:00Z
}
Repare no Z: isso e “UTC”. Para Instant, e importante que a string defina um instante de tempo sem ambiguidade (por exemplo, via Z ou um deslocamento). Caso contrario, voce acaba com “relogios locais sem regras de conversao”, que ja e outra entidade.
Por que parse() pode falhar — e por que isso e normal
As palavras “pode lancar uma excecao” parecem assustadoras, mas no mundo do parsing isso e o dia a dia. Se o usuario digitou 2026-13-40, a data e impossivel. Se digitou hello, isso nem parece uma data. E nesse momento, a biblioteca tem todo o direito moral de dizer “Eu nao entendo” — via excecao.
Em topicos anteriores, ja vimos algo parecido com numeros: toInt() pode lancar NumberFormatException se a string nao for um numero, entao para entrada do usuario e melhor usar toIntOrNull().
Com datas/horas, a ideia e a mesma, so que em vez de toIntOrNull() geralmente teremos uma “nossa funcao …OrNull”, construida com runCatching.
3. Parsing seguro: try/catch e runCatching
Quando voce escreve um programa de console, a entrada do usuario e o mundo externo. E o mundo externo, como sabemos, nao e obrigado a ser cuidadoso. Por isso o parsing precisa ser seguro: uma entrada invalida nao deve “derrubar” o programa. Em Kotlin, existe o caminho classico com try/catch e um caminho mais compacto com runCatching.
Opcao com try/catch
Nessa abordagem, capturamos explicitamente a excecao e retornamos null se o parsing falhar:
import kotlinx.datetime.LocalDate
fun parseIsoDateOrNull(input: String): LocalDate? {
return try {
LocalDate.parse(input.trim())
} catch (e: Exception) {
null
}
}
Esse codigo e legivel, mas cresce rapido se o numero de parsers aumentar.
Opcao com runCatching
runCatching permite escrever a ideia “tente — se nao der, nao caia” de forma compacta:
import kotlinx.datetime.LocalDate
fun parseIsoDateOrNull(input: String): LocalDate? =
runCatching { LocalDate.parse(input.trim()) }.getOrNull()
Aqui, getOrNull() retorna o resultado em caso de sucesso ou null em caso de erro. Isso e ideal para o padrao “leu → preparou → fez parsing → validou”.
Historicamente, existem nuances de contratos e comportamento em casos complexos ao redor de runCatching, algo que aparece nos changelog’ de Kotlin. Mas no nosso nivel, o importante e o simples: runCatching ajuda a nao transformar o codigo em uma floresta de try/catch.
Por que retornamos T?, e nao uma string de erro
Da vontade de retornar um texto de erro, como "Formato de data invalido". Mas isso vira confusao rapidamente: a funcao aparentemente faz parsing, mas na pratica retorna ora uma data, ora um texto. O contrato T? e mais simples: ou o tipo foi obtido, ou null. E a mensagem para o usuario passa a ser responsabilidade da camada de entrada/saida (CLI), que decide o que imprimir.
4. toString() como formato padrao
Quando voce imprime um objeto em println, Kotlin chama automaticamente toString(). Para tipos de kotlinx.datetime, toString() normalmente retorna uma representacao ISO. Isso e conveniente por dois motivos: e inequivoco e depois pode ser parseado novamente via parse().
Ou seja, voce ganha um “ciclo natural de dados” no programa:
flowchart LR
A["Tipo de data/hora"] -->|"toString()"| B["String (normalmente ISO)"]
B -->|"parse()"| A
Nesse ciclo, e especialmente importante que o tipo continue sendo tipo, e a string continue sendo string — e cada ferramenta faca seu trabalho.
Exemplo:
import kotlinx.datetime.LocalDate
fun main() {
val original = LocalDate.parse("2026-01-14")
val text = original.toString()
val parsedBack = LocalDate.parse(text)
println(text) // 2026-01-14
println(parsedBack) // 2026-01-14
}
Nesse ponto, costuma surgir a ideia: “Otimo, entao vamos sempre mostrar ISO para o usuario”. E… as vezes isso e ok. Mas muitas vezes e mais natural para o usuario ver 14.01.2026, e ai comeca a formatacao para o usuario.
5. Formato personalizado e entrada
Formatacao “para humanos” nao e sobre matematica inteligente, e sim sobre montar a string com cuidado. No curso basico, nao vamos usar formatadores complexos nem localizacoes; vamos criar uma funcao de formatacao manual, usando padStart, interpolacao e os campos da data.
Formato dd.MM.yyyy para LocalDate
import kotlinx.datetime.LocalDate
fun formatRu(date: LocalDate): String {
val dd = date.dayOfMonth.toString().padStart(2, '0')
val mm = date.monthNumber.toString().padStart(2, '0')
return "$dd.$mm.${date.year}"
}
fun main() {
val d = LocalDate.parse("2026-01-04")
println(formatRu(d)) // 04.01.2026
}
O importante aqui e termos uma regra centralizada. Se amanha voce decidir que precisa do formato dd/MM/yyyy, voce muda uma funcao, e nao 25 println no projeto.
Formato dd.MM.yyyy HH:mm para LocalDateTime
LocalDateTime contem data e hora. Podemos estender a formatacao:
import kotlinx.datetime.LocalDateTime
fun formatRu(dt: LocalDateTime): String {
val dd = dt.dayOfMonth.toString().padStart(2, '0')
val mm = dt.monthNumber.toString().padStart(2, '0')
val hh = dt.hour.toString().padStart(2, '0')
val min = dt.minute.toString().padStart(2, '0')
return "$dd.$mm.${dt.year} $hh:$min"
}
fun main() {
val dt = LocalDateTime.parse("2026-01-14T09:05:00")
println(formatRu(dt)) // 14.01.2026 09:05
}
Nao tentamos “vencer todos os formatos do mundo”. Fazemos um formato especifico que o nosso aplicativo precisa — e isso e uma pratica de engenharia totalmente normal.
Entrada do usuario: fazer parsing de dd.MM.yyyy com seguranca
Agora vem a parte desagradavel: LocalDate.parse() nao e obrigado a entender o formato 14.01.2026. Entao, para o formato do usuario, fazemos um parser separado.
E facil cair na armadilha do “em cinco segundos eu resolvo com split”. E sim, voce resolve. Mas depois descobre que 32.01.2026 tambem “foi parseado”, e isso nao e uma data, e sim uma fanfic do calendario. Portanto, o correto e: separar os numeros e depois criar um LocalDate pelo construtor e capturar a excecao caso a data seja impossivel.
import kotlinx.datetime.LocalDate
fun parseRuDateOrNull(input: String): LocalDate? {
val parts = input.trim().split(".")
if (parts.size != 3) return null
val day = parts[0].toIntOrNull() ?: return null
val month = parts[1].toIntOrNull() ?: return null
val year = parts[2].toIntOrNull() ?: return null
return runCatching { LocalDate(year, month, day) }.getOrNull()
}
Repare: toIntOrNull() e o nosso conhecido “parsing seguro de numero”, que evita NumberFormatException. Na documentacao de excecoes, esse padrao aparece como alternativa recomendada ao toInt() nao seguro.
Parser “inteligente”: aceitar ISO ou dd.MM.yyyy
Em programas reais, muitas vezes voce quer ser gentil com o usuario e aceitar mais de um formato. Mas essa gentileza precisa ser controlada; caso contrario, voce comeca a “adivinhar” a entrada e pode interpretar uma coisa como outra.
Uma opcao de compromisso: primeiro tentamos ISO (por ser o mais estrito e inequivoco), depois tentamos o formato “ru” (dd.MM.yyyy).
import kotlinx.datetime.LocalDate
fun parseDateFlexibleOrNull(input: String): LocalDate? {
val s = input.trim()
return runCatching { LocalDate.parse(s) }.getOrNull()
?: parseRuDateOrNull(s)
}
Esse codigo e bom porque nao espalha a logica pelo projeto inteiro. Idealmente, funcoes assim devem viver na sua “camada de entrada/parsing”: pequenas, com um unico objetivo, e com um contrato claro T?.
6. Integrando no aplicativo: adicionando a data da operacao
Vamos imaginar que o nosso projeto pratico de console e um controle simples de despesas. Antes, uma despesa podia ter valor e categoria. Agora vamos adicionar a data da operacao como LocalDate. O objetivo aqui nao e “fazer um super aplicativo”, e sim mostrar como parsing/formatacao deixam de ser caos quando ficam em funcoes.
Modelo de dados Expense com data
import kotlinx.datetime.LocalDate
data class Expense(
val amount: Int,
val category: String,
val date: LocalDate,
)
Sim, sao apenas tres campos — e sim, isso ja torna o programa muito mais claro: data nao e string, e sim LocalDate.
Entrada de despesa: ler a data como string e fazer parsing
import kotlinx.datetime.LocalDate
fun readExpenseDate(): LocalDate {
while (true) {
print("Data (YYYY-MM-DD ou dd.MM.yyyy): ")
val input = readln()
val date = parseDateFlexibleOrNull(input)
if (date != null) return date
println("Nao entendi a data. Exemplo: 2026-01-14 ou 14.01.2026")
}
}
Aqui, o loop faz o que deve: nao “implora por uma entrada perfeita”, apenas pergunta de novo com calma. E o mais importante: o parsing nao esta espalhado pelo codigo; ele vive em parseDateFlexibleOrNull.
Impressao bonita da lista de despesas: formatar a data separadamente
fun printExpense(e: Expense) {
val dateText = formatRu(e.date)
println("${dateText} | ${e.category} | ${e.amount}")
// 14.01.2026 | Food | 1200
}
Como resultado, a saida fica “humana”, enquanto os dados por dentro continuam “para maquina”.
7. Mensagens de erro: nao perder a causa com cuidado
As vezes da muita vontade de explicar ao usuario “por que” nao foi possivel fazer parsing: “o mes deve estar entre 1 e 12”. Para isso, durante o debug, da para extrair a excecao de runCatching, mas nao vale a pena fazer isso em todo lugar o tempo todo: voce rapidamente comeca a imprimir textos assustadores como IllegalArgumentException: Invalid date..., que fazem sentido para a biblioteca, mas nao para uma pessoa.
A documentacao sobre excecoes mostra bem que existem tipos diferentes de excecao e que o texto delas e, antes de tudo, uma ferramenta do desenvolvedor — nao uma interface para o usuario. Entao, normalmente se faz assim: para o usuario, curto e claro; e a causa detalhada vai para o log (se existir) ou para a saida de debug.
Mini-variacao para “pegar o motivo” (por exemplo, para debug):
import kotlinx.datetime.LocalDate
fun parseIsoDateDebug(input: String): LocalDate? {
val result = runCatching { LocalDate.parse(input.trim()) }
result.exceptionOrNull()?.let { e ->
println("DEBUG: parse error = ${e.message}")
}
return result.getOrNull()
}
Essa abordagem e util, mas e importante nao abusar: caso contrario, voce transforma o programa de console em um fluxo de “sofrimento da JVM”.
8. Erros tipicos
Erro №1: fazer parsing de data “do jeito que der” com split, mas sem validar no calendario.
Novatos frequentemente quebram a string em partes, convertem em numeros e acham que acabou. O problema e que assim voce pode aceitar 99.99.2026 como “data valida” no nivel dos tipos Int. O correto e: depois de extrair os numeros, ainda criar um LocalDate(...) e capturar o erro via runCatching, para que o calendario diga um “nao” honesto.
Erro №2: chamar parse() diretamente na entrada do usuario sem tratamento de erro.
LocalDate.parse(...) e Instant.parse(...) foram feitos para uma string correta. Entrada externa quase nunca garante correcoes, entao o codigo comeca a falhar “na pior hora” e estraga o UX. Em vez disso, e melhor envolver o parsing em runCatching { ... }.getOrNull(), retornar null e, em seguida, perguntar ao usuario novamente com tranquilidade.
Erro №3: misturar “como armazenar” com “como exibir”.
Se voce comeca a armazenar a data ja como string formatada 14.01.2026, voce mesmo complica sua vida: ordenacao, comparacao, filtros — tudo volta a ser trabalho com strings. E muito mais robusto armazenar LocalDate e fazer a formatacao apenas antes de imprimir, em uma funcao separada como formatRu(date).
Erro №4: espalhar a formatacao por todo o codigo em vez de uma funcao unica.
Quando o formato de data fica espalhado por println, em uma semana voce tera tres tipos diferentes de data no mesmo aplicativo: em algum lugar 2026-01-14, em outro 14.1.2026, e em outro sem zeros a esquerda. Corrigir vira arqueologia. Uma funcao centralizada de formatacao resolve isso de imediato e disciplina o projeto.
Erro №5: tentar criar um “parser universal de tudo” cedo demais.
As vezes voce quer suportar dez formatos, espacos, barras, virgulas e calendario lunar. Na pratica, isso transforma a entrada em adivinhacao, e o codigo em um conjunto de heuristicas frageis. No nivel basico, e melhor manter um formato de troca (ISO via parse()) e um formato para o usuario (por exemplo, dd.MM.yyyy); e se voce suportar os dois, faca isso de modo estrito e previsivel, como em parseDateFlexibleOrNull.
GO TO FULL VERSION