[ADD] csv: reconcile tag spellings across an imported file
An imported list writes the same tag several ways (Finance, finance, FINANCE). The reconciliation keeps one spelling per tag key, the one from the first row that uses it, rewrites the others, and lists every merge with its row count, so no rewrite is silent. Order is by code point, never by locale, so two machines agree. New file only: the CSV writer and its formula rule are untouched. Checked: 28 tests, each tie-break caught by a named mutation. --- FR --- [ADD] csv : réconcilier les orthographes d'une étiquette dans un fichier Une liste importée écrit la même étiquette de plusieurs façons (Finance, finance, FINANCE). La réconciliation garde une orthographe par clé, celle de la première ligne qui l'emploie, réécrit les autres et liste chaque fusion avec son nombre de lignes : aucune réécriture n'est muette. L'ordre suit les points de code, jamais la locale. Fichier neuf seulement : l'écriture du CSV et sa règle des formules restent intactes. Vérifié : 28 épreuves, chaque départage pris par une mutation nommée. Assisted-by: Claude Opus 5.5
This commit is contained in:
parent
91c844725b
commit
52ff9649ab
2 changed files with 404 additions and 0 deletions
135
src/csv/etiquettes.js
Normal file
135
src/csv/etiquettes.js
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
// Identité et réconciliation des étiquettes (§ 4.5, § 10.1). Deux étiquettes
|
||||
// de même clé normalisée sont la même étiquette : une saisie ou une ligne
|
||||
// importée reçoit l'orthographe que l'événement porte déjà pour sa clé, la
|
||||
// plus petite par points de code quand il en porte plusieurs, et à défaut la
|
||||
// plus petite des orthographes lues. Le résultat ne dépend ainsi ni de
|
||||
// l'ordre des existantes ni de celui des lignes. La forme d'une étiquette et
|
||||
// la découpe d'une cellule vivent dans src/stockage/etiquettes.js : ce module
|
||||
// reçoit des étiquettes déjà découpées et n'en juge pas la forme.
|
||||
import { cleNormalisee } from './normalisation.js';
|
||||
|
||||
/**
|
||||
* Clé d'identité d'une étiquette : la clé normalisée du § 10.1, la même
|
||||
* fonction que celle des appartenances et de la clé de doublon.
|
||||
*
|
||||
* @type {(texte: string) => string}
|
||||
*/
|
||||
export const cleEtiquette = cleNormalisee;
|
||||
|
||||
// Ordre par points de code. L'opérateur « < » des chaînes compare des unités
|
||||
// de code UTF-16 : une lettre hors du plan de base, codée par une paire dont
|
||||
// la première moitié vaut 0xD800 à 0xDBFF, y passerait avant U+E000 à U+FFFF.
|
||||
// Deux préfixes égaux ont la même découpe en unités, si bien qu'un seul
|
||||
// indice parcourt les deux textes ; un préfixe propre vient avant le texte
|
||||
// qui le prolonge. Rend un nombre négatif, nul ou positif.
|
||||
function comparerPointsDeCode(a, b) {
|
||||
let i = 0;
|
||||
while (i < a.length && i < b.length) {
|
||||
const pointA = a.codePointAt(i);
|
||||
const pointB = b.codePointAt(i);
|
||||
if (pointA !== pointB) return pointA - pointB;
|
||||
i += pointA > 0xffff ? 2 : 1;
|
||||
}
|
||||
return a.length - b.length;
|
||||
}
|
||||
|
||||
// La plus petite d'une suite non vide de textes, par points de code.
|
||||
function plusPetite(textes) {
|
||||
let retenue = null;
|
||||
for (const texte of textes) {
|
||||
if (retenue === null || comparerPointsDeCode(texte, retenue) < 0) retenue = texte;
|
||||
}
|
||||
return retenue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Orthographe que reçoit une saisie (§ 4.5) : la plus petite, par points de
|
||||
* code, des existantes de même clé ; la saisie elle-même quand aucune
|
||||
* existante ne partage sa clé. Une saisie déjà présente reçoit elle aussi la
|
||||
* plus petite de sa clé. L'appelant compare le résultat à la saisie pour
|
||||
* savoir s'il doit dire l'orthographe retenue.
|
||||
*
|
||||
* @param {Iterable<string>} existantes les étiquettes de l'événement,
|
||||
* personnes et objectifs de table confondus, doublons admis
|
||||
* @param {string} saisie une étiquette déjà mise en forme
|
||||
* @returns {string}
|
||||
*/
|
||||
export function orthographeRecue(existantes, saisie) {
|
||||
const cle = cleEtiquette(saisie);
|
||||
return plusPetite([...existantes].filter((texte) => cleEtiquette(texte) === cle)) ?? saisie;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} FusionEtiquettes
|
||||
* @property {string} retenue l'orthographe que reçoivent les lignes
|
||||
* @property {string[]} orthographes les orthographes distinctes de la clé,
|
||||
* existantes et lues, retenue comprise, par points de code croissants
|
||||
* @property {number} lignes le nombre de lignes dont la liste porte la clé
|
||||
* sous une autre orthographe que la retenue, donc réécrite
|
||||
*/
|
||||
|
||||
/**
|
||||
* Réconcilie les étiquettes des lignes d'un import (§ 10.1) avec celles de
|
||||
* l'événement, puis entre elles. Une clé que l'événement porte reçoit la plus
|
||||
* petite de ses orthographes existantes ; une clé que seules les lignes
|
||||
* portent, la plus petite des orthographes lues. Chaque liste est réécrite
|
||||
* dans son ordre, une étiquette prenant l'orthographe de sa clé ; une
|
||||
* orthographe qui en rejoint une autre de la même liste se fond à la place
|
||||
* de la première. Les existantes ne sont jamais réécrites.
|
||||
*
|
||||
* Une fusion est rendue pour chaque clé qui réécrit au moins une ligne, et
|
||||
* aucune ligne n'est réécrite sans la sienne : une ligne qui porte déjà
|
||||
* l'orthographe retenue ne fait pas de fusion, même quand l'événement porte
|
||||
* d'autres orthographes de sa clé. Les fusions se rangent par leur première
|
||||
* ligne réécrite, puis par l'orthographe retenue.
|
||||
*
|
||||
* @param {Iterable<string>} existantes les étiquettes de l'événement,
|
||||
* doublons admis
|
||||
* @param {Array<string[]|null>} listes les étiquettes de chaque ligne, déjà
|
||||
* découpées et mises en forme ; null pour une ligne qui n'en donne pas
|
||||
* @returns {{listes: Array<string[]|null>, fusions: FusionEtiquettes[]}}
|
||||
* listes : une liste neuve par ligne, au même rang, null gardé
|
||||
*/
|
||||
export function reconcilier(existantes, listes) {
|
||||
const groupes = new Map();
|
||||
const groupeDe = (texte) => {
|
||||
const cle = cleEtiquette(texte);
|
||||
let groupe = groupes.get(cle);
|
||||
if (groupe === undefined) {
|
||||
groupe = { existantes: new Set(), lues: new Set(), retenue: null, reecrites: [] };
|
||||
groupes.set(cle, groupe);
|
||||
}
|
||||
return groupe;
|
||||
};
|
||||
for (const texte of existantes) groupeDe(texte).existantes.add(texte);
|
||||
for (const liste of listes) {
|
||||
if (liste !== null) for (const texte of liste) groupeDe(texte).lues.add(texte);
|
||||
}
|
||||
for (const groupe of groupes.values()) {
|
||||
groupe.retenue = plusPetite(groupe.existantes.size > 0 ? groupe.existantes : groupe.lues);
|
||||
}
|
||||
|
||||
const reecrites = listes.map((liste, rang) => {
|
||||
if (liste === null) return null;
|
||||
const vues = new Set();
|
||||
for (const texte of liste) {
|
||||
const groupe = groupes.get(cleEtiquette(texte));
|
||||
if (texte !== groupe.retenue && groupe.reecrites.at(-1) !== rang) groupe.reecrites.push(rang);
|
||||
vues.add(groupe.retenue);
|
||||
}
|
||||
return [...vues];
|
||||
});
|
||||
|
||||
const fusions = [...groupes.values()]
|
||||
.filter((groupe) => groupe.reecrites.length > 0)
|
||||
.sort((a, b) => a.reecrites[0] - b.reecrites[0] || comparerPointsDeCode(a.retenue, b.retenue))
|
||||
.map((groupe) => ({
|
||||
retenue: groupe.retenue,
|
||||
orthographes: [...new Set([...groupe.existantes, ...groupe.lues])].sort(comparerPointsDeCode),
|
||||
lignes: groupe.reecrites.length,
|
||||
}));
|
||||
return { listes: reecrites, fusions };
|
||||
}
|
||||
269
src/csv/etiquettes.test.js
Normal file
269
src/csv/etiquettes.test.js
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
// Épreuves de la réconciliation des étiquettes (§ 4.5, § 10.1) : la clé, la
|
||||
// plus petite orthographe par points de code, la réécriture des listes et le
|
||||
// rapport des fusions. Les lettres décomposées, les espaces insécables et les
|
||||
// marques combinantes 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 { cleEtiquette, orthographeRecue, reconcilier } from './etiquettes.js';
|
||||
import { cleNormalisee } from './normalisation.js';
|
||||
|
||||
// « bénévole » composé (NFC) et décomposé (NFD), en capitales.
|
||||
const BENEVOLE = 'b\u{E9}n\u{E9}vole';
|
||||
const BENEVOLE_DECOMPOSE = 'BE\u{301}NE\u{301}VOLE';
|
||||
|
||||
// Deux orthographes de même clé — « a » suivi d'une marque combinante — dont
|
||||
// l'ordre par points de code (U+FE20 avant U+1D167) contredit l'ordre par
|
||||
// unités de code UTF-16 (0xD834, la demi-paire de U+1D167, avant 0xFE20).
|
||||
const MARQUE_BMP = 'a\u{FE20}';
|
||||
const MARQUE_ASTRALE = 'a\u{1D167}';
|
||||
|
||||
// Toutes les permutations d'une liste courte.
|
||||
function permutations(liste) {
|
||||
if (liste.length <= 1) return [liste];
|
||||
return liste.flatMap((tete, i) =>
|
||||
permutations([...liste.slice(0, i), ...liste.slice(i + 1)]).map((reste) => [tete, ...reste]),
|
||||
);
|
||||
}
|
||||
|
||||
// Nombre de lignes qu'une réconciliation a changées : une liste réécrite
|
||||
// diffère de celle reçue, élément par élément.
|
||||
function lignesChangees(avant, apres) {
|
||||
return avant.filter((liste, i) => liste !== null && JSON.stringify(liste) !== JSON.stringify(apres[i])).length;
|
||||
}
|
||||
|
||||
// Nombre de couples (ligne, clé) dont la ligne porte la clé sous une autre
|
||||
// orthographe que celle rendue : une ligne compte une fois par clé réécrite,
|
||||
// quel que soit le nombre de ses orthographes de cette clé.
|
||||
function couplesReecrits(avant, apres) {
|
||||
let total = 0;
|
||||
avant.forEach((liste, i) => {
|
||||
if (liste === null) return;
|
||||
const recues = new Map(apres[i].map((texte) => [cleEtiquette(texte), texte]));
|
||||
const cles = new Set(liste.filter((texte) => texte !== recues.get(cleEtiquette(texte))).map(cleEtiquette));
|
||||
total += cles.size;
|
||||
});
|
||||
return total;
|
||||
}
|
||||
|
||||
describe('cleEtiquette (§ 10.1)', () => {
|
||||
test('est la clé normalisée du § 10.1, et non une seconde règle', () => {
|
||||
assert.equal(cleEtiquette, cleNormalisee);
|
||||
});
|
||||
|
||||
test('ignore la casse, les accents et les blancs', () => {
|
||||
for (const texte of ['Finance', 'FINANCE', ' finance ', 'finance']) {
|
||||
assert.equal(cleEtiquette(texte), 'finance', texte);
|
||||
}
|
||||
assert.equal(cleEtiquette(BENEVOLE_DECOMPOSE), 'benevole');
|
||||
assert.equal(cleEtiquette('Ressources\u{A0}\u{A0}Humaines'), 'ressources humaines');
|
||||
});
|
||||
});
|
||||
|
||||
describe('orthographeRecue (§ 4.5)', () => {
|
||||
test("une saisie sans orthographe existante de même clé se garde telle quelle", () => {
|
||||
assert.equal(orthographeRecue([], 'Finance'), 'Finance');
|
||||
assert.equal(orthographeRecue(['Montréal', BENEVOLE], 'Finance'), 'Finance');
|
||||
});
|
||||
|
||||
test("la casse est ignorée : la saisie reçoit l'orthographe existante", () => {
|
||||
assert.equal(orthographeRecue(['finance'], 'Finance'), 'finance');
|
||||
assert.equal(orthographeRecue(['finance'], 'FINANCE'), 'finance');
|
||||
assert.equal(orthographeRecue(['FINANCE'], 'finance'), 'FINANCE');
|
||||
});
|
||||
|
||||
test('les accents composés, décomposés ou absents sont ignorés', () => {
|
||||
assert.equal(orthographeRecue([BENEVOLE], BENEVOLE_DECOMPOSE), BENEVOLE);
|
||||
assert.equal(orthographeRecue([BENEVOLE], 'Benevole'), BENEVOLE);
|
||||
assert.equal(orthographeRecue(['benevole'], BENEVOLE), 'benevole');
|
||||
});
|
||||
|
||||
test('les blancs intérieurs et de bord sont ignorés', () => {
|
||||
assert.equal(orthographeRecue(['ressources humaines'], 'Ressources\u{A0}humaines'), 'ressources humaines');
|
||||
assert.equal(orthographeRecue(['ressources humaines'], 'ressources humaines'), 'ressources humaines');
|
||||
});
|
||||
|
||||
test("deux orthographes existantes : la plus petite par points de code, quel que soit l'ordre", () => {
|
||||
for (const existantes of permutations(['finance', 'Finance', 'FINANCE'])) {
|
||||
assert.equal(orthographeRecue(existantes, 'fInance'), 'FINANCE', existantes.join(' / '));
|
||||
}
|
||||
});
|
||||
|
||||
test('la plus petite se lit par points de code, non par unités de code', () => {
|
||||
for (const existantes of permutations([MARQUE_ASTRALE, MARQUE_BMP])) {
|
||||
assert.equal(orthographeRecue(existantes, 'A'), MARQUE_BMP);
|
||||
}
|
||||
});
|
||||
|
||||
test("un préfixe propre est la plus petite orthographe, quel que soit l'ordre", () => {
|
||||
// « a » et « a » suivi d'une marque combinante sans forme précomposée ont
|
||||
// la même clé et ne diffèrent que par leur longueur.
|
||||
for (const existantes of permutations([MARQUE_BMP, 'a'])) {
|
||||
assert.equal(orthographeRecue(existantes, 'A'), 'a', existantes.join(' / '));
|
||||
}
|
||||
});
|
||||
|
||||
test("une saisie déjà présente reçoit encore la plus petite orthographe de sa clé", () => {
|
||||
assert.equal(orthographeRecue(['finance'], 'finance'), 'finance');
|
||||
assert.equal(orthographeRecue(['finance', 'Finance'], 'finance'), 'Finance');
|
||||
assert.equal(orthographeRecue(['finance', 'Finance', 'finance'], 'Finance'), 'Finance');
|
||||
});
|
||||
|
||||
test("l'existante l'emporte sur une saisie plus petite", () => {
|
||||
assert.equal(orthographeRecue(['finance'], 'Finance'), 'finance');
|
||||
});
|
||||
|
||||
test("accepte un itérable quelconque d'existantes", () => {
|
||||
assert.equal(orthographeRecue(new Set(['finance', 'Finance']), 'FINANCE'), 'Finance');
|
||||
});
|
||||
});
|
||||
|
||||
describe('reconcilier (§ 10.1)', () => {
|
||||
test('sans orthographe concurrente, les listes reviennent égales et aucune fusion', () => {
|
||||
const listes = [['finance', 'Montréal'], [], null, [BENEVOLE]];
|
||||
const resultat = reconcilier(['finance', 'ressources humaines'], listes);
|
||||
assert.deepEqual(resultat, { listes: [['finance', 'Montréal'], [], null, [BENEVOLE]], fusions: [] });
|
||||
});
|
||||
|
||||
test("entre les lignes, la plus petite orthographe lue est reçue, et la fusion se dit", () => {
|
||||
const { listes, fusions } = reconcilier([], [['finance'], ['Finance'], ['finance', BENEVOLE]]);
|
||||
assert.deepEqual(listes, [['Finance'], ['Finance'], ['Finance', BENEVOLE]]);
|
||||
assert.deepEqual(fusions, [{ retenue: 'Finance', orthographes: ['Finance', 'finance'], lignes: 2 }]);
|
||||
});
|
||||
|
||||
test("l'orthographe de l'événement l'emporte sur celles des lignes", () => {
|
||||
const { listes, fusions } = reconcilier(['finance', 'Montréal'], [['FINANCE'], ['Finance'], ['finance']]);
|
||||
assert.deepEqual(listes, [['finance'], ['finance'], ['finance']]);
|
||||
assert.deepEqual(fusions, [{ retenue: 'finance', orthographes: ['FINANCE', 'Finance', 'finance'], lignes: 2 }]);
|
||||
});
|
||||
|
||||
test("deux orthographes dans l'événement : la plus petite est reçue", () => {
|
||||
for (const existantes of permutations(['finance', 'Finance', 'FINANCE'])) {
|
||||
const { listes, fusions } = reconcilier(existantes, [['finance'], ['Finance']]);
|
||||
assert.deepEqual(listes, [['FINANCE'], ['FINANCE']], existantes.join(' / '));
|
||||
assert.deepEqual(fusions, [{ retenue: 'FINANCE', orthographes: ['FINANCE', 'Finance', 'finance'], lignes: 2 }]);
|
||||
}
|
||||
});
|
||||
|
||||
test("une ligne qui porte déjà l'orthographe reçue n'est pas une fusion", () => {
|
||||
const { listes, fusions } = reconcilier(['finance', 'Finance'], [['Finance'], ['Montréal']]);
|
||||
assert.deepEqual(listes, [['Finance'], ['Montréal']]);
|
||||
assert.deepEqual(fusions, []);
|
||||
});
|
||||
|
||||
test("accents composés et décomposés, blancs : une seule étiquette", () => {
|
||||
const { listes, fusions } = reconcilier([BENEVOLE], [[BENEVOLE_DECOMPOSE], ['Ressources\u{A0}humaines'], ['ressources humaines']]);
|
||||
assert.deepEqual(listes, [[BENEVOLE], ['Ressources\u{A0}humaines'], ['Ressources\u{A0}humaines']]);
|
||||
assert.deepEqual(fusions, [
|
||||
{ retenue: BENEVOLE, orthographes: [BENEVOLE_DECOMPOSE, BENEVOLE], lignes: 1 },
|
||||
{ retenue: 'Ressources\u{A0}humaines', orthographes: ['Ressources\u{A0}humaines', 'ressources humaines'], lignes: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("deux orthographes d'une même ligne n'en font qu'une, à la place de la première", () => {
|
||||
const { listes, fusions } = reconcilier([], [[BENEVOLE, 'finance', 'Montréal', 'Finance']]);
|
||||
assert.deepEqual(listes, [[BENEVOLE, 'Finance', 'Montréal']]);
|
||||
assert.deepEqual(fusions, [{ retenue: 'Finance', orthographes: ['Finance', 'finance'], lignes: 1 }]);
|
||||
});
|
||||
|
||||
test("une ligne qui porte deux orthographes réécrites d'une clé compte une fois", () => {
|
||||
const { listes, fusions } = reconcilier([BENEVOLE], [['BENEVOLE', 'Benevole']]);
|
||||
assert.deepEqual(listes, [[BENEVOLE]]);
|
||||
assert.deepEqual(fusions, [{ retenue: BENEVOLE, orthographes: ['BENEVOLE', 'Benevole', BENEVOLE], lignes: 1 }]);
|
||||
});
|
||||
|
||||
test('la plus petite se lit par points de code, non par unités de code', () => {
|
||||
for (const lignes of permutations([[MARQUE_ASTRALE], [MARQUE_BMP]])) {
|
||||
const { listes, fusions } = reconcilier([], lignes);
|
||||
assert.deepEqual(listes, [[MARQUE_BMP], [MARQUE_BMP]]);
|
||||
assert.deepEqual(fusions, [{ retenue: MARQUE_BMP, orthographes: [MARQUE_BMP, MARQUE_ASTRALE], lignes: 1 }]);
|
||||
}
|
||||
});
|
||||
|
||||
test("l'ordre des lignes ne change ni l'orthographe reçue ni le groupe", () => {
|
||||
const lignes = [['finance'], ['FINANCE', BENEVOLE], ['Finance'], [BENEVOLE_DECOMPOSE]];
|
||||
for (const ordre of permutations(lignes)) {
|
||||
const { listes, fusions } = reconcilier(['Montréal'], ordre);
|
||||
assert.deepEqual(
|
||||
listes.map((liste) => liste.join('|')).sort(),
|
||||
[`FINANCE|${BENEVOLE_DECOMPOSE}`, BENEVOLE_DECOMPOSE, 'FINANCE', 'FINANCE'].sort(),
|
||||
);
|
||||
assert.deepEqual(
|
||||
fusions.map(({ retenue, orthographes, lignes: nombre }) => [retenue, orthographes, nombre]).sort(),
|
||||
[
|
||||
[BENEVOLE_DECOMPOSE, [BENEVOLE_DECOMPOSE, BENEVOLE], 1],
|
||||
['FINANCE', ['FINANCE', 'Finance', 'finance'], 2],
|
||||
].sort(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('les fusions se rangent par la première ligne réécrite', () => {
|
||||
const existantes = ['finance', BENEVOLE];
|
||||
const avant = reconcilier(existantes, [['Benevole'], ['Finance']]).fusions.map(({ retenue }) => retenue);
|
||||
const apres = reconcilier(existantes, [['Finance'], ['Benevole']]).fusions.map(({ retenue }) => retenue);
|
||||
assert.deepEqual(avant, [BENEVOLE, 'finance']);
|
||||
assert.deepEqual(apres, ['finance', BENEVOLE]);
|
||||
// « finance » est réécrite aux lignes 0 et 2, « Montréal » à la ligne 1
|
||||
// seulement : la première ligne réécrite range, non la dernière.
|
||||
const lignes = [['Finance'], ['montréal'], ['FINANCE']];
|
||||
const fusions = reconcilier(['finance', 'Montréal'], lignes).fusions.map(({ retenue }) => retenue);
|
||||
assert.deepEqual(fusions, ['finance', 'Montréal']);
|
||||
});
|
||||
|
||||
test("à première ligne égale, les fusions se rangent par l'orthographe retenue", () => {
|
||||
// Les deux clés sont réécrites pour la première fois à la ligne 0 ;
|
||||
// l'ordre des existantes, qui est celui de leur découverte, ne range pas.
|
||||
for (const existantes of permutations(['finance', BENEVOLE])) {
|
||||
const { fusions } = reconcilier(existantes, [['Finance', 'Benevole']]);
|
||||
assert.deepEqual(fusions.map(({ retenue }) => retenue), [BENEVOLE, 'finance'], existantes.join(' / '));
|
||||
}
|
||||
});
|
||||
|
||||
test("un préfixe propre est la plus petite orthographe, quel que soit l'ordre des lignes", () => {
|
||||
// « a » et « a » suivi d'une marque combinante sans forme précomposée ont
|
||||
// la même clé et ne diffèrent que par leur longueur.
|
||||
for (const lignes of permutations([[MARQUE_BMP], ['a']])) {
|
||||
const { listes, fusions } = reconcilier([], lignes);
|
||||
assert.deepEqual(listes, [['a'], ['a']]);
|
||||
assert.deepEqual(fusions, [{ retenue: 'a', orthographes: ['a', MARQUE_BMP], lignes: 1 }]);
|
||||
}
|
||||
});
|
||||
|
||||
test("aucune fusion silencieuse : chaque ligne changée est comptée par une fusion", () => {
|
||||
const cas = [
|
||||
[[], [['finance'], ['Finance'], ['Montréal']]],
|
||||
[['finance'], [['FINANCE', 'Montréal'], ['montreal'], null, []]],
|
||||
[['finance', 'Finance'], [['finance'], ['Finance', 'finance'], [BENEVOLE_DECOMPOSE]]],
|
||||
[[BENEVOLE], [[BENEVOLE_DECOMPOSE, 'Benevole'], ['Montréal', 'MONTRÉAL']]],
|
||||
];
|
||||
for (const [existantes, lignes] of cas) {
|
||||
const { listes, fusions } = reconcilier(existantes, lignes);
|
||||
const comptees = fusions.reduce((total, { lignes: nombre }) => total + nombre, 0);
|
||||
assert.ok(fusions.every(({ lignes: nombre }) => nombre > 0), JSON.stringify(fusions));
|
||||
assert.ok(comptees >= lignesChangees(lignes, listes), JSON.stringify({ existantes, lignes }));
|
||||
assert.equal(comptees, couplesReecrits(lignes, listes), JSON.stringify({ existantes, lignes }));
|
||||
assert.ok(lignesChangees(lignes, listes) > 0);
|
||||
}
|
||||
});
|
||||
|
||||
test('une ligne absente reste absente et ne compte pas', () => {
|
||||
const { listes, fusions } = reconcilier(['finance'], [null, ['Finance'], null]);
|
||||
assert.deepEqual(listes, [null, ['finance'], null]);
|
||||
assert.deepEqual(fusions, [{ retenue: 'finance', orthographes: ['Finance', 'finance'], lignes: 1 }]);
|
||||
});
|
||||
|
||||
test('ne modifie ni les existantes ni les listes reçues, et ne les partage pas', () => {
|
||||
const existantes = ['finance', 'Finance'];
|
||||
const lignes = [['finance', 'Montréal'], ['Montréal']];
|
||||
const copies = structuredClone({ existantes, lignes });
|
||||
const { listes } = reconcilier(existantes, lignes);
|
||||
assert.deepEqual({ existantes, lignes }, copies);
|
||||
listes[1].push('bénévole');
|
||||
assert.deepEqual(lignes[1], ['Montréal']);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue