CodeGym /Cursos /Kotlin SELF /Parsing e formatacao: parse(), toString(), runCatching e ...

Parsing e formatacao: parse(), toString(), runCatching e formato personalizado

Kotlin SELF
Nível 51, Lição 3
Disponível

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.

1
Tarefa
Kotlin SELF, nível 51, lição 3
Bloqueado
Data para pessoas
Data para pessoas
1
Tarefa
Kotlin SELF, nível 51, lição 3
Bloqueado
Data sem travamentos
Data sem travamentos
1
Tarefa
Kotlin SELF, nível 51, lição 3
Bloqueado
Pontos e verificação
Pontos e verificação
1
Tarefa
Kotlin SELF, nível 51, lição 3
Bloqueado
Tempo curto
Tempo curto
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION