gestion_table_tournante_libre/src/csv/lecture.js

198 lines
8.8 KiB
JavaScript
Raw Normal View History

[ADD] csv: decode in four steps, choose the separator, cut records Bytes decode in the order of § 10.1: UTF-8 mark, UTF-16 mark, a NUL in the first 4 KiB without a mark refused with its remedy, then strict UTF-8, then windows-1252. A mark its bytes contradict, and any decoded NUL, are refused: decoded, they would give names no later check sees. The separator is the candidate among ";", "," and tab that leaves no quote open and gives one field count above one on the first twenty records; a lone recognised header reads as one column. Records follow RFC 4180 and are numbered as a spreadsheet numbers its rows. Checked: 89 tests, 60 mutants killed; review found no behaviour defect. --- FR --- [ADD] csv : décodage en quatre temps, choix du séparateur, découpage Les octets se décodent dans l'ordre du § 10.1 : marque UTF-8, marque UTF-16, octet nul dans les 4 Kio sans marque refusé avec son remède, UTF-8 strict, puis windows-1252. Une marque contredite par ses octets, et tout nul décodé, sont refusés : décodés, ils donneraient des noms qu'aucun contrôle ne verrait. Le séparateur parmi « ; », « , » et la tabulation ne laisse aucun guillemet ouvert et donne un même nombre de champs, plus d'un, sur vingt enregistrements ; un en-tête seul reconnu se lit en une colonne. Découpage RFC 4180, rangs comme dans un tableur. Vérifié : 89 épreuves, 60 mutants tués ; aucun défaut à la revue. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 07:05:05 -04:00
// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Lecture d'un texte CSV déjà décodé (§ 10.1, § 10.2) : sa découpe en
// enregistrements et le choix de son séparateur. L'import d'un fichier et le
// collage en bloc passent par ces mêmes fonctions : le texte qu'elles reçoivent
// n'a plus d'origine, et un second analyseur « simplifié » divergerait du
// premier sur les accents et les guillemets.
import { ErreurCsv } from './erreurs.js';
const GUILLEMET = '"';
// Candidats du séparateur, dans l'ordre qui départage deux candidats à égalité
// de tout le reste.
const CANDIDATS = [';', ',', '\t'];
// Nombre d'enregistrements que le choix du séparateur lit.
const FENETRE = 20;
// Parcourt texte en un seul passage et rend ses enregistrements, avec l'état
// des guillemets à la fin de la lecture. La lecture s'arrête après limite
// enregistrements terminés ; guillemetOuvert est alors faux, la lecture
// s'arrêtant hors d'un champ cité.
//
// La découpe suit RFC 4180, avec ces précisions :
// - un guillemet ouvre un champ cité quand il en est le premier caractère ;
// ailleurs, c'est un caractère comme un autre ;
// - dans un champ cité, le séparateur et les fins de ligne font partie du
// texte, deux guillemets font un guillemet, un seul referme le champ ; ce qui
// suit le guillemet fermant, jusqu'au séparateur, reste dans le champ ;
// - CRLF, LF ou CR terminent un enregistrement, CRLF comptant pour une seule
// fin ;
// - une ligne vide est un enregistrement d'un seul champ vide ; la ligne vide
// qui suit la dernière fin de ligne ne fait pas d'enregistrement ;
// - un séparateur null ne sépare rien : chaque enregistrement est un champ ;
// - aucun blanc n'est retiré.
function analyser(texte, separateur, limite) {
const enregistrements = [];
let enregistrement = [];
let champ = '';
let cite = false;
let debutDeChamp = true;
let debutDeLigne = true;
for (let i = 0; i < texte.length; i += 1) {
const c = texte[i];
if (cite) {
if (c !== GUILLEMET) {
champ += c;
} else if (texte[i + 1] === GUILLEMET) {
champ += GUILLEMET;
i += 1;
} else {
cite = false;
}
continue;
}
if (c === '\n' || c === '\r') {
if (c === '\r' && texte[i + 1] === '\n') i += 1;
enregistrement.push(champ);
enregistrements.push(enregistrement);
if (enregistrements.length >= limite) return { enregistrements, guillemetOuvert: false };
enregistrement = [];
champ = '';
debutDeChamp = true;
debutDeLigne = true;
continue;
}
debutDeLigne = false;
if (c === separateur) {
enregistrement.push(champ);
champ = '';
debutDeChamp = true;
} else if (c === GUILLEMET && debutDeChamp) {
cite = true;
} else {
champ += c;
debutDeChamp = false;
}
}
if (!debutDeLigne) {
enregistrement.push(champ);
enregistrements.push(enregistrement);
}
return { enregistrements, guillemetOuvert: cite };
}
/**
* Découpe un texte en enregistrements de champs, selon RFC 4180 : champs cités,
* guillemets doublés, séparateur et fins de ligne dans un champ cité, fins de
* ligne CRLF, LF ou CR. Une ligne vide est un enregistrement d'un seul champ
* vide ; la ligne vide qui suit la dernière fin de ligne ne fait pas
* d'enregistrement. Aucun blanc n'est retiré des champs.
*
* Un enregistrement se désigne par son rang à partir de 1, l'en-tête compris :
* le numéro de ligne qu'un tableur lui donne. Un champ cité sur plusieurs
* lignes ne fait qu'un enregistrement ; la découpe ne rend aucun numéro de
* ligne du texte.
*
* @param {string} texte
* @param {string|null} separateur un caractère, ou null pour un fichier à une
* colonne, dont chaque ligne est un seul champ
* @returns {{ enregistrements: string[][], guillemetOuvert: boolean }}
* guillemetOuvert est vrai quand le texte s'achève dans un champ cité : le
* dernier enregistrement porte alors le texte lu jusque-là.
*/
export function decouper(texte, separateur) {
return analyser(texte, separateur, Infinity);
}
/**
* Vrai quand chaque champ de l'enregistrement est vide ou réduit à des blancs :
* une ligne vide, une ligne de séparateurs seuls, une ligne de blancs. Le choix
* du séparateur ne compte pas ces enregistrements.
*
* @param {string[]} enregistrement
* @returns {boolean}
*/
export function enregistrementVide(enregistrement) {
return enregistrement.every((champ) => champ.trim() === '');
}
// Lecture d'un texte par un candidat dans la fenêtre du choix. L'en-tête est
// le premier enregistrement non vide parmi ceux que la fenêtre termine : un
// guillemet resté ouvert court jusqu'à la fin du texte, et l'enregistrement
// où il s'ouvre, le dernier lu, reste inachevé. Rend null quand l'en-tête
// manque ou n'a qu'un champ : la constance seule ne suffit pas, et le candidat
// n'a pas de défaut à nommer. Sinon, rend le nombre d'en-têtes reconnus et le
// premier défaut qui écarte le candidat, ou null : un enregistrement non vide
// dont le nombre de champs diffère de celui de l'en-tête, à son rang ; à
// défaut, le guillemet resté ouvert, au rang où il s'ouvre.
function lireAvec(texte, candidat, reconnaitre) {
const { enregistrements, guillemetOuvert } = analyser(texte, candidat, FENETRE);
const termines = guillemetOuvert ? enregistrements.slice(0, -1) : enregistrements;
const entete = termines.find((e) => !enregistrementVide(e));
if (entete === undefined || entete.length < 2) return null;
const ecart = termines.findIndex((e) => !enregistrementVide(e) && e.length !== entete.length);
let defaut = null;
if (ecart !== -1) defaut = { ligne: ecart + 1, cause: 'NOMBRE_DE_CHAMPS' };
else if (guillemetOuvert) defaut = { ligne: enregistrements.length, cause: 'GUILLEMET_OUVERT' };
return { separateur: candidat, reconnus: entete.filter((champ) => reconnaitre(champ)).length, defaut };
}
// La lecture qui l'emporte, de la meilleure jusqu'ici et d'une suivante dans
// l'ordre des candidats : celle qui reconnaît le plus d'en-têtes, la première
// à égalité.
const meilleure = (jusquIci, suivante) =>
jusquIci === null || suivante.reconnus > jusquIci.reconnus ? suivante : jusquIci;
[ADD] csv: decode in four steps, choose the separator, cut records Bytes decode in the order of § 10.1: UTF-8 mark, UTF-16 mark, a NUL in the first 4 KiB without a mark refused with its remedy, then strict UTF-8, then windows-1252. A mark its bytes contradict, and any decoded NUL, are refused: decoded, they would give names no later check sees. The separator is the candidate among ";", "," and tab that leaves no quote open and gives one field count above one on the first twenty records; a lone recognised header reads as one column. Records follow RFC 4180 and are numbered as a spreadsheet numbers its rows. Checked: 89 tests, 60 mutants killed; review found no behaviour defect. --- FR --- [ADD] csv : décodage en quatre temps, choix du séparateur, découpage Les octets se décodent dans l'ordre du § 10.1 : marque UTF-8, marque UTF-16, octet nul dans les 4 Kio sans marque refusé avec son remède, UTF-8 strict, puis windows-1252. Une marque contredite par ses octets, et tout nul décodé, sont refusés : décodés, ils donneraient des noms qu'aucun contrôle ne verrait. Le séparateur parmi « ; », « , » et la tabulation ne laisse aucun guillemet ouvert et donne un même nombre de champs, plus d'un, sur vingt enregistrements ; un en-tête seul reconnu se lit en une colonne. Découpage RFC 4180, rangs comme dans un tableur. Vérifié : 89 épreuves, 60 mutants tués ; aucun défaut à la revue. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 07:05:05 -04:00
/**
* Choisit le séparateur parmi « ; », « , » et la tabulation en lisant les vingt
* premiers enregistrements avec chacun. Un candidat convient quand la lecture
* s'achève sans guillemet ouvert et que les enregistrements non vides ont tous
* le même nombre de champs, supérieur à 1 : sur un fichier séparé par des
* virgules, « ; » donne lui aussi un nombre de champs constant, un seul, et la
* constance ne suffit pas.
*
* À égalité, le candidat dont le premier enregistrement non vide compte le plus
* d'en-têtes reconnus l'emporte ; à égalité encore, le premier dans l'ordre
* « ; », « , », tabulation.
*
* Quand aucun candidat ne convient et que la première ligne entière, ses blancs
* de bord retirés, est un en-tête reconnu, le fichier n'a qu'une colonne :
* separateur est null, et un nom qui porte une virgule reste entier. Sinon,
* ErreurCsv('SEPARATEUR_INTROUVABLE'), dont les détails nomment ce qui écarte
* le candidat que le même départage désigne parmi ceux dont l'en-tête a au
* moins deux champs : { separateur, ligne, cause }, ligne étant le rang de
* l'enregistrement fautif et cause NOMBRE_DE_CHAMPS — le premier
* enregistrement non vide dont le nombre de champs diffère de celui de
* l'en-tête — ou GUILLEMET_OUVERT — sans écart, le guillemet resté ouvert,
* au rang où il s'ouvre. Sans un tel candidat, les détails sont vides.
[ADD] csv: decode in four steps, choose the separator, cut records Bytes decode in the order of § 10.1: UTF-8 mark, UTF-16 mark, a NUL in the first 4 KiB without a mark refused with its remedy, then strict UTF-8, then windows-1252. A mark its bytes contradict, and any decoded NUL, are refused: decoded, they would give names no later check sees. The separator is the candidate among ";", "," and tab that leaves no quote open and gives one field count above one on the first twenty records; a lone recognised header reads as one column. Records follow RFC 4180 and are numbered as a spreadsheet numbers its rows. Checked: 89 tests, 60 mutants killed; review found no behaviour defect. --- FR --- [ADD] csv : décodage en quatre temps, choix du séparateur, découpage Les octets se décodent dans l'ordre du § 10.1 : marque UTF-8, marque UTF-16, octet nul dans les 4 Kio sans marque refusé avec son remède, UTF-8 strict, puis windows-1252. Une marque contredite par ses octets, et tout nul décodé, sont refusés : décodés, ils donneraient des noms qu'aucun contrôle ne verrait. Le séparateur parmi « ; », « , » et la tabulation ne laisse aucun guillemet ouvert et donne un même nombre de champs, plus d'un, sur vingt enregistrements ; un en-tête seul reconnu se lit en une colonne. Découpage RFC 4180, rangs comme dans un tableur. Vérifié : 89 épreuves, 60 mutants tués ; aucun défaut à la revue. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 07:05:05 -04:00
*
* @param {string} texte
* @param {(entete: string) => unknown} reconnaitre rend une valeur vraie pour
* un en-tête connu ; reçoit chaque champ du premier enregistrement non vide
* tel qu'il est écrit, ou la première ligne entière d'un fichier à une
* colonne, ses blancs de bord retirés
* @returns {{ separateur: string|null }}
* @throws {ErreurCsv} SEPARATEUR_INTROUVABLE, détails
* { separateur, ligne, cause } ou {}
[ADD] csv: decode in four steps, choose the separator, cut records Bytes decode in the order of § 10.1: UTF-8 mark, UTF-16 mark, a NUL in the first 4 KiB without a mark refused with its remedy, then strict UTF-8, then windows-1252. A mark its bytes contradict, and any decoded NUL, are refused: decoded, they would give names no later check sees. The separator is the candidate among ";", "," and tab that leaves no quote open and gives one field count above one on the first twenty records; a lone recognised header reads as one column. Records follow RFC 4180 and are numbered as a spreadsheet numbers its rows. Checked: 89 tests, 60 mutants killed; review found no behaviour defect. --- FR --- [ADD] csv : décodage en quatre temps, choix du séparateur, découpage Les octets se décodent dans l'ordre du § 10.1 : marque UTF-8, marque UTF-16, octet nul dans les 4 Kio sans marque refusé avec son remède, UTF-8 strict, puis windows-1252. Une marque contredite par ses octets, et tout nul décodé, sont refusés : décodés, ils donneraient des noms qu'aucun contrôle ne verrait. Le séparateur parmi « ; », « , » et la tabulation ne laisse aucun guillemet ouvert et donne un même nombre de champs, plus d'un, sur vingt enregistrements ; un en-tête seul reconnu se lit en une colonne. Découpage RFC 4180, rangs comme dans un tableur. Vérifié : 89 épreuves, 60 mutants tués ; aucun défaut à la revue. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 07:05:05 -04:00
*/
export function choisirSeparateur(texte, reconnaitre) {
let retenu = null;
let ecarte = null;
[ADD] csv: decode in four steps, choose the separator, cut records Bytes decode in the order of § 10.1: UTF-8 mark, UTF-16 mark, a NUL in the first 4 KiB without a mark refused with its remedy, then strict UTF-8, then windows-1252. A mark its bytes contradict, and any decoded NUL, are refused: decoded, they would give names no later check sees. The separator is the candidate among ";", "," and tab that leaves no quote open and gives one field count above one on the first twenty records; a lone recognised header reads as one column. Records follow RFC 4180 and are numbered as a spreadsheet numbers its rows. Checked: 89 tests, 60 mutants killed; review found no behaviour defect. --- FR --- [ADD] csv : décodage en quatre temps, choix du séparateur, découpage Les octets se décodent dans l'ordre du § 10.1 : marque UTF-8, marque UTF-16, octet nul dans les 4 Kio sans marque refusé avec son remède, UTF-8 strict, puis windows-1252. Une marque contredite par ses octets, et tout nul décodé, sont refusés : décodés, ils donneraient des noms qu'aucun contrôle ne verrait. Le séparateur parmi « ; », « , » et la tabulation ne laisse aucun guillemet ouvert et donne un même nombre de champs, plus d'un, sur vingt enregistrements ; un en-tête seul reconnu se lit en une colonne. Découpage RFC 4180, rangs comme dans un tableur. Vérifié : 89 épreuves, 60 mutants tués ; aucun défaut à la revue. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 07:05:05 -04:00
for (const candidat of CANDIDATS) {
const lecture = lireAvec(texte, candidat, reconnaitre);
if (lecture === null) continue;
if (lecture.defaut === null) retenu = meilleure(retenu, lecture);
else ecarte = meilleure(ecarte, lecture);
[ADD] csv: decode in four steps, choose the separator, cut records Bytes decode in the order of § 10.1: UTF-8 mark, UTF-16 mark, a NUL in the first 4 KiB without a mark refused with its remedy, then strict UTF-8, then windows-1252. A mark its bytes contradict, and any decoded NUL, are refused: decoded, they would give names no later check sees. The separator is the candidate among ";", "," and tab that leaves no quote open and gives one field count above one on the first twenty records; a lone recognised header reads as one column. Records follow RFC 4180 and are numbered as a spreadsheet numbers its rows. Checked: 89 tests, 60 mutants killed; review found no behaviour defect. --- FR --- [ADD] csv : décodage en quatre temps, choix du séparateur, découpage Les octets se décodent dans l'ordre du § 10.1 : marque UTF-8, marque UTF-16, octet nul dans les 4 Kio sans marque refusé avec son remède, UTF-8 strict, puis windows-1252. Une marque contredite par ses octets, et tout nul décodé, sont refusés : décodés, ils donneraient des noms qu'aucun contrôle ne verrait. Le séparateur parmi « ; », « , » et la tabulation ne laisse aucun guillemet ouvert et donne un même nombre de champs, plus d'un, sur vingt enregistrements ; un en-tête seul reconnu se lit en une colonne. Découpage RFC 4180, rangs comme dans un tableur. Vérifié : 89 épreuves, 60 mutants tués ; aucun défaut à la revue. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 07:05:05 -04:00
}
if (retenu !== null) return { separateur: retenu.separateur };
const premiere = analyser(texte, null, FENETRE).enregistrements.find((e) => !enregistrementVide(e));
if (premiere !== undefined && reconnaitre(premiere[0].trim())) return { separateur: null };
if (ecarte === null) throw new ErreurCsv('SEPARATEUR_INTROUVABLE');
throw new ErreurCsv('SEPARATEUR_INTROUVABLE', { separateur: ecarte.separateur, ...ecarte.defaut });
[ADD] csv: decode in four steps, choose the separator, cut records Bytes decode in the order of § 10.1: UTF-8 mark, UTF-16 mark, a NUL in the first 4 KiB without a mark refused with its remedy, then strict UTF-8, then windows-1252. A mark its bytes contradict, and any decoded NUL, are refused: decoded, they would give names no later check sees. The separator is the candidate among ";", "," and tab that leaves no quote open and gives one field count above one on the first twenty records; a lone recognised header reads as one column. Records follow RFC 4180 and are numbered as a spreadsheet numbers its rows. Checked: 89 tests, 60 mutants killed; review found no behaviour defect. --- FR --- [ADD] csv : décodage en quatre temps, choix du séparateur, découpage Les octets se décodent dans l'ordre du § 10.1 : marque UTF-8, marque UTF-16, octet nul dans les 4 Kio sans marque refusé avec son remède, UTF-8 strict, puis windows-1252. Une marque contredite par ses octets, et tout nul décodé, sont refusés : décodés, ils donneraient des noms qu'aucun contrôle ne verrait. Le séparateur parmi « ; », « , » et la tabulation ne laisse aucun guillemet ouvert et donne un même nombre de champs, plus d'un, sur vingt enregistrements ; un en-tête seul reconnu se lit en une colonne. Découpage RFC 4180, rangs comme dans un tableur. Vérifié : 89 épreuves, 60 mutants tués ; aucun défaut à la revue. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 07:05:05 -04:00
}