Columns associate by header, never by position, insensitive to case, accents and spaces, with the § 10.1 synonyms; two columns for one field associate neither and ask. The preview imports nothing: it lists valid rows, refused rows with their reason, each group of merged spellings with its counts, duplicates never merged, and global refusals, an open quote or a NUL included. A paste goes through the same parser, and the separator rule lives once, in lecture.js, whose refusal names its line. Checked: 185 CSV tests, eight spreadsheet-shaped fixtures; two reviews. --- FR --- [ADD] csv : colonnes par en-tête, valeurs d'exclu fermées, aperçu Les colonnes s'associent par en-tête, jamais par position, sans égard à la casse, aux accents ni aux espaces, avec les synonymes du § 10.1 ; deux colonnes d'un même champ ne s'associent pas, et le logiciel demande. L'aperçu n'importe rien : lignes valides, lignes refusées et leur motif, chaque groupe d'orthographes fondues et ses comptes, doublons jamais fondus, refus globaux, guillemet ouvert et nul compris. Le collage passe par le même analyseur, et la règle du séparateur ne s'écrit qu'une fois, dans lecture.js, dont le refus nomme sa ligne. Vérifié : 185 épreuves CSV, huit données de tableur ; deux revues. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
110 lines
5 KiB
JavaScript
110 lines
5 KiB
JavaScript
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||
|
||
// Épreuves des clés de comparaison (§ 10.1) : la clé normalisée, que
|
||
// partagent la réconciliation des appartenances et la clé de doublon, et la
|
||
// clé d'en-tête, qui en dérive. Les lettres décomposées et les blancs autres
|
||
// que l'espace s'écrivent par leur point de code : un éditeur qui recompose
|
||
// une lettre ou remplace un caractère invisible changerait l'épreuve sans
|
||
// qu'elle le montre.
|
||
import assert from 'node:assert/strict';
|
||
import { describe, test } from '../../test/lanceur.js';
|
||
import { cleEntete, cleNormalisee } from './normalisation.js';
|
||
|
||
describe('cleNormalisee (§ 10.1)', () => {
|
||
test('la casse et les accents sont ignorés', () => {
|
||
for (const texte of ['Coopérative Bleue', 'COOPÉRATIVE BLEUE', 'coopérative bleue', 'Cooperative Bleue']) {
|
||
assert.equal(cleNormalisee(texte), 'cooperative bleue', texte);
|
||
}
|
||
assert.equal(cleNormalisee('Ça gèle à Noël, où ?'), 'ca gele a noel, ou ?');
|
||
});
|
||
|
||
test('une lettre accentuée, composée ou décomposée, donne la même clé', () => {
|
||
const composee = 'Th\u{E9}o';
|
||
const decomposee = 'The\u{301}o';
|
||
assert.notEqual(composee, decomposee);
|
||
assert.equal(cleNormalisee(composee), 'theo');
|
||
assert.equal(cleNormalisee(decomposee), 'theo');
|
||
});
|
||
|
||
test('toute marque combinante est retirée, pas seulement les accents du français', () => {
|
||
// Cédille, rond en chef, tréma, double accent aigu, ogonek, puis un
|
||
// cercle englobant, marque combinante qui n'est pas un accent.
|
||
assert.equal(cleNormalisee('Gar\u{E7}on'), 'garcon');
|
||
assert.equal(cleNormalisee('\u{C5}ngstr\u{F6}m'), 'angstrom');
|
||
assert.equal(cleNormalisee('\u{150}rs\u{105}'), 'orsa');
|
||
assert.equal(cleNormalisee('a\u{20DD}b'), 'ab');
|
||
});
|
||
|
||
test('la décomposition est canonique : une ligature ou une lettre sans décomposition reste', () => {
|
||
assert.equal(cleNormalisee('\u{152}uvre'), '\u{153}uvre');
|
||
assert.equal(cleNormalisee('\u{FB01}n'), '\u{FB01}n');
|
||
assert.equal(cleNormalisee('\u{141}ukasz'), '\u{142}ukasz');
|
||
});
|
||
|
||
test('les blancs se réduisent à une espace, les bouts retirés', () => {
|
||
assert.equal(cleNormalisee(' cooperative bleue '), 'cooperative bleue');
|
||
assert.equal(cleNormalisee('club\tdes\u{A0}merles\r\nnord'), 'club des merles nord');
|
||
assert.equal(cleNormalisee('\u{202F}chorale\u{3000}du\u{2009}givre\u{FEFF}'), 'chorale du givre');
|
||
});
|
||
|
||
test("le trait d'union, le trait de soulignement et l'espace restent : seule la clé d'en-tête les retire", () => {
|
||
// La réconciliation des appartenances et la clé de doublon comparent par
|
||
// cette clé : « Club-des-Merles » et « Club des Merles » y restent deux.
|
||
assert.equal(cleNormalisee('Club-des-Merles'), 'club-des-merles');
|
||
assert.equal(cleNormalisee('Club_des_Merles'), 'club_des_merles');
|
||
assert.equal(cleNormalisee('Clubdes Merles'), 'clubdes merles');
|
||
assert.notEqual(cleNormalisee('Val-Brume'), cleNormalisee('Val Brume'));
|
||
});
|
||
|
||
test('un texte vide ou blanc donne la clé vide', () => {
|
||
for (const texte of ['', ' ', '\t\r\n', '\u{A0}\u{3000}\u{202F}']) {
|
||
assert.equal(cleNormalisee(texte), '', JSON.stringify(texte));
|
||
}
|
||
});
|
||
|
||
test('les minuscules ne suivent aucune langue', () => {
|
||
// Une minuscule turque ferait de I un ı sans point.
|
||
assert.equal(cleNormalisee('IRIS'), 'iris');
|
||
// I à point suscrit : le point est une marque, retirée.
|
||
assert.equal(cleNormalisee('\u{130}LE'), 'ile');
|
||
});
|
||
|
||
test('la clé est un point fixe : la normaliser de nouveau ne la change pas', () => {
|
||
for (const texte of ['Coopérative Bleue', ' The\u{301}o PERVENCHE ', '\u{130}LE', 'a\u{20DD}b', '\u{152}UVRE']) {
|
||
const cle = cleNormalisee(texte);
|
||
assert.equal(cleNormalisee(cle), cle, texte);
|
||
}
|
||
});
|
||
});
|
||
|
||
describe('cleEntete (§ 10.1)', () => {
|
||
test('sans égard à la casse, aux accents, aux espaces, à « _ » ni à « - »', () => {
|
||
const cas = [
|
||
['NOM', 'nom'],
|
||
[' Prénom ', 'prenom'],
|
||
['Équipe', 'equipe'],
|
||
['Rôle', 'role'],
|
||
['E X C L U', 'exclu'],
|
||
['titre_pressenti', 'titrepressenti'],
|
||
['Titre-Pressenti', 'titrepressenti'],
|
||
['titre pressenti', 'titrepressenti'],
|
||
['titre\u{A0}_\u{9}pressenti', 'titrepressenti'],
|
||
[' Ti_tre--Pres sen-ti ', 'titrepressenti'],
|
||
];
|
||
assert.deepEqual(
|
||
cas.map(([entete]) => cleEntete(entete)),
|
||
cas.map(([, cle]) => cle),
|
||
);
|
||
});
|
||
|
||
test('seuls les blancs, « _ » et « - » sont retirés', () => {
|
||
assert.equal(cleEntete('titre.pressenti'), 'titre.pressenti');
|
||
assert.equal(cleEntete('nom/prenom'), 'nom/prenom');
|
||
assert.equal(cleEntete('titre\u{2013}pressenti'), 'titre\u{2013}pressenti');
|
||
});
|
||
|
||
test('un en-tête vide ou fait de blancs, de « _ » et de « - » donne la clé vide', () => {
|
||
for (const entete of ['', ' ', '_-_', ' - ']) assert.equal(cleEntete(entete), '', JSON.stringify(entete));
|
||
});
|
||
});
|