An update file without a prénom or appartenance column matched nobody who had one, and added everyone again. Prénom and appartenance now narrow the match only when both the line and the participant carry them. A quote that opened a field and closed anywhere later swallowed records and could merge two people; a closing quote not followed by a separator or a record end now refuses the file with GUILLEMET_OUVERT. The preview takes the import mode, so in replace mode no line is shown as a duplicate of someone deleted. Checked: 1519 node tests, the new cases red on the previous code. --- FR --- [FIX] csv : rapprochement, guillemet égaré, aperçu selon le mode Un fichier de mise à jour sans colonne prénom ou appartenance ne retrouvait personne qui en avait une, et ajoutait tout le monde de nouveau. Prénom et appartenance ne restreignent désormais le rapprochement que s'ils sont des deux côtés. Un guillemet qui ouvrait un champ et se fermait plus loin avalait des lignes et pouvait fondre deux personnes ; un guillemet fermant que ne suit ni séparateur ni fin d'enregistrement refuse le fichier (GUILLEMET_OUVERT). L'aperçu prend le mode : en remplacement, aucune ligne n'est un doublon d'une personne supprimée. Vérifié : 1519 épreuves node ; les nouveaux cas, rouges sur l'ancien code. Assisted-by: Claude Opus 5.5
1281 lines
57 KiB
JavaScript
1281 lines
57 KiB
JavaScript
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
|
|
|
// Épreuves de l'aperçu d'un import ou d'un collage (§ 10.1, § 10.2, § 14.10) :
|
|
// les lignes valides et leurs champs, les lignes refusées et leur motif, les
|
|
// refus globaux, la valeur d'« exclu », le rapport des orthographes fondues,
|
|
// les doublons signalés et jamais fusionnés, le même aperçu pour un texte
|
|
// collé et pour le même texte décodé d'un fichier, et les données d'épreuve
|
|
// de test/fixtures/csv/, chacune un CSV tel qu'un tableur l'écrit. Les lettres
|
|
// décomposées et les blancs autres que l'espace s'écrivent par leur point de
|
|
// code.
|
|
import assert from 'node:assert/strict';
|
|
import { readdirSync, readFileSync } from 'node:fs';
|
|
import { describe, test } from '../../test/lanceur.js';
|
|
import { VALEURS_EXCLU, apercevoir } from './apercu.js';
|
|
import { contientNul, decoder } from './encodage.js';
|
|
import { cleNormalisee } from './normalisation.js';
|
|
|
|
const FIXTURES = new URL('../../test/fixtures/csv/', import.meta.url);
|
|
|
|
// Texte d'un CSV : chaque ligne donnée, terminée par CRLF.
|
|
const csv = (...lignes) => lignes.map((ligne) => `${ligne}\r\n`).join('');
|
|
|
|
// Vingt patronymes d'épreuve, inventés.
|
|
const PATRONYMES = [
|
|
'Ardoise', 'Brindille', 'Châtaigne', 'Dune', 'Écorce', 'Fougère', 'Galet', 'Houle', 'Iode', 'Jonc',
|
|
'Lichen', 'Mousse', 'Nacre', 'Ocre', 'Prêle', 'Quartz', 'Roseau', 'Sauge', 'Tourbe', 'Ulve',
|
|
];
|
|
|
|
// Les lignes qui suivent l'en-tête dans la fenêtre du choix du séparateur,
|
|
// ses vingt premiers enregistrements : une ligne par patronyme, rendue par
|
|
// ligne ; dix-neuf par défaut, moins pour placer un enregistrement choisi
|
|
// dans la fenêtre. Un enregistrement dont le nombre de champs diffère de
|
|
// celui de l'en-tête n'est une ligne de l'aperçu qu'au-delà, à partir du
|
|
// rang 21 : dans la fenêtre, il écarte le séparateur, et le texte entier est
|
|
// refusé à son rang.
|
|
const fenetre = (ligne, nombre = 19) => PATRONYMES.slice(0, nombre).map(ligne);
|
|
|
|
// Participant existant, ses autres champs vides.
|
|
const existant = (id, nom, prenom, appartenance) => ({
|
|
id, nom, prenom, appartenance, courriel: null, titrePressenti: null, notes: null, exclu: false,
|
|
});
|
|
|
|
// Champs d'une ligne valide, dans l'ordre de l'aperçu ; un champ omis vaut null.
|
|
const champs = ({
|
|
nom, prenom = null, appartenance = null, courriel = null, titrePressenti = null, notes = null, exclu = null,
|
|
}) => ({ nom, prenom, appartenance, courriel, titrePressenti, notes, exclu });
|
|
|
|
// Aperçu d'un texte refusé en entier : ce que la lecture a établi avant le
|
|
// refus, aucune ligne.
|
|
const refusGlobal = (
|
|
code,
|
|
{ separateur = null, entetes = [], association = { colonnes: {}, ambigus: [], nonReconnues: [] } } = {},
|
|
ligne = null,
|
|
) => ({
|
|
mode: 'ajouter',
|
|
separateur,
|
|
entetes,
|
|
association,
|
|
lignes: [],
|
|
refusees: [],
|
|
fusions: [],
|
|
doublons: [],
|
|
homonymes: [],
|
|
multilignes: [],
|
|
refusGlobal: code,
|
|
refusGlobalLigne: ligne,
|
|
});
|
|
|
|
// Octets d'une donnée d'épreuve.
|
|
const lireFixture = (nom) => readFileSync(new URL(nom, FIXTURES));
|
|
|
|
// Fins de ligne des octets d'un texte à un octet par unité : les CRLF, puis
|
|
// les LF et CR isolés.
|
|
function finsDeLigne(octets) {
|
|
let crlf = 0;
|
|
let isolees = 0;
|
|
for (let i = 0; i < octets.length; i += 1) {
|
|
if (octets[i] === 0x0d && octets[i + 1] === 0x0a) {
|
|
crlf += 1;
|
|
i += 1;
|
|
} else if (octets[i] === 0x0a || octets[i] === 0x0d) {
|
|
isolees += 1;
|
|
}
|
|
}
|
|
return { crlf, isolees };
|
|
}
|
|
|
|
// Rang et champs de chaque ligne valide.
|
|
const lignesDe = (apercu) => apercu.lignes.map(({ ligne, champs: lus }) => [ligne, lus]);
|
|
|
|
describe("apercevoir : les lignes d'un texte sans défaut (§ 10.1)", () => {
|
|
test("chaque partie de l'aperçu : séparateur, en-têtes tels qu'écrits, association, lignes", () => {
|
|
const texte = csv(
|
|
'Nom;Prénom;Équipe;Courriel;Rôle;Exclu;Notes',
|
|
'Ombrelle;Iris;Club des Merles;iris@exemple.test;animation;non;arrive tôt',
|
|
'Pervenche;Théo;;;;oui;',
|
|
);
|
|
assert.deepEqual(apercevoir(texte), {
|
|
mode: 'ajouter',
|
|
separateur: ';',
|
|
entetes: ['Nom', 'Prénom', 'Équipe', 'Courriel', 'Rôle', 'Exclu', 'Notes'],
|
|
association: {
|
|
colonnes: { nom: 0, prenom: 1, appartenance: 2, courriel: 3, titre_pressenti: 4, exclu: 5, notes: 6 },
|
|
ambigus: [],
|
|
nonReconnues: [],
|
|
},
|
|
lignes: [
|
|
{
|
|
ligne: 2,
|
|
champs: champs({
|
|
nom: 'Ombrelle', prenom: 'Iris', appartenance: 'Club des Merles', courriel: 'iris@exemple.test',
|
|
titrePressenti: 'animation', notes: 'arrive tôt', exclu: false,
|
|
}),
|
|
brut: ['Ombrelle', 'Iris', 'Club des Merles', 'iris@exemple.test', 'animation', 'non', 'arrive tôt'],
|
|
},
|
|
{
|
|
ligne: 3,
|
|
champs: champs({ nom: 'Pervenche', prenom: 'Théo', exclu: true }),
|
|
brut: ['Pervenche', 'Théo', '', '', '', 'oui', ''],
|
|
},
|
|
],
|
|
refusees: [],
|
|
fusions: [],
|
|
doublons: [],
|
|
homonymes: [],
|
|
multilignes: [],
|
|
refusGlobal: null,
|
|
refusGlobalLigne: null,
|
|
});
|
|
});
|
|
|
|
test("les champs d'une ligne, dans l'ordre du participant, quel que soit celui des colonnes", () => {
|
|
const [ligne] = apercevoir(csv('notes;exclu;titre;courriel;équipe;prénom;nom', 'n;oui;t;c@exemple.test;a;p;Ombrelle'))
|
|
.lignes;
|
|
assert.equal(
|
|
JSON.stringify(ligne.champs),
|
|
'{"nom":"Ombrelle","prenom":"p","appartenance":"a","courriel":"c@exemple.test","titrePressenti":"t","notes":"n","exclu":true}',
|
|
);
|
|
});
|
|
|
|
test("un champ : NFC, bouts retirés, vide ou blanc → null ; le brut reste tel qu'écrit", () => {
|
|
const texte = csv(
|
|
'nom;prenom;appartenance;courriel;notes',
|
|
' Ombrelle ;The\u{301}o; Club des Merles ; \u{A0} ;"ligne un\nligne deux "',
|
|
);
|
|
const [ligne] = apercevoir(texte).lignes;
|
|
assert.deepEqual(
|
|
ligne.champs,
|
|
champs({ nom: 'Ombrelle', prenom: 'Th\u{E9}o', appartenance: 'Club des Merles', notes: 'ligne un\nligne deux' }),
|
|
);
|
|
assert.deepEqual(ligne.brut, [' Ombrelle ', 'The\u{301}o', ' Club des Merles ', ' \u{A0} ', 'ligne un\nligne deux ']);
|
|
});
|
|
|
|
test('un champ sans colonne vaut null', () => {
|
|
assert.deepEqual(lignesDe(apercevoir(csv('nom', 'Ombrelle'))), [[2, champs({ nom: 'Ombrelle' })]]);
|
|
});
|
|
|
|
test("l'en-tête est le premier enregistrement non vide ; les rangs comptent les lignes qui le précèdent", () => {
|
|
const apercu = apercevoir(csv('', ';', 'nom;prenom', 'Ombrelle;Iris'));
|
|
assert.deepEqual(apercu.entetes, ['nom', 'prenom']);
|
|
assert.deepEqual(lignesDe(apercu), [[4, champs({ nom: 'Ombrelle', prenom: 'Iris' })]]);
|
|
});
|
|
|
|
test('un enregistrement vide, blanc ou fait de séparateurs est ignoré, et garde son rang', () => {
|
|
const apercu = apercevoir(csv('nom;prenom', 'Ombrelle;Iris', '', ';', ' ;\u{A0}\t', 'Pervenche;Théo'));
|
|
assert.deepEqual(apercu.lignes.map(({ ligne }) => ligne), [2, 6]);
|
|
assert.deepEqual(apercu.refusees, []);
|
|
assert.equal(apercu.refusGlobal, null);
|
|
});
|
|
|
|
test("le choix tranche une ambiguïté de l'en-tête, et les lignes en suivent", () => {
|
|
const texte = csv('nom;organisation;entreprise', 'Ombrelle;Club des Merles;Chorale du Givre');
|
|
const sansChoix = apercevoir(texte);
|
|
assert.deepEqual(sansChoix.association.ambigus, [{ champ: 'appartenance', rangs: [1, 2] }]);
|
|
assert.equal(sansChoix.refusGlobal, null);
|
|
assert.equal(sansChoix.lignes[0].champs.appartenance, null);
|
|
|
|
const avecChoix = apercevoir(texte, { choix: { appartenance: 2 } });
|
|
assert.deepEqual(avecChoix.association, { colonnes: { nom: 0, appartenance: 2 }, ambigus: [], nonReconnues: [] });
|
|
assert.equal(avecChoix.lignes[0].champs.appartenance, 'Chorale du Givre');
|
|
});
|
|
|
|
test("un texte qui n'est pas une chaîne est refusé par un TypeError : des octets passent par decoder", () => {
|
|
// Un objet String se lirait comme sa chaîne ; les autres entrées lèveraient
|
|
// plus loin, par accident, un TypeError d'un autre texte.
|
|
const entrees = [
|
|
new TextEncoder().encode('nom\nOmbrelle\n'),
|
|
new String('nom\nOmbrelle\n'),
|
|
['nom', 'Ombrelle'],
|
|
undefined,
|
|
null,
|
|
42,
|
|
];
|
|
for (const entree of entrees) {
|
|
assert.throws(() => apercevoir(entree), { name: 'TypeError', message: 'texte : chaîne attendue' });
|
|
}
|
|
});
|
|
|
|
test('apercevoir ne modifie ni les participants ni le choix', () => {
|
|
const participants = Object.freeze([
|
|
Object.freeze(existant(7, 'Givre', null, 'cooperative bleue')),
|
|
Object.freeze(existant(3, 'Houle', null, 'Coopérative Bleue')),
|
|
]);
|
|
const choix = Object.freeze({ appartenance: 2 });
|
|
const texte = csv('nom;organisation;entreprise', 'Givre;;COOPÉRATIVE BLEUE');
|
|
const apercu = apercevoir(texte, { participants, choix });
|
|
assert.equal(apercu.lignes[0].champs.appartenance, 'Coopérative Bleue');
|
|
assert.deepEqual(participants.map(({ id }) => id), [7, 3]);
|
|
});
|
|
|
|
test("l'ordre des participants donnés ne compte pas : l'identifiant ordonne", () => {
|
|
const participants = [
|
|
existant(3, 'Houle', null, 'Coopérative Bleue'),
|
|
existant(7, 'Givre', null, 'cooperative bleue'),
|
|
existant(5, 'Ombrelle', 'Iris', null),
|
|
];
|
|
const texte = csv('nom;prenom;appartenance', 'Ombrelle;Iris;COOPÉRATIVE BLEUE', 'Givre;;cooperative bleue');
|
|
const apercu = apercevoir(texte, { participants });
|
|
assert.deepEqual(apercevoir(texte, { participants: [...participants].reverse() }), apercu);
|
|
assert.equal(apercu.fusions[0].affichee, 'Coopérative Bleue');
|
|
assert.deepEqual(apercu.doublons, [{ cle: '["givre","","cooperative bleue"]', lignes: [3], existants: [7] }]);
|
|
});
|
|
});
|
|
|
|
describe('apercevoir : la ligne refusée, le reste importé (§ 10.1)', () => {
|
|
test('un nom vide ou blanc refuse la ligne : NOM_ABSENT', () => {
|
|
const apercu = apercevoir(csv('prenom;nom;appartenance', 'Iris;;Club des Merles', 'Théo; \u{A0}\t;', 'Ondine;Sarcelle;'));
|
|
assert.deepEqual(apercu.refusees, [
|
|
{ ligne: 2, code: 'NOM_ABSENT', valeur: null, brut: ['Iris', '', 'Club des Merles'] },
|
|
{ ligne: 3, code: 'NOM_ABSENT', valeur: null, brut: ['Théo', ' \u{A0}\t', ''] },
|
|
]);
|
|
assert.deepEqual(lignesDe(apercu), [[4, champs({ nom: 'Sarcelle', prenom: 'Ondine' })]]);
|
|
assert.equal(apercu.refusGlobal, null);
|
|
});
|
|
|
|
test('un enregistrement trop court pour porter le nom refuse la ligne : NOM_ABSENT', () => {
|
|
const apercu = apercevoir(csv('prenom;nom', ...fenetre((patronyme) => `x;${patronyme}`), 'Iris'));
|
|
assert.equal(apercu.separateur, ';');
|
|
assert.deepEqual(apercu.refusees, [{ ligne: 21, code: 'NOM_ABSENT', valeur: null, brut: ['Iris'] }]);
|
|
assert.equal(apercu.lignes.length, 19);
|
|
});
|
|
|
|
test("un champ non vide au-delà de l'en-tête refuse la ligne : CHAMPS_EN_TROP", () => {
|
|
const apercu = apercevoir(csv(
|
|
'nom;prenom',
|
|
...fenetre((patronyme) => `${patronyme};x`),
|
|
'Ombrelle;Iris;Club des Merles',
|
|
'Pervenche;Théo;; \t',
|
|
'Sarcelle',
|
|
));
|
|
assert.equal(apercu.separateur, ';');
|
|
assert.deepEqual(apercu.refusees, [
|
|
{ ligne: 21, code: 'CHAMPS_EN_TROP', valeur: null, brut: ['Ombrelle', 'Iris', 'Club des Merles'] },
|
|
]);
|
|
// Un champ en trop vide ou blanc ne perd rien ; un enregistrement plus
|
|
// court laisse ses champs manquants à null.
|
|
assert.deepEqual(apercu.lignes.slice(-2), [
|
|
{ ligne: 22, champs: champs({ nom: 'Pervenche', prenom: 'Théo' }), brut: ['Pervenche', 'Théo', '', ' \t'] },
|
|
{ ligne: 23, champs: champs({ nom: 'Sarcelle' }), brut: ['Sarcelle'] },
|
|
]);
|
|
});
|
|
|
|
test('un seul motif par ligne : champs en trop, puis nom absent, puis exclu inconnu', () => {
|
|
const apercu = apercevoir(csv(
|
|
'nom;exclu',
|
|
...fenetre((patronyme) => `${patronyme};non`),
|
|
';peut-être;en trop',
|
|
';peut-être',
|
|
'Ombrelle;peut-être',
|
|
));
|
|
assert.deepEqual(
|
|
apercu.refusees.map(({ ligne, code }) => [ligne, code]),
|
|
[[21, 'CHAMPS_EN_TROP'], [22, 'NOM_ABSENT'], [23, 'EXCLU_INCONNU']],
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('apercevoir : « exclu », une liste fermée (§ 10.1)', () => {
|
|
test('VALEURS_EXCLU : oui, o, vrai, 1 et x valent oui ; non, n, faux, 0 et vide valent non', () => {
|
|
assert.deepEqual(
|
|
[...VALEURS_EXCLU],
|
|
[
|
|
['oui', true], ['o', true], ['vrai', true], ['1', true], ['x', true],
|
|
['non', false], ['n', false], ['faux', false], ['0', false], ['', false],
|
|
],
|
|
);
|
|
for (const [cle] of VALEURS_EXCLU) assert.equal(cleNormalisee(cle), cle);
|
|
});
|
|
|
|
test('Oui, X, 0 et vide sont admis ; « peut-être » refuse la ligne, sa valeur nommée', () => {
|
|
const apercu = apercevoir(csv(
|
|
'nom;exclu',
|
|
'Ardoise;Oui',
|
|
'Brindille;X',
|
|
'Châtaigne;0',
|
|
'Dune;',
|
|
'Écorce;peut-être',
|
|
'Fougère; \u{A0}',
|
|
));
|
|
assert.deepEqual(
|
|
apercu.lignes.map(({ ligne, champs: lus }) => [ligne, lus.exclu]),
|
|
[[2, true], [3, true], [4, false], [5, null], [7, null]],
|
|
);
|
|
assert.deepEqual(apercu.refusees, [
|
|
{ ligne: 6, code: 'EXCLU_INCONNU', valeur: 'peut-être', brut: ['Écorce', 'peut-être'] },
|
|
]);
|
|
});
|
|
|
|
test('chaque valeur admise, sans égard à la casse, aux accents ni aux blancs de bord', () => {
|
|
const cas = [
|
|
['OUI', true], [' o ', true], ['Vrai', true], ['1', true], ['x', true], ['Ouï', true],
|
|
['NON', false], ['N', false], ['Faux', false], ['0', false], ['\tnon\u{A0}', false],
|
|
];
|
|
const apercu = apercevoir(csv('nom;exclu', ...cas.map(([valeur], rang) => `${PATRONYMES[rang]};${valeur}`)));
|
|
assert.deepEqual(apercu.refusees, []);
|
|
assert.deepEqual(apercu.lignes.map(({ champs: lus }) => lus.exclu), cas.map(([, exclu]) => exclu));
|
|
});
|
|
|
|
test("toute autre valeur refuse la ligne, sa valeur nommée sans ses blancs de bord", () => {
|
|
const valeurs = ['yes', 'true', 'oui.', '2', 'nn', '-', 'exclu', 'o u i', ' non merci '];
|
|
const apercu = apercevoir(csv('nom;exclu', ...valeurs.map((valeur, rang) => `${PATRONYMES[rang]};${valeur}`)));
|
|
assert.deepEqual(
|
|
apercu.refusees.map(({ ligne, code, valeur }) => [ligne, code, valeur]),
|
|
valeurs.map((valeur, rang) => [rang + 2, 'EXCLU_INCONNU', valeur.trim()]),
|
|
);
|
|
assert.equal(apercu.refusGlobal, 'AUCUNE_LIGNE_VALIDE');
|
|
});
|
|
});
|
|
|
|
describe("apercevoir : les orthographes d'une appartenance (§ 10.1, § 14.10)", () => {
|
|
test("le rapport nomme chaque fusion : trois orthographes, leurs lignes, l'existante affichée", () => {
|
|
const participants = [existant(1, 'Ombrelle', 'Iris', 'Coopérative Bleue')];
|
|
const apercu = apercevoir(
|
|
csv(
|
|
'nom;prenom;appartenance',
|
|
'Pervenche;Théo;cooperative bleue',
|
|
'Sarcelle;Ondine;COOPÉRATIVE BLEUE',
|
|
'Bruyère;Anouk;cooperative bleue',
|
|
),
|
|
{ participants },
|
|
);
|
|
assert.deepEqual(apercu.fusions, [
|
|
{
|
|
affichee: 'Coopérative Bleue',
|
|
orthographes: [
|
|
{ texte: 'Coopérative Bleue', lignes: [] },
|
|
{ texte: 'cooperative bleue', lignes: [2, 4] },
|
|
{ texte: 'COOPÉRATIVE BLEUE', lignes: [3] },
|
|
],
|
|
},
|
|
]);
|
|
assert.deepEqual(
|
|
apercu.lignes.map(({ champs: lus }) => lus.appartenance),
|
|
['Coopérative Bleue', 'Coopérative Bleue', 'Coopérative Bleue'],
|
|
);
|
|
});
|
|
|
|
test("sans participant existant, la première orthographe du fichier s'affiche", () => {
|
|
const apercu = apercevoir(csv(
|
|
'nom;appartenance',
|
|
'Ombrelle;club des merles',
|
|
'Pervenche;Club des Merles',
|
|
'Sarcelle; CLUB DES MERLES ',
|
|
));
|
|
assert.deepEqual(apercu.fusions, [
|
|
{
|
|
affichee: 'club des merles',
|
|
orthographes: [
|
|
{ texte: 'club des merles', lignes: [2] },
|
|
{ texte: 'Club des Merles', lignes: [3] },
|
|
{ texte: 'CLUB DES MERLES', lignes: [4] },
|
|
],
|
|
},
|
|
]);
|
|
assert.deepEqual(apercu.lignes.map(({ champs: lus }) => lus.appartenance), [
|
|
'club des merles', 'club des merles', 'club des merles',
|
|
]);
|
|
});
|
|
|
|
test("une orthographe répétée, ou la même en NFC et en NFD, n'est pas une fusion", () => {
|
|
const apercu = apercevoir(csv(
|
|
'nom;appartenance',
|
|
'Ombrelle;Club des Merles',
|
|
'Pervenche;Club des Merles',
|
|
'Sarcelle;Chorale d\u{2019}\u{E9}t\u{E9}',
|
|
'Bruyère;Chorale d\u{2019}e\u{301}te\u{301}',
|
|
));
|
|
assert.deepEqual(apercu.fusions, []);
|
|
assert.deepEqual(apercu.lignes.map(({ champs: lus }) => lus.appartenance), [
|
|
'Club des Merles', 'Club des Merles', 'Chorale d\u{2019}\u{E9}t\u{E9}', 'Chorale d\u{2019}\u{E9}t\u{E9}',
|
|
]);
|
|
});
|
|
|
|
test("une appartenance vide vaut absence, jamais une appartenance nommée « »", () => {
|
|
const participants = [existant(1, 'Houle', null, null), existant(2, 'Givre', null, '')];
|
|
const apercu = apercevoir(csv('nom;appartenance', 'Ombrelle;', 'Pervenche; ', 'Sarcelle;\u{A0}'), { participants });
|
|
assert.deepEqual(apercu.lignes.map(({ champs: lus }) => lus.appartenance), [null, null, null]);
|
|
assert.deepEqual(apercu.fusions, []);
|
|
});
|
|
|
|
test("une appartenance blanche ou faite de marques combinantes seules vaut absence, dans le fichier comme chez un existant", () => {
|
|
// Sa clé normalisée est vide, celle que la clé de doublon donne à une
|
|
// appartenance absente : elle n'est l'orthographe d'aucun groupe.
|
|
const participants = [existant(1, 'Houle', null, ' '), existant(2, 'Givre', null, '\u{301}')];
|
|
const apercu = apercevoir(csv('nom;appartenance', 'Ombrelle;\u{301}', 'Pervenche;\u{301}\u{20DD}', 'Givre;'), {
|
|
participants,
|
|
});
|
|
assert.deepEqual(apercu.lignes.map(({ champs: lus }) => lus.appartenance), [null, null, null]);
|
|
assert.deepEqual(apercu.fusions, []);
|
|
assert.deepEqual(apercu.doublons, [{ cle: '["givre","",""]', lignes: [4], existants: [2] }]);
|
|
});
|
|
|
|
test("un trait d'union, un trait de soulignement ou une espace de moins distinguent deux appartenances", () => {
|
|
// La clé d'en-tête les ignore, la clé normalisée non : la réconciliation
|
|
// compare par la seconde, comme la clé de doublon.
|
|
const orthographes = ['Club des Merles', 'Club-des-Merles', 'Club_des_Merles', 'Clubdes Merles', 'Val-Brume', 'Val Brume'];
|
|
const apercu = apercevoir(csv('nom;appartenance', ...orthographes.map((orthographe, rang) => `${PATRONYMES[rang]};${orthographe}`)));
|
|
assert.deepEqual(apercu.fusions, []);
|
|
assert.deepEqual(apercu.lignes.map(({ champs: lus }) => lus.appartenance), orthographes);
|
|
});
|
|
|
|
test("les orthographes existantes d'abord, par identifiant croissant, puis celles du fichier", () => {
|
|
const participants = [existant(7, 'Givre', null, 'cooperative bleue'), existant(3, 'Houle', null, 'Coopérative Bleue')];
|
|
const apercu = apercevoir(csv('nom;appartenance', 'Ombrelle;COOPÉRATIVE BLEUE'), { participants });
|
|
assert.deepEqual(apercu.fusions, [
|
|
{
|
|
affichee: 'Coopérative Bleue',
|
|
orthographes: [
|
|
{ texte: 'Coopérative Bleue', lignes: [] },
|
|
{ texte: 'cooperative bleue', lignes: [] },
|
|
{ texte: 'COOPÉRATIVE BLEUE', lignes: [2] },
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("l'orthographe existante s'affiche telle qu'elle est enregistrée", () => {
|
|
const participants = [existant(4, 'Houle', null, 'Club des Merles ')];
|
|
const apercu = apercevoir(csv('nom;appartenance', 'Ombrelle;club des merles'), { participants });
|
|
assert.equal(apercu.lignes[0].champs.appartenance, 'Club des Merles ');
|
|
assert.equal(apercu.fusions[0].affichee, 'Club des Merles ');
|
|
});
|
|
|
|
test("un groupe sans ligne du fichier n'est pas une fusion de l'import", () => {
|
|
const participants = [existant(3, 'Houle', null, 'Coopérative Bleue'), existant(7, 'Givre', null, 'cooperative bleue')];
|
|
const apercu = apercevoir(csv('nom;appartenance', 'Ombrelle;Club des Merles'), { participants });
|
|
assert.deepEqual(apercu.fusions, []);
|
|
});
|
|
|
|
test("la ligne qui reprend l'orthographe existante n'ajoute pas d'orthographe", () => {
|
|
const participants = [existant(1, 'Houle', null, 'Club des Merles')];
|
|
const apercu = apercevoir(csv('nom;appartenance', 'Ombrelle;Club des Merles'), { participants });
|
|
assert.deepEqual(apercu.fusions, []);
|
|
assert.equal(apercu.lignes[0].champs.appartenance, 'Club des Merles');
|
|
});
|
|
|
|
test('deux groupes, rangés par leur première ligne du fichier', () => {
|
|
const participants = [existant(1, 'Houle', null, 'chorale du givre')];
|
|
const apercu = apercevoir(
|
|
csv(
|
|
'nom;appartenance',
|
|
'Ombrelle;Club des Merles',
|
|
'Pervenche;Chorale du Givre',
|
|
'Sarcelle;club des merles',
|
|
'Bruyère;CHORALE DU GIVRE',
|
|
),
|
|
{ participants },
|
|
);
|
|
assert.deepEqual(apercu.fusions, [
|
|
{
|
|
affichee: 'Club des Merles',
|
|
orthographes: [
|
|
{ texte: 'Club des Merles', lignes: [2] },
|
|
{ texte: 'club des merles', lignes: [4] },
|
|
],
|
|
},
|
|
{
|
|
affichee: 'chorale du givre',
|
|
orthographes: [
|
|
{ texte: 'chorale du givre', lignes: [] },
|
|
{ texte: 'Chorale du Givre', lignes: [3] },
|
|
{ texte: 'CHORALE DU GIVRE', lignes: [5] },
|
|
],
|
|
},
|
|
]);
|
|
assert.deepEqual(apercu.lignes.map(({ champs: lus }) => lus.appartenance), [
|
|
'Club des Merles', 'chorale du givre', 'Club des Merles', 'chorale du givre',
|
|
]);
|
|
});
|
|
|
|
test("une ligne refusée ne prend part à aucun groupe", () => {
|
|
const apercu = apercevoir(csv(
|
|
'nom;appartenance;exclu',
|
|
'Ombrelle;CLUB DES MERLES;peut-être',
|
|
'Pervenche;Club des Merles;',
|
|
'Sarcelle;club des merles;',
|
|
));
|
|
assert.deepEqual(apercu.fusions, [
|
|
{
|
|
affichee: 'Club des Merles',
|
|
orthographes: [
|
|
{ texte: 'Club des Merles', lignes: [3] },
|
|
{ texte: 'club des merles', lignes: [4] },
|
|
],
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('apercevoir : les doublons, signalés et jamais fusionnés (§ 10.1)', () => {
|
|
test('deux lignes du même triplet normalisé : un doublon, et deux lignes restent deux', () => {
|
|
const apercu = apercevoir(csv(
|
|
'nom;prenom;appartenance',
|
|
'Ombrelle;Iris;Club des Merles',
|
|
'OMBRELLE; iris ;club des merles',
|
|
'Ombrelle;Iris;Chorale du Givre',
|
|
));
|
|
assert.deepEqual(apercu.lignes.map(({ ligne }) => ligne), [2, 3, 4]);
|
|
assert.deepEqual(apercu.doublons, [
|
|
{ cle: '["ombrelle","iris","club des merles"]', lignes: [2, 3], existants: [] },
|
|
]);
|
|
});
|
|
|
|
test("une ligne du triplet d'un participant existant : ses identifiants, croissants", () => {
|
|
const participants = [existant(9, 'Ombrelle', 'Iris', 'Club des Merles'), existant(4, 'ombrelle', 'IRIS', 'club des merles')];
|
|
const apercu = apercevoir(csv('nom;prenom;appartenance', 'Ombrelle;Iris;Club des Merles'), { participants });
|
|
assert.deepEqual(apercu.doublons, [
|
|
{ cle: '["ombrelle","iris","club des merles"]', lignes: [2], existants: [4, 9] },
|
|
]);
|
|
assert.equal(apercu.lignes.length, 1);
|
|
});
|
|
|
|
test("un triplet qui diffère par le prénom ou par l'appartenance n'est pas un doublon", () => {
|
|
const participants = [existant(1, 'Ombrelle', 'Ondine', null)];
|
|
const apercu = apercevoir(
|
|
csv(
|
|
'nom;prenom;appartenance',
|
|
'Ombrelle;Iris;Club des Merles',
|
|
'Ombrelle;Iris;Chorale du Givre',
|
|
'Ombrelle;;Club des Merles',
|
|
'Ombrelle;Ondine;Club des Merles',
|
|
),
|
|
{ participants },
|
|
);
|
|
assert.deepEqual(apercu.doublons, []);
|
|
});
|
|
|
|
test("un nom seul : le prénom et l'appartenance absents comptent comme vides", () => {
|
|
const participants = [existant(2, 'Givre', '', null)];
|
|
const apercu = apercevoir(csv('nom', 'Givre', 'givre ', 'Houle'), { participants });
|
|
assert.equal(apercu.separateur, null);
|
|
assert.deepEqual(apercu.doublons, [{ cle: '["givre","",""]', lignes: [2, 3], existants: [2] }]);
|
|
});
|
|
|
|
test('les doublons, rangés par leur première ligne du fichier, non par leur premier existant', () => {
|
|
const participants = [existant(1, 'Givre', 'Théo', null)];
|
|
const apercu = apercevoir(csv('nom;prenom', 'Houle;Iris', 'Givre;Théo', 'Givre;The\u{301}o', 'Houle;Iris'), {
|
|
participants,
|
|
});
|
|
assert.deepEqual(apercu.doublons, [
|
|
{ cle: '["houle","iris",""]', lignes: [2, 5], existants: [] },
|
|
{ cle: '["givre","theo",""]', lignes: [3, 4], existants: [1] },
|
|
]);
|
|
});
|
|
|
|
test("un triplet propre aux participants existants n'est pas un doublon de l'import ; une ligne refusée non plus", () => {
|
|
const participants = [existant(1, 'Givre', null, null), existant(2, 'Givre', null, null)];
|
|
const apercu = apercevoir(csv('nom;exclu', 'Houle;non', 'Houle;peut-être'), { participants });
|
|
assert.deepEqual(apercu.doublons, []);
|
|
});
|
|
});
|
|
|
|
describe("apercevoir : le mode de l'import (§ 10.1)", () => {
|
|
// Deux participants existants, dont l'appartenance de l'un s'écrit
|
|
// autrement dans le fichier.
|
|
const participants = [existant(1, 'Ombrelle', 'Iris', 'Club des Merles'), existant(2, 'Lacasse', 'Ondine', null)];
|
|
const texte = csv('nom;prenom;appartenance', 'Ombrelle;Iris;club des merles', 'Lacasse;Ondine;', 'Lacasse;Ondine;');
|
|
|
|
test("ajouter, par défaut, et mettre à jour lisent le fichier contre les existants : doublons et fusions les comptent", () => {
|
|
for (const mode of [undefined, 'ajouter', 'mettreAJour']) {
|
|
const apercu = apercevoir(texte, { participants, mode });
|
|
assert.equal(apercu.mode, mode ?? 'ajouter');
|
|
assert.deepEqual(apercu.doublons, [
|
|
{ cle: '["ombrelle","iris","club des merles"]', lignes: [2], existants: [1] },
|
|
{ cle: '["lacasse","ondine",""]', lignes: [3, 4], existants: [2] },
|
|
], String(mode));
|
|
assert.deepEqual(apercu.fusions, [
|
|
{ affichee: 'Club des Merles', orthographes: [{ texte: 'Club des Merles', lignes: [] }, { texte: 'club des merles', lignes: [2] }] },
|
|
], String(mode));
|
|
assert.equal(apercu.lignes[0].champs.appartenance, 'Club des Merles', String(mode));
|
|
}
|
|
});
|
|
|
|
test("remplacer retire chaque existant : aucune ligne n'en est le doublon, aucune orthographe existante ne s'affiche ni ne fond", () => {
|
|
const apercu = apercevoir(texte, { participants, mode: 'remplacer' });
|
|
assert.equal(apercu.mode, 'remplacer');
|
|
assert.deepEqual(apercu.doublons, [{ cle: '["lacasse","ondine",""]', lignes: [3, 4], existants: [] }]);
|
|
assert.deepEqual(apercu.fusions, []);
|
|
assert.equal(apercu.lignes[0].champs.appartenance, 'club des merles');
|
|
assert.deepEqual(apercu, apercevoir(texte, { participants: [], mode: 'remplacer' }));
|
|
});
|
|
|
|
test("mettre à jour nomme chaque ligne qu'il ajoute à côté d'un existant de même nom, que le prénom ou l'appartenance écarte des deux côtés", () => {
|
|
// Iris passe du Club des Merles à la Société Alpha : la ligne ne désigne
|
|
// personne, et l'import ajouterait une seconde Iris Ombrelle.
|
|
const existants = [
|
|
existant(1, 'Ombrelle', 'Iris', 'Club des Merles'),
|
|
existant(2, 'Ombrelle', 'Théo', null),
|
|
existant(3, 'Lacasse', 'Ondine', null),
|
|
];
|
|
const changeDeClub = csv(
|
|
'nom;prenom;appartenance',
|
|
// Iris écarte 2, la Société Alpha écarte 1 : un homonyme de 1 et 2.
|
|
'OMBRELLE;Iris;Société Alpha',
|
|
// Désigne 3, qui n'a pas d'appartenance : aucun homonyme.
|
|
'Lacasse;Ondine;Club des Merles',
|
|
// Aucun existant de ce nom : un ajout, pas un homonyme.
|
|
'Pervenche;Théo;',
|
|
// Sans prénom ni appartenance : les deux Ombrelle, un refus de
|
|
// l'import, pas un ajout.
|
|
'Ombrelle;;',
|
|
// Désigne 2, sans prénom ni appartenance de part et d'autre.
|
|
'Ombrelle;Théo;Chorale du Givre',
|
|
);
|
|
assert.deepEqual(apercevoir(changeDeClub, { participants: existants, mode: 'mettreAJour' }).homonymes, [
|
|
{ ligne: 2, participants: [1, 2] },
|
|
]);
|
|
// Ajouter et remplacer n'en nomment aucun : ajouter crée chaque ligne,
|
|
// remplacer retire chaque existant.
|
|
for (const mode of ['ajouter', 'remplacer']) {
|
|
assert.deepEqual(apercevoir(changeDeClub, { participants: existants, mode }).homonymes, [], mode);
|
|
}
|
|
// Les lignes qui désignent un existant ne retirent rien aux candidats
|
|
// des suivantes ; les identifiants sont croissants, quel que soit
|
|
// l'ordre des existants.
|
|
const inverse = csv('nom;prenom', 'Ombrelle;Théo', 'Ombrelle;Iris', 'Ombrelle;Anouk');
|
|
assert.deepEqual(
|
|
apercevoir(inverse, { participants: [existants[1], existants[0]], mode: 'mettreAJour' }).homonymes,
|
|
[{ ligne: 4, participants: [1, 2] }],
|
|
);
|
|
});
|
|
|
|
test('un aperçu refusé porte son mode', () => {
|
|
assert.equal(apercevoir('', { mode: 'remplacer' }).mode, 'remplacer');
|
|
assert.equal(apercevoir(csv('prenom', 'Iris'), { mode: 'mettreAJour' }).mode, 'mettreAJour');
|
|
});
|
|
|
|
test('un mode inconnu : TypeError, qui nomme les trois modes', () => {
|
|
assert.throws(() => apercevoir(texte, { mode: 'fusionner' }), {
|
|
name: 'TypeError',
|
|
message: 'mode : ajouter, mettreAJour, remplacer attendu, reçu fusionner',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('apercevoir : la réconciliation et la clé de doublon, une seule forme normalisée (§ 10.1)', () => {
|
|
test('deux personnes du même nom et du même prénom forment un doublon si et seulement si leurs appartenances se fondent', () => {
|
|
// Chaque orthographe éprouve une dimension de la clé : casse, accent
|
|
// composé ou décomposé, blancs, trait d'union, trait de soulignement,
|
|
// absence. L'appartenance affichée d'une ligne désigne son groupe ; une
|
|
// appartenance absente n'en a aucun, et la clé de doublon la compte vide.
|
|
const orthographes = [
|
|
'Club des Merles', 'CLUB DES MERLES', ' club des\u{A0}merles ', 'Clu\u{301}b des Merles', 'Club-des-Merles',
|
|
'Club_des_Merles', 'Clubdes Merles', 'Val-Brume', 'Val Brume', 'VAL BRUME', '', ' ', '\u{301}',
|
|
];
|
|
const participants = [existant(1, 'Ombrelle', 'Iris', 'club des merles')];
|
|
const apercu = apercevoir(
|
|
csv('nom;prenom;appartenance', ...orthographes.map((orthographe) => `Ombrelle;Iris;${orthographe}`)),
|
|
{ participants },
|
|
);
|
|
// Les groupes attendus : la propriété ne se vérifie pas sur du vide.
|
|
assert.deepEqual(apercu.lignes.map(({ champs: lus }) => lus.appartenance), [
|
|
'club des merles', 'club des merles', 'club des merles', 'club des merles', 'Club-des-Merles',
|
|
'Club_des_Merles', 'Clubdes Merles', 'Val-Brume', 'Val Brume', 'Val Brume', null, null, null,
|
|
]);
|
|
const doublonDe = new Map();
|
|
apercu.doublons.forEach(({ lignes }, rang) => {
|
|
for (const ligne of lignes) doublonDe.set(ligne, rang);
|
|
});
|
|
for (const a of apercu.lignes) {
|
|
const avecExistant = apercu.doublons.some(({ lignes, existants }) => lignes.includes(a.ligne) && existants.includes(1));
|
|
assert.equal(avecExistant, a.champs.appartenance === 'club des merles', `ligne ${a.ligne} et l'existant`);
|
|
for (const b of apercu.lignes) {
|
|
if (b.ligne <= a.ligne) continue;
|
|
const memeDoublon = doublonDe.has(a.ligne) && doublonDe.get(a.ligne) === doublonDe.get(b.ligne);
|
|
assert.equal(memeDoublon, a.champs.appartenance === b.champs.appartenance, `lignes ${a.ligne} et ${b.ligne}`);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('apercevoir : les refus globaux (§ 10.1)', () => {
|
|
test("aucun en-tête reconnu : AUCUN_ENTETE, les en-têtes et l'association rendus", () => {
|
|
assert.deepEqual(
|
|
apercevoir(csv('personne;groupe', 'Ombrelle;Club des Merles')),
|
|
refusGlobal('AUCUN_ENTETE', {
|
|
separateur: ';',
|
|
entetes: ['personne', 'groupe'],
|
|
association: { colonnes: {}, ambigus: [], nonReconnues: [0, 1] },
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('un texte vide ou blanc : AUCUN_ENTETE', () => {
|
|
for (const texte of ['', '\r\n', ' \t\r\n\r\n\u{A0}']) {
|
|
assert.deepEqual(apercevoir(texte), refusGlobal('AUCUN_ENTETE'), JSON.stringify(texte));
|
|
}
|
|
});
|
|
|
|
test('aucune colonne associée à nom : NOM_NON_ASSOCIE', () => {
|
|
assert.deepEqual(
|
|
apercevoir(csv('prenom;appartenance', 'Iris;Club des Merles')),
|
|
refusGlobal('NOM_NON_ASSOCIE', {
|
|
separateur: ';',
|
|
entetes: ['prenom', 'appartenance'],
|
|
association: { colonnes: { prenom: 0, appartenance: 1 }, ambigus: [], nonReconnues: [] },
|
|
}),
|
|
);
|
|
});
|
|
|
|
test('deux colonnes nom : NOM_NON_ASSOCIE, tant que le choix ne tranche pas', () => {
|
|
const texte = csv('Nom;NOM;prenom', 'Ombrelle;Givre;Iris');
|
|
assert.deepEqual(
|
|
apercevoir(texte),
|
|
refusGlobal('NOM_NON_ASSOCIE', {
|
|
separateur: ';',
|
|
entetes: ['Nom', 'NOM', 'prenom'],
|
|
association: { colonnes: { prenom: 2 }, ambigus: [{ champ: 'nom', rangs: [0, 1] }], nonReconnues: [] },
|
|
}),
|
|
);
|
|
const tranche = apercevoir(texte, { choix: { nom: 1 } });
|
|
assert.equal(tranche.refusGlobal, null);
|
|
assert.deepEqual(tranche.lignes, [
|
|
{ ligne: 2, champs: champs({ nom: 'Givre', prenom: 'Iris' }), brut: ['Ombrelle', 'Givre', 'Iris'] },
|
|
]);
|
|
});
|
|
|
|
test('zéro ligne valide : AUCUNE_LIGNE_VALIDE, les lignes refusées rendues', () => {
|
|
assert.deepEqual(apercevoir(csv('nom;exclu', ';oui', 'Ardoise;peut-être')), {
|
|
...refusGlobal('AUCUNE_LIGNE_VALIDE', {
|
|
separateur: ';',
|
|
entetes: ['nom', 'exclu'],
|
|
association: { colonnes: { nom: 0, exclu: 1 }, ambigus: [], nonReconnues: [] },
|
|
}),
|
|
refusees: [
|
|
{ ligne: 2, code: 'NOM_ABSENT', valeur: null, brut: ['', 'oui'] },
|
|
{ ligne: 3, code: 'EXCLU_INCONNU', valeur: 'peut-être', brut: ['Ardoise', 'peut-être'] },
|
|
],
|
|
});
|
|
});
|
|
|
|
test("l'en-tête seul, ou suivi de lignes vides : AUCUNE_LIGNE_VALIDE", () => {
|
|
for (const texte of [csv('nom;prenom'), 'nom;prenom', csv('nom'), csv('nom;prenom', '', ';', ' ; ')]) {
|
|
const apercu = apercevoir(texte);
|
|
assert.equal(apercu.refusGlobal, 'AUCUNE_LIGNE_VALIDE', JSON.stringify(texte));
|
|
assert.deepEqual(apercu.refusees, []);
|
|
}
|
|
});
|
|
|
|
test("un guillemet ouvert jusqu'à la fin : GUILLEMET_OUVERT, au rang où il s'ouvre", () => {
|
|
// Au-delà de la fenêtre du séparateur : sans le refus, Pervenche entrerait
|
|
// dans les notes d'Ombrelle, deux personnes fondues en une.
|
|
const texte = csv(
|
|
'nom;notes',
|
|
...fenetre((patronyme) => `${patronyme};x`),
|
|
'Ombrelle;"arrive tard',
|
|
'Pervenche;rien à signaler',
|
|
);
|
|
assert.deepEqual(
|
|
apercevoir(texte),
|
|
refusGlobal(
|
|
'GUILLEMET_OUVERT',
|
|
{
|
|
separateur: ';',
|
|
entetes: ['nom', 'notes'],
|
|
association: { colonnes: { nom: 0, notes: 1 }, ambigus: [], nonReconnues: [] },
|
|
},
|
|
21,
|
|
),
|
|
);
|
|
});
|
|
|
|
test("un guillemet ouvert l'emporte sur un en-tête inconnu : le texte est faux avant d'être mal nommé", () => {
|
|
for (const rang of [2, 21]) {
|
|
const texte = csv('personne;groupe', ...fenetre((patronyme) => `${patronyme};x`, rang - 2), 'Ombrelle;"arrive tard');
|
|
assert.deepEqual(
|
|
apercevoir(texte),
|
|
refusGlobal(
|
|
'GUILLEMET_OUVERT',
|
|
{
|
|
separateur: ';',
|
|
entetes: ['personne', 'groupe'],
|
|
association: { colonnes: {}, ambigus: [], nonReconnues: [0, 1] },
|
|
},
|
|
rang,
|
|
),
|
|
String(rang),
|
|
);
|
|
}
|
|
});
|
|
|
|
test("un guillemet ouvert dans un fichier à une colonne : GUILLEMET_OUVERT", () => {
|
|
assert.deepEqual(
|
|
apercevoir(csv('nom', 'Ombrelle', '"Pervenche', 'Sarcelle')),
|
|
refusGlobal(
|
|
'GUILLEMET_OUVERT',
|
|
{ separateur: null, entetes: ['nom'], association: { colonnes: { nom: 0 }, ambigus: [], nonReconnues: [] } },
|
|
3,
|
|
),
|
|
);
|
|
});
|
|
|
|
test("un guillemet ouvert dans la fenêtre du séparateur : GUILLEMET_OUVERT, au rang où il s'ouvre", () => {
|
|
// choisirSeparateur écarte chaque candidat, l'un pour son guillemet
|
|
// ouvert, les autres pour leur champ unique. Sans le refus, Pervenche
|
|
// entrerait dans les notes d'Ombrelle.
|
|
for (const separateur of [';', ',', '\t']) {
|
|
const ligne = (...valeurs) => valeurs.join(separateur);
|
|
const texte = csv(ligne('nom', 'prenom', 'notes'), ligne('Ombrelle', 'Iris', '"arrive tard'), ligne('Pervenche', 'Théo', ''));
|
|
assert.deepEqual(
|
|
apercevoir(texte),
|
|
refusGlobal(
|
|
'GUILLEMET_OUVERT',
|
|
{
|
|
separateur,
|
|
entetes: ['nom', 'prenom', 'notes'],
|
|
association: { colonnes: { nom: 0, prenom: 1, notes: 2 }, ambigus: [], nonReconnues: [] },
|
|
},
|
|
2,
|
|
),
|
|
JSON.stringify(texte),
|
|
);
|
|
}
|
|
});
|
|
|
|
test("le même refus de part et d'autre de la fenêtre : GUILLEMET_OUVERT au rang 2, au rang 20, au rang 21", () => {
|
|
for (const rang of [2, 20, 21]) {
|
|
const texte = csv('nom;notes', ...fenetre((patronyme) => `${patronyme};x`, rang - 2), 'Ombrelle;"arrive tard');
|
|
assert.deepEqual(
|
|
apercevoir(texte),
|
|
refusGlobal(
|
|
'GUILLEMET_OUVERT',
|
|
{
|
|
separateur: ';',
|
|
entetes: ['nom', 'notes'],
|
|
association: { colonnes: { nom: 0, notes: 1 }, ambigus: [], nonReconnues: [] },
|
|
},
|
|
rang,
|
|
),
|
|
String(rang),
|
|
);
|
|
}
|
|
});
|
|
|
|
test("un guillemet égaré au-delà de la fenêtre, refermé plus loin sur une lettre : GUILLEMET_OUVERT au rang où il s'ouvre, aucune personne fondue dans une autre", () => {
|
|
// Vingt-cinq enregistrements : la note du rang 23 ouvre un guillemet que
|
|
// rien ne referme à sa place ; celui de « "Bob" », au rang 25, le
|
|
// referme, suivi d'une lettre. Les rangs 24 et 25 tiendraient sinon dans
|
|
// la note du rang 23, et l'aperçu compterait 22 lignes sans un refus.
|
|
const lignes = Array.from({ length: 21 }, (_, i) => `Nom${i + 1};Pre${i + 1};`);
|
|
const texte = csv('nom;prenom;notes', ...lignes, 'Nom22;Pre22;"VIP au fond', 'Nom23;Pre23;', 'Nom24;Pre24;surnommé "Bob" par tous');
|
|
assert.deepEqual(
|
|
apercevoir(texte),
|
|
refusGlobal(
|
|
'GUILLEMET_OUVERT',
|
|
{
|
|
separateur: ';',
|
|
entetes: ['nom', 'prenom', 'notes'],
|
|
association: { colonnes: { nom: 0, prenom: 1, notes: 2 }, ambigus: [], nonReconnues: [] },
|
|
},
|
|
23,
|
|
),
|
|
);
|
|
// Le même texte dont la note se referme à sa place s'importe en entier.
|
|
const referme = texte.replace('"VIP au fond', '"VIP au fond"');
|
|
assert.equal(apercevoir(referme).refusGlobal, null);
|
|
assert.equal(apercevoir(referme).lignes.length, 24);
|
|
});
|
|
|
|
test("un guillemet égaré refermé plus loin par un pouce, au-delà de la fenêtre : la cellule fondue est nommée, à son rang", () => {
|
|
// Le guillemet de « écran 27" » referme la note ouverte au rang 23 :
|
|
// les rangs 24 et 25 tiennent dans cette note, que rien ne refuse.
|
|
// L'aperçu nomme la cellule, qui s'étend sur plusieurs lignes du texte
|
|
// et porte le séparateur.
|
|
const lignes = Array.from({ length: 21 }, (_, i) => `Nom${i + 1};Pre${i + 1};`);
|
|
const texte = csv('nom;prenom;notes', ...lignes, 'Nom22;Pre22;"VIP au fond', 'Nom23;Pre23;', 'Nom24;Pre24;écran 27"', 'Nom25;Pre25;');
|
|
const apercu = apercevoir(texte);
|
|
assert.equal(apercu.refusGlobal, null);
|
|
assert.equal(apercu.lignes.length, 23);
|
|
assert.equal(apercu.lignes[21].champs.notes, 'VIP au fond\r\nNom23;Pre23;\r\nNom24;Pre24;écran 27');
|
|
assert.deepEqual(apercu.multilignes, [{ ligne: 23, rangs: [2] }]);
|
|
});
|
|
|
|
test("une cellule nommée : sur plusieurs lignes du texte et portant le séparateur, dans la fenêtre comme au-delà ; un nom sur plusieurs lignes d'un fichier à une colonne", () => {
|
|
// Dans la fenêtre, le guillemet égaré du rang 3 se referme au rang 4 :
|
|
// l'enregistrement fondu a la largeur de l'en-tête, et le séparateur
|
|
// est retenu.
|
|
const fondu = csv('nom;prenom;notes', 'Ombrelle;Iris;', 'Lacasse;Ondine;"à l\'entrée', 'Pervenche;Théo;écran 27"', 'Givre;Silas;');
|
|
assert.deepEqual(apercevoir(fondu).multilignes, [{ ligne: 3, rangs: [2] }]);
|
|
// Une note sur plusieurs lignes sans le séparateur, ou le séparateur
|
|
// sur une seule ligne, n'est pas nommée ; une cellule qui porte les
|
|
// deux l'est, une fois par cellule, quel que soit le séparateur.
|
|
const notes = csv(
|
|
'nom,prenom,appartenance,notes',
|
|
'Ombrelle,Iris,"Club, nord","arrive tôt\nrepart tard"',
|
|
'Lacasse,Ondine,"Chorale\ndu Givre","noix, arachides\r\nsans gluten"',
|
|
'Pervenche,Théo,,"au fond\rprès de la scène, côté jardin"',
|
|
);
|
|
assert.deepEqual(apercevoir(notes).multilignes, [{ ligne: 3, rangs: [3] }, { ligne: 4, rangs: [3] }]);
|
|
// Fichier à une colonne : un nom ne s'étend jamais sur deux lignes, et
|
|
// aucun séparateur n'est à chercher.
|
|
assert.deepEqual(apercevoir(csv('nom', 'Ombrelle', '"Lacasse', 'Pervenche"', 'Givre')).multilignes, [
|
|
{ ligne: 3, rangs: [0] },
|
|
]);
|
|
// Une ligne refusée revient dans le CSV des refus : elle n'est pas
|
|
// nommée ici.
|
|
const refusee = csv('nom;notes;exclu', 'Ombrelle;"a\nb;c";peut-être', 'Lacasse;;');
|
|
assert.deepEqual(apercevoir(refusee).multilignes, []);
|
|
});
|
|
|
|
test("sous GUILLEMET_OUVERT, l'association suit le choix, dans la fenêtre comme au-delà", () => {
|
|
for (const rang of [2, 21]) {
|
|
const texte = csv('nom;organisation;entreprise', ...fenetre((patronyme) => `${patronyme};x;y`, rang - 2), 'Ombrelle;"x');
|
|
const apercu = apercevoir(texte, { choix: { appartenance: 2 } });
|
|
assert.equal(apercu.refusGlobal, 'GUILLEMET_OUVERT', String(rang));
|
|
assert.deepEqual(apercu.association, { colonnes: { nom: 0, appartenance: 2 }, ambigus: [], nonReconnues: [] }, String(rang));
|
|
}
|
|
});
|
|
|
|
test("le refus suit la lecture dont l'en-tête porte le plus d'en-têtes reconnus, puis l'ordre « ; », « , », tabulation", () => {
|
|
// Collé d'un tableur, un en-tête qui porte un « ; » compte deux champs
|
|
// sous « ; » aussi, aucun reconnu : lu ainsi, le texte serait refusé au
|
|
// rang 2, là où le défaut est le guillemet du rang 3.
|
|
assert.deepEqual(
|
|
apercevoir(csv('nom\tprenom\tnotes; remarques', 'Ombrelle\tIris\tx', 'Pervenche\tThéo\t"arrive tard')),
|
|
refusGlobal(
|
|
'GUILLEMET_OUVERT',
|
|
{
|
|
separateur: '\t',
|
|
entetes: ['nom', 'prenom', 'notes; remarques'],
|
|
association: { colonnes: { nom: 0, prenom: 1 }, ambigus: [], nonReconnues: [2] },
|
|
},
|
|
3,
|
|
),
|
|
);
|
|
// À égalité, aucun en-tête reconnu, « ; » passe avant « , ».
|
|
assert.deepEqual(
|
|
apercevoir(csv('personne;groupe,section', 'Ombrelle;"arrive tard')),
|
|
refusGlobal(
|
|
'GUILLEMET_OUVERT',
|
|
{
|
|
separateur: ';',
|
|
entetes: ['personne', 'groupe,section'],
|
|
association: { colonnes: {}, ambigus: [], nonReconnues: [0, 1] },
|
|
},
|
|
2,
|
|
),
|
|
);
|
|
});
|
|
|
|
test("un enregistrement de la fenêtre qui n'a pas le nombre de champs de l'en-tête : SEPARATEUR_INTROUVABLE, à son rang", () => {
|
|
// Le § 10.1 veut le même nombre de champs partout dans les vingt premiers
|
|
// enregistrements : aucun séparateur ne convient, le texte entier est
|
|
// refusé, et le refus nomme la ligne à corriger. Au-delà, le même
|
|
// enregistrement n'est qu'une ligne refusée, ou complétée de null.
|
|
const cas = [
|
|
[csv('nom;prenom;notes', 'Ombrelle;Iris;arrive tard; vers 20 h', 'Pervenche;Théo;'), 2],
|
|
[csv('nom;prenom;appartenance', 'Ombrelle;Iris', 'Pervenche;Théo;Club des Merles'), 2],
|
|
[csv('nom;prenom;notes', 'Ombrelle;Iris;', 'Pervenche;Théo;arrive tard; vers 20 h', 'Sarcelle;Ondine;'), 3],
|
|
[csv('nom;prenom', 'Ombrelle'), 2],
|
|
// Un enregistrement vide ne compte pas, avant l'en-tête ni après.
|
|
[csv('', 'nom;prenom', '', ';;', 'Ombrelle;Iris', 'Pervenche'), 6],
|
|
// Le premier défaut dans l'ordre du texte : l'enregistrement court
|
|
// passe avant le guillemet ouvert qui le suit.
|
|
[csv('nom;prenom;notes', 'Ombrelle;Iris', 'Pervenche;Théo;"arrive tard'), 2],
|
|
// Le dernier enregistrement de la fenêtre.
|
|
[csv('nom;prenom', ...fenetre((patronyme) => `${patronyme};x`, 18), 'Ombrelle;Iris;Club des Merles'), 20],
|
|
];
|
|
for (const [texte, rang] of cas) {
|
|
assert.deepEqual(apercevoir(texte), refusGlobal('SEPARATEUR_INTROUVABLE', {}, rang), JSON.stringify(texte));
|
|
}
|
|
});
|
|
|
|
test("aucune lecture ne donne à l'en-tête deux champs : SEPARATEUR_INTROUVABLE, sans rang", () => {
|
|
// Une colonne unique d'en-tête inconnu, guillemet ouvert compris : aucune
|
|
// lecture à un champ ne désigne de séparateur ; un séparateur hors des
|
|
// trois candidats ; un guillemet ouvert dès l'en-tête, qui n'en laisse
|
|
// aucun terminé ; vingt lignes vides, qui laissent la fenêtre sans
|
|
// en-tête, et ce qui les suit sans effet sur le refus.
|
|
const vingtLignesVides = Array.from({ length: 20 }, () => '');
|
|
const textes = [
|
|
csv('personnes', 'Ombrelle'),
|
|
csv('personnes', '"Ombrelle'),
|
|
csv('nom|prenom', 'Ombrelle|"Iris'),
|
|
csv('nom;"prenom', 'Ombrelle;Iris'),
|
|
csv(...vingtLignesVides, 'nom;prenom', 'Ombrelle;Iris;Club des Merles'),
|
|
csv(...vingtLignesVides, 'nom;prenom', 'Ombrelle;"Iris'),
|
|
];
|
|
for (const texte of textes) {
|
|
assert.deepEqual(apercevoir(texte), refusGlobal('SEPARATEUR_INTROUVABLE'), JSON.stringify(texte));
|
|
}
|
|
});
|
|
|
|
test("des lignes vides insérées avant un guillemet ouvert ne changent pas le refus, en deçà de la fenêtre comme au-delà", () => {
|
|
// Une ligne vide ne compte ni pour le choix du séparateur ni pour
|
|
// l'aperçu : elle repousse d'un rang ce qui la suit. De zéro à quarante
|
|
// lignes vides, le guillemet passe des vingt enregistrements que lit
|
|
// choisirSeparateur, où il écarte le séparateur et où le refus nomme ce
|
|
// qui l'écarte, à l'au-delà, où le texte se lit sous le séparateur
|
|
// retenu. Les deux rendent le même refus : le guillemet à son rang, ou le
|
|
// défaut qui le précède au sien. Chaque cas éprouve une part de la règle
|
|
// du § 10.1 : les trois candidats, et eux seuls ; leur ordre ; le
|
|
// départage par les en-têtes reconnus ; un en-tête d'au moins deux
|
|
// champs ; le même nombre de champs partout.
|
|
const lu = (separateur, entetes, colonnes, nonReconnues = []) => ({
|
|
separateur,
|
|
entetes,
|
|
association: { colonnes, ambigus: [], nonReconnues },
|
|
});
|
|
// L'espace et chaque signe ASCII qui n'est ni une lettre, ni un chiffre,
|
|
// ni le guillemet, ni un candidat : l'en-tête n'a qu'un champ, inconnu,
|
|
// et le guillemet, au milieu d'un champ, n'ouvre rien.
|
|
const autresSignes = Array.from({ length: 0x7f - 0x20 }, (_, i) => String.fromCharCode(0x20 + i)).filter(
|
|
(signe) => !/[\p{L}\p{N}";,]/u.test(signe),
|
|
);
|
|
const cas = [
|
|
...[';', ',', '\t'].map((signe) => ({
|
|
avant: [`nom${signe}prenom`, `Pervenche${signe}Théo`],
|
|
guillemet: `Ombrelle${signe}"Iris`,
|
|
code: 'GUILLEMET_OUVERT',
|
|
etabli: lu(signe, ['nom', 'prenom'], { nom: 0, prenom: 1 }),
|
|
})),
|
|
...autresSignes.map((signe) => ({
|
|
avant: [`nom${signe}prenom`, `Pervenche${signe}Théo`],
|
|
guillemet: `Ombrelle${signe}"Iris`,
|
|
code: 'SEPARATEUR_INTROUVABLE',
|
|
})),
|
|
// L'en-tête a deux champs sous « ; » comme sous « , », aucun reconnu :
|
|
// « ; » passe avant « , ». Un guillemet en tête de ligne s'ouvre sous
|
|
// chaque candidat.
|
|
{
|
|
avant: ['personne;groupe,section', 'Pervenche;Théo,x'],
|
|
guillemet: '"Ombrelle;y,z',
|
|
code: 'GUILLEMET_OUVERT',
|
|
etabli: lu(';', ['personne', 'groupe,section'], {}, [0, 1]),
|
|
},
|
|
// Sous « , », l'en-tête porte un en-tête reconnu, sous « ; » aucun :
|
|
// « , » l'emporte.
|
|
{
|
|
avant: ['personne;nom,prenom', 'Pervenche;Théo,x'],
|
|
guillemet: '"Ombrelle;y,z',
|
|
code: 'GUILLEMET_OUVERT',
|
|
etabli: lu(',', ['personne;nom', 'prenom'], { prenom: 1 }, [0]),
|
|
},
|
|
// Un enregistrement plus court, puis plus long, que l'en-tête : le refus
|
|
// reste au rang de l'enregistrement, quel que soit celui du guillemet.
|
|
{ avant: ['nom;prenom;notes', 'Pervenche;Théo'], guillemet: 'Ombrelle;Iris;"x', code: 'SEPARATEUR_INTROUVABLE', rang: 2 },
|
|
{ avant: ['nom;prenom', 'Pervenche;Théo;x'], guillemet: 'Ombrelle;"Iris', code: 'SEPARATEUR_INTROUVABLE', rang: 2 },
|
|
// Une colonne unique : reconnue, elle se lit sans séparateur ; inconnue,
|
|
// rien ne se lit.
|
|
{ avant: ['nom', 'Pervenche'], guillemet: '"Ombrelle', code: 'GUILLEMET_OUVERT', etabli: lu(null, ['nom'], { nom: 0 }) },
|
|
{ avant: ['personnes', 'Pervenche'], guillemet: '"Ombrelle', code: 'SEPARATEUR_INTROUVABLE' },
|
|
];
|
|
for (const { avant, guillemet, code, etabli = {}, rang = null } of cas) {
|
|
for (let vides = 0; vides <= 40; vides += 1) {
|
|
const texte = csv(...avant, ...new Array(vides).fill(''), guillemet);
|
|
const rangDuRefus = code === 'GUILLEMET_OUVERT' ? avant.length + vides + 1 : rang;
|
|
assert.deepEqual(
|
|
apercevoir(texte),
|
|
refusGlobal(code, etabli, rangDuRefus),
|
|
`${JSON.stringify(avant[0])} … ${JSON.stringify(guillemet)}, ${vides} lignes vides`,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("un caractère nul refuse le texte entier : CARACTERE_NUL, avant toute lecture", () => {
|
|
const textes = [
|
|
'\u{0}',
|
|
csv('nom\u{0};prenom', 'Ombrelle;Iris'),
|
|
csv('nom;prenom', 'Ombrelle;Iris\u{0}'),
|
|
`${csv('nom;prenom', 'Ombrelle;Iris')}\u{0}`,
|
|
csv('nom;notes', 'Ombrelle;"ouvert\u{0}'),
|
|
];
|
|
for (const texte of textes) {
|
|
assert.deepEqual(apercevoir(texte), refusGlobal('CARACTERE_NUL'), JSON.stringify(texte));
|
|
}
|
|
});
|
|
|
|
test('le caractère nul suit la règle de contientNul, la même que pour un fichier décodé', () => {
|
|
const textes = [
|
|
csv('nom', 'Ombrelle\u{0}'),
|
|
csv('nom', 'Ombrelle\u{2400}'),
|
|
csv('nom', 'Ombrelle\\0'),
|
|
csv('nom', 'Ombrelle0'),
|
|
csv('nom', 'Ombrelle\u{1}\u{7F}'),
|
|
csv('\u{0}nom', 'Ombrelle'),
|
|
];
|
|
const refuses = textes.map((texte) => apercevoir(texte).refusGlobal === 'CARACTERE_NUL');
|
|
assert.deepEqual(refuses, textes.map((texte) => contientNul(texte)));
|
|
assert.deepEqual(refuses, [true, false, false, false, false, true]);
|
|
});
|
|
});
|
|
|
|
// Le texte qu'un collage depuis un tableur donne : tabulations, CRLF, et une
|
|
// cellule à deux lignes, citée. C'est aussi le texte du fichier
|
|
// utf16le_tabulation.csv, l'enregistrement « Texte Unicode » d'un tableur.
|
|
const COLLAGE = csv(
|
|
'nom\tprenom\tappartenance\tnotes',
|
|
'Ombrelle\tIris\tCh\u{153}ur de l\u{2019}Anse\tcaf\u{E9} \u{E0} 8 h',
|
|
'Pervenche\tTh\u{E9}o\tClub des Merles\t"dit ""oui""\r\npuis part"',
|
|
);
|
|
|
|
// Ses lignes, que rend aussi le même contenu en windows-1252, séparé par « ; ».
|
|
const LIGNES_DU_COLLAGE = [
|
|
{
|
|
ligne: 2,
|
|
champs: champs({
|
|
nom: 'Ombrelle', prenom: 'Iris', appartenance: 'Ch\u{153}ur de l\u{2019}Anse', notes: 'caf\u{E9} \u{E0} 8 h',
|
|
}),
|
|
brut: ['Ombrelle', 'Iris', 'Ch\u{153}ur de l\u{2019}Anse', 'caf\u{E9} \u{E0} 8 h'],
|
|
},
|
|
{
|
|
ligne: 3,
|
|
champs: champs({
|
|
nom: 'Pervenche', prenom: 'Th\u{E9}o', appartenance: 'Club des Merles', notes: 'dit "oui"\r\npuis part',
|
|
}),
|
|
brut: ['Pervenche', 'Th\u{E9}o', 'Club des Merles', 'dit "oui"\r\npuis part'],
|
|
},
|
|
];
|
|
|
|
describe("apercevoir : le collage et l'import, un seul analyseur (§ 10.2, § 14.10)", () => {
|
|
test("le texte collé et le même texte décodé d'un fichier rendent le même aperçu", () => {
|
|
const fichier = decoder(lireFixture('utf16le_tabulation.csv'));
|
|
assert.equal(fichier.encodage, 'utf-16le');
|
|
assert.equal(fichier.texte, COLLAGE);
|
|
|
|
const colle = apercevoir(COLLAGE);
|
|
assert.equal(colle.separateur, '\t');
|
|
assert.deepEqual(colle.lignes, LIGNES_DU_COLLAGE);
|
|
assert.deepEqual(apercevoir(fichier.texte), colle);
|
|
|
|
const marque = decoder(Uint8Array.from([0xef, 0xbb, 0xbf, ...new TextEncoder().encode(COLLAGE)]));
|
|
assert.equal(marque.encodage, 'utf-8-bom');
|
|
assert.deepEqual(apercevoir(marque.texte), colle);
|
|
});
|
|
|
|
test('le même contenu en windows-1252, séparé par « ; », rend les mêmes lignes', () => {
|
|
const octets = lireFixture('windows1252.csv');
|
|
assert.deepEqual(finsDeLigne(octets), { crlf: 4, isolees: 0 });
|
|
const { texte, encodage } = decoder(octets);
|
|
assert.equal(encodage, 'windows-1252');
|
|
const apercu = apercevoir(texte);
|
|
assert.equal(apercu.separateur, ';');
|
|
assert.deepEqual(apercu.lignes, LIGNES_DU_COLLAGE);
|
|
assert.deepEqual(apercu.entetes, apercevoir(COLLAGE).entetes);
|
|
});
|
|
});
|
|
|
|
// Noms des fichiers de test/fixtures/csv/ que ce fichier éprouve, chacun dans
|
|
// son épreuve ci-dessous.
|
|
const DONNEES = [
|
|
'champ_cite.csv',
|
|
'derniere_ligne_sans_fin.csv',
|
|
'entete_vide.csv',
|
|
'espaces_autour.csv',
|
|
'ligne_vide_au_milieu.csv',
|
|
'marque_crlf.csv',
|
|
'utf16le_tabulation.csv',
|
|
'windows1252.csv',
|
|
];
|
|
|
|
// Aperçu d'une donnée d'épreuve, une fois vérifié que decoder la lit en
|
|
// UTF-8 avec marque, et que ses octets ne portent que des fins de ligne CRLF :
|
|
// git ne les a pas converties.
|
|
function apercuDe(nom) {
|
|
const octets = lireFixture(nom);
|
|
assert.equal(finsDeLigne(octets).isolees, 0, `${nom} : fin de ligne isolée`);
|
|
const { texte, encodage } = decoder(octets);
|
|
assert.equal(encodage, 'utf-8-bom', nom);
|
|
return apercevoir(texte);
|
|
}
|
|
|
|
describe("apercevoir : les données d'épreuve, un CSV tel qu'un tableur l'écrit", () => {
|
|
test("chaque donnée d'épreuve de test/fixtures/csv/ est éprouvée ici, et il y en a", () => {
|
|
const presentes = readdirSync(FIXTURES).sort();
|
|
assert.ok(presentes.length > 0, "aucune donnée d'épreuve");
|
|
assert.deepEqual(presentes, DONNEES);
|
|
});
|
|
|
|
test('marque UTF-8 et CRLF : sept colonnes, chaque fin de ligne lue, la marque hors du premier en-tête', () => {
|
|
assert.deepEqual(finsDeLigne(lireFixture('marque_crlf.csv')), { crlf: 4, isolees: 0 });
|
|
const apercu = apercuDe('marque_crlf.csv');
|
|
assert.equal(apercu.separateur, ';');
|
|
assert.deepEqual(apercu.entetes, ['nom', 'prenom', 'appartenance', 'courriel', 'titre_pressenti', 'exclu', 'notes']);
|
|
assert.deepEqual(lignesDe(apercu), [
|
|
[2, champs({
|
|
nom: 'Ombrelle', prenom: 'Iris', appartenance: 'Club des Merles', courriel: 'iris.ombrelle@exemple.test',
|
|
titrePressenti: 'animation', exclu: false,
|
|
})],
|
|
[3, champs({ nom: 'Pervenche', prenom: 'Théo', courriel: 'theo.pervenche@exemple.test', notes: 'arrive tard' })],
|
|
[4, champs({ nom: 'Sarcelle', prenom: 'Ondine', appartenance: 'Chorale du Givre', exclu: true })],
|
|
]);
|
|
assert.deepEqual(apercu.refusees, []);
|
|
assert.equal(apercu.refusGlobal, null);
|
|
});
|
|
|
|
test('une dernière ligne sans fin de ligne est lue en entier', () => {
|
|
const octets = lireFixture('derniere_ligne_sans_fin.csv');
|
|
assert.notEqual(octets[octets.length - 1], 0x0a);
|
|
assert.deepEqual(finsDeLigne(octets), { crlf: 2, isolees: 0 });
|
|
assert.deepEqual(lignesDe(apercuDe('derniere_ligne_sans_fin.csv')), [
|
|
[2, champs({ nom: 'Ombrelle', prenom: 'Iris', appartenance: 'Club des Merles' })],
|
|
[3, champs({ nom: 'Bruyère', prenom: 'Anouk', appartenance: 'Chorale du Givre' })],
|
|
]);
|
|
});
|
|
|
|
test('une ligne vide et une ligne de séparateurs au milieu : ignorées, les rangs suivants gardés', () => {
|
|
const apercu = apercuDe('ligne_vide_au_milieu.csv');
|
|
assert.deepEqual(lignesDe(apercu), [
|
|
[2, champs({ nom: 'Ombrelle', prenom: 'Iris', appartenance: 'Club des Merles' })],
|
|
[4, champs({ nom: 'Pervenche', prenom: 'Théo' })],
|
|
[6, champs({ nom: 'Sarcelle', prenom: 'Ondine', appartenance: 'Chorale du Givre' })],
|
|
]);
|
|
assert.deepEqual(apercu.refusees, []);
|
|
});
|
|
|
|
test('un champ cité qui porte des guillemets doublés et le séparateur ; un autre sur deux lignes', () => {
|
|
const apercu = apercuDe('champ_cite.csv');
|
|
assert.deepEqual(lignesDe(apercu), [
|
|
[2, champs({
|
|
nom: 'Ombrelle', prenom: 'Iris', appartenance: 'Club "Les Merles" ; section nord',
|
|
notes: 'dit "bonjour" ; puis part',
|
|
})],
|
|
[3, champs({
|
|
nom: 'Pervenche', prenom: 'Théo', appartenance: 'Chorale du Givre', notes: 'première ligne\r\ndeuxième ligne',
|
|
})],
|
|
[4, champs({ nom: 'Sarcelle', prenom: 'Ondine', notes: 'fin' })],
|
|
]);
|
|
assert.deepEqual(apercu.refusees, []);
|
|
});
|
|
|
|
test('des espaces autour des valeurs et des en-têtes : retirés des champs, gardés dans le brut', () => {
|
|
const apercu = apercuDe('espaces_autour.csv');
|
|
assert.deepEqual(apercu.entetes, [' nom ', ' prenom ', ' appartenance ', ' courriel ', ' exclu ']);
|
|
assert.deepEqual(apercu.association.colonnes, { nom: 0, prenom: 1, appartenance: 2, courriel: 3, exclu: 4 });
|
|
assert.deepEqual(apercu.lignes, [
|
|
{
|
|
ligne: 2,
|
|
champs: champs({
|
|
nom: 'Ombrelle', prenom: 'Iris', appartenance: 'Club des Merles', courriel: 'iris@exemple.test', exclu: true,
|
|
}),
|
|
brut: [' Ombrelle ', ' Iris ', ' Club des Merles ', ' iris@exemple.test ', ' Oui '],
|
|
},
|
|
{
|
|
ligne: 3,
|
|
champs: champs({ nom: 'Pervenche', prenom: 'Théo' }),
|
|
brut: ['Pervenche\u{A0}', ' Théo ', ' ', ' ', ' '],
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("une colonne d'en-tête vide : non reconnue, sa valeur ignorée, la ligne importée", () => {
|
|
const apercu = apercuDe('entete_vide.csv');
|
|
assert.deepEqual(apercu.association, {
|
|
colonnes: { nom: 0, prenom: 1, appartenance: 3 },
|
|
ambigus: [],
|
|
nonReconnues: [2],
|
|
});
|
|
assert.deepEqual(apercu.lignes, [
|
|
{
|
|
ligne: 2,
|
|
champs: champs({ nom: 'Ombrelle', prenom: 'Iris', appartenance: 'Club des Merles' }),
|
|
brut: ['Ombrelle', 'Iris', 'note sans en-tête', 'Club des Merles'],
|
|
},
|
|
{ ligne: 3, champs: champs({ nom: 'Pervenche', prenom: 'Théo' }), brut: ['Pervenche', 'Théo', '', ''] },
|
|
]);
|
|
assert.deepEqual(apercu.refusees, []);
|
|
});
|
|
});
|