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

Todos os artigos

Importação de dados

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

Detetar convenções CSV sem alterar silenciosamente datas, unidades ou o estado do depósito. Um importador TypeScript com revisão antes de guardar.

Atualizado em

Neste artigo

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

Se a importar como 9 de março quando o utilizador queria dizer 3 de setembro, o parser não lança um erro. A base de dados aceita o registo. 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 noutra aplicação ou numa folha de cálculo. Obrigar essa pessoa a abandonar o histórico cria um custo considerável antes de ela sequer decidir se gosta da nova aplicação.

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

Inspecionar, construir e só depois guardar

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 detetada, as convenções e as questões por resolver. A construção produz objetos VehicleRecordDraft normais e uma lista de linhas ignoradas, com os respetivos motivos. A escrita acontece separadamente, através do mecanismo de importação já existente na garagem.

Esta separação permite ao utilizador ver uma linha do seu próprio ficheiro antes de acrescentar qualquer registo.

Detetar o delimitador interpretando o ficheiro

Detetar o delimitador é o primeiro problema que parece mais simples do que é. Uma folha de cálculo 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 ficheiro 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 ficheiros que as pessoas guardam e editam. Não significam que o importador reconheça todas as variantes de CSV malformado. As etapas seguintes ainda têm de confirmar se a tabela resultante é utilizável.

Procurar evidência no ficheiro inteiro

Os números exigem uma decisão para o ficheiro. 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 detetor analisa quantidade, preço unitário, custo total e odó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 por defeito, a inspeção devolve null e o ecrã pergunta ao utilizador.

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é ao dia doze, a coluna pode não dar uma resposta.

É um resultado legítimo. A região do utilizador não prova quais foram as convenções de um ficheiro produzido noutro contexto.

Dar prioridade ao ficheiro, não ao perfil

O importador tem perfis Fuelly e Drivvo, mas a evidência do ficheiro 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 ficheiro prevalece sobre a predefinição do perfil.

Um ficheiro com estrutura Fuelly pode vir de uma conta métrica ou ter sido aberto e guardado numa folha de cálculo 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 odó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 em falta 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 por defeito continuam a ser pressupostos, 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 razão 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 depósito

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

Alguns formatos exprimem 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 depósito 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 cadeia ISO que só contém a data como meia-noite UTC. Num 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 utilizador deve continuar a ser 3 de setembro na cronologia e nos totais mensais.

Antes da escrita, o construtor rejeita valores negativos e valores acima dos limites partilhados da aplicação. Um odó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 registos válidos.

Uma linha com data, mas sem odó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 utilizador pode corrigir o ficheiro 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 guardar registos. Mostra o delimitador, as convenções, as correspondências, exemplos e linhas recusadas. Quando usa valores provisórios para diagnóstico, identifica-os como pressupostos. Assim consigo inspecionar um novo exemplo sem abrir a aplicação 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 depósito 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 já guardas abastecimentos num CSV, este é o processo que estou a construir no OdoKeep: inspecionar o ficheiro, confirmar colunas e unidades, e juntar os registos utilizáveis ao mesmo histórico das novas entradas. Podes experimentá-lo na aplicação iOS.