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
87 lines
3.5 KiB
JavaScript
87 lines
3.5 KiB
JavaScript
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
|
|
|
// Association des colonnes d'un CSV aux champs d'un participant (§ 10.1). Une
|
|
// colonne s'associe par son en-tête, jamais par sa position : un tableur dont
|
|
// une colonne a été déplacée importerait sinon les noms dans les
|
|
// appartenances, sans rien signaler. Deux colonnes reconnues sous le même
|
|
// champ ne se départagent pas toutes seules ; le choix de l'opérateur tranche.
|
|
|
|
import { cleEntete } from './normalisation.js';
|
|
|
|
/** Champs d'un participant que porte un CSV, dans l'ordre de l'export. */
|
|
export const CHAMPS = Object.freeze(['nom', 'prenom', 'appartenance', 'courriel', 'titre_pressenti', 'exclu', 'notes']);
|
|
|
|
// En-têtes de chaque champ tels que le § 10.1 les écrit : son nom, puis ses
|
|
// synonymes.
|
|
const ENTETES_DES_CHAMPS = [
|
|
['nom', ['nom']],
|
|
['prenom', ['prenom']],
|
|
['appartenance', ['appartenance', 'organisation', 'entreprise', 'équipe']],
|
|
['courriel', ['courriel']],
|
|
['titre_pressenti', ['titre_pressenti', 'titre', 'rôle']],
|
|
['exclu', ['exclu']],
|
|
['notes', ['notes']],
|
|
];
|
|
|
|
// Champ de chaque clé d'en-tête, la clé tirée de l'écriture du § 10.1 par la
|
|
// même règle que celle d'un en-tête lu.
|
|
const CHAMP_DE_CLE = new Map(
|
|
ENTETES_DES_CHAMPS.flatMap(([champ, entetes]) => entetes.map((entete) => [cleEntete(entete), champ])),
|
|
);
|
|
|
|
/**
|
|
* Champ qu'un en-tête désigne, synonymes compris, sans égard à la casse, aux
|
|
* accents, aux espaces, à « _ » ni à « - » ; null pour un en-tête inconnu,
|
|
* comme « ligne » et « motif », les deux colonnes qu'ajoute le CSV des refus.
|
|
*
|
|
* @param {string} entete
|
|
* @returns {string|null} un élément de CHAMPS, ou null
|
|
*/
|
|
export function reconnaitre(entete) {
|
|
return CHAMP_DE_CLE.get(cleEntete(entete)) ?? null;
|
|
}
|
|
|
|
/**
|
|
* Associe les colonnes aux champs par leurs en-têtes. Un rang de colonne est
|
|
* son indice dans entetes, à partir de 0.
|
|
*
|
|
* Un champ que reconnaît une seule colonne lui est associé. Un champ que
|
|
* reconnaissent plusieurs colonnes n'est associé à aucune et devient une
|
|
* ambiguïté, sauf quand choix[champ] désigne l'une d'elles : celle-là est
|
|
* associée, et l'ambiguïté levée. Un choix qui ne désigne pas l'une des
|
|
* colonnes en conflit d'un champ ambigu est sans effet.
|
|
*
|
|
* @param {string[]} entetes en-têtes tels qu'écrits
|
|
* @param {Object<string, number>} [choix] { champ: rang }
|
|
* @returns {{
|
|
* colonnes: Object<string, number>,
|
|
* ambigus: Array<{champ: string, rangs: number[]}>,
|
|
* nonReconnues: number[],
|
|
* }}
|
|
* colonnes : { champ: rang } des champs associés, dans l'ordre de CHAMPS ;
|
|
* ambigus : dans l'ordre de CHAMPS, rangs croissants ; nonReconnues : rangs
|
|
* croissants des colonnes dont l'en-tête est inconnu, vide compris.
|
|
*/
|
|
export function associer(entetes, choix = {}) {
|
|
const rangsDesChamps = new Map(CHAMPS.map((champ) => [champ, []]));
|
|
const nonReconnues = [];
|
|
entetes.forEach((entete, rang) => {
|
|
const champ = reconnaitre(entete);
|
|
if (champ === null) nonReconnues.push(rang);
|
|
else rangsDesChamps.get(champ).push(rang);
|
|
});
|
|
|
|
const colonnes = {};
|
|
const ambigus = [];
|
|
for (const champ of CHAMPS) {
|
|
const rangs = rangsDesChamps.get(champ);
|
|
if (rangs.length === 1) {
|
|
colonnes[champ] = rangs[0];
|
|
} else if (rangs.length > 1) {
|
|
if (Object.hasOwn(choix, champ) && rangs.includes(choix[champ])) colonnes[champ] = choix[champ];
|
|
else ambigus.push({ champ, rangs });
|
|
}
|
|
}
|
|
return { colonnes, ambigus, nonReconnues };
|
|
}
|