O OdoKeep já está na App Store. Baixe grátis.

Todos os artigos

Importação de dados

Um importador CSV em TypeScript que não adivinha o histórico de abastecimentos

Detectar convenções CSV sem alterar silenciosamente datas, unidades ou o estado do tanque. Um importador TypeScript com revisão antes de salvar.

Atualizado em

Neste artigo

03/09/2026 é uma data válida em duas histórias diferentes.

Se eu a importar como 9 de março quando o usuário queria dizer 3 de setembro, o parser não lança um erro. O banco de dados aceita o registro. A cronologia parece organizada. Todos os relatórios que dependem daquela data passam a estar errados.

Foi este problema que quis resolver no importador CSV do OdoKeep: erros que produzem dados válidos.

O OdoKeep é um diário de veículos construído com React Native, Expo e TypeScript. Quem o experimenta pode já ter anos de abastecimentos em outro app ou em uma planilha. Obrigar essa pessoa a abandonar o histórico cria um custo considerável antes de ela sequer decidir se gosta do novo app.

Ler um CSV ajuda a reduzir esse custo. Preservar o significado dos números é a parte mais importante.

Inspecionar, construir e só depois salvar

A implementação está em lib/csv-import.ts e produz dois resultados distintos. A inspeção devolve a tabela interpretada, sugestões de correspondência entre colunas, a origem detectada, as convenções e as questões por resolver. A construção produz objetos VehicleRecordDraft normais e uma lista de linhas ignoradas, com os respectivos motivos. A escrita acontece separadamente, através do mecanismo de importação já existente na garagem.

Esta separação permite ao usuário ver uma linha do seu próprio arquivo antes de acrescentar qualquer registro.

Detectar o delimitador interpretando o arquivo

Detectar o delimitador é o primeiro problema que parece mais simples do que é. Uma planilha portuguesa pode separar colunas com ponto e vírgula e usar vírgulas em quase todos os valores numéricos:

Data;Odometro;Litros;Preco/l;Valor total
03/09/2026;142350;31,93;1,899;60,64
04/09/2026;142890;28,5;1,879;53,55

Contar vírgulas e pontos e vírgulas não permite decidir com segurança qual separa os campos. As vírgulas podem pertencer aos números. As notas entre aspas também podem conter qualquer um desses caracteres.

O importador experimenta vírgula, ponto e vírgula, tabulação e barra vertical, interpretando o texto com cada candidato. Avalia a consistência entre o número de campos de cada linha e o cabeçalho, com uma pequena contribuição do número de colunas. Um delimitador que nunca divide o arquivo em pelo menos duas colunas não fornece informação útil.

O parser percorre os caracteres e mantém o estado das aspas. Suporta delimitadores dentro de aspas, quebras de linha em campos citados, aspas duplicadas, uma marca de ordem de bytes e os formatos de mudança de linha esperados. Dividir primeiro o texto em linhas já teria destruído uma nota com vários parágrafos.

Estas escolhas acomodam arquivos que as pessoas salvam e editam. Não significam que o importador reconheça todas as variantes de CSV malformado. As etapas seguintes ainda precisam confirmar se a tabela resultante é utilizável.

Procurar evidência no arquivo inteiro

Os números exigem uma decisão para o arquivo. 1.234 pode ser um decimal ou um inteiro com separador de milhares. Um valor isolado raramente resolve a dúvida, mas outras colunas numéricas podem ajudar.

O detector analisa quantidade, preço unitário, custo total e hodômetro. Um valor com os dois separadores é uma indicação forte da convenção usada. Caso contrário, um separador seguido de um número de algarismos diferente de três pode distinguir casas decimais de um agrupamento de milhares.

Os valores ambíguos não contam como evidência. Se os dados mapeados não resolverem a convenção e nenhum perfil reconhecido fornecer um valor padrão, a inspeção devolve null e a tela pergunta ao usuário.

A ordem da data segue a mesma ideia. Um ano no início identifica esse formato. Quando o ano está no fim, um componente superior a doze pode mostrar qual dos lados representa o dia. Se todas as datas forem até o dia doze, a coluna pode não dar uma resposta.

É um resultado legítimo. A região do usuário não prova quais foram as convenções de um arquivo produzido em outro contexto.

Dar prioridade ao arquivo, não ao perfil

O importador tem perfis Fuelly e Drivvo, mas a evidência do arquivo tem prioridade. O código relevante é curto:

const named = unitFromHeaders(table.headers, mapping);

return {
  table,
  mapping,
  source,
  decimal: detectDecimalStyle(numeric) ?? source?.decimal ?? null,
  dateOrder: detectDateOrder(column("date")) ?? source?.dateOrder ?? null,
  odometerUnit: named.odometerUnit ?? source?.odometerUnit ?? null,
  // Other inspection fields are omitted here.
};

A mesma prioridade aplica-se às unidades de quantidade suportadas: uma unidade indicada pelo arquivo prevalece sobre a configuração padrão do perfil.

Um arquivo com estrutura Fuelly pode vir de uma conta métrica ou ter sido aberto e salvo em uma planilha europeia. Reconhecer a origem ajuda a sugerir correspondências. Não autoriza o perfil a contradizer o conteúdo.

Algumas correspondências exigem conhecimento do domínio. O perfil Fuelly distingue odometer de miles e fuelup date de date added. Usar a distância desde o abastecimento anterior como hodômetro produz números plausíveis com o significado errado. Usar a data de introdução como data da compra pode deslocar anos de histórico para a tarde em que alguém os escreveu.

O campo price representa o preço unitário, permitindo calcular um total ausente a partir da quantidade e do preço. Nos dados com estrutura Drivvo, as colunas de preço unitário são consideradas antes das de custo total, para evitar que cabeçalhos semelhantes ocupem o mesmo campo.

Há um limite importante nestes perfis. Foram construídos a partir de documentação e de importadores existentes, não de exportações feitas manualmente em contas reais de Fuelly e Drivvo durante este trabalho. Alguns cabeçalhos e valores padrão continuam sendo suposições, sobretudo a grafia exata dos cabeçalhos Drivvo. Os exemplos de teste verificam a interpretação dessas estruturas, não todas as versões e idiomas que os fornecedores possam exportar.

Essa incerteza ajuda a explicar por que as correspondências continuam visíveis e editáveis. Um erro no perfil deve poder ser corrigido antes de passar a fazer parte do histórico.

Desconhecido também é um estado do tanque

O estado do tanque cria outro erro discreto. Os cálculos de consumo precisam de distinguir um abastecimento completo, um parcial e um cujo estado é desconhecido.

Alguns formatos representam o campo como abastecimento parcial. Nesses casos, o perfil inverte um valor não vazio. Mas uma célula vazia continua desconhecida, mesmo nessa coluna invertida:

isFullTank: rawFull
  ? options.fullTankInverted
    ? !isAffirmative(rawFull)
    : isAffirmative(rawFull)
  : undefined

Um campo parcial vazio não prova que o tanque ficou cheio. A ausência da coluna também não prova que todos os abastecimentos foram parciais. Ambos os atalhos alterariam os cálculos de consumo sem causar um erro de interpretação visível.

Validar o dia do calendário e a linha

Depois de resolver a ordem, o armazenamento da data ainda tem uma armadilha. JavaScript interpreta uma string ISO que só contém a data como meia-noite UTC. Em um fuso horário a oeste de UTC, esse instante pode aparecer na noite anterior.

O importador começa por validar explicitamente o dia, incluindo a duração real do mês. O dia 30 de fevereiro não transita para março. Depois escreve a meia-noite local com o desvio de fuso horário do dispositivo, seguindo a convenção dos formulários manuais. O dia 3 de setembro escolhido pelo usuário deve continuar sendo 3 de setembro na cronologia e nos totais mensais.

Antes da escrita, o construtor rejeita valores negativos e valores acima dos limites compartilhados do app. Um hodômetro mal escrito ou uma linha que representa um reembolso tornam-se linhas ignoradas identificáveis, em vez de provocarem a falha de um bloco inteiro de registros válidos.

Uma linha com data, mas sem hodômetro, quantidade ou custo utilizável, também é ignorada. Acrescentar uma entrada que não descreve nada só tornaria o histórico mais comprido.

O resultado separa os rascunhos importados das linhas ignoradas. O usuário pode corrigir o arquivo ou decidir avançar com o que foi lido. Depois da confirmação, os rascunhos passam pelo importador e pela fila offline existentes. O leitor de CSV não cria outro modelo de persistência.

Investigar sem escrever

Durante o desenvolvimento, o pequeno script csv:probe reutiliza a inspeção e a construção sem salvar registros. Mostra o delimitador, as convenções, as correspondências, exemplos e linhas recusadas. Quando usa valores provisórios para diagnóstico, identifica esses valores como suposições. Assim consigo inspecionar um novo exemplo sem abrir o app ou uma conta.

Os testes incluem ponto e vírgula com vírgula decimal, os dois perfis, convenções ambíguas, notas entre aspas com várias linhas, datas inválidas, valores negativos, unidades explícitas que prevalecem sobre um perfil e estados de tanque desconhecidos. O exemplo com formatos mistos é particularmente útil: estes problemas costumam chegar juntos.

Não há aqui uma garantia de importar qualquer exportação sem intervenção. A implementação sugere, valida o que consegue e dá às decisões restantes um lugar na interface. Para quem muda um diário com vários anos, uma pergunta visível sobre unidades custa muito menos do que reinterpretar silenciosamente todas as entradas.

Se você já guarda abastecimentos em um CSV, este é o processo que estou desenvolvendo no OdoKeep: inspecionar o arquivo, confirmar colunas e unidades, e juntar os registros utilizáveis ao mesmo histórico das novas entradas. Você pode experimentar esse recurso no app iOS.