[ADD] engine: indicators measured by full recomputation
Full recomputation is the measure every displayed figure comes from (§ 5.10). It keeps the three collision units apart — cumulative occurrences, distinct pairs, their excess — splits table returns and repeated meetings into those the engine chose and those the reservations impose, and gives null, never zero, for an empty population. Checked: the small demonstration's perfect plan gives 8 met, A = 2, r = 6, d = 0.25 for all twelve; spread and concentrated plans give excess 0 and 4. --- FR --- [ADD] moteur : indicateurs mesurés par recalcul complet Le recalcul complet est la mesure dont sort tout chiffre affiché (§ 5.10). Il tient séparées les trois unités de collision — occurrences cumulées, paires distinctes, leur excédent —, partage les retours à une table et les rencontres répétées entre ceux que le moteur a choisis et ceux qu'imposent les réservations, et rend null, jamais zéro, pour une population vide. Vérifié : le plan parfait de la petite démonstration donne 8 rencontres, A = 2, r = 6, d = 0,25 pour les douze ; plans réparti et concentré donnent un excédent de 0 et de 4. Assisted-by: Claude Opus 5.5
This commit is contained in:
parent
4e31c8d3e5
commit
30045182e7
2 changed files with 885 additions and 0 deletions
323
src/moteur/indicateurs.js
Normal file
323
src/moteur/indicateurs.js
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
// Indicateurs d'un plan (§ 5.4), par recalcul complet : la mesure qui fait
|
||||
// foi, dont sortent les chiffres affichés et ceux du classement (§ 5.10). Le
|
||||
// plan se mesure entier, les N participants et les R tours ; les
|
||||
// réservations ne retirent rien de la mesure, elles séparent seulement, dans
|
||||
// les retours et les rencontres répétées, ce qu'elles imposent de ce que le
|
||||
// moteur choisit. Tout tableau par personne suit l'ordre de instance.ids.
|
||||
//
|
||||
// Une rencontre est une paire de personnes assises à la même table au même
|
||||
// tour : la réserve n'est pas une table, une table vide ou à un seul occupant
|
||||
// ne forme aucune paire, et personne ne se rencontre soi-même. Deux personnes
|
||||
// sans appartenance ne sont pas collègues. Un ensemble vide rend null, jamais
|
||||
// 0 : le taux de diversité de qui ne rencontre aucun affilié, le minimum et la
|
||||
// moyenne d'une population sans membre.
|
||||
|
||||
/**
|
||||
* @typedef {Object} Agregat
|
||||
* @property {number|null} min null pour une population vide
|
||||
* @property {number|null} moyenne null pour une population vide
|
||||
* @property {number} effectif
|
||||
*
|
||||
* @typedef {Object} TroisAgregats
|
||||
* @property {Agregat} tous
|
||||
* @property {Agregat} mobiles mobiles et partiellement fixés (§ 5.4)
|
||||
* @property {Agregat} ancres
|
||||
*
|
||||
* @typedef {Object} Mesures
|
||||
* @property {number[]} rencontres |met(p)|, ordre canonique
|
||||
* @property {number[]} affilies |F(p)|
|
||||
* @property {number[]} appartenancesVues A(p) — appartenances des affiliés
|
||||
* rencontrés, la sienne comprise quand
|
||||
* un collègue est rencontré
|
||||
* @property {number[]} redondance r(p) = |F(p)| − A(p)
|
||||
* @property {(number|null)[]} diversite d(p) = A(p) / |F(p)|, null si |F(p)| = 0
|
||||
* @property {number[]} toursAssis
|
||||
* @property {number[]} retoursChoisis par personne
|
||||
* @property {number[]} retoursImposes par personne
|
||||
* @property {TroisAgregats} aggRencontres
|
||||
* @property {TroisAgregats} aggRedondance
|
||||
* @property {number} collisionsCumulees Σ paires de même groupe × tours ensemble
|
||||
* @property {number} pairesDistinctes paires de même groupe réunies ≥ 1 fois
|
||||
* @property {number} excedentCollisions cumulées − distinctes
|
||||
* @property {{choisies: number, imposees: number}} rencontresRepetees
|
||||
* paires réunies ≥ 2 fois ; « imposée » quand chaque rencontre de la
|
||||
* paire a lieu à une table où les deux sont réservés ce tour-là
|
||||
* @property {number} maxRencontresPaire sur toutes les paires ; 0 si aucune
|
||||
* @property {number} totalRetoursChoisis
|
||||
* @property {number} totalRetoursImposes
|
||||
* @property {Array<{groupe: string, effectif: number, collisionsCumulees: number,
|
||||
* pairesDistinctes: number, excedent: number}>} parGroupe
|
||||
* ordre de instance.groupes
|
||||
*/
|
||||
|
||||
// Case d'un plan indexé : la réserve.
|
||||
const RESERVE = -1;
|
||||
// instance.groupe d'un participant sans appartenance.
|
||||
const SANS_GROUPE = -1;
|
||||
// instance.statut d'un ancré ; 0 marque un mobile, 1 un partiellement fixé.
|
||||
const ANCRE = 2;
|
||||
// Plus grand compte que tient un Uint16Array.
|
||||
const UINT16_MAX = 0xffff;
|
||||
|
||||
// Valeur citée dans un message : une chaîne s'écrit entre guillemets, pour
|
||||
// ne pas se lire comme le nombre qu'elle contient.
|
||||
const decrire = (valeur) => (typeof valeur === 'string' ? JSON.stringify(valeur) : String(valeur));
|
||||
|
||||
// Lève RangeError quand tableDe n'a pas N × R cases, ou qu'une case n'est ni
|
||||
// −1 ni un index de table entier ; le message nomme alors le participant et
|
||||
// le tour. Sans cette garde, deux écarts passeraient sans bruit : une case de
|
||||
// trop serait ignorée ; une chaîne numérique comme "0" assiérait la personne
|
||||
// à la table qu'elle désigne, mais compterRetours, qui la compare strictement
|
||||
// à instance.fixe, n'y reconnaîtrait pas sa réservation. Une case manquante
|
||||
// ou toute autre valeur lèverait dans compterRencontres une TypeError qui ne
|
||||
// nomme ni la personne ni le tour.
|
||||
function exigerForme({ N, T, R, ids }, tableDe) {
|
||||
if (tableDe.length !== N * R) {
|
||||
throw new RangeError(`plan indexé de ${tableDe.length} cases, N × R = ${N * R} attendues`);
|
||||
}
|
||||
for (let p = 0; p < N; p += 1) {
|
||||
for (let r = 0; r < R; r += 1) {
|
||||
const t = tableDe[p * R + r];
|
||||
if (t !== RESERVE && !(Number.isInteger(t) && t >= 0 && t < T)) {
|
||||
throw new RangeError(
|
||||
`plan indexé : participant ${ids[p]}, tour ${r + 1}, `
|
||||
+ `index de table ${decrire(t)} hors de −1..${T - 1}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rang de la paire (a, b), a < b, dans le triangle supérieur d'une matrice
|
||||
// N × N rangé ligne par ligne : la ligne a s'ouvre après les
|
||||
// (N − 1) + (N − 2) + … + (N − a) paires des lignes qui la précèdent.
|
||||
const rangPaire = (N, a, b) => (a * (2 * N - a - 1)) / 2 + (b - a - 1);
|
||||
|
||||
// Rencontres de chaque paire, rangées par rangPaire, en parcourant chaque
|
||||
// table de chaque tour. choisie vaut 1 pour une paire dont une rencontre au
|
||||
// moins a lieu hors d'une table où les deux sont réservés ce tour-là. Une
|
||||
// paire se réunit au plus une fois par tour : R borne chaque compte, qu'un
|
||||
// Uint16Array tient tant que R ≤ 65 535.
|
||||
function compterRencontres({ N, T, R, fixe }, tableDe) {
|
||||
const paires = (N * (N - 1)) / 2;
|
||||
const compte = R <= UINT16_MAX ? new Uint16Array(paires) : new Uint32Array(paires);
|
||||
const choisie = new Uint8Array(paires);
|
||||
for (let r = 0; r < R; r += 1) {
|
||||
// Occupants de chaque table, par index croissants : a < b pour toute
|
||||
// paire (liste[i], liste[j]) avec i < j. La boucle intérieure part du
|
||||
// voisin suivant : une liste vide ou d'un seul occupant ne forme aucune
|
||||
// paire, et nul n'est apparié à lui-même.
|
||||
const assis = Array.from({ length: T }, () => []);
|
||||
for (let p = 0; p < N; p += 1) {
|
||||
const t = tableDe[p * R + r];
|
||||
if (t !== RESERVE) assis[t].push(p);
|
||||
}
|
||||
for (let t = 0; t < T; t += 1) {
|
||||
const liste = assis[t];
|
||||
for (let i = 0; i < liste.length; i += 1) {
|
||||
const a = liste[i];
|
||||
const aReserve = fixe[a * R + r] === t;
|
||||
for (let j = i + 1; j < liste.length; j += 1) {
|
||||
const b = liste[j];
|
||||
const k = rangPaire(N, a, b);
|
||||
compte[k] += 1;
|
||||
if (!aReserve || fixe[b * R + r] !== t) choisie[k] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { compte, choisie };
|
||||
}
|
||||
|
||||
// Lecture du triangle des paires, dans l'ordre de rangPaire. Par personne :
|
||||
// |met(p)|, |F(p)| et A(p), vu[p * G + g] notant que p a déjà croisé un
|
||||
// affilié du groupe g. Par groupe : une paire de collègues réunie n fois
|
||||
// porte n collisions cumulées et une paire distincte. Sur toutes les paires :
|
||||
// les rencontres répétées, choisies ou imposées, et le plus grand compte.
|
||||
function lirePaires({ N, groupe, groupes }, { compte, choisie }) {
|
||||
const G = groupes.length;
|
||||
const rencontres = new Int32Array(N);
|
||||
const affilies = new Int32Array(N);
|
||||
const vues = new Int32Array(N);
|
||||
const vu = new Uint8Array(N * G);
|
||||
const collisions = new Array(G).fill(0);
|
||||
const distinctes = new Array(G).fill(0);
|
||||
let choisies = 0;
|
||||
let imposees = 0;
|
||||
let maxRencontresPaire = 0;
|
||||
|
||||
// p rencontre q : q compte dans met(p) ; un affilié compte aussi dans F(p),
|
||||
// et son groupe compte dans A(p) au premier membre de ce groupe que p
|
||||
// rencontre.
|
||||
const rencontrer = (p, q) => {
|
||||
rencontres[p] += 1;
|
||||
const g = groupe[q];
|
||||
if (g === SANS_GROUPE) return;
|
||||
affilies[p] += 1;
|
||||
if (vu[p * G + g] === 0) {
|
||||
vu[p * G + g] = 1;
|
||||
vues[p] += 1;
|
||||
}
|
||||
};
|
||||
|
||||
let k = 0;
|
||||
for (let a = 0; a < N; a += 1) {
|
||||
for (let b = a + 1; b < N; b += 1, k += 1) {
|
||||
const n = compte[k];
|
||||
if (n === 0) continue;
|
||||
rencontrer(a, b);
|
||||
rencontrer(b, a);
|
||||
if (n > maxRencontresPaire) maxRencontresPaire = n;
|
||||
if (n >= 2) {
|
||||
if (choisie[k] === 1) choisies += 1;
|
||||
else imposees += 1;
|
||||
}
|
||||
const g = groupe[a];
|
||||
if (g !== SANS_GROUPE && g === groupe[b]) {
|
||||
collisions[g] += n;
|
||||
distinctes[g] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
rencontres,
|
||||
affilies,
|
||||
vues,
|
||||
collisions,
|
||||
distinctes,
|
||||
rencontresRepetees: { choisies, imposees },
|
||||
maxRencontresPaire,
|
||||
};
|
||||
}
|
||||
|
||||
// Tours assis et retours de chaque personne (§ 5.4). Pour une table visitée
|
||||
// m fois, dont f à un tour où la personne y est réservée : max(0, f − 1)
|
||||
// retours imposés et (m − 1) − imposés retours choisis. visites et reservees
|
||||
// comptent par table les tours de la personne en cours ; le second parcours
|
||||
// de ses tours solde chaque table à sa première occurrence et remet ses
|
||||
// compteurs à zéro, si bien que la personne suivante les trouve nuls.
|
||||
function compterRetours({ N, T, R, fixe }, tableDe) {
|
||||
const toursAssis = [];
|
||||
const retoursChoisis = [];
|
||||
const retoursImposes = [];
|
||||
const visites = new Int32Array(T);
|
||||
const reservees = new Int32Array(T);
|
||||
for (let p = 0; p < N; p += 1) {
|
||||
let assis = 0;
|
||||
for (let r = 0; r < R; r += 1) {
|
||||
const t = tableDe[p * R + r];
|
||||
if (t === RESERVE) continue;
|
||||
assis += 1;
|
||||
visites[t] += 1;
|
||||
if (fixe[p * R + r] === t) reservees[t] += 1;
|
||||
}
|
||||
let choisis = 0;
|
||||
let imposes = 0;
|
||||
for (let r = 0; r < R; r += 1) {
|
||||
const t = tableDe[p * R + r];
|
||||
if (t === RESERVE || visites[t] === 0) continue;
|
||||
const imposesTable = Math.max(0, reservees[t] - 1);
|
||||
imposes += imposesTable;
|
||||
choisis += visites[t] - 1 - imposesTable;
|
||||
visites[t] = 0;
|
||||
reservees[t] = 0;
|
||||
}
|
||||
toursAssis.push(assis);
|
||||
retoursChoisis.push(choisis);
|
||||
retoursImposes.push(imposes);
|
||||
}
|
||||
return { toursAssis, retoursChoisis, retoursImposes };
|
||||
}
|
||||
|
||||
// Ajoute une valeur à une population. min reste null tant que la population
|
||||
// est vide : aucun Infinity ne sert de minimum initial.
|
||||
function ajouter(population, valeur) {
|
||||
if (population.min === null || valeur < population.min) population.min = valeur;
|
||||
population.somme += valeur;
|
||||
population.effectif += 1;
|
||||
}
|
||||
|
||||
// Agrégat d'une population ; la moyenne d'une population vide vaut null.
|
||||
const conclure = ({ min, somme, effectif }) => ({
|
||||
min,
|
||||
moyenne: effectif === 0 ? null : somme / effectif,
|
||||
effectif,
|
||||
});
|
||||
|
||||
// Minimum, moyenne et effectif de valeurs[p] sur les trois populations, en un
|
||||
// passage : chacun compte dans « tous », puis parmi les ancrés ou parmi les
|
||||
// mobiles, qui comprennent les partiellement fixés (§ 5.4).
|
||||
function troisAgregats(valeurs, statut) {
|
||||
const tous = { min: null, somme: 0, effectif: 0 };
|
||||
const mobiles = { min: null, somme: 0, effectif: 0 };
|
||||
const ancres = { min: null, somme: 0, effectif: 0 };
|
||||
for (let p = 0; p < valeurs.length; p += 1) {
|
||||
ajouter(tous, valeurs[p]);
|
||||
ajouter(statut[p] === ANCRE ? ancres : mobiles, valeurs[p]);
|
||||
}
|
||||
return { tous: conclure(tous), mobiles: conclure(mobiles), ancres: conclure(ancres) };
|
||||
}
|
||||
|
||||
const somme = (valeurs) => valeurs.reduce((total, valeur) => total + valeur, 0);
|
||||
|
||||
/**
|
||||
* Mesure complète d'un plan indexé : la mesure qui fait foi (§ 5.10). Ne
|
||||
* modifie ni l'instance ni le plan. Lève RangeError quand tableDe n'a pas
|
||||
* N × R cases, ou qu'une case n'est ni −1 ni un index de table.
|
||||
*
|
||||
* @param {import('./types.js').Instance} instance
|
||||
* @param {ArrayLike<number>} tableDe tableDe[p * R + r] = index de table, −1 pour la réserve
|
||||
* @returns {Mesures}
|
||||
*/
|
||||
export function mesurer(instance, tableDe) {
|
||||
exigerForme(instance, tableDe);
|
||||
const { N, groupe, groupes, statut } = instance;
|
||||
const paires = lirePaires(instance, compterRencontres(instance, tableDe));
|
||||
const { collisions, distinctes } = paires;
|
||||
|
||||
const rencontres = Array.from(paires.rencontres);
|
||||
const affilies = Array.from(paires.affilies);
|
||||
const appartenancesVues = Array.from(paires.vues);
|
||||
const redondance = affilies.map((f, p) => f - appartenancesVues[p]);
|
||||
const diversite = affilies.map((f, p) => (f === 0 ? null : appartenancesVues[p] / f));
|
||||
|
||||
const effectifs = new Array(groupes.length).fill(0);
|
||||
for (let p = 0; p < N; p += 1) {
|
||||
if (groupe[p] !== SANS_GROUPE) effectifs[groupe[p]] += 1;
|
||||
}
|
||||
const parGroupe = groupes.map((libelle, g) => ({
|
||||
groupe: libelle,
|
||||
effectif: effectifs[g],
|
||||
collisionsCumulees: collisions[g],
|
||||
pairesDistinctes: distinctes[g],
|
||||
excedent: collisions[g] - distinctes[g],
|
||||
}));
|
||||
const collisionsCumulees = somme(collisions);
|
||||
const pairesDistinctes = somme(distinctes);
|
||||
|
||||
const { toursAssis, retoursChoisis, retoursImposes } = compterRetours(instance, tableDe);
|
||||
|
||||
return {
|
||||
rencontres,
|
||||
affilies,
|
||||
appartenancesVues,
|
||||
redondance,
|
||||
diversite,
|
||||
toursAssis,
|
||||
retoursChoisis,
|
||||
retoursImposes,
|
||||
aggRencontres: troisAgregats(rencontres, statut),
|
||||
aggRedondance: troisAgregats(redondance, statut),
|
||||
collisionsCumulees,
|
||||
pairesDistinctes,
|
||||
excedentCollisions: collisionsCumulees - pairesDistinctes,
|
||||
rencontresRepetees: paires.rencontresRepetees,
|
||||
maxRencontresPaire: paires.maxRencontresPaire,
|
||||
totalRetoursChoisis: somme(retoursChoisis),
|
||||
totalRetoursImposes: somme(retoursImposes),
|
||||
parGroupe,
|
||||
};
|
||||
}
|
||||
562
src/moteur/indicateurs.test.js
Normal file
562
src/moteur/indicateurs.test.js
Normal file
|
|
@ -0,0 +1,562 @@
|
|||
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
// Épreuves des indicateurs (§ 5.4, § 12.6, § 14.10). Chaque valeur attendue
|
||||
// est écrite en clair : recopiée de la spécification, ou comptée à la main
|
||||
// sur le plan qui la précède, jamais calculée par le module éprouvé. Un plan
|
||||
// s'écrit par identifiants, une liste par table, et passe par normaliser et
|
||||
// indexerPlan comme un plan enregistré.
|
||||
import assert from 'node:assert/strict';
|
||||
import { describe, test } from '../../test/lanceur.js';
|
||||
import { indexerPlan, normaliser } from './configuration.js';
|
||||
import { mesurer } from './indicateurs.js';
|
||||
|
||||
const SANS_CONTRAINTE = {
|
||||
separerAppartenances: false,
|
||||
nouveauxVoisins: false,
|
||||
nouvelleTable: false,
|
||||
varierAppartenances: false,
|
||||
};
|
||||
|
||||
const tous = (participant, table) => ({ participant, table, portee: 'tous' });
|
||||
const auTour = (participant, table, tour) => ({ participant, table, portee: 'tour', tour });
|
||||
|
||||
// Agrégat d'une population vide.
|
||||
const AUCUN = { min: null, moyenne: null, effectif: 0 };
|
||||
|
||||
// Appartenances des participants 1..N : le libellé du groupe qui contient
|
||||
// l'identifiant, null hors de tout groupe. groupes est une liste de couples
|
||||
// [libellé, identifiants].
|
||||
function appartenances(N, groupes) {
|
||||
return Array.from({ length: N }, (_, i) => {
|
||||
const groupe = groupes.find(([, membres]) => membres.includes(i + 1));
|
||||
return groupe === undefined ? null : groupe[0];
|
||||
});
|
||||
}
|
||||
|
||||
// Instance et plan indexé d'un plan écrit par identifiants. Le participant
|
||||
// i + 1 porte appartenance[i] ; les tables portent les identifiants 1..T,
|
||||
// toutes de la capacité donnée ; tours[r][i] liste les participants assis à
|
||||
// la table i + 1 au tour r + 1, et reserves[r] ceux qui n'y sont assis nulle
|
||||
// part.
|
||||
function preparer({
|
||||
appartenance,
|
||||
capacite,
|
||||
tours,
|
||||
reserves = tours.map(() => []),
|
||||
reservations = [],
|
||||
}) {
|
||||
const participants = appartenance.map((libelle, i) => ({
|
||||
id: i + 1,
|
||||
nom: `P${i + 1}`,
|
||||
appartenance: libelle,
|
||||
}));
|
||||
const tables = tours[0].map((_, i) => ({ id: i + 1, numero: i + 1, capacite }));
|
||||
const instance = normaliser({
|
||||
participants,
|
||||
tables,
|
||||
tours: tours.length,
|
||||
reservations,
|
||||
contraintes: SANS_CONTRAINTE,
|
||||
});
|
||||
const plan = { tables: tables.map(({ id }) => id), tours, reserves };
|
||||
return { instance, tableDe: indexerPlan(instance, plan) };
|
||||
}
|
||||
|
||||
function mesurerPlan(description) {
|
||||
const { instance, tableDe } = preparer(description);
|
||||
return mesurer(instance, tableDe);
|
||||
}
|
||||
|
||||
// Plan parfait de la petite démonstration (§ 15.3) : quatre tables de trois
|
||||
// sièges, quatre tours, chaque table réunit à chaque tour un membre de A, de
|
||||
// B et de C.
|
||||
const PLAN_PARFAIT = [
|
||||
[[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]],
|
||||
[[6, 9, 12], [3, 8, 11], [2, 5, 10], [1, 4, 7]],
|
||||
[[4, 8, 10], [2, 7, 12], [1, 6, 11], [3, 5, 9]],
|
||||
[[5, 7, 11], [1, 9, 10], [3, 4, 12], [2, 6, 8]],
|
||||
];
|
||||
const PETITE = appartenances(12, [
|
||||
['A', [1, 5, 8, 12]],
|
||||
['B', [2, 4, 9, 11]],
|
||||
['C', [3, 6, 7, 10]],
|
||||
]);
|
||||
// Variante « conflit inévitable » : 10 passe de C à A.
|
||||
const VARIANTE = appartenances(12, [
|
||||
['A', [1, 5, 8, 10, 12]],
|
||||
['B', [2, 4, 9, 11]],
|
||||
['C', [3, 6, 7]],
|
||||
]);
|
||||
|
||||
// Trois tables de trois sièges, quatre tours. 1 est ancré à la table 1 ; 2
|
||||
// est mobile ; 3, réservé à la table 2 au tour 3, et 4, réservé à la table 3
|
||||
// aux tours 1 et 3, sont partiellement fixés. Chaque réservation est tenue.
|
||||
const PLAN_RESERVATIONS = {
|
||||
appartenance: [null, null, null, null],
|
||||
capacite: 3,
|
||||
tours: [
|
||||
[[1], [2, 3], [4]],
|
||||
[[1, 3], [], [2, 4]],
|
||||
[[1, 2], [3], [4]],
|
||||
[[1, 4], [2], [3]],
|
||||
],
|
||||
reservations: [tous(1, 1), auTour(3, 2, 3), auTour(4, 3, 1), auTour(4, 3, 3)],
|
||||
};
|
||||
|
||||
// Deux ancrés à la table 1, seule table, quatre tours.
|
||||
const DEUX_ANCRES = {
|
||||
appartenance: [null, null],
|
||||
capacite: 2,
|
||||
tours: [[[1, 2]], [[1, 2]], [[1, 2]], [[1, 2]]],
|
||||
reservations: [tous(1, 1), tous(2, 1)],
|
||||
};
|
||||
|
||||
describe('mesurer', () => {
|
||||
test('plan parfait de la petite démonstration : huit rencontres chacun, ni collision ni répétition', () => {
|
||||
// Aucun retour : chacun visite les quatre tables. Sans réservation, la
|
||||
// population des ancrés est vide.
|
||||
assert.deepEqual(mesurerPlan({ appartenance: PETITE, capacite: 3, tours: PLAN_PARFAIT }), {
|
||||
rencontres: new Array(12).fill(8),
|
||||
affilies: new Array(12).fill(8),
|
||||
appartenancesVues: new Array(12).fill(2),
|
||||
redondance: new Array(12).fill(6),
|
||||
diversite: new Array(12).fill(0.25),
|
||||
toursAssis: new Array(12).fill(4),
|
||||
retoursChoisis: new Array(12).fill(0),
|
||||
retoursImposes: new Array(12).fill(0),
|
||||
aggRencontres: {
|
||||
tous: { min: 8, moyenne: 8, effectif: 12 },
|
||||
mobiles: { min: 8, moyenne: 8, effectif: 12 },
|
||||
ancres: AUCUN,
|
||||
},
|
||||
aggRedondance: {
|
||||
tous: { min: 6, moyenne: 6, effectif: 12 },
|
||||
mobiles: { min: 6, moyenne: 6, effectif: 12 },
|
||||
ancres: AUCUN,
|
||||
},
|
||||
collisionsCumulees: 0,
|
||||
pairesDistinctes: 0,
|
||||
excedentCollisions: 0,
|
||||
rencontresRepetees: { choisies: 0, imposees: 0 },
|
||||
maxRencontresPaire: 1,
|
||||
totalRetoursChoisis: 0,
|
||||
totalRetoursImposes: 0,
|
||||
parGroupe: [
|
||||
{ groupe: 'A', effectif: 4, collisionsCumulees: 0, pairesDistinctes: 0, excedent: 0 },
|
||||
{ groupe: 'B', effectif: 4, collisionsCumulees: 0, pairesDistinctes: 0, excedent: 0 },
|
||||
{ groupe: 'C', effectif: 4, collisionsCumulees: 0, pairesDistinctes: 0, excedent: 0 },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('variante 5, 4, 3 : quatre collisions cumulées sur quatre paires distinctes, excédent nul', () => {
|
||||
const mesures = mesurerPlan({ appartenance: VARIANTE, capacite: 3, tours: PLAN_PARFAIT });
|
||||
const { collisionsCumulees, pairesDistinctes, excedentCollisions } = mesures;
|
||||
assert.deepEqual(
|
||||
{ collisionsCumulees, pairesDistinctes, excedentCollisions },
|
||||
{ collisionsCumulees: 4, pairesDistinctes: 4, excedentCollisions: 0 },
|
||||
);
|
||||
assert.deepEqual(mesures.parGroupe, [
|
||||
{ groupe: 'A', effectif: 5, collisionsCumulees: 4, pairesDistinctes: 4, excedent: 0 },
|
||||
{ groupe: 'B', effectif: 4, collisionsCumulees: 0, pairesDistinctes: 0, excedent: 0 },
|
||||
{ groupe: 'C', effectif: 3, collisionsCumulees: 0, pairesDistinctes: 0, excedent: 0 },
|
||||
]);
|
||||
// 1, 5, 8 et 12 croisent chacun 10, leur collègue : A(p) compte leur
|
||||
// propre appartenance avec les deux autres. 10 croise A et B, jamais C.
|
||||
assert.deepEqual(mesures.appartenancesVues, [3, 2, 2, 2, 3, 2, 2, 3, 2, 2, 2, 3]);
|
||||
});
|
||||
|
||||
test("l'excédent compte les retrouvailles de collègues au-delà de la première", () => {
|
||||
// Deux tables de quatre, trois tours ; X = {1, 2, 3, 4}, les autres sans
|
||||
// appartenance.
|
||||
const appartenance = appartenances(8, [['X', [1, 2, 3, 4]]]);
|
||||
const collisions = ({ collisionsCumulees, pairesDistinctes, excedentCollisions, parGroupe }) => ({
|
||||
collisionsCumulees,
|
||||
pairesDistinctes,
|
||||
excedentCollisions,
|
||||
parGroupe,
|
||||
});
|
||||
const reparti = mesurerPlan({
|
||||
appartenance,
|
||||
capacite: 4,
|
||||
tours: [
|
||||
[[1, 2, 5, 6], [3, 4, 7, 8]],
|
||||
[[1, 3, 5, 7], [2, 4, 6, 8]],
|
||||
[[1, 4, 5, 8], [2, 3, 6, 7]],
|
||||
],
|
||||
});
|
||||
assert.deepEqual(collisions(reparti), {
|
||||
collisionsCumulees: 6,
|
||||
pairesDistinctes: 6,
|
||||
excedentCollisions: 0,
|
||||
parGroupe: [{ groupe: 'X', effectif: 4, collisionsCumulees: 6, pairesDistinctes: 6, excedent: 0 }],
|
||||
});
|
||||
// 5 et 6, puis 7 et 8, se retrouvent aussi à chaque tour : deux personnes
|
||||
// sans appartenance ne sont pas des collègues.
|
||||
const concentre = mesurerPlan({
|
||||
appartenance,
|
||||
capacite: 4,
|
||||
tours: [
|
||||
[[1, 2, 5, 6], [3, 4, 7, 8]],
|
||||
[[1, 2, 5, 6], [3, 4, 7, 8]],
|
||||
[[1, 2, 5, 6], [3, 4, 7, 8]],
|
||||
],
|
||||
});
|
||||
assert.deepEqual(collisions(concentre), {
|
||||
collisionsCumulees: 6,
|
||||
pairesDistinctes: 2,
|
||||
excedentCollisions: 4,
|
||||
parGroupe: [{ groupe: 'X', effectif: 4, collisionsCumulees: 6, pairesDistinctes: 2, excedent: 4 }],
|
||||
});
|
||||
});
|
||||
|
||||
test("une table vide et une table à un seul occupant n'ajoutent aucune paire ni aucune rencontre de soi", () => {
|
||||
// Au tour 1, la table 3 est vide, 4 et 5 sont seuls à leur table. Au
|
||||
// tour 2, 5 l'est encore, et la table 1 est vide.
|
||||
const mesures = mesurerPlan({
|
||||
appartenance: ['X', 'X', 'Y', 'X', null],
|
||||
capacite: 3,
|
||||
tours: [
|
||||
[[1, 2, 3], [4], [], [5]],
|
||||
[[], [1, 4], [2, 3], [5]],
|
||||
],
|
||||
});
|
||||
assert.deepEqual(mesures.rencontres, [3, 2, 2, 1, 0]);
|
||||
assert.deepEqual(mesures.affilies, [3, 2, 2, 1, 0]);
|
||||
assert.deepEqual(mesures.appartenancesVues, [2, 2, 1, 1, 0]);
|
||||
assert.deepEqual(mesures.redondance, [1, 0, 1, 0, 0]);
|
||||
// 5 ne rencontre personne : son taux de diversité n'a pas de diviseur.
|
||||
assert.deepEqual(mesures.diversite, [2 / 3, 1, 1 / 2, 1, null]);
|
||||
// (1, 2) au tour 1 et (1, 4) au tour 2 ; 4, seul de X à sa table au tour
|
||||
// 1, ne s'y croise pas lui-même.
|
||||
assert.equal(mesures.collisionsCumulees, 2);
|
||||
// (2, 3), réunis aux deux tours, est la seule paire répétée.
|
||||
assert.equal(mesures.maxRencontresPaire, 2);
|
||||
assert.deepEqual(mesures.rencontresRepetees, { choisies: 1, imposees: 0 });
|
||||
});
|
||||
|
||||
test("F(p) ne retient que les affiliés : qui ne croise aucun affilié n'a pas de taux de diversité", () => {
|
||||
// 3, seul affilié de la table, rencontre deux personnes sans
|
||||
// appartenance ; chacune d'elles rencontre un seul affilié, 3.
|
||||
const mesures = mesurerPlan({ appartenance: [null, null, 'X'], capacite: 3, tours: [[[1, 2, 3]]] });
|
||||
assert.deepEqual(mesures.rencontres, [2, 2, 2]);
|
||||
assert.deepEqual(mesures.affilies, [1, 1, 0]);
|
||||
assert.deepEqual(mesures.appartenancesVues, [1, 1, 0]);
|
||||
assert.deepEqual(mesures.diversite, [1, 1, null]);
|
||||
// La redondance se lit sur les affiliés rencontrés, |F(p)| − A(p), et non
|
||||
// sur toutes les rencontres : personne ne recroise une appartenance.
|
||||
assert.deepEqual(mesures.redondance, [0, 0, 0]);
|
||||
assert.deepEqual(mesures.aggRedondance, {
|
||||
tous: { min: 0, moyenne: 0, effectif: 3 },
|
||||
mobiles: { min: 0, moyenne: 0, effectif: 3 },
|
||||
ancres: AUCUN,
|
||||
});
|
||||
});
|
||||
|
||||
test("la réserve n'est pas une table : seuls les tours assis comptent", () => {
|
||||
// 6 est en réserve aux tours 1 et 2, 5 au tour 2. Ils partagent
|
||||
// l'appartenance X et ne sont jamais assis ensemble.
|
||||
const mesures = mesurerPlan({
|
||||
appartenance: [null, null, null, null, 'X', 'X'],
|
||||
capacite: 3,
|
||||
tours: [
|
||||
[[1, 2, 5], [3, 4]],
|
||||
[[1, 3], [2, 4]],
|
||||
[[1, 4, 6], [2, 3, 5]],
|
||||
],
|
||||
reserves: [[6], [5, 6], []],
|
||||
});
|
||||
assert.deepEqual(mesures.toursAssis, [3, 3, 3, 3, 2, 1]);
|
||||
assert.deepEqual(mesures.rencontres, [5, 4, 4, 4, 3, 2]);
|
||||
assert.equal(mesures.collisionsCumulees, 0);
|
||||
// Deux passages en réserve ne font pas un retour.
|
||||
assert.deepEqual(mesures.retoursChoisis, [2, 1, 1, 1, 0, 0]);
|
||||
// Une personne en réserve à un tour reste dans les agrégats.
|
||||
assert.deepEqual(mesures.aggRencontres.tous, { min: 2, moyenne: 22 / 6, effectif: 6 });
|
||||
});
|
||||
|
||||
test('les retours imposés par les réservations se comptent à part des retours choisis', () => {
|
||||
// 1 visite quatre fois la table 1, réservée à chaque tour : trois retours
|
||||
// imposés. 2 visite deux fois la table 2, sans réservation : un retour
|
||||
// choisi. 3 est placé à la table 2 au tour 1, puis y revient au tour 3 où
|
||||
// il y est réservé : un retour choisi. 4 visite trois fois la table 3,
|
||||
// dont deux réservées : un retour imposé et un choisi.
|
||||
const mesures = mesurerPlan(PLAN_RESERVATIONS);
|
||||
assert.deepEqual(mesures.retoursImposes, [3, 0, 0, 1]);
|
||||
assert.deepEqual(mesures.retoursChoisis, [0, 1, 1, 1]);
|
||||
assert.equal(mesures.totalRetoursImposes, 4);
|
||||
assert.equal(mesures.totalRetoursChoisis, 3);
|
||||
});
|
||||
|
||||
test('les agrégats portent sur tous et rangent les partiellement fixés parmi les mobiles', () => {
|
||||
// 1 est ancré, 2 mobile, 3 et 4 partiellement fixés.
|
||||
const mesures = mesurerPlan(PLAN_RESERVATIONS);
|
||||
assert.deepEqual(mesures.rencontres, [3, 3, 2, 2]);
|
||||
assert.deepEqual(mesures.aggRencontres, {
|
||||
tous: { min: 2, moyenne: 10 / 4, effectif: 4 },
|
||||
mobiles: { min: 2, moyenne: 7 / 3, effectif: 3 },
|
||||
ancres: { min: 3, moyenne: 3, effectif: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
test('une population vide rend null, jamais 0 ni Infinity', () => {
|
||||
const sansAncre = mesurerPlan({ appartenance: PETITE, capacite: 3, tours: PLAN_PARFAIT });
|
||||
assert.deepEqual(sansAncre.aggRencontres.ancres, AUCUN);
|
||||
assert.deepEqual(sansAncre.aggRedondance.ancres, AUCUN);
|
||||
const sansMobile = mesurerPlan(DEUX_ANCRES);
|
||||
assert.deepEqual(sansMobile.aggRencontres.mobiles, AUCUN);
|
||||
assert.deepEqual(sansMobile.aggRedondance.mobiles, AUCUN);
|
||||
});
|
||||
|
||||
test('deux ancrés à la même table : une rencontre répétée, imposée et non choisie', () => {
|
||||
const mesures = mesurerPlan(DEUX_ANCRES);
|
||||
assert.deepEqual(mesures.rencontresRepetees, { choisies: 0, imposees: 1 });
|
||||
assert.equal(mesures.maxRencontresPaire, 4);
|
||||
assert.deepEqual(mesures.retoursImposes, [3, 3]);
|
||||
assert.deepEqual(mesures.retoursChoisis, [0, 0]);
|
||||
});
|
||||
|
||||
test('une seule rencontre hors réservation rend la répétition choisie', () => {
|
||||
// 1 et 2 sont ancrés à la table 1 ; 3 y est réservé au seul tour 1 et y
|
||||
// reste au tour 2 ; 4 est mobile. (1, 2) se réunit quatre fois, chaque
|
||||
// fois réservés tous deux ; (1, 3) et (2, 3) au tour 1 réservés, au tour
|
||||
// 2 non ; (3, 4) aux tours 3 et 4, sans réservation.
|
||||
const mesures = mesurerPlan({
|
||||
appartenance: [null, null, null, null],
|
||||
capacite: 3,
|
||||
tours: [
|
||||
[[1, 2, 3], [4]],
|
||||
[[1, 2, 3], [4]],
|
||||
[[1, 2], [3, 4]],
|
||||
[[1, 2], [3, 4]],
|
||||
],
|
||||
reservations: [tous(1, 1), tous(2, 1), auTour(3, 1, 1)],
|
||||
});
|
||||
assert.deepEqual(mesures.rencontresRepetees, { choisies: 3, imposees: 1 });
|
||||
});
|
||||
|
||||
describe('une rencontre hors réservation rend la répétition choisie, quels que soient son rang et le membre non réservé', () => {
|
||||
// 1 et 2 sont assis ensemble à la table 1 aux trois tours. L'un y est
|
||||
// ancré ; l'autre n'y est réservé qu'aux deux tours autres que « libre ».
|
||||
// La rencontre de ce tour-là n'est réservée que pour l'ancré : la
|
||||
// répétition est choisie, que cette rencontre soit la première, celle du
|
||||
// milieu ou la dernière, et que le membre non réservé soit le premier de
|
||||
// la paire ou le second. Le partiellement fixé visite trois fois la
|
||||
// table, dont deux réservées : un retour imposé, un choisi. L'ancré,
|
||||
// réservé aux trois visites, compte deux retours imposés. Chaque cas est
|
||||
// une épreuve : un écart nomme chacun des cas qu'il touche.
|
||||
const cas = [
|
||||
{ libre: 1, partiel: 1, ancre: 2, retoursImposes: [1, 2], retoursChoisis: [1, 0] },
|
||||
{ libre: 2, partiel: 1, ancre: 2, retoursImposes: [1, 2], retoursChoisis: [1, 0] },
|
||||
{ libre: 3, partiel: 1, ancre: 2, retoursImposes: [1, 2], retoursChoisis: [1, 0] },
|
||||
{ libre: 1, partiel: 2, ancre: 1, retoursImposes: [2, 1], retoursChoisis: [0, 1] },
|
||||
{ libre: 2, partiel: 2, ancre: 1, retoursImposes: [2, 1], retoursChoisis: [0, 1] },
|
||||
{ libre: 3, partiel: 2, ancre: 1, retoursImposes: [2, 1], retoursChoisis: [0, 1] },
|
||||
];
|
||||
for (const { libre, partiel, ancre, retoursImposes, retoursChoisis } of cas) {
|
||||
test(`rencontre du tour ${libre} réservée pour ${ancre} seul`, () => {
|
||||
const mesures = mesurerPlan({
|
||||
appartenance: [null, null],
|
||||
capacite: 2,
|
||||
tours: [[[1, 2]], [[1, 2]], [[1, 2]]],
|
||||
reservations: [
|
||||
tous(ancre, 1),
|
||||
...[1, 2, 3].filter((tour) => tour !== libre).map((tour) => auTour(partiel, 1, tour)),
|
||||
],
|
||||
});
|
||||
assert.deepEqual(mesures.rencontresRepetees, { choisies: 1, imposees: 0 });
|
||||
assert.deepEqual(mesures.retoursImposes, retoursImposes);
|
||||
assert.deepEqual(mesures.retoursChoisis, retoursChoisis);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('une répétition imposée se lit sur les réservations du tour des deux membres, ancrés ou non', () => {
|
||||
// 2 est ancré à la table 1 ; 3 et 4, réservés à la table 2 aux tours 2 et
|
||||
// 3, sont partiellement fixés ; 1 est mobile. (1, 2) se réunit aux trois
|
||||
// tours : 1, le premier membre de la paire, n'est jamais réservé, la
|
||||
// répétition est choisie. (3, 4) se réunit aux tours 2 et 3, chaque fois
|
||||
// à la table où tous deux sont réservés ce tour-là : la répétition est
|
||||
// imposée, bien qu'aucun des deux ne soit ancré et que 3 ne soit réservé
|
||||
// nulle part au tour 1.
|
||||
const mesures = mesurerPlan({
|
||||
appartenance: [null, null, null, null],
|
||||
capacite: 3,
|
||||
tours: [
|
||||
[[1, 2, 4], [3]],
|
||||
[[1, 2], [3, 4]],
|
||||
[[1, 2], [3, 4]],
|
||||
],
|
||||
reservations: [tous(2, 1), auTour(3, 2, 2), auTour(3, 2, 3), auTour(4, 2, 2), auTour(4, 2, 3)],
|
||||
});
|
||||
assert.deepEqual(mesures.rencontresRepetees, { choisies: 1, imposees: 1 });
|
||||
});
|
||||
|
||||
test("une visite n'est réservée qu'à la table de la réservation : un plan qui ne la tient pas n'impose rien", () => {
|
||||
// 1 et 2 sont ancrés à la table 1 ; le plan les assoit ensemble à la
|
||||
// table 2 aux deux tours. Un tel plan, en dérive, se conserve et se
|
||||
// mesure (§ 9) : la réservation qu'il ne tient pas n'impose ni leurs
|
||||
// retours ni leur répétition, qui sont choisis.
|
||||
const mesures = mesurerPlan({
|
||||
appartenance: [null, null],
|
||||
capacite: 2,
|
||||
tours: [
|
||||
[[], [1, 2]],
|
||||
[[], [1, 2]],
|
||||
],
|
||||
reservations: [tous(1, 1), tous(2, 1)],
|
||||
});
|
||||
assert.deepEqual(mesures.rencontresRepetees, { choisies: 1, imposees: 0 });
|
||||
assert.deepEqual(mesures.retoursImposes, [0, 0]);
|
||||
assert.deepEqual(mesures.retoursChoisis, [1, 1]);
|
||||
});
|
||||
|
||||
test("un seul membre réservé ailleurs suffit à rendre la répétition choisie : réservé ce tour-là ne suffit pas, il faut l'être à cette table", () => {
|
||||
// Deux plans en dérive, la paire assise à la table 2 aux deux tours. Dans
|
||||
// le premier, le premier membre est réservé à la table 1 et le second à la
|
||||
// table 2 ; dans le second, l'inverse. Chacun des deux membres est donc
|
||||
// tour à tour celui qui n'est pas réservé à la table de la rencontre.
|
||||
for (const reservations of [[tous(1, 1), tous(2, 2)], [tous(1, 2), tous(2, 1)]]) {
|
||||
const mesures = mesurerPlan({
|
||||
appartenance: [null, null],
|
||||
capacite: 2,
|
||||
tours: [
|
||||
[[], [1, 2]],
|
||||
[[], [1, 2]],
|
||||
],
|
||||
reservations,
|
||||
});
|
||||
assert.deepEqual(
|
||||
mesures.rencontresRepetees,
|
||||
{ choisies: 1, imposees: 0 },
|
||||
JSON.stringify(reservations),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("le compte d'une paire tient 65 536 tours", () => {
|
||||
const R = 65_536;
|
||||
const instance = normaliser({
|
||||
participants: [
|
||||
{ id: 1, nom: 'P1', appartenance: null },
|
||||
{ id: 2, nom: 'P2', appartenance: null },
|
||||
],
|
||||
tables: [{ id: 1, numero: 1, capacite: 2 }],
|
||||
tours: R,
|
||||
reservations: [],
|
||||
contraintes: SANS_CONTRAINTE,
|
||||
});
|
||||
// Toutes les cases valent 0 : les deux participants partagent la table
|
||||
// d'index 0 à chaque tour.
|
||||
const mesures = mesurer(instance, new Int32Array(2 * R));
|
||||
assert.equal(mesures.maxRencontresPaire, R);
|
||||
assert.deepEqual(mesures.rencontres, [1, 1]);
|
||||
assert.deepEqual(mesures.rencontresRepetees, { choisies: 1, imposees: 0 });
|
||||
});
|
||||
|
||||
test('personne à la même table : aucune rencontre, maximum par paire à 0', () => {
|
||||
const mesures = mesurerPlan({
|
||||
appartenance: ['X', 'X'],
|
||||
capacite: 2,
|
||||
tours: [
|
||||
[[1], [2]],
|
||||
[[2], [1]],
|
||||
],
|
||||
});
|
||||
assert.deepEqual(mesures.rencontres, [0, 0]);
|
||||
assert.deepEqual(mesures.diversite, [null, null]);
|
||||
assert.equal(mesures.maxRencontresPaire, 0);
|
||||
assert.equal(mesures.collisionsCumulees, 0);
|
||||
// L'effectif d'un groupe compte tous ses membres, qu'ils rencontrent
|
||||
// quelqu'un ou non.
|
||||
assert.deepEqual(mesures.parGroupe, [
|
||||
{ groupe: 'X', effectif: 2, collisionsCumulees: 0, pairesDistinctes: 0, excedent: 0 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("parGroupe suit l'ordre de instance.groupes, non l'ordre alphabétique", () => {
|
||||
// instance.groupes range les appartenances par première apparition, en
|
||||
// parcourant les identifiants croissants : Y, portée par 1, puis X, par
|
||||
// 2. 1 et 3, tous deux de Y, se rencontrent une fois.
|
||||
const mesures = mesurerPlan({ appartenance: ['Y', 'X', 'Y'], capacite: 3, tours: [[[1, 2, 3]]] });
|
||||
assert.deepEqual(mesures.parGroupe, [
|
||||
{ groupe: 'Y', effectif: 2, collisionsCumulees: 1, pairesDistinctes: 1, excedent: 0 },
|
||||
{ groupe: 'X', effectif: 1, collisionsCumulees: 0, pairesDistinctes: 0, excedent: 0 },
|
||||
]);
|
||||
});
|
||||
|
||||
test('sans participant présent : tableaux vides, agrégats à null, compteurs à 0', () => {
|
||||
const instance = normaliser({
|
||||
participants: [{ id: 1, nom: 'P1', appartenance: 'X', exclu: true }],
|
||||
tables: [{ id: 1, numero: 1, capacite: 2 }],
|
||||
tours: 2,
|
||||
reservations: [],
|
||||
contraintes: SANS_CONTRAINTE,
|
||||
});
|
||||
assert.deepEqual(mesurer(instance, new Int32Array(0)), {
|
||||
rencontres: [],
|
||||
affilies: [],
|
||||
appartenancesVues: [],
|
||||
redondance: [],
|
||||
diversite: [],
|
||||
toursAssis: [],
|
||||
retoursChoisis: [],
|
||||
retoursImposes: [],
|
||||
aggRencontres: { tous: AUCUN, mobiles: AUCUN, ancres: AUCUN },
|
||||
aggRedondance: { tous: AUCUN, mobiles: AUCUN, ancres: AUCUN },
|
||||
collisionsCumulees: 0,
|
||||
pairesDistinctes: 0,
|
||||
excedentCollisions: 0,
|
||||
rencontresRepetees: { choisies: 0, imposees: 0 },
|
||||
maxRencontresPaire: 0,
|
||||
totalRetoursChoisis: 0,
|
||||
totalRetoursImposes: 0,
|
||||
parGroupe: [],
|
||||
});
|
||||
});
|
||||
|
||||
test("un plan indexé qui n'a pas la forme de l'instance lève RangeError, qui nomme le participant et le tour", () => {
|
||||
const { instance, tableDe } = preparer({ appartenance: PETITE, capacite: 3, tours: PLAN_PARFAIT });
|
||||
// Douze participants, quatre tours : 48 cases, ni une de moins ni une de
|
||||
// trop.
|
||||
assert.throws(() => mesurer(instance, tableDe.subarray(1)), RangeError);
|
||||
assert.throws(() => mesurer(instance, Int32Array.of(...tableDe, 0)), RangeError);
|
||||
// Quatre tables : une case vaut −1 ou un index entier de 0 à 3. Les
|
||||
// chaînes "0" et "-1" ne sont ni l'un ni l'autre : une comparaison qui
|
||||
// convertit lirait l'une comme la table d'index 0, l'autre comme la
|
||||
// réserve. Chaque intrus se pose à la première case, à une case du milieu
|
||||
// et à la dernière.
|
||||
for (const intrus of [4, -2, 1.5, '0', '-1']) {
|
||||
for (const k of [0, 5, 47]) {
|
||||
const fautif = Array.from(tableDe);
|
||||
fautif[k] = intrus;
|
||||
assert.throws(() => mesurer(instance, fautif), RangeError, `case ${k} à ${JSON.stringify(intrus)}`);
|
||||
}
|
||||
}
|
||||
// La case p × R + r est celle du participant d'index p au tour r + 1 : la
|
||||
// dernière est celle de 12 au tour 4. Le message cite une chaîne entre
|
||||
// guillemets, pour qu'elle ne se lise pas comme l'index qu'elle contient.
|
||||
const fautif = Array.from(tableDe);
|
||||
fautif[47] = '0';
|
||||
assert.throws(() => mesurer(instance, fautif), {
|
||||
name: 'RangeError',
|
||||
message: /participant 12, tour 4, index de table "0" hors de −1\.\.3/,
|
||||
});
|
||||
});
|
||||
|
||||
test("mesurer ne modifie ni l'instance ni le plan", () => {
|
||||
const { instance, tableDe } = preparer(PLAN_RESERVATIONS);
|
||||
const avant = {
|
||||
tableDe: tableDe.slice(),
|
||||
fixe: instance.fixe.slice(),
|
||||
statut: instance.statut.slice(),
|
||||
groupe: instance.groupe.slice(),
|
||||
};
|
||||
mesurer(instance, tableDe);
|
||||
assert.deepEqual(
|
||||
{ tableDe, fixe: instance.fixe, statut: instance.statut, groupe: instance.groupe },
|
||||
avant,
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue