1127 lines
49 KiB
JavaScript
1127 lines
49 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,
|
||
|
|
) => ({
|
||
|
|
separateur,
|
||
|
|
entetes,
|
||
|
|
association,
|
||
|
|
lignes: [],
|
||
|
|
refusees: [],
|
||
|
|
fusions: [],
|
||
|
|
doublons: [],
|
||
|
|
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), {
|
||
|
|
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: [],
|
||
|
|
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 : 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("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, []);
|
||
|
|
});
|
||
|
|
});
|