The search recomputed the collision excess and the gaps that the measure already gives — two arithmetics for one quantity. It now reads them from the measure, and the redundancy total has one owner. Contract values, the indexed-plan guard and the population split are defined once. regenerer rebuilds a proposition from its derived seed, stop count and history length, which the spec now lists (§ 8.9). A property grid runs seeded random rooms through the whole chain. Checked: 435 node and 20 long tests; redefining the excess or the gaps in the measure now fails the search tests too. --- FR --- [IMP] moteur : propriétaires uniques, régénération, grille de propriétés La recherche recalculait l'excédent de collisions et les écarts que la mesure donne déjà — deux arithmétiques pour une grandeur. Elle les lit désormais dans la mesure, et le total de redondance a un seul propriétaire. Valeurs du contrat, garde du plan indexé et partition des populations sont définies une fois. regenerer reconstruit une proposition depuis sa graine dérivée, son arrêt et la longueur de son historique, que le spec énumère désormais (§ 8.9). Une grille de propriétés fait traverser toute la chaîne à des salles tirées d'une graine. Vérifié : 435 épreuves node et 20 longues ; redéfinir l'excédent ou les écarts dans la mesure fait désormais tomber aussi la recherche. Assisted-by: Claude Opus 5.5
555 lines
22 KiB
JavaScript
555 lines
22 KiB
JavaScript
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||
|
||
// Passage de la configuration, forme par identifiants, à l'instance indexée
|
||
// que consomme le moteur (§ 4, § 5.2), et conversion d'un plan entre ses deux
|
||
// formes (§ 8.9). Les formes sont décrites dans types.js.
|
||
//
|
||
// Ce module définit aussi, une seule fois pour tout le moteur (§ 13.2), ce
|
||
// que les autres modules lisent de ces formes : les valeurs du contrat —
|
||
// STATUT, LIBRE, RESERVE, SANS_GROUPE —, la garde de forme d'un plan indexé,
|
||
// le nombre de places d'un tour, et la répartition des personnes entre les
|
||
// trois populations du § 5.4. Les autres modules et leurs épreuves les
|
||
// importent, sans en redéfinir aucun.
|
||
//
|
||
// Trois familles de refus :
|
||
// - ErreurConfiguration, avec un code, pour ce qu'une saisie, un import ou un
|
||
// plan périmé peut porter : une donnée incohérente avec le reste de la
|
||
// configuration, ou une capacité, un nombre de tours, un tour désigné hors
|
||
// de son domaine ;
|
||
// - TypeError, quand la configuration sort de l'une des formes que garantit
|
||
// le code qui la bâtit — stockage, import, interface — et qu'aucune saisie
|
||
// ne produit : un identifiant de participant ou de table qui n'est pas un
|
||
// entier ≥ 1, une appartenance ni chaîne ni null, un exclu présent et non
|
||
// booléen, une portée ni « tous » ni « tour », des contraintes qui ne sont
|
||
// pas un objet de quatre booléens, une liste qui n'est pas un tableau. Ces
|
||
// formes sont les seules que normaliser contrôle par TypeError ; une valeur
|
||
// qui en sort est une faute de l'appelant, qu'aucune valeur par défaut ne
|
||
// masque ;
|
||
// - RangeError, pour un plan indexé qui n'a pas la forme de l'instance : seul
|
||
// le moteur produit cette forme, et l'écart y est une faute de code.
|
||
//
|
||
// Les champs que normaliser ne lit pas — nom, prénom, numéro de table — ne
|
||
// sont pas examinés. Aucune fonction ne modifie ce qu'elle reçoit.
|
||
import { ErreurConfiguration } from './erreurs.js';
|
||
|
||
/**
|
||
* Statuts d'un participant (§ 4.2), valeurs de instance.statut. normaliser
|
||
* les dérive des réservations ; aucun n'est déclaré. Un partiellement fixé
|
||
* compte parmi les mobiles dans les populations du § 5.4.
|
||
*/
|
||
export const STATUT = Object.freeze({ MOBILE: 0, PARTIELLEMENT_FIXE: 1, ANCRE: 2 });
|
||
|
||
/** Case de instance.fixe sans table imposée. */
|
||
export const LIBRE = -1;
|
||
|
||
/**
|
||
* Case d'un plan indexé : la réserve. Elle vaut LIBRE, et une rangée de
|
||
* instance.fixe se lit ainsi comme le plan qui assied chacun à ses seuls
|
||
* tours fixés et le laisse en réserve aux autres : le diagnostic mesure ce
|
||
* plan, le plafond a priori part de cet itinéraire.
|
||
*/
|
||
export const RESERVE = -1;
|
||
|
||
/** instance.groupe d'un participant sans appartenance. */
|
||
export const SANS_GROUPE = -1;
|
||
|
||
// Case d'un plan indexé qu'indexerPlan n'a pas encore remplie ; jamais rendue.
|
||
const NON_PLACE = -2;
|
||
|
||
// Plus grand entier que représente un Int32Array : une capacité au-delà s'y
|
||
// tronquerait sans bruit.
|
||
const ENTIER_32_MAX = 2 ** 31 - 1;
|
||
|
||
const NOMS_CONTRAINTES = [
|
||
'separerAppartenances',
|
||
'nouveauxVoisins',
|
||
'nouvelleTable',
|
||
'varierAppartenances',
|
||
];
|
||
|
||
// 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));
|
||
|
||
function exigerListe(valeur, nom) {
|
||
if (!Array.isArray(valeur)) throw new TypeError(`configuration.${nom} : liste attendue`);
|
||
}
|
||
|
||
function exigerIdentifiant(id, quoi) {
|
||
if (!Number.isInteger(id) || id < 1) {
|
||
throw new TypeError(`${quoi} d'identifiant ${decrire(id)} : entier ≥ 1 attendu`);
|
||
}
|
||
}
|
||
|
||
function exigerAppartenance({ id, appartenance }) {
|
||
if (appartenance !== null && typeof appartenance !== 'string') {
|
||
throw new TypeError(
|
||
`participant ${id} : appartenance chaîne ou null attendue, reçu ${decrire(appartenance)}`,
|
||
);
|
||
}
|
||
}
|
||
|
||
// Vrai pour un participant exclu ; un champ exclu absent vaut false.
|
||
function estExclu({ id, exclu }) {
|
||
if (exclu === undefined) return false;
|
||
if (typeof exclu !== 'boolean') {
|
||
throw new TypeError(`participant ${id} : exclu booléen attendu, reçu ${decrire(exclu)}`);
|
||
}
|
||
return exclu;
|
||
}
|
||
|
||
// Copie des quatre contraintes : une modification ultérieure de la
|
||
// configuration n'atteint pas l'instance.
|
||
function copierContraintes(contraintes) {
|
||
if (typeof contraintes !== 'object' || contraintes === null) {
|
||
throw new TypeError('configuration.contraintes : objet attendu');
|
||
}
|
||
const copie = {};
|
||
for (const nom of NOMS_CONTRAINTES) {
|
||
if (typeof contraintes[nom] !== 'boolean') {
|
||
throw new TypeError(`contrainte ${nom} : booléen attendu, reçu ${decrire(contraintes[nom])}`);
|
||
}
|
||
copie[nom] = contraintes[nom];
|
||
}
|
||
return copie;
|
||
}
|
||
|
||
// Groupe de chaque participant présent, présents triés par identifiant
|
||
// croissant : un libellé reçoit le numéro de sa première apparition,
|
||
// SANS_GROUPE marque l'absence d'appartenance. L'égalité de chaînes fait le
|
||
// groupe. La Map ne sert qu'à retrouver un libellé déjà numéroté.
|
||
function numeroterGroupes(presents) {
|
||
const groupe = new Int32Array(presents.length);
|
||
const groupes = [];
|
||
const numeroDe = new Map();
|
||
for (let p = 0; p < presents.length; p += 1) {
|
||
const { appartenance } = presents[p];
|
||
if (appartenance === null) {
|
||
groupe[p] = SANS_GROUPE;
|
||
continue;
|
||
}
|
||
if (!numeroDe.has(appartenance)) {
|
||
numeroDe.set(appartenance, groupes.length);
|
||
groupes.push(appartenance);
|
||
}
|
||
groupe[p] = numeroDe.get(appartenance);
|
||
}
|
||
return { groupe, groupes };
|
||
}
|
||
|
||
// Tables imposées par les réservations : fixe[p * R + r] = index de table,
|
||
// LIBRE sinon. Chaque réservation est vérifiée dans sa forme — participant et
|
||
// table connus, tour dans 1..R —, exclus compris ; celle d'un exclu s'arrête
|
||
// là, suspendue, sans effet (§ 4.4). Une réservation qui répète la table déjà
|
||
// imposée au même tour ne change rien ; une autre table au même tour lève
|
||
// RESERVATION_CONFLIT.
|
||
function fixerReservations(reservations, contexte) {
|
||
const { connus, indexDe, indexTableDe, idsTables, N, R } = contexte;
|
||
const fixe = new Int32Array(N * R).fill(LIBRE);
|
||
for (let i = 0; i < reservations.length; i += 1) {
|
||
const { participant: id, table: idTable, portee, tour } = reservations[i];
|
||
if (!connus.has(id)) {
|
||
throw new ErreurConfiguration('RESERVATION_INCONNUE', { reservation: i, participant: id });
|
||
}
|
||
const t = indexTableDe.get(idTable);
|
||
if (t === undefined) {
|
||
throw new ErreurConfiguration('RESERVATION_INCONNUE', { reservation: i, table: idTable });
|
||
}
|
||
// Tours couverts, numérotés à partir de 0 : chacun des R tours pour la
|
||
// portée « tous », le tour désigné pour la portée « tour ».
|
||
let premier;
|
||
let dernier;
|
||
if (portee === 'tous') {
|
||
premier = 0;
|
||
dernier = R - 1;
|
||
} else if (portee === 'tour') {
|
||
if (!Number.isInteger(tour) || tour < 1 || tour > R) {
|
||
throw new ErreurConfiguration('RESERVATION_TOUR', { reservation: i, tour });
|
||
}
|
||
premier = tour - 1;
|
||
dernier = tour - 1;
|
||
} else {
|
||
throw new TypeError(
|
||
`réservation ${i} : portée ${decrire(portee)}, « tous » ou « tour » attendue`,
|
||
);
|
||
}
|
||
const p = indexDe.get(id);
|
||
if (p === undefined) continue;
|
||
for (let r = premier; r <= dernier; r += 1) {
|
||
const imposee = fixe[p * R + r];
|
||
if (imposee === LIBRE) {
|
||
fixe[p * R + r] = t;
|
||
} else if (imposee !== t) {
|
||
throw new ErreurConfiguration('RESERVATION_CONFLIT', {
|
||
participant: id,
|
||
tour: r + 1,
|
||
tables: [idsTables[imposee], idTable],
|
||
});
|
||
}
|
||
}
|
||
}
|
||
return fixe;
|
||
}
|
||
|
||
// Lève SURRESERVATION quand, à un tour, une table reçoit plus de personnes
|
||
// fixées que de sièges (§ 5.9). Le compte porte sur les personnes, lues dans
|
||
// fixe : une réservation répétée ne compte qu'une fois. Les tables se
|
||
// parcourent par index, chacune par tours croissants.
|
||
function verifierSurreservation(fixe, { N, T, R, capacite, idsTables }) {
|
||
const fixees = new Int32Array(T * R);
|
||
for (let p = 0; p < N; p += 1) {
|
||
for (let r = 0; r < R; r += 1) {
|
||
const t = fixe[p * R + r];
|
||
if (t !== LIBRE) fixees[t * R + r] += 1;
|
||
}
|
||
}
|
||
for (let t = 0; t < T; t += 1) {
|
||
for (let r = 0; r < R; r += 1) {
|
||
if (fixees[t * R + r] > capacite[t]) {
|
||
throw new ErreurConfiguration('SURRESERVATION', {
|
||
table: idsTables[t],
|
||
tour: r + 1,
|
||
reservees: fixees[t * R + r],
|
||
capacite: capacite[t],
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Statut de chaque participant, dérivé de fixe et non de la forme des
|
||
// réservations (§ 4.2) : ancré quand la même table est imposée à chacun des R
|
||
// tours, mobile quand aucune ne l'est, partiellement fixé sinon. Un ancré
|
||
// compte dans k et dans ancresParTable ; un partiellement fixé compte parmi
|
||
// les mobiles.
|
||
function deriverStatuts(fixe, N, T, R) {
|
||
const statut = new Uint8Array(N);
|
||
const ancresParTable = new Int32Array(T);
|
||
let k = 0;
|
||
for (let p = 0; p < N; p += 1) {
|
||
const premiere = fixe[p * R];
|
||
let memeTable = true;
|
||
let aucune = true;
|
||
for (let r = 0; r < R; r += 1) {
|
||
const t = fixe[p * R + r];
|
||
if (t !== premiere) memeTable = false;
|
||
if (t !== LIBRE) aucune = false;
|
||
}
|
||
if (aucune) {
|
||
statut[p] = STATUT.MOBILE;
|
||
} else if (memeTable) {
|
||
statut[p] = STATUT.ANCRE;
|
||
ancresParTable[premiere] += 1;
|
||
k += 1;
|
||
} else {
|
||
statut[p] = STATUT.PARTIELLEMENT_FIXE;
|
||
}
|
||
}
|
||
return { statut, ancresParTable, k };
|
||
}
|
||
|
||
/**
|
||
* Instance indexée d'une configuration (§ 4, § 5.2). Les participants exclus
|
||
* en sont absents (§ 4.4) : ni index ni groupe, et leurs réservations,
|
||
* suspendues, ne fixent rien et ne prennent aucun siège.
|
||
*
|
||
* Lève ErreurConfiguration sur la première règle enfreinte — participants,
|
||
* puis tables, nombre de tours, réservations une à une, surréservation :
|
||
* PARTICIPANT_DOUBLON, TABLE_DOUBLON, CAPACITE (non entière, < 2, ou au-delà
|
||
* d'un Int32Array), TOURS (R non entier ou < 1), RESERVATION_INCONNUE
|
||
* (participant ou table), RESERVATION_TOUR (tour absent ou hors de 1..R),
|
||
* RESERVATION_CONFLIT (une personne à deux tables d'un même tour),
|
||
* SURRESERVATION (plus de personnes fixées que de sièges à une table, à un
|
||
* tour). Une salle trop petite n'est pas refusée : nombrePlacesManquantes la
|
||
* chiffre, et la recherche la refuse (§ 5.9). Les détails désignent
|
||
* participants et tables par identifiant, les tours par leur numéro à partir
|
||
* de 1, et la réservation fautive par son rang dans
|
||
* configuration.reservations, à partir de 0.
|
||
*
|
||
* Lève TypeError sur les seules formes qu'énumère l'en-tête du module :
|
||
* identifiants de participant et de table, appartenance, exclu, portée,
|
||
* contraintes, listes. Une capacité, un nombre de tours ou un tour désigné
|
||
* hors de leur forme lèvent CAPACITE, TOURS ou RESERVATION_TOUR ; une
|
||
* réservation qui désigne un participant ou une table absents de la
|
||
* configuration lève RESERVATION_INCONNUE, quelle que soit la forme de la
|
||
* valeur qui les désigne.
|
||
*
|
||
* @param {import('./types.js').Configuration} configuration
|
||
* @returns {import('./types.js').Instance}
|
||
*/
|
||
export function normaliser(configuration) {
|
||
const { participants, tables, tours: R, reservations } = configuration;
|
||
exigerListe(participants, 'participants');
|
||
exigerListe(tables, 'tables');
|
||
exigerListe(reservations, 'reservations');
|
||
const contraintes = copierContraintes(configuration.contraintes);
|
||
|
||
// connus porte tous les identifiants, exclus compris : un doublon reste un
|
||
// doublon, et la réservation d'un exclu n'est pas inconnue.
|
||
const connus = new Set();
|
||
const presents = [];
|
||
const exclus = [];
|
||
for (const participant of participants) {
|
||
exigerIdentifiant(participant.id, 'participant');
|
||
if (connus.has(participant.id)) {
|
||
throw new ErreurConfiguration('PARTICIPANT_DOUBLON', { participant: participant.id });
|
||
}
|
||
connus.add(participant.id);
|
||
exigerAppartenance(participant);
|
||
if (estExclu(participant)) exclus.push(participant.id);
|
||
else presents.push(participant);
|
||
}
|
||
presents.sort((a, b) => a.id - b.id);
|
||
exclus.sort((a, b) => a - b);
|
||
|
||
const T = tables.length;
|
||
const idsTables = [];
|
||
const indexTableDe = new Map();
|
||
const capacite = new Int32Array(T);
|
||
for (let t = 0; t < T; t += 1) {
|
||
const { id, capacite: places } = tables[t];
|
||
exigerIdentifiant(id, 'table');
|
||
if (indexTableDe.has(id)) throw new ErreurConfiguration('TABLE_DOUBLON', { table: id });
|
||
if (!Number.isInteger(places) || places < 2 || places > ENTIER_32_MAX) {
|
||
throw new ErreurConfiguration('CAPACITE', { table: id, capacite: places });
|
||
}
|
||
idsTables.push(id);
|
||
indexTableDe.set(id, t);
|
||
capacite[t] = places;
|
||
}
|
||
|
||
if (!Number.isInteger(R) || R < 1) throw new ErreurConfiguration('TOURS', { tours: R });
|
||
|
||
const N = presents.length;
|
||
const ids = presents.map((participant) => participant.id);
|
||
const indexDe = new Map(ids.map((id, p) => [id, p]));
|
||
const { groupe, groupes } = numeroterGroupes(presents);
|
||
const fixe = fixerReservations(reservations, { connus, indexDe, indexTableDe, idsTables, N, R });
|
||
verifierSurreservation(fixe, { N, T, R, capacite, idsTables });
|
||
const { statut, ancresParTable, k } = deriverStatuts(fixe, N, T, R);
|
||
|
||
return {
|
||
N,
|
||
T,
|
||
R,
|
||
ids,
|
||
indexDe,
|
||
exclus: new Set(exclus),
|
||
idsTables,
|
||
indexTableDe,
|
||
capacite,
|
||
groupe,
|
||
groupes,
|
||
fixe,
|
||
statut,
|
||
ancresParTable,
|
||
k,
|
||
n: N - k,
|
||
contraintes,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Plan indexé d'un plan par identifiants : tableDe[p * R + r] = index de
|
||
* table, −1 pour la réserve. plan.tables déclare chaque table de l'instance
|
||
* une fois, et chaque liste se rattache à sa table par l'identifiant déclaré
|
||
* à la même position ; l'ordre des tables et celui des ids dans une liste
|
||
* sont indifférents. Un plan accepté revient donc de planDepuisIndex à
|
||
* l'identique, à ces deux ordres près.
|
||
*
|
||
* La forme indexée n'a pas de case pour un participant absent d'un tour,
|
||
* placé deux fois ou étranger à l'instance, ni pour une table que le plan ne
|
||
* déclare pas : au lieu de perdre l'écart, indexerPlan lève
|
||
* ErreurConfiguration — PLAN_TOURS (tours ou réserves en nombre ≠ R),
|
||
* PLAN_TABLE_INCONNUE, PLAN_TABLE_DOUBLON, PLAN_TABLE_ABSENTE (une table de
|
||
* l'instance que plan.tables ne déclare pas), PLAN_LISTES (un tour sans
|
||
* exactement une liste par table déclarée), PLAN_INCONNU, PLAN_EXCLU_PLACE
|
||
* (participant exclu de l'instance), PLAN_DOUBLE_PLACE (deux fois dans un
|
||
* tour, réserve comprise), PLAN_NON_ASSIS (ni à une table ni en réserve à un
|
||
* tour). Les capacités et les réservations ne sont pas examinées.
|
||
*
|
||
* @param {import('./types.js').Instance} instance
|
||
* @param {import('./types.js').Plan} plan
|
||
* @returns {Int32Array}
|
||
*/
|
||
export function indexerPlan(instance, plan) {
|
||
const { N, T, R, ids, idsTables, indexDe, indexTableDe, exclus } = instance;
|
||
if (plan.tours.length !== R || plan.reserves.length !== R) {
|
||
throw new ErreurConfiguration('PLAN_TOURS', {
|
||
attendu: R,
|
||
tours: plan.tours.length,
|
||
reserves: plan.reserves.length,
|
||
});
|
||
}
|
||
// Index de table de chaque liste, par position dans plan.tables.
|
||
const tableDeListe = [];
|
||
const declarees = new Set();
|
||
for (const id of plan.tables) {
|
||
const t = indexTableDe.get(id);
|
||
if (t === undefined) throw new ErreurConfiguration('PLAN_TABLE_INCONNUE', { table: id });
|
||
if (declarees.has(id)) throw new ErreurConfiguration('PLAN_TABLE_DOUBLON', { table: id });
|
||
declarees.add(id);
|
||
tableDeListe.push(t);
|
||
}
|
||
// Sans id inconnu ni doublon, une table manque exactement quand le plan en
|
||
// déclare moins que l'instance ; la première absente, dans l'ordre de
|
||
// l'instance, est nommée.
|
||
if (tableDeListe.length !== T) {
|
||
const absente = idsTables.find((id) => !declarees.has(id));
|
||
throw new ErreurConfiguration('PLAN_TABLE_ABSENTE', { table: absente });
|
||
}
|
||
|
||
const tableDe = new Int32Array(N * R).fill(NON_PLACE);
|
||
const placer = (id, r, t) => {
|
||
const p = indexDe.get(id);
|
||
if (p === undefined) {
|
||
const code = exclus.has(id) ? 'PLAN_EXCLU_PLACE' : 'PLAN_INCONNU';
|
||
throw new ErreurConfiguration(code, { participant: id, tour: r + 1 });
|
||
}
|
||
if (tableDe[p * R + r] !== NON_PLACE) {
|
||
throw new ErreurConfiguration('PLAN_DOUBLE_PLACE', { participant: id, tour: r + 1 });
|
||
}
|
||
tableDe[p * R + r] = t;
|
||
};
|
||
for (let r = 0; r < R; r += 1) {
|
||
const listes = plan.tours[r];
|
||
if (listes.length !== tableDeListe.length) {
|
||
throw new ErreurConfiguration('PLAN_LISTES', {
|
||
tour: r + 1,
|
||
listes: listes.length,
|
||
tables: tableDeListe.length,
|
||
});
|
||
}
|
||
for (let i = 0; i < listes.length; i += 1) {
|
||
for (const id of listes[i]) placer(id, r, tableDeListe[i]);
|
||
}
|
||
for (const id of plan.reserves[r]) placer(id, r, RESERVE);
|
||
for (let p = 0; p < N; p += 1) {
|
||
if (tableDe[p * R + r] === NON_PLACE) {
|
||
throw new ErreurConfiguration('PLAN_NON_ASSIS', { participant: ids[p], tour: r + 1 });
|
||
}
|
||
}
|
||
}
|
||
return tableDe;
|
||
}
|
||
|
||
/**
|
||
* La garde de forme d'un plan indexé, la seule du moteur : planDepuisIndex,
|
||
* mesurer et plafondsRealises l'appellent avant de lire une case. Lève
|
||
* RangeError quand tableDe n'a pas N × R cases, ou qu'une case n'est ni
|
||
* RESERVE ni un index de table entier de 0 à T − 1. Le message nomme alors
|
||
* le participant, par identifiant, le tour et la valeur de la première case
|
||
* fautive dans l'ordre des cases, p × R + r croissant ; une chaîne s'y cite
|
||
* entre guillemets, pour ne pas se lire comme le nombre qu'elle contient.
|
||
*
|
||
* Sans elle, une case de trop serait ignorée, et une chaîne numérique comme
|
||
* "0" assiérait la personne à la table qu'elle désigne là où une
|
||
* comparaison stricte à instance.fixe n'y reconnaîtrait pas sa réservation.
|
||
*
|
||
* @param {import('./types.js').Instance} instance
|
||
* @param {ArrayLike<number>} tableDe
|
||
*/
|
||
export function exigerPlanIndexe({ 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 i = 0; i < tableDe.length; i += 1) {
|
||
const t = tableDe[i];
|
||
if (t !== RESERVE && !(Number.isInteger(t) && t >= 0 && t < T)) {
|
||
throw new RangeError(
|
||
`plan indexé : participant ${ids[Math.floor(i / R)]}, tour ${(i % R) + 1}, `
|
||
+ `index de table ${decrire(t)} hors de −1..${T - 1}`,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Plan par identifiants d'un plan indexé : les tables dans l'ordre de
|
||
* l'instance, chaque liste de table et chaque réserve en ids croissants
|
||
* (§ 8.9). Lève ce que lève exigerPlanIndexe.
|
||
*
|
||
* @param {import('./types.js').Instance} instance
|
||
* @param {ArrayLike<number>} tableDe
|
||
* @returns {import('./types.js').Plan}
|
||
*/
|
||
export function planDepuisIndex(instance, tableDe) {
|
||
exigerPlanIndexe(instance, tableDe);
|
||
const { N, T, R, ids, idsTables } = instance;
|
||
const tours = [];
|
||
const reserves = [];
|
||
for (let r = 0; r < R; r += 1) {
|
||
const listes = Array.from({ length: T }, () => []);
|
||
const reserve = [];
|
||
// Les ids croissent avec l'index : parcourir les participants par index
|
||
// range chaque liste en ids croissants, sans tri.
|
||
for (let p = 0; p < N; p += 1) {
|
||
const t = tableDe[p * R + r];
|
||
if (t === RESERVE) reserve.push(ids[p]);
|
||
else listes[t].push(ids[p]);
|
||
}
|
||
tours.push(listes);
|
||
reserves.push(reserve);
|
||
}
|
||
return { tables: [...idsTables], tours, reserves };
|
||
}
|
||
|
||
/**
|
||
* Places qu'offre chaque tour : Σ c_t, la somme des capacités. Ni les
|
||
* participants ni les réservations n'y entrent. Le nombre de places
|
||
* manquantes et le diagnostic d'une salle tendue la lisent ici.
|
||
*
|
||
* @param {import('./types.js').Instance} instance T et capacite sont lus
|
||
* @returns {number}
|
||
*/
|
||
export function nombrePlaces({ T, capacite }) {
|
||
let places = 0;
|
||
for (let t = 0; t < T; t += 1) places += capacite[t];
|
||
return places;
|
||
}
|
||
|
||
/**
|
||
* Places qui manquent à chaque tour pour asseoir tous les participants :
|
||
* max(0, N − Σ c_t). La quantité égale max(0, n − Σ (c_t − a_t)) et ne
|
||
* dépend d'aucune réservation (§ 5.9) : un ancré retire une personne des
|
||
* mobiles et un siège de la capacité libre, un partiellement fixé reste
|
||
* parmi les mobiles et ne retire rien.
|
||
*
|
||
* @param {import('./types.js').Instance} instance
|
||
* @returns {number}
|
||
*/
|
||
export function nombrePlacesManquantes(instance) {
|
||
return Math.max(0, instance.N - nombrePlaces(instance));
|
||
}
|
||
|
||
/**
|
||
* Replie valeurs[p] sur les trois populations du § 5.4, en un passage, dans
|
||
* l'ordre canonique : chacun compte dans « tous », puis parmi les ancrés, ou
|
||
* parmi les mobiles, partiellement fixés compris. C'est le seul endroit qui
|
||
* range une personne dans sa population : les agrégats des indicateurs et
|
||
* les chiffres du manque le lisent tous deux.
|
||
*
|
||
* replier(cumul, valeur) rend le cumul suivant sans modifier celui qu'il
|
||
* reçoit : les trois populations partent du même initial, et une population
|
||
* sans membre le garde.
|
||
*
|
||
* @template C
|
||
* @param {ArrayLike<*>} valeurs une par personne, dans l'ordre de instance.ids
|
||
* @param {ArrayLike<number>} statut instance.statut
|
||
* @param {C} initial
|
||
* @param {function(C, *): C} replier
|
||
* @returns {{tous: C, mobiles: C, ancres: C}}
|
||
*/
|
||
export function replierParPopulation(valeurs, statut, initial, replier) {
|
||
let tous = initial;
|
||
let mobiles = initial;
|
||
let ancres = initial;
|
||
for (let p = 0; p < valeurs.length; p += 1) {
|
||
const valeur = valeurs[p];
|
||
tous = replier(tous, valeur);
|
||
if (statut[p] === STATUT.ANCRE) ancres = replier(ancres, valeur);
|
||
else mobiles = replier(mobiles, valeur);
|
||
}
|
||
return { tous, mobiles, ancres };
|
||
}
|