// © 2026 TechnoLibre (http://www.technolibre.ca) // License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) // Épreuves du détail d'un placement (§ 12, D27 à D37, D51, D52) : le // pourcentage et ses bords, le profil trié et ses marques, la répartition, // la synthèse, les bornes des tuiles, les barres des collisions, le mélange // des appartenances à chaque table et ses trois ordres, les objectifs tour // par tour, le profil des animateurs, et detailler qui les réunit. Chaque // configuration est inventée et petite ; ses valeurs sont comptées à la main // dans le commentaire qui la précède, et une lecture directe du plan, qui // n'emprunte rien au module éprouvé, recoupe les comptes par table et par // tour. Identifiants et index diffèrent quand l'ordre en dépend. import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { describe, test } from '../../test/lanceur.js'; import { CATALOGUE, PLAN_PARFAIT_PETITE } from '../demo/catalogue.js'; import { LIBRE, indexerPlan, normaliser } from './configuration.js'; import { diagnostiquer } from './diagnostic.js'; import { diversiteAnimateurs } from './diversite.js'; import { mesurer, parcourirTours } from './indicateurs.js'; import { matriceRencontres, pairesRepetees } from './paires.js'; import { borneDiversite, occupations, plafondsAPriori, plafondsRealises } from './plafond.js'; import { barresCollisions, bornesDesTuiles, detailler, mixTables, objectifsParTour, planchersParGroupe, pourcentage, profilAnimateurs, profilRencontres, repartition, synthese, } from './qualite.js'; const AUBIER = 'Coopérative Aubier'; const RESSAC = 'Atelier Ressac'; const SANS_CONTRAINTE = { separerAppartenances: false, nouveauxVoisins: false, nouvelleTable: false, varierAppartenances: false, }; // La clé normalisée d'une appartenance, telle que l'appelant la passe : le // moteur n'importe pas le CSV, et l'épreuve n'en a pas besoin. const cle = (libelle) => libelle.toLowerCase(); const tous = (participant, table) => ({ participant, table, portee: 'tous' }); const auTour = (participant, table, tour) => ({ participant, table, portee: 'tour', tour }); // Participant présent : [id, appartenance, étiquettes, tours animés], les // trois derniers facultatifs. const participant = ([id, appartenance = null, etiquettes, anime]) => ({ id, nom: `P${id}`, appartenance, ...(etiquettes === undefined ? {} : { etiquettes }), ...(anime === undefined ? {} : { anime }), }); // Configuration, instance et plan indexé. tables : [id, capacité], dans // l'ordre de la configuration ; plan[r] associe à un identifiant de table // les identifiants qui s'y assoient au tour r + 1 ; un présent absent d'un // tour est en réserve. function preparer({ personnes, tables, plan, reservations = [], objectifs = [], contraintes = SANS_CONTRAINTE }) { const configuration = { participants: personnes.map(participant), tables: tables.map(([id, capacite], i) => ({ id, numero: i + 1, capacite })), tours: plan.length, reservations, contraintes, ...(objectifs.length === 0 ? {} : { objectifs }), }; const instance = normaliser(configuration); const ids = configuration.tables.map(({ id }) => id); const tours = plan.map((tour) => ids.map((id) => tour[id] ?? [])); const reserves = tours.map((listes) => { const assis = new Set(listes.flat()); return instance.ids.filter((id) => !assis.has(id)); }); const tableDe = indexerPlan(instance, { tables: ids, tours, reserves }); return { configuration, instance, tableDe, tours }; } // Les mesures d'un plan préparé, ses plafonds et sa borne de diversité. function mesurerTout(prepare) { const { instance, tableDe } = prepare; return { mesures: mesurer(instance, tableDe), realises: plafondsRealises(instance, tableDe), aPriori: plafondsAPriori(instance), aMax: borneDiversite(instance, tableDe).aMax, }; } const index = (instance, id) => instance.indexDe.get(id); const idsDe = (instance, lignes) => lignes.map(({ p }) => instance.ids[p]); const somme = (valeurs) => valeurs.reduce((total, valeur) => total + valeur, 0); describe('pourcentage (D52, § 5.4)', () => { test('les bords : 0 à numérateur nul, 100 au numérateur égal, null à dénominateur nul', () => { assert.equal(pourcentage(0, 27), 0); assert.equal(pourcentage(27, 27), 100); assert.equal(pourcentage(5, 0), null); assert.equal(pourcentage(0, 0), null); }); test('entre les bords, la partie entière, jamais 0 ni 100 : 259 sur 260 donne 99, 1 sur 300 donne 1', () => { assert.equal(pourcentage(259, 260), 99); assert.equal(pourcentage(1, 300), 1); assert.equal(pourcentage(24, 27), 88); assert.equal(pourcentage(2, 3), 66); assert.equal(pourcentage(1, 2), 50); assert.equal(pourcentage(5, 8), 62); // Le centième exact n'est pas arrondi à la baisse d'un cran. assert.equal(pourcentage(29, 100), 29); assert.equal(pourcentage(57, 100), 57); }); test('au-delà du dénominateur, que la garde refuse, le quotient entier : rien ne le ramène à 100', () => { assert.equal(pourcentage(28, 27), 103); }); test('lève RangeError sur un compte qui n’est pas un entier positif ou nul', () => { for (const [n, d] of [[-1, 4], [1.5, 4], [1, -4], [1, 2.5], [Number.NaN, 4], ['3', 4], [1, null]]) { assert.throws(() => pourcentage(n, d), RangeError, `${String(n)} sur ${String(d)}`); } }); }); // Six personnes, deux tables de trois, deux tours ; 1 en réserve au tour 2. // Rencontres et plafonds réalisés, comptés à la main (aucun ancré, n_p = 5) : // 1 : {2, 3}, 2 sur 2 ; 2 : {1, 3, 4, 5}, 4 sur 4 ; 3 : {1, 2, 6}, 3 sur 3 ; // 4 : {5, 6, 2}, 3 sur 4 ; 5 : {4, 6, 2}, 3 sur 4 ; 6 : {4, 5, 3}, 3 sur 3. // À trois rencontres, 4 et 5, au plafond le plus haut, passent devant 3 et // 6 ; trié sur le pourcentage, 4 et 5, à 75, viendraient en tête. // Appartenances : 1, 3 Aubier ; 2, 4 Ressac ; 5, 6 sans. A(p) vaut 1 pour 4 // et 5 seulement ; A_max(4) = min(Φ_aff = 1, G = 2, 4 − 2 + 1) = 1, // A_max(5) = min(Φ_aff = 3, 2) = 2. const PROFIL = { personnes: [[1, AUBIER], [2, RESSAC], [3, AUBIER], [4, RESSAC], [5], [6]], tables: [[10, 3], [20, 3]], plan: [ { 10: [1, 2, 3], 20: [4, 5, 6] }, { 10: [2, 4, 5], 20: [3, 6] }, ], }; describe('profilRencontres (§ 12.3, D28, D51)', () => { test('rencontres croissantes, puis plafond réalisé décroissant, puis identifiant : jamais le pourcentage', () => { const prepare = preparer(PROFIL); const { instance } = prepare; const { mesures, realises, aPriori, aMax } = mesurerTout(prepare); const profil = profilRencontres(instance, mesures, realises, aPriori, aMax); assert.deepEqual(idsDe(instance, profil), [1, 4, 5, 3, 6, 2]); assert.deepEqual(profil.map(({ rencontres, plafondRealise }) => [rencontres, plafondRealise]), [[2, 2], [3, 4], [3, 4], [3, 3], [3, 3], [4, 4]]); assert.deepEqual(profil.map(({ pourcent }) => pourcent), [100, 75, 75, 100, 100, 100]); }); test('chaque ligne porte ce que les mesures donnent pour sa personne, lu sans recompte', () => { const prepare = preparer(PROFIL); const { instance } = prepare; const { mesures, realises, aPriori, aMax } = mesurerTout(prepare); for (const ligne of profilRencontres(instance, mesures, realises, aPriori, aMax)) { const { p } = ligne; assert.deepEqual(ligne, { p, rencontres: mesures.rencontres[p], plafondRealise: realises[p], pourcent: pourcentage(mesures.rencontres[p], realises[p]), plafondAPriori: aPriori[p], appartenances: mesures.appartenancesVues[p], affilies: mesures.affilies[p], borneDiversite: aMax[p], toursNonAssis: 2 - mesures.toursAssis[p], marque: p === index(instance, 1) ? 'reserve' : null, }); } // A(p) et A_max, comptés à la main. assert.deepEqual(mesures.appartenancesVues, [2, 2, 2, 1, 1, 2]); assert.deepEqual([aMax[3], aMax[4]], [1, 2]); }); test('plafond a priori inconnu : null sur chaque ligne', () => { const prepare = preparer(PROFIL); const { mesures, realises, aMax } = mesurerTout(prepare); const profil = profilRencontres(prepare.instance, mesures, realises, null, aMax); assert.ok(profil.every(({ plafondAPriori }) => plafondAPriori === null)); }); // Tables 10, 20, 30, 40 de deux places, deux tours. 1 ancré à la table // 10 ; 2 réservé à la table 20 au tour 1 seulement, en réserve au tour 2 ; // 5 en réserve au tour 1 ; 6 seul à la table 30 aux deux tours, plafond // réalisé nul ; 7 seul à la table 40 au tour 1 et en réserve au tour 2, // plafond réalisé nul aussi : la réserve passe avant le plafond nul ; 3 et // 4 assis aux deux tours, chacun à son plafond. test('les marques, la première qui s’applique : ancré, partiellement fixé, en réserve, plafond nul ; le pourcentage null à plafond nul', () => { const prepare = preparer({ personnes: [[1], [2], [3], [4], [5], [6], [7]], tables: [[10, 2], [20, 2], [30, 2], [40, 2]], plan: [ { 10: [1, 3], 20: [2, 4], 30: [6], 40: [7] }, { 10: [1, 5], 20: [3, 4], 30: [6] }, ], reservations: [tous(1, 10), auTour(2, 20, 1)], }); const { instance } = prepare; const { mesures, realises, aPriori, aMax } = mesurerTout(prepare); const profil = profilRencontres(instance, mesures, realises, aPriori, aMax); const deId = (id) => profil.find(({ p }) => p === index(instance, id)); assert.deepEqual([1, 2, 3, 4, 5, 6, 7].map((id) => deId(id).marque), ['ancre', 'partiel', null, null, 'reserve', 'plafondNul', 'reserve']); assert.equal(deId(2).toursNonAssis, 1); assert.deepEqual([deId(7).toursNonAssis, deId(7).plafondRealise], [1, 0]); assert.deepEqual([deId(6).plafondRealise, deId(6).pourcent], [0, null]); assert.deepEqual(idsDe(instance, profil).slice(0, 1), [6]); }); test('lève RangeError sur une liste qui n’a pas une case par personne', () => { const prepare = preparer(PROFIL); const { mesures, realises, aPriori, aMax } = mesurerTout(prepare); const { instance } = prepare; assert.throws(() => profilRencontres(instance, mesures, realises.slice(1), aPriori, aMax), RangeError); assert.throws(() => profilRencontres(instance, mesures, realises, aPriori.slice(1), aMax), RangeError); assert.throws(() => profilRencontres(instance, mesures, realises, aPriori, aMax.slice(1)), RangeError); }); }); describe('repartition et synthese (§ 12.3)', () => { test('une ligne par valeur présente, croissante, avec l’effectif, le cumul et les personnes à leur plafond, contre une lecture directe', () => { const prepare = preparer(PROFIL); const { mesures, realises } = mesurerTout(prepare); const lignes = repartition(mesures.rencontres, realises); assert.deepEqual(lignes, [ { valeur: 2, effectif: 1, cumul: 1, auPlafond: 1 }, { valeur: 3, effectif: 4, cumul: 5, auPlafond: 2 }, { valeur: 4, effectif: 1, cumul: 6, auPlafond: 1 }, ]); const valeurs = [...new Set(mesures.rencontres)].sort((a, b) => a - b); assert.deepEqual(lignes.map(({ valeur, effectif, auPlafond }) => ({ valeur, effectif, auPlafond })), valeurs.map((valeur) => ({ valeur, effectif: mesures.rencontres.filter((v) => v === valeur).length, auPlafond: mesures.rencontres.filter((v, p) => v === valeur && v === realises[p]).length, }))); }); test('sans plafonds, auPlafond vaut null ; sans valeur, aucune ligne', () => { assert.deepEqual(repartition([3, 1, 3], null), [ { valeur: 1, effectif: 1, cumul: 1, auPlafond: null }, { valeur: 3, effectif: 2, cumul: 3, auPlafond: null }, ]); assert.deepEqual(repartition([], []), []); assert.throws(() => repartition([1, 2], [1]), RangeError); }); test('la synthèse : le minimum, son effectif, et les personnes sous leur plafond réalisé', () => { const prepare = preparer(PROFIL); const { instance } = prepare; const { mesures, realises, aPriori, aMax } = mesurerTout(prepare); const resume = synthese(profilRencontres(instance, mesures, realises, aPriori, aMax)); assert.deepEqual(resume, { minimum: 2, effectifAuMinimum: 1, effectifSousPlafond: 2 }); assert.equal(resume.minimum, mesures.aggRencontres.tous.min); assert.deepEqual(synthese([]), { minimum: null, effectifAuMinimum: 0, effectifSousPlafond: 0 }); }); }); // Dix « finance », 1 à 5 libres, 4 réservé à la table 2 au tour 1, 6 ancré à // la table 1 ; 7 à 9 sans étiquette. Table 1, trois places, « finance » aux // tours 1 et 2 ; tables 2 (trois places) et 3 (quatre places), sans objectif ; // trois tours, le troisième sans objectif. 9 est en réserve au tour 2. // // Couples possibles : tour 1, 1, 2, 3, 5 et 6 (4 est fixé à une table sans // objectif) ; tour 2, 1 à 6 ; tour 3, aucun. P_r, les non fixés, et L_r, les // deux sièges libres de la table 1 : planchers 4 − 2 = 2, 5 − 2 = 3, 0. // Tour 1 : 6, 1, 2 servis ; hors 2, plancher 2, reste 0 ; 4, 7, 8, 9 sans // table possible. Tour 2 : 6 et 3 servis ; hors 4, plancher 3, reste 1 ; 7 // et 8 sans table possible, 9 en réserve. Tour 3 : rien de possible, les neuf // assis sans table possible. // s(p) : 1, 1, 1, 0, 0, 2 ; P(p) : 2, 2, 2, 1, 2, 2. Jamais servies : 5, puis // 4 ; au minimum de s(p), 5, aux tours possibles les plus nombreux. const OBJECTIFS = { personnes: [[1, null, ['finance']], [2, null, ['finance']], [3, null, ['finance']], [4, null, ['finance']], [5, null, ['finance']], [6, null, ['finance']], [7], [8], [9]], tables: [[1, 3], [2, 3], [3, 4]], reservations: [tous(6, 1), auTour(4, 2, 1)], objectifs: [ { table: 1, tour: 1, regle: 'ou', etiquettes: ['finance'] }, { table: 1, tour: 2, regle: 'ou', etiquettes: ['finance'] }, ], plan: [ { 1: [6, 1, 2], 2: [3, 4, 7], 3: [5, 8, 9] }, { 1: [6, 3, 7], 2: [1, 2, 8], 3: [4, 5] }, { 1: [6, 1, 2], 2: [3, 4, 5], 3: [7, 8, 9] }, ], }; // Lecture directe des objectifs d'un tour (§ 4.5, § 5.6), sans rien du // module éprouvé : P_r, les couples possibles dont la personne n'est pas // fixée ; S^nf_r, les servis non fixés ; σ_r, les assis sans couple possible. // tour, de 1 à R ; listes, celles du plan préparé à ce tour. function tourLu(configuration, tour, listes) { const { participants, tables, reservations, objectifs = [] } = configuration; const couvre = (res) => res.portee === 'tous' || res.tour === tour; const fixee = (id) => reservations.find((res) => res.participant === id && couvre(res))?.table; const libre = (table) => reservations.filter((res) => res.table === table.id && couvre(res)).length < table.capacite; const objectif = (table) => objectifs.find((o) => o.table === table && o.tour === tour) ?? objectifs.find((o) => o.table === table && o.tour === null); const correspond = (personne, table) => { const o = objectif(table); return o !== undefined && o.etiquettes.some((e) => (personne.etiquettes ?? []).includes(e)); }; const possible = (personne) => { const t = fixee(personne.id); return t === undefined ? tables.some((table) => libre(table) && correspond(personne, table.id)) : correspond(personne, t); }; let nonFixes = 0; let servisNonFixes = 0; let sansCouple = 0; for (const personne of participants) { const i = listes.findIndex((occupants) => occupants.includes(personne.id)); if (possible(personne) && fixee(personne.id) === undefined) nonFixes += 1; if (i === -1) continue; if (!possible(personne)) sansCouple += 1; const table = tables[i]; if (fixee(personne.id) === undefined && libre(table) && correspond(personne, table.id)) servisNonFixes += 1; } return { nonFixes, servisNonFixes, sansCouple }; } // Le parcours, le diagnostic et les objectifs par tour d'un plan préparé. function objectifsDe(prepare) { const { configuration, instance, tableDe } = prepare; const parcours = parcourirTours(instance, tableDe); const diagnostic = diagnostiquer(configuration); return { parcours, diagnostic, objectifs: objectifsParTour(instance, parcours, diagnostic.objectifsInevitables.parTour) }; } describe('objectifsParTour (§ 12.12, D36, B21)', () => { test('chaque tour, compté à la main : possibles, servis, pourcentage, hors objectif, plancher, assis sans table possible', () => { const { objectifs } = objectifsDe(preparer(OBJECTIFS)); assert.deepEqual(objectifs.parTour, [ { possibles: 5, servis: 3, pourcent: 60, hors: 2, plancher: 2, sansCouple: 4 }, { possibles: 6, servis: 2, pourcent: 33, hors: 4, plancher: 3, sansCouple: 2 }, { possibles: 0, servis: null, pourcent: null, hors: null, plancher: null, sansCouple: 9 }, ]); assert.equal(objectifs.pireTour, 1); assert.deepEqual(objectifs.soiree, { possibles: 11, servis: 5, pourcent: 45, hors: 6, plancher: 5 }); }); test('les servis des tours somment s(p) ; Π_r − S_r = P_r − S^nf_r ; chaque plancher borne son tour ; σ_r + Π_r ne dépasse pas les présents', () => { const prepare = preparer(OBJECTIFS); const { configuration, instance, tableDe, tours } = prepare; const { objectifs, diagnostic } = objectifsDe(prepare); const mesures = mesurer(instance, tableDe); assert.equal(somme(objectifs.parTour.map(({ servis }) => servis ?? 0)), somme(mesures.objectifs.tours)); assert.equal(objectifs.soiree.servis, somme(mesures.objectifs.tours)); assert.equal(objectifs.soiree.possibles, somme(mesures.objectifs.possibles)); objectifs.parTour.forEach(({ possibles, servis, hors, plancher, sansCouple }, r) => { const lu = tourLu(configuration, r + 1, tours[r]); assert.equal(sansCouple, lu.sansCouple, `tour ${r + 1}`); assert.ok(sansCouple + possibles <= instance.N, `tour ${r + 1}`); if (possibles === 0) return; assert.equal(possibles - servis, lu.nonFixes - lu.servisNonFixes, `tour ${r + 1}`); assert.ok(plancher <= hors, `tour ${r + 1}`); }); // La somme des planchers des tours est le plancher que le diagnostic écrit. assert.equal(objectifs.soiree.plancher, diagnostic.objectifsInevitables.horsObjectif); }); test('les personnes jamais servies : celles de la mesure, aux tours possibles les plus nombreux d’abord ; la répartition de P(p) − s(p), de la plus grande valeur à la plus petite', () => { const prepare = preparer(OBJECTIFS); const { instance, tableDe } = prepare; const { objectifs } = objectifsDe(prepare); assert.deepEqual(objectifs.jamaisServies, [{ p: index(instance, 5), possibles: 2 }, { p: index(instance, 4), possibles: 1 }]); assert.equal(objectifs.jamaisServies.length, mesurer(instance, tableDe).objectifs.jamaisServies); assert.deepEqual(objectifs.repartition, [{ valeur: 2, effectif: 1 }, { valeur: 1, effectif: 4 }, { valeur: 0, effectif: 1 }]); // 2 passe le tour 1 à la table 2 : jamais servi, deux tours possibles // comme 5 ; à égalité, l'identifiant. const plan = OBJECTIFS.plan.map((tour) => ({ ...tour })); plan[0] = { 1: [6, 1, 3], 2: [2, 4, 7], 3: [5, 8, 9] }; const autre = preparer({ ...OBJECTIFS, plan }); assert.deepEqual(idsDe(autre.instance, objectifsDe(autre).objectifs.jamaisServies), [2, 5, 4]); }); test('à reste égal, le premier tour est le pire ; la personne fixée à une table à objectif est servie hors du plancher', () => { // Au tour 2, 1 rejoint 6 et 3 à la table 1 : trois servis, hors 3, // plancher 3, reste 0, comme au tour 1. 6, ancré, est servi à chaque // tour où la table porte un objectif, sans compter dans P_r. const plan = OBJECTIFS.plan.map((tour) => ({ ...tour })); plan[1] = { 1: [6, 3, 1], 2: [2, 7, 8], 3: [4, 5] }; const prepare = preparer({ ...OBJECTIFS, plan }); const { objectifs } = objectifsDe(prepare); assert.deepEqual(objectifs.parTour.map(({ hors, plancher }) => [hors, plancher]), [[2, 2], [3, 3], [null, null]]); assert.equal(objectifs.pireTour, 0); const { tours } = prepare; const lu = tourLu(prepare.configuration, 1, tours[0]); assert.equal(lu.nonFixes, 4); assert.equal(objectifs.parTour[0].possibles, lu.nonFixes + 1); }); test('critère décoché : les planchers des tours sont ceux du diagnostic, non nuls', () => { const contraintes = { ...SANS_CONTRAINTE, respecterObjectifs: false }; const { objectifs } = objectifsDe(preparer({ ...OBJECTIFS, contraintes })); assert.deepEqual(objectifs.parTour.map(({ plancher }) => plancher), [2, 3, null]); }); test('sans personne concernée, null ; une liste de planchers d’une autre longueur lève RangeError', () => { const prepare = preparer({ ...OBJECTIFS, objectifs: [] }); const { objectifs, parcours } = objectifsDe(prepare); assert.equal(objectifs, null); const avec = preparer(OBJECTIFS); assert.throws(() => objectifsParTour(avec.instance, parcourirTours(avec.instance, avec.tableDe), [2, 3]), RangeError); assert.ok(parcours.possiblesParTour.every((n) => n === 0)); }); }); // Onze personnes, cinq tables de trois, deux tours, sans réservation. // « atelier ressac » (1, 2), « Atelier Ressac » (3, 4), « Zinc » (5, 6), // « Grès » (7, 8, 9), « Coopérative Aubier » (10, 11), dans cet ordre // d'apparition. Tour 1 : chaque appartenance à sa table, Grès à trois. Tour 2 : // 1, 3, 5 ; 2, 4, 6 ; 7 ; 8 ; 9, 10, 11. Aubier : 2 collisions, 1 paire, // excédent 1 ; Grès : 3, 3, 0 ; les trois autres : 1, 1, 0. const COLLISIONS = { personnes: [[1, 'atelier ressac'], [2, 'atelier ressac'], [3, RESSAC], [4, RESSAC], [5, 'Zinc'], [6, 'Zinc'], [7, 'Grès'], [8, 'Grès'], [9, 'Grès'], [10, AUBIER], [11, AUBIER]], tables: [[1, 3], [2, 3], [3, 3], [4, 3], [5, 3]], plan: [ { 1: [1, 2], 2: [3, 4], 3: [5, 6], 4: [7, 8, 9], 5: [10, 11] }, { 1: [1, 3, 5], 2: [2, 4, 6], 3: [7], 4: [8], 5: [9, 10, 11] }, ], }; describe('barresCollisions et planchersParGroupe (§ 12.4, D31, B13)', () => { test('l’excédent décroissant, puis les collisions cumulées, puis la clé normalisée, puis la chaîne ; jamais la longueur de la barre', () => { const prepare = preparer(COLLISIONS); const { configuration, instance, tableDe } = prepare; const mesures = mesurer(instance, tableDe); const planchers = planchersParGroupe(instance, diagnostiquer(configuration).collisions); const lignes = barresCollisions(instance, mesures, planchers, true, cle); assert.deepEqual(lignes.map(({ groupe }) => groupe), [AUBIER, 'Grès', RESSAC, 'atelier ressac', 'Zinc']); assert.deepEqual(lignes[0], { g: 4, groupe: AUBIER, effectif: 2, cumulees: 2, prouve: 0, reste: 2, pairesDistinctes: 1, excedent: 1 }); assert.deepEqual(lignes.map(({ cumulees, pairesDistinctes, excedent }) => [cumulees, pairesDistinctes, excedent]), [[2, 1, 1], [3, 3, 0], [1, 1, 0], [1, 1, 0], [1, 1, 0]]); // Sans réservation, le diagnostic se tait : « prouvé inévitable » vaut 0. assert.deepEqual(planchers, [0, 0, 0, 0, 0]); }); test('hors du domaine des planchers, prouvé et reste valent null ; le compte reste', () => { const prepare = preparer(COLLISIONS); const { instance, tableDe } = prepare; const lignes = barresCollisions(instance, mesurer(instance, tableDe), [0, 0, 0, 0, 0], false, cle); assert.ok(lignes.every(({ prouve, reste }) => prouve === null && reste === null)); assert.equal(somme(lignes.map(({ cumulees }) => cumulees)), 8); }); test('la variante conflit : le plancher du diagnostic, 4, est prouvé inévitable pour l’appartenance de cinq', () => { const configuration = CATALOGUE.find(({ cle: nom }) => nom === 'petite-conflit').construire(); const instance = normaliser(configuration); const tableDe = indexerPlan(instance, PLAN_PARFAIT_PETITE); const planchers = planchersParGroupe(instance, diagnostiquer(configuration).collisions); const lignes = barresCollisions(instance, mesurer(instance, tableDe), planchers, true, cle); const conflit = lignes.find(({ effectif }) => effectif === 5); assert.equal(conflit.prouve, 4); assert.equal(conflit.reste, conflit.cumulees - 4); assert.ok(lignes.filter((ligne) => ligne !== conflit).every(({ prouve }) => prouve === 0)); }); test('un plancher d’une appartenance absente de l’instance, ou une liste d’une autre longueur, lève RangeError', () => { const { instance, tableDe } = preparer(COLLISIONS); assert.throws(() => planchersParGroupe(instance, [{ groupe: 'Ardoise', plancherCumulees: 2 }]), RangeError); assert.throws(() => barresCollisions(instance, mesurer(instance, tableDe), [0, 0], true, cle), RangeError); }); }); // Lecture directe d'une case (§ 5.4, B4) : capacité, occupants, affiliés et // appartenances distinctes de la table i au tour r, sur le plan préparé. function caseLue({ configuration, tours }, r, i) { const appartenance = new Map(configuration.participants.map(({ id, appartenance: a }) => [id, a])); const occupants = tours[r][i]; const affilies = occupants.filter((id) => appartenance.get(id) !== null); return { capacite: configuration.tables[i].capacite, occupants: occupants.length, affilies: affilies.length, appartenances: new Set(affilies.map((id) => appartenance.get(id))).size, }; } // Le mélange d'un plan préparé. function melange({ instance, tableDe }) { return mixTables(instance, tableDe, occupations(instance, tableDe, { affilies: true })); } // Une table par cas, un seul tour, quatre appartenances : W, en tête de // l'ordre des tables, quatre places et deux sans appartenance ; X, huit // places, quatre affiliés de quatre appartenances et un sans ; Y, quatre // places, deux Aubier, un Ressac, un Grès ; Z, quatre places, un Aubier et // trois sans ; V, quatre places, pleine de quatre affiliés de quatre // appartenances. Sièges : —, 4, 1, 3, 0 ; assis : —, 1, 1, 3, 0 ; // doublons : —, 0, 1, 0, 0. W, à l'index 0, ne finit chaque ordre que par // la règle des tables toutes « — », non par l'ordre des tables ; V, sans // rien à compter, finit les tables mesurées. const TROIS_ORDRES = { personnes: [ [1, AUBIER], [2, RESSAC], [3, 'Grès'], [4, 'Zinc'], [5], [6, AUBIER], [7, AUBIER], [8, RESSAC], [9, 'Grès'], [10, AUBIER], [11], [12], [13], [14], [15], [16, AUBIER], [17, RESSAC], [18, 'Grès'], [19, 'Zinc'], ], tables: [[10, 4], [40, 8], [30, 4], [20, 4], [50, 4]], plan: [{ 40: [1, 2, 3, 4, 5], 30: [6, 7, 8, 9], 20: [10, 11, 12, 13], 10: [14, 15], 50: [16, 17, 18, 19] }], }; describe('mixTables (§ 12.11, D35, B20)', () => { test('chaque case, contre une lecture directe : les trois comptes, emboîtés, et leurs trois pourcentages', () => { const prepare = preparer(TROIS_ORDRES); const { lignes } = melange(prepare); assert.deepEqual(lignes.map(({ t, capacite }) => [t, capacite]), [[0, 4], [1, 8], [2, 4], [3, 4], [4, 4]]); lignes.forEach(({ t, tours }) => { const lu = caseLue(prepare, 0, t); const [cas] = tours; if (lu.affilies === 0) { assert.equal(cas, null); return; } const { capacite: c, occupants: o, affilies: a, appartenances: d } = lu; assert.deepEqual(cas, { occupants: o, affilies: a, appartenances: d, comptes: { sieges: c - d, assis: o - d, doublons: a - d }, pourcents: { sieges: pourcentage(d, c), assis: pourcentage(d, o), doublons: pourcentage(d, a) }, }); assert.ok(cas.comptes.sieges >= cas.comptes.assis && cas.comptes.assis >= cas.comptes.doublons && cas.comptes.doublons >= 0); }); // La table incomplète, avec un assis sans appartenance : 4 ≥ 1 ≥ 0, // quatre appartenances pour huit sièges, cinq assis, quatre affiliés. assert.deepEqual(lignes[1].tours[0].comptes, { sieges: 4, assis: 1, doublons: 0 }); assert.deepEqual(lignes[1].tours[0].pourcents, { sieges: 50, assis: 80, doublons: 100 }); // Le sans-appartenance de Z n'est pas un doublon. assert.deepEqual(lignes[3].tours[0].comptes, { sieges: 3, assis: 3, doublons: 0 }); // La table pleine d'affiliés tous distincts : rien à compter. assert.deepEqual(lignes[4].tours[0].comptes, { sieges: 0, assis: 0, doublons: 0 }); }); test('trois ordres, un par position, qui diffèrent : la pire table d’abord, celle dont toutes les cases sont vides en dernier', () => { const { ordres, lignes } = melange(preparer(TROIS_ORDRES)); // La table toute « — » est la première des tables : seul son rang à part // la met en dernier, jamais l'ordre des tables. assert.deepEqual(ordres, { sieges: [1, 3, 2, 4, 0], assis: [3, 1, 2, 4, 0], doublons: [2, 1, 3, 4, 0] }); assert.deepEqual(lignes.map(({ pire }) => pire), [ null, { sieges: 4, assis: 1, doublons: 0 }, { sieges: 1, assis: 1, doublons: 1 }, { sieges: 3, assis: 3, doublons: 0 }, { sieges: 0, assis: 0, doublons: 0 }, ]); assert.equal(lignes[0].somme, null); }); // Deux tours, cinq tables de trois ; 1 à 4 Aubier, les autres sans. A : // tour 1, 1, 2 et 5, un doublon ; tour 2, 1, 5, 6, aucun. B : 3, 4, 6, un // doublon ; puis 2, 3, 7, un doublon. C : sans affilié au tour 1 ; 4, 8, 9 // au tour 2. D et E, vides aux deux tours, dans l'ordre des tables. test('à pire égal, la somme sur les tours départage, puis l’ordre des tables ; une case vide ou sans affilié est ignorée', () => { const prepare = preparer({ personnes: [[1, AUBIER], [2, AUBIER], [3, AUBIER], [4, AUBIER], [5], [6], [7], [8], [9]], tables: [[1, 3], [2, 3], [3, 3], [4, 3], [5, 3]], plan: [ { 1: [1, 2, 5], 2: [3, 4, 6], 3: [7, 8, 9] }, { 1: [1, 5, 6], 2: [2, 3, 7], 3: [4, 8, 9] }, ], }); const { ordres, lignes } = melange(prepare); assert.deepEqual(lignes.map(({ tours }) => tours.map((cas) => (cas === null ? null : cas.comptes.doublons))), [[1, 0], [1, 1], [null, 0], [null, null], [null, null]]); assert.deepEqual(lignes.map(({ somme: s }) => s), [ { sieges: 4, assis: 4, doublons: 1 }, { sieges: 4, assis: 4, doublons: 2 }, { sieges: 2, assis: 2, doublons: 0 }, null, null, ]); assert.deepEqual(ordres, { sieges: [0, 1, 2, 3, 4], assis: [0, 1, 2, 3, 4], doublons: [1, 0, 2, 3, 4] }); }); // Deux tables de trois, trois tours ; 1 à 5 Aubier, les autres sans. La // table 1 reçoit trois Aubier au tour 1, un seul ensuite : doublons 2, 0, // 0, pire 2, somme 2. La table 2 en reçoit deux à chaque tour : doublons // 1, 1, 1, pire 1, somme 3. Le plus grand compte d'une case passe avant la // somme : la table 1 vient d'abord. test('le plus grand compte d’une case, puis la somme : une seule case lourde l’emporte sur trois cases légères', () => { const { ordres, lignes } = melange( preparer({ personnes: [[1, AUBIER], [2, AUBIER], [3, AUBIER], [4, AUBIER], [5, AUBIER], [6], [7], [8]], tables: [[1, 3], [2, 3]], plan: [ { 1: [1, 2, 3], 2: [4, 5, 6] }, { 1: [1, 6, 7], 2: [2, 3, 8] }, { 1: [4, 6, 7], 2: [1, 5, 8] }, ], }), ); assert.deepEqual(lignes.map(({ tours }) => tours.map(({ comptes }) => comptes.doublons)), [[2, 0, 0], [1, 1, 1]]); assert.deepEqual(lignes.map(({ pire, somme: s }) => [pire.doublons, s.doublons]), [[2, 2], [1, 3]]); assert.deepEqual(ordres.doublons, [0, 1]); }); test('un doublon d’appartenance est non nul exactement là où parcourirTours compte des collègues', () => { for (const description of [COLLISIONS, TROIS_ORDRES, PROFIL]) { const prepare = preparer(description); const { parTour } = parcourirTours(prepare.instance, prepare.tableDe); const { lignes } = melange(prepare); lignes.forEach(({ t, tours }) => { tours.forEach((cas, r) => { const doublons = cas === null ? 0 : cas.comptes.doublons; assert.equal(doublons > 0, parTour[r][t].collegues.length > 0, `tour ${r + 1}, table ${t}`); }); }); } }); test('sans les affiliés de l’occupation, lève TypeError', () => { const { instance, tableDe } = preparer(TROIS_ORDRES); assert.throws(() => mixTables(instance, tableDe, occupations(instance, tableDe)), TypeError); }); }); // Trois tables de deux, deux tours. 1 anime au tour 1 à la table 1, 2 au // tour 2 à la table 2 ; 3, ancré à la table 3 où personne n'anime, n'est pas // concerné (B(3) = 0). B(1) = B(2) = 1, l'autre animateur ; B(4) = B(5) = // B(6) = 2. 4 rencontre les deux ; 5, 6, 1, 2 aucun. Ordre : 5 et 6, à la // borne la plus haute, avant 1 et 2, puis 4. const ANIMATEURS = { personnes: [[1, null, undefined, [1]], [2, null, undefined, [2]], [3], [4], [5], [6]], tables: [[1, 2], [2, 2], [3, 2]], reservations: [auTour(1, 1, 1), auTour(2, 2, 2), tous(3, 3)], plan: [ { 1: [1, 4], 2: [2, 5], 3: [3, 6] }, { 1: [1, 5], 2: [2, 4], 3: [3, 6] }, ], }; describe('profilAnimateurs (§ 12.13, D37)', () => { test('les seules personnes concernées, m(p) croissant, puis la borne décroissante, puis l’identifiant, avec leur statut', () => { const prepare = preparer(ANIMATEURS); const { instance, tableDe } = prepare; const mesures = mesurer(instance, tableDe); const profil = profilAnimateurs(instance, mesures); assert.deepEqual(idsDe(instance, profil), [5, 6, 1, 2, 4]); assert.deepEqual(profil.map(({ rencontres, borne, statut }) => [rencontres, borne, statut]), [ [0, 2, 'mobile'], [0, 2, 'mobile'], [0, 1, 'partiel'], [0, 1, 'partiel'], [2, 2, 'mobile'], ]); assert.deepEqual(Object.keys(profil[0]), ['p', 'rencontres', 'borne', 'statut']); }); test('sans personne concernée, null', () => { const prepare = preparer({ ...ANIMATEURS, personnes: [[1], [2], [3], [4], [5], [6]], reservations: [] }); assert.equal(profilAnimateurs(prepare.instance, mesurer(prepare.instance, prepare.tableDe)), null); }); }); describe('bornesDesTuiles (§ 12.2, D29, D34, D52, B11)', () => { test('le rang 1 du profil, la tête du profil de manque, le minimum de A(p) au même tri : chacun une personne nommée', () => { const prepare = preparer(PROFIL); const { instance } = prepare; const { mesures, realises, aPriori, aMax } = mesurerTout(prepare); const profil = profilRencontres(instance, mesures, realises, aPriori, aMax); const bornes = bornesDesTuiles(instance, { mesures, plafondsRealises: realises, aMax, profil, animateurs: null, objectifs: null }); const un = index(instance, 1); assert.deepEqual(bornes.rang1, { p: un, rencontres: 2, plafondRealise: 2, plafondAPriori: aPriori[un], pourcent: 100 }); // Manque 1 pour 4 et 5, au même plafond 4 : l'identifiant le plus petit. assert.deepEqual(bornes.manque, { p: index(instance, 4), plafondRealise: 4 }); // A(p) = 1 pour 4 et 5 : 5, à la borne de diversité la plus haute. assert.deepEqual(bornes.diversite, { p: index(instance, 5), aMax: 2 }); assert.equal(bornes.animateurs, null); assert.equal(bornes.objectifs, null); assert.equal(bornes.soiree, null); }); test('aucun pourcentage pour le manque, la diversité ni les animateurs ; celui du minimum de s(p) et de la soirée', () => { const avec = preparer(OBJECTIFS); const { instance, tableDe } = avec; const { mesures, realises, aPriori, aMax } = mesurerTout(avec); const profil = profilRencontres(instance, mesures, realises, aPriori, aMax); const { objectifs } = objectifsDe(avec); const bornes = bornesDesTuiles(instance, { mesures, plafondsRealises: realises, aMax, profil, animateurs: null, objectifs }); // s(p) = 0 pour 4 et 5 : 5, aux tours possibles les plus nombreux. assert.deepEqual(bornes.objectifs, { p: index(instance, 5), servis: 0, possibles: 2, pourcent: 0 }); assert.deepEqual(bornes.soiree, { servis: 5, possibles: 11, pourcent: 45, jamaisServies: 2 }); assert.equal(bornes.soiree.jamaisServies, mesurer(instance, tableDe).objectifs.jamaisServies); assert.deepEqual(Object.keys(bornes.manque), ['p', 'plafondRealise']); assert.equal(bornes.diversite, null); // La table 1 à quatre places : chaque « finance » libre est servi une // fois, 6 deux fois. Le minimum de s(p) vaut 1 chez les concernées ; 7, // 8 et 9, à s(p) = P(p) = 0, n'en sont pas et ne le portent jamais. const servis = preparer({ ...OBJECTIFS, tables: [[1, 4], [2, 3], [3, 4]], plan: [ { 1: [6, 1, 2, 3], 2: [4, 7, 8], 3: [5, 9] }, { 1: [6, 4, 5, 7], 2: [1, 2, 8], 3: [3, 9] }, { 1: [6, 1, 2], 2: [3, 4, 5], 3: [7, 8, 9] }, ], }); const lu = mesurerTout(servis); const parServis = bornesDesTuiles(servis.instance, { mesures: lu.mesures, plafondsRealises: lu.realises, aMax: lu.aMax, profil: profilRencontres(servis.instance, lu.mesures, lu.realises, lu.aPriori, lu.aMax), animateurs: null, objectifs: objectifsDe(servis).objectifs, }); assert.deepEqual(lu.mesures.objectifs.tours, [1, 1, 1, 1, 1, 2, 0, 0, 0]); // s(p) = 1 pour 1, 2, 3, 5 à P(p) = 2 et pour 4 à P(p) = 1 : 1, le // premier aux tours possibles les plus nombreux. assert.deepEqual(parServis.objectifs, { p: index(servis.instance, 1), servis: 1, possibles: 2, pourcent: 50 }); const animes = preparer(ANIMATEURS); const m = mesurer(animes.instance, animes.tableDe); const tout = mesurerTout(animes); const profilA = profilRencontres(animes.instance, m, tout.realises, tout.aPriori, tout.aMax); const deAnimateurs = bornesDesTuiles(animes.instance, { mesures: m, plafondsRealises: tout.realises, aMax: tout.aMax, profil: profilA, animateurs: profilAnimateurs(animes.instance, m), objectifs: null, }); assert.deepEqual(deAnimateurs.animateurs, { p: index(animes.instance, 5), borne: 2 }); }); test('sans personne présente : ni rang 1, ni tête de manque', () => { const vide = normaliser({ participants: [{ id: 1, nom: 'P1', appartenance: AUBIER, exclu: true }], tables: [{ id: 1, numero: 1, capacite: 2 }], tours: 1, reservations: [], contraintes: SANS_CONTRAINTE, }); const plan = new Int32Array(0); const mesures = mesurer(vide, plan); const bornes = bornesDesTuiles(vide, { mesures, plafondsRealises: [], aMax: [], profil: [], animateurs: null, objectifs: null }); assert.deepEqual(bornes, { rang1: null, manque: null, diversite: null, animateurs: null, objectifs: null, soiree: null }); }); }); // Tous les plans indexés d'une instance qui assoient chacun à chaque tour, // honorent les réservations et tiennent les capacités ; le même tableau est // rendu, rempli, à chaque plan. function* tousLesPlans({ N, T, R, capacite, fixe }) { const tableDe = new Int32Array(N * R); const occupation = new Int32Array(T * R); function* placer(i) { if (i === N * R) { yield tableDe; return; } const r = i % R; for (let t = 0; t < T; t += 1) { if ((fixe[i] !== LIBRE && fixe[i] !== t) || occupation[t * R + r] === capacite[t]) continue; occupation[t * R + r] += 1; tableDe[i] = t; yield* placer(i + 1); occupation[t * R + r] -= 1; } } yield* placer(0); } describe('le détail sur chaque plan d’une petite configuration (§ 14.10, B25)', () => { // Six personnes, trois tables de deux, deux tours ; 1 ancré à la table 1, // « finance » pour la soirée ; la table 2, « finance » au seul tour 2 ; la // table 3 sans objectif. Trente plans par tour, neuf cents en tout. test('les comptes par table et par tour tiennent leurs identités sur chacun des neuf cents plans', () => { const configuration = { participants: [ participant([1, AUBIER, ['finance']]), participant([2, AUBIER, ['finance']]), participant([3, RESSAC, ['finance']]), participant([4, RESSAC]), participant([5, null, ['finance']]), participant([6]), ], tables: [{ id: 1, numero: 1, capacite: 2 }, { id: 2, numero: 2, capacite: 2 }, { id: 3, numero: 3, capacite: 2 }], tours: 2, reservations: [tous(1, 1)], contraintes: SANS_CONTRAINTE, objectifs: [ { table: 1, tour: null, regle: 'ou', etiquettes: ['finance'] }, { table: 2, tour: 2, regle: 'ou', etiquettes: ['finance'] }, ], }; const instance = normaliser(configuration); const { N, R, T, ids } = instance; const diagnostic = diagnostiquer(configuration); const appartenance = new Map(configuration.participants.map(({ id, appartenance: a }) => [id, a])); let plans = 0; for (const tableDe of tousLesPlans(instance)) { plans += 1; const tours = Array.from({ length: R }, (_, r) => Array.from({ length: T }, (__, t) => ids.filter((___, p) => tableDe[p * R + r] === t))); const parcours = parcourirTours(instance, tableDe); const { lignes } = mixTables(instance, tableDe, occupations(instance, tableDe, { affilies: true }), parcours); for (const { t, tours: cases } of lignes) { cases.forEach((cas, r) => { const affilies = tours[r][t].filter((id) => appartenance.get(id) !== null); if (affilies.length === 0) { assert.equal(cas, null); return; } const d = new Set(affilies.map((id) => appartenance.get(id))).size; assert.equal(cas.appartenances, d); assert.equal(cas.comptes.doublons, affilies.length - d); assert.equal(cas.comptes.doublons > 0, parcours.parTour[r][t].collegues.length > 0); assert.ok(cas.comptes.sieges >= cas.comptes.assis && cas.comptes.assis >= cas.comptes.doublons); }); } const objectifs = objectifsParTour(instance, parcours, diagnostic.objectifsInevitables.parTour); assert.equal(objectifs.soiree.servis, somme(parcours.servis)); assert.equal(objectifs.soiree.plancher, diagnostic.objectifsInevitables.horsObjectif); objectifs.parTour.forEach(({ possibles, servis, hors, plancher, sansCouple }, r) => { const lu = tourLu(configuration, r + 1, tours[r]); assert.equal(sansCouple, lu.sansCouple); assert.ok(sansCouple + possibles <= N); assert.equal(possibles - servis, lu.nonFixes - lu.servisNonFixes); assert.ok(plancher <= hors); }); } assert.equal(plans, 900); }); }); // Un objet simple ou une liste, à toute profondeur, sans tableau typé. function sansTableauType(valeur, chemin = 'détail') { if (valeur === null || typeof valeur !== 'object') return; assert.ok(!ArrayBuffer.isView(valeur), `${chemin} est un tableau typé`); assert.ok(Array.isArray(valeur) || Object.getPrototypeOf(valeur) === Object.prototype, `${chemin} n'est ni une liste ni un objet simple`); for (const [cle2, element] of Object.entries(valeur)) sansTableauType(element, `${chemin}.${cle2}`); } describe('detailler (§ 12.9, D27)', () => { // Les deux configurations réunies : appartenances, objectifs et animateurs. function complet() { const prepare = preparer({ ...OBJECTIFS, personnes: OBJECTIFS.personnes.map(([id, , etiquettes], i) => [id, [AUBIER, RESSAC, null][i % 3], etiquettes, id === 7 ? [2] : undefined]), reservations: [...OBJECTIFS.reservations, auTour(7, 1, 2)], }); const { configuration, instance, tableDe } = prepare; const { mesures, realises, aPriori, aMax } = mesurerTout(prepare); const options = { diagnostic: diagnostiquer(configuration), mesures, plafondsRealises: realises, plafondsAPriori: aPriori, planchersValides: true, cle, ordreMatrice: null, }; return { prepare, instance, tableDe, mesures, realises, aPriori, aMax, options }; } test('réunit chaque fonction du détail sur le même plan, et ne rend aucun tableau typé', () => { const { instance, tableDe, mesures, realises, aPriori, aMax, options } = complet(); const detail = detailler(instance, tableDe, options); sansTableauType(detail); const profil = profilRencontres(instance, mesures, realises, aPriori, aMax); assert.deepEqual(detail.profil, profil); assert.deepEqual(detail.synthese, synthese(profil)); assert.deepEqual(detail.repartitionRencontres, repartition(mesures.rencontres, realises)); assert.deepEqual(detail.collisions, { planchersValides: true, lignes: barresCollisions(instance, mesures, planchersParGroupe(instance, options.diagnostic.collisions), true, cle), }); assert.deepEqual(detail.tables, mixTables(instance, tableDe, occupations(instance, tableDe, { affilies: true }))); assert.deepEqual(detail.paires, pairesRepetees(instance, tableDe)); assert.equal(detail.paires.choisies.length, mesures.rencontresRepetees.choisies); assert.equal(detail.paires.imposees.length, mesures.rencontresRepetees.imposees); const parcours = parcourirTours(instance, tableDe); const objectifs = objectifsParTour(instance, parcours, options.diagnostic.objectifsInevitables.parTour); assert.deepEqual(detail.objectifs, objectifs); const animateurs = profilAnimateurs(instance, mesures); assert.deepEqual(detail.animateurs, { profil: animateurs, repartition: repartition(animateurs.map(({ rencontres }) => rencontres), null), diversite: diversiteAnimateurs(instance, tableDe), }); assert.deepEqual(detail.bornes, bornesDesTuiles(instance, { mesures, plafondsRealises: realises, aMax, profil, animateurs, objectifs })); assert.equal(detail.matrice, null); }); test('la borne de diversité reçue sert telle quelle ; la matrice suit l’ordre demandé', () => { const { instance, tableDe, options } = complet(); const aMax = new Array(instance.N).fill(1); const ordre = [4, 0, 2]; const detail = detailler(instance, tableDe, { ...options, aMax, ordreMatrice: ordre }); assert.ok(detail.profil.every(({ borneDiversite: b }) => b === 1)); assert.deepEqual(detail.matrice, { ordre, comptes: matriceRencontres(instance, tableDe, ordre) }); assert.notEqual(detail.matrice.ordre, ordre); sansTableauType(detail); }); test('sans animateur ni personne concernée, les animateurs valent null ; hors du domaine, les barres n’ont qu’un segment', () => { const prepare = preparer(PROFIL); const { configuration, instance, tableDe } = prepare; const { mesures, realises, aPriori } = mesurerTout(prepare); const detail = detailler(instance, tableDe, { diagnostic: diagnostiquer(configuration), mesures, plafondsRealises: realises, plafondsAPriori: aPriori, planchersValides: false, cle, ordreMatrice: null, }); assert.equal(detail.animateurs, null); assert.equal(detail.objectifs, null); assert.ok(detail.collisions.lignes.every(({ prouve, reste }) => prouve === null && reste === null)); assert.equal(detail.collisions.planchersValides, false); }); test('des animateurs sans personne concernée : le profil et sa répartition valent null, la diversité reste', () => { // 1 anime, seul présent : il ne se rencontre pas lui-même, et personne // d'autre ne peut le rejoindre. const prepare = preparer({ personnes: [[1, null, undefined, [1]]], tables: [[1, 2]], reservations: [auTour(1, 1, 1)], plan: [{ 1: [1] }], }); const { configuration, instance, tableDe } = prepare; const { mesures, realises, aPriori } = mesurerTout(prepare); assert.equal(mesures.animateurs, null); const detail = detailler(instance, tableDe, { diagnostic: diagnostiquer(configuration), mesures, plafondsRealises: realises, plafondsAPriori: aPriori, planchersValides: true, cle, ordreMatrice: null, }); assert.equal(detail.animateurs.profil, null); assert.equal(detail.animateurs.repartition, null); assert.equal(detail.animateurs.diversite.lignes.length, 1); }); }); describe('qualite : la source du module', () => { const source = readFileSync(new URL('./qualite.js', import.meta.url), 'utf8'); test('aucun couple possible ni correspondance relus : les comptes par tour viennent de parcourirTours', () => { assert.doesNotMatch(source, /\bpossible\[/); assert.doesNotMatch(source, /\bcorrespond\[/); assert.doesNotMatch(source, /\bobjectifDe\[/); }); test('une seule multiplication par cent : celle de pourcentage', () => { assert.equal(source.match(/\b100\s*\*/g).length, 1); }); test('aucun plan relu case à case : le détail lit le parcours et l’occupation', () => { assert.doesNotMatch(source, /\btableDe\[/); }); });