gestion_table_tournante_libre/src/csv/colonnes.js

88 lines
3.5 KiB
JavaScript
Raw Normal View History

// © 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 };
}