[ADD] storage: positional placements, faulty versus stale propositions
Placements are stored positionally: per proposition its seed, stop count, history length and building version, then per round the ordered tables and the reserve. A proposition that contradicts itself — wrong length, repeated, unknown or missing id, an id already taken — is named and left out, and the event still opens. One that no longer matches the event — person excluded or added, table removed, capacity or rounds changed — is kept and reported as drift. A named form serves reading. Checked: 42 tests on the small demonstration; reviewed, mutants killed. --- FR --- [ADD] stockage : placements positionnels, fautes et dérives Les placements se rangent par position : pour chaque proposition sa graine, son compte d'arrêt, la longueur d'historique et la version qui l'a produite, puis par tour les tables ordonnées et la réserve. Une proposition qui se contredit — longueur fausse, identifiant répété, inconnu, manquant ou déjà pris — est nommée et écartée, et l'événement s'ouvre. Celle qui ne décrit plus l'événement — personne exclue ou ajoutée, table retirée, capacité ou tours changés — est gardée et signalée comme dérive. Une forme nommée sert à la lecture. Vérifié : 42 épreuves sur la petite démonstration ; revue, mutants tués. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
This commit is contained in:
parent
e1ea16d244
commit
d0f4081961
2 changed files with 1827 additions and 0 deletions
545
src/stockage/placements.js
Normal file
545
src/stockage/placements.js
Normal file
|
|
@ -0,0 +1,545 @@
|
|||
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
|
||||
|
||||
// Les placements du fichier d'état, sous leur forme positionnelle (§ 8.9) :
|
||||
// la proposition du moteur mise en forme de fichier, et le plan du moteur
|
||||
// qu'on en tire ; le contrôle de cohérence d'une proposition ou du retenu ;
|
||||
// leur dérive contre la charge courante ; leur forme nommée ; le partage
|
||||
// d'une charge lue entre propositions gardées et écartées. Les formes sont
|
||||
// décrites dans types.js et dans ce module.
|
||||
//
|
||||
// Fautif et périmé sont deux choses (§ 8.9, point 3). Un placement fautif se
|
||||
// contredit lui-même : fautes nomme chaque contradiction, et examiner écarte
|
||||
// la proposition fautive de la charge sans refuser le fichier ; chacune tombe
|
||||
// seule. examiner compare aussi les identifiants des propositions entre eux
|
||||
// et à prochainsIds.proposition, que l'analyse du fichier ne lit pas, et
|
||||
// rend ce compteur relevé au-delà de chaque identifiant lu : aucun ne se
|
||||
// réattribue. Un placement cohérent qui ne décrit plus la configuration
|
||||
// courante est périmé : derive dit pourquoi, et examiner le garde (§ 9). Le
|
||||
// retenu fautif est signalé et gardé : sa reprise par le .precedent
|
||||
// appartient au dépôt (§ 8.8).
|
||||
//
|
||||
// La forme d'une proposition et du retenu suit les règles de SCHEMA, le
|
||||
// schéma du fichier d'état, dont l'analyse n'applique que celle du
|
||||
// conteneur : fautes applique les autres, avant toute cohérence, par le
|
||||
// parcours même de l'analyse, premiereFaute, qui nomme les chemins à sa
|
||||
// manière. Aucune fonction ne modifie ce qu'elle reçoit ; aucune ne lit
|
||||
// l'horloge ni un aléa.
|
||||
import { SCHEMA, capacite, premiereFaute } from './document.js';
|
||||
|
||||
/**
|
||||
* Une faute d'un placement ; types.js en donne la table des codes. FORME :
|
||||
* une valeur sort de sa règle, ou la déclaration se contredit ; chemin
|
||||
* désigne l'élément fautif dans le placement, à la manière de l'analyse du
|
||||
* fichier, '' pour le placement lui-même. LONGUEUR : une longueur écrite
|
||||
* contredit celle que le placement déclare ; tour et table null désignent le
|
||||
* nombre de tours, table null seule le nombre de listes du tour. DOUBLON,
|
||||
* INCONNU, MANQUANT : un identifiant écrit plus d'une fois, écrit sans être
|
||||
* déclaré, déclaré sans être écrit, dans les listes de table et la réserve
|
||||
* d'un tour. Les tours se numérotent à partir de 1, tables et participants
|
||||
* par identifiant. Ces quatre codes sont des contradictions internes, que
|
||||
* fautes nomme. IDENTIFIANT_REPETE : une proposition cohérente porte
|
||||
* l'identifiant d'une proposition gardée avant elle.
|
||||
* IDENTIFIANT_HORS_COMPTEUR : une proposition cohérente, ou le retenu par sa
|
||||
* proposition d'origine, porte un identifiant qui atteint
|
||||
* prochainsIds.proposition. examiner seul nomme ces deux-là, en comparant
|
||||
* les propositions entre elles et à la charge.
|
||||
*
|
||||
* @typedef {{code: 'FORME', chemin: string}
|
||||
* | {code: 'LONGUEUR', tour: number|null, table: number|null, declare: number, ecrit: number}
|
||||
* | {code: 'DOUBLON'|'INCONNU'|'MANQUANT', tour: number, participant: number}
|
||||
* | {code: 'IDENTIFIANT_REPETE'|'IDENTIFIANT_HORS_COMPTEUR'}} Faute
|
||||
*
|
||||
* Une raison de dérive d'un placement cohérent contre la charge courante.
|
||||
*
|
||||
* @typedef {{code: 'PARTICIPANT_EXCLU'|'PARTICIPANT_SUPPRIME'|'PARTICIPANT_NON_PLACE', participant: number}
|
||||
* | {code: 'TABLE_SUPPRIMEE'|'TABLE_AJOUTEE', table: number}
|
||||
* | {code: 'CAPACITE_CHANGEE', table: number, avant: number, maintenant: number}
|
||||
* | {code: 'TOURS_CHANGES', avant: number, maintenant: number}} Raison
|
||||
*
|
||||
* Une ligne de la forme nommée : une personne placée à un tour.
|
||||
*
|
||||
* @typedef {Object} Ligne
|
||||
* @property {number} tour
|
||||
* @property {number|null} table identifiant ; null pour la réserve
|
||||
* @property {number|null} numero numéro affiché de la table dans la
|
||||
* charge ; null pour la réserve et pour
|
||||
* une table que la charge n'a plus
|
||||
* @property {number|null} siege rang dans la liste, à partir de 1, quand
|
||||
* les sièges sont attribués ; null sinon
|
||||
* @property {number} participant
|
||||
* @property {string|null} nom null pour une personne que la charge
|
||||
* n'a plus
|
||||
* @property {string|null} prenom
|
||||
*
|
||||
* Ce que rend examiner.
|
||||
*
|
||||
* @typedef {Object} Examen
|
||||
* @property {import('./types.js').Charge} charge sans les propositions
|
||||
* fautives, son compteur des propositions relevé au-delà de chaque
|
||||
* identifiant lu
|
||||
* @property {Array<{rang: number, id: number|null, fautes: Faute[]}>} fautives
|
||||
* par rang dans la liste lue ; id null quand il ne se lit pas
|
||||
* @property {Array<{id: number, raisons: Raison[]}>} derives par identifiant
|
||||
* @property {{fautes: Faute[], raisons: Raison[]}} retenu
|
||||
*/
|
||||
|
||||
// Règle du champ cle d'un objet du schéma.
|
||||
const regleDuChamp = (regle, cle) => regle.champs.find(([nom]) => nom === cle)[1];
|
||||
const CHARGE = regleDuChamp(SCHEMA, 'charge');
|
||||
// Règles d'une proposition et du retenu. Aucune des valeurs qu'elles
|
||||
// contiennent n'admet null ; examiner traite le retenu nul, son absence,
|
||||
// avant de lire le retenu.
|
||||
const PROPOSITION = regleDuChamp(CHARGE, 'propositions').element;
|
||||
const RETENU = regleDuChamp(CHARGE, 'retenu');
|
||||
const IDENTIFIANT = regleDuChamp(PROPOSITION, 'id');
|
||||
|
||||
const croissant = (a, b) => a - b;
|
||||
// Deux chaînes, comparées unité UTF-16 par unité.
|
||||
const comparerTextes = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
||||
|
||||
// Chemin du second exemplaire du premier identifiant que liste porte deux
|
||||
// fois, ou null.
|
||||
function doublon(liste, nom) {
|
||||
const vus = new Set();
|
||||
for (let rang = 0; rang < liste.length; rang += 1) {
|
||||
if (vus.has(liste[rang])) return `${nom}[${rang}]`;
|
||||
vus.add(liste[rang]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Chemin de la première contradiction de la déclaration d'un placement de
|
||||
// forme conforme, ou null, dans l'ordre du schéma : une table déclarée deux
|
||||
// fois ; des capacités qui ne correspondent pas aux tables rang à rang ; un
|
||||
// participant déclaré deux fois. D'un doublon, le second est nommé, comme
|
||||
// l'analyse le fait des identifiants de la charge.
|
||||
function fauteDeDeclaration({ tables, capacites, participants }) {
|
||||
return (
|
||||
doublon(tables, 'tables')
|
||||
?? (capacites.length === tables.length ? null : 'capacites')
|
||||
?? doublon(participants, 'participants')
|
||||
);
|
||||
}
|
||||
|
||||
// Fautes de cohérence d'un placement de forme et de déclaration conformes
|
||||
// (§ 8.9, points 1 et 2). D'abord le nombre de tours écrits contre celui
|
||||
// déclaré. Puis, tour après tour : le nombre de listes de table contre celui
|
||||
// des tables déclarées ; la longueur de chaque liste appariée à une table
|
||||
// déclarée, dans l'ordre déclaré, contre sa capacité ; le multiensemble du
|
||||
// tour, listes de table et réserve comprises, où chaque identifiant déclaré
|
||||
// figure exactement une fois — doublons, inconnus, puis manquants, chacun
|
||||
// nommé une fois par tour, par identifiant croissant.
|
||||
//
|
||||
// La capacité borne une liste sans la fixer : quand la salle offre plus de
|
||||
// sièges que de personnes, des tables gardent des places vides, et une liste
|
||||
// plus courte que sa capacité n'est pas fautive. Un décalage qui remplit une
|
||||
// table au-delà de sa capacité l'est.
|
||||
//
|
||||
// Les occurrences d'un tour se comptent dans un Map ; aucune décision ne
|
||||
// suit son ordre : doublons et manquants se lisent dans l'ordre des
|
||||
// identifiants déclarés, triés, et les inconnus se trient avant d'être
|
||||
// nommés.
|
||||
function fautesDeCoherence({ tables, capacites, tours, participants, placement }) {
|
||||
const liste = [];
|
||||
if (placement.length !== tours) {
|
||||
liste.push({ code: 'LONGUEUR', tour: null, table: null, declare: tours, ecrit: placement.length });
|
||||
}
|
||||
const declares = [...participants].sort(croissant);
|
||||
const estDeclare = new Set(declares);
|
||||
placement.forEach(({ sieges, reserve }, rang) => {
|
||||
const tour = rang + 1;
|
||||
if (sieges.length !== tables.length) {
|
||||
liste.push({ code: 'LONGUEUR', tour, table: null, declare: tables.length, ecrit: sieges.length });
|
||||
}
|
||||
const appariees = Math.min(sieges.length, tables.length);
|
||||
for (let i = 0; i < appariees; i += 1) {
|
||||
if (sieges[i].length > capacites[i]) {
|
||||
liste.push({ code: 'LONGUEUR', tour, table: tables[i], declare: capacites[i], ecrit: sieges[i].length });
|
||||
}
|
||||
}
|
||||
const occurrences = new Map();
|
||||
const inconnus = [];
|
||||
for (const ids of [...sieges, reserve]) {
|
||||
for (const id of ids) {
|
||||
const deja = occurrences.get(id) ?? 0;
|
||||
occurrences.set(id, deja + 1);
|
||||
if (deja === 0 && !estDeclare.has(id)) inconnus.push(id);
|
||||
}
|
||||
}
|
||||
for (const id of declares) {
|
||||
if (occurrences.get(id) > 1) liste.push({ code: 'DOUBLON', tour, participant: id });
|
||||
}
|
||||
for (const id of inconnus.sort(croissant)) liste.push({ code: 'INCONNU', tour, participant: id });
|
||||
for (const id of declares) {
|
||||
if (!occurrences.has(id)) liste.push({ code: 'MANQUANT', tour, participant: id });
|
||||
}
|
||||
});
|
||||
return liste;
|
||||
}
|
||||
|
||||
// Fautes de valeur sous regle : la première faute de forme ou de
|
||||
// déclaration seule, qui rend la cohérence illisible ; sinon les fautes de
|
||||
// cohérence.
|
||||
function fautesSelon(valeur, regle) {
|
||||
const chemin = premiereFaute(valeur, regle) ?? fauteDeDeclaration(valeur);
|
||||
return chemin === null ? fautesDeCoherence(valeur) : [{ code: 'FORME', chemin }];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fautes internes d'un placement, proposition ou retenu (§ 8.9, points 1 et
|
||||
* 2) : il se compare à lui-même, jamais à la charge. La règle est celle du
|
||||
* retenu quand p porte la clé proposition, celle d'une proposition sinon.
|
||||
*
|
||||
* La forme se contrôle d'abord, dans l'ordre du schéma, puis la
|
||||
* déclaration : la première faute de l'une ou de l'autre est seule rendue,
|
||||
* { code: 'FORME', chemin }. Sinon, dans l'ordre des tours puis des tables :
|
||||
* { code: 'LONGUEUR', tour, table, declare, ecrit } — tour null pour le
|
||||
* nombre de tours, rendu en premier ; table null pour le nombre de listes
|
||||
* d'un tour ; une table pour une liste plus longue que sa capacité — puis,
|
||||
* dans chaque tour, { code: 'DOUBLON'|'INCONNU'|'MANQUANT', tour,
|
||||
* participant } par identifiant croissant. Rend [] pour un placement
|
||||
* cohérent ; ne lève jamais.
|
||||
*
|
||||
* Une permutation dans une table, ou l'échange du contenu de deux tables de
|
||||
* même capacité, gardent longueurs et multiensemble : rien dans le fichier
|
||||
* ne les distingue d'un placement légitime, et fautes rend [].
|
||||
*
|
||||
* @param {*} p
|
||||
* @returns {Faute[]}
|
||||
*/
|
||||
export function fautes(p) {
|
||||
const retenu = typeof p === 'object' && p !== null && Object.hasOwn(p, 'proposition');
|
||||
return fautesSelon(p, retenu ? RETENU : PROPOSITION);
|
||||
}
|
||||
|
||||
// Lève TypeError, au nom de fonction, quand p n'est pas un placement
|
||||
// cohérent : le lire rendrait sans le dire une lecture fausse.
|
||||
function exigerCoherent(p, fonction) {
|
||||
const [premiere] = fautes(p);
|
||||
if (premiere !== undefined) throw new TypeError(`${fonction} : placement fautif ${JSON.stringify(premiere)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forme de fichier d'une proposition du moteur (§ 8.9). Son identifiant est
|
||||
* decalage + proposition.id : la place de la proposition dans la suite des
|
||||
* graines dérivées de sa génération (§ 5.7), que le moteur numérote à partir
|
||||
* de 1, décalée au-delà de tout identifiant de proposition jamais attribué.
|
||||
* L'appelant passe pour decalage prochainsIds.proposition − 1, ce compteur
|
||||
* lu dans la charge qu'examiner rend, puis porte le compteur au-delà du
|
||||
* dernier identifiant écrit. examiner le relève au-delà de chaque
|
||||
* identifiant que le fichier porte, propositions écartées et origine du
|
||||
* retenu comprises : ce compteur dépasse les identifiants de la liste, celui
|
||||
* de la proposition d'origine du retenu et ceux qu'un effacement a retirés,
|
||||
* et ne recule jamais, même abaissé à la main dans le fichier. Les
|
||||
* générations s'accumulent ainsi (§ 5.7) sans répéter ni réattribuer un
|
||||
* identifiant, et examiner, qui écarte à l'ouverture la seconde proposition
|
||||
* d'un identifiant répété, les garde toutes.
|
||||
*
|
||||
* Elle porte ensuite graine, arret et historique de la proposition,
|
||||
* produitVersion, et siegesAttribues, qui vaut attribuerSieges au moment où
|
||||
* elle est produite ; puis ce que la configuration déclare : les tables du
|
||||
* plan, dans l'ordre de ses listes, chacune avec sa capacité dans la
|
||||
* configuration, lue par identifiant ; le nombre de tours ; les participants
|
||||
* non exclus, par identifiant croissant. Le placement reprend le plan tour
|
||||
* par tour : sans attribution de sièges, chaque liste de table se trie par
|
||||
* identifiant croissant ; avec, elle garde son ordre, celui des sièges. La
|
||||
* réserve, un ensemble, se trie dans les deux cas. Les clés suivent l'ordre
|
||||
* du schéma ; rien n'est partagé avec les arguments, qui ne sont pas
|
||||
* modifiés.
|
||||
*
|
||||
* Lève TypeError, sans rien rendre, quand decalage n'est pas un entier ≥ 0,
|
||||
* ou quand la forme produite serait fautive : attribuerSieges qui n'est pas
|
||||
* un booléen, version vide ou absente, réglage hors de son domaine, plan que
|
||||
* la configuration contredit — table inconnue d'elle, nombre de tours,
|
||||
* personne non placée, liste au-delà d'une capacité.
|
||||
*
|
||||
* @param {import('../moteur/recherche.js').Proposition} proposition
|
||||
* @param {import('../moteur/types.js').Configuration} configuration
|
||||
* @param {{produitVersion: string, attribuerSieges: boolean, decalage: number}} options
|
||||
* @returns {import('./types.js').PropositionFichier}
|
||||
*/
|
||||
export function versFichier(proposition, configuration, { produitVersion, attribuerSieges, decalage }) {
|
||||
// Le décalage se contrôle avant la forme : négatif, il peut rendre un
|
||||
// identifiant que la forme admet et qu'une autre proposition porte déjà.
|
||||
if (!Number.isSafeInteger(decalage) || decalage < 0) {
|
||||
throw new TypeError(`versFichier : décalage entier ≥ 0 attendu, reçu ${JSON.stringify(decalage)}`);
|
||||
}
|
||||
const { plan } = proposition;
|
||||
const capaciteDe = new Map(configuration.tables.map(({ id, capacite: places }) => [id, places]));
|
||||
const ordonner = (liste) => (attribuerSieges ? [...liste] : [...liste].sort(croissant));
|
||||
const fichier = {
|
||||
id: decalage + proposition.id,
|
||||
graine: proposition.graine,
|
||||
arret: proposition.arret,
|
||||
historique: proposition.historique,
|
||||
produitVersion,
|
||||
siegesAttribues: attribuerSieges,
|
||||
tables: [...plan.tables],
|
||||
capacites: plan.tables.map((id) => capaciteDe.get(id)),
|
||||
tours: configuration.tours,
|
||||
participants: configuration.participants
|
||||
.filter(({ exclu }) => exclu !== true)
|
||||
.map(({ id }) => id)
|
||||
.sort(croissant),
|
||||
placement: plan.tours.map((listes, rang) => ({
|
||||
sieges: listes.map((liste) => ordonner(liste)),
|
||||
reserve: [...plan.reserves[rang]].sort(croissant),
|
||||
})),
|
||||
};
|
||||
exigerCoherent(fichier, 'versFichier');
|
||||
return fichier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plan du moteur d'un placement de fichier, proposition ou retenu : les
|
||||
* tables dans l'ordre déclaré, chaque liste de table et chaque réserve par
|
||||
* identifiant croissant, comme le moteur les rend ; l'ordre des sièges n'y
|
||||
* entre pas. Aucune liste n'est partagée avec p. Lève TypeError quand p
|
||||
* n'est pas cohérent (fautes).
|
||||
*
|
||||
* @param {import('./types.js').PropositionFichier|import('./types.js').Retenu} p
|
||||
* @returns {import('../moteur/types.js').Plan}
|
||||
*/
|
||||
export function planDepuisFichier(p) {
|
||||
exigerCoherent(p, 'planDepuisFichier');
|
||||
return {
|
||||
tables: [...p.tables],
|
||||
tours: p.placement.map(({ sieges }) => sieges.map((liste) => [...liste].sort(croissant))),
|
||||
reserves: p.placement.map(({ reserve }) => [...reserve].sort(croissant)),
|
||||
};
|
||||
}
|
||||
|
||||
// Code, comparé unité UTF-16 par unité, puis identifiant du participant ou
|
||||
// de la table que la raison désigne ; TOURS_CHANGES n'en désigne aucun et
|
||||
// n'apparaît qu'une fois.
|
||||
const ordreDesRaisons = (a, b) =>
|
||||
comparerTextes(a.code, b.code) || (a.participant ?? a.table ?? 0) - (b.participant ?? b.table ?? 0);
|
||||
|
||||
// Raisons de dérive d'un placement cohérent, sans contrôle : derive les
|
||||
// rend après l'avoir contrôlé, examiner après l'avoir fait lui-même.
|
||||
function raisonsDeDerive({ tables, capacites, tours, participants }, charge) {
|
||||
const raisons = [];
|
||||
const personneDe = new Map(charge.participants.map((personne) => [personne.id, personne]));
|
||||
const places = new Set(participants);
|
||||
for (const id of participants) {
|
||||
const personne = personneDe.get(id);
|
||||
if (personne === undefined) raisons.push({ code: 'PARTICIPANT_SUPPRIME', participant: id });
|
||||
else if (personne.exclu) raisons.push({ code: 'PARTICIPANT_EXCLU', participant: id });
|
||||
}
|
||||
for (const { id, exclu } of charge.participants) {
|
||||
if (!exclu && !places.has(id)) raisons.push({ code: 'PARTICIPANT_NON_PLACE', participant: id });
|
||||
}
|
||||
const tableDe = new Map(charge.tables.map((table) => [table.id, table]));
|
||||
tables.forEach((id, rang) => {
|
||||
const table = tableDe.get(id);
|
||||
if (table === undefined) {
|
||||
raisons.push({ code: 'TABLE_SUPPRIMEE', table: id });
|
||||
return;
|
||||
}
|
||||
const maintenant = capacite(charge, table);
|
||||
if (maintenant !== capacites[rang]) {
|
||||
raisons.push({ code: 'CAPACITE_CHANGEE', table: id, avant: capacites[rang], maintenant });
|
||||
}
|
||||
});
|
||||
const declarees = new Set(tables);
|
||||
for (const { id } of charge.tables) {
|
||||
if (!declarees.has(id)) raisons.push({ code: 'TABLE_AJOUTEE', table: id });
|
||||
}
|
||||
if (tours !== charge.evenement.tours) {
|
||||
raisons.push({ code: 'TOURS_CHANGES', avant: tours, maintenant: charge.evenement.tours });
|
||||
}
|
||||
return raisons.sort(ordreDesRaisons);
|
||||
}
|
||||
|
||||
/**
|
||||
* Raisons de dérive d'un placement cohérent, proposition ou retenu, contre
|
||||
* la charge courante (§ 8.9, point 3 ; § 9) : ce qu'il déclare comparé à la
|
||||
* configuration d'aujourd'hui. Une personne qu'il place et que la charge
|
||||
* exclut (§ 4.4), ou n'a plus : PARTICIPANT_EXCLU, PARTICIPANT_SUPPRIME
|
||||
* {participant}. Une personne non exclue qu'il ne place pas, ajoutée ou
|
||||
* réintégrée depuis : PARTICIPANT_NON_PLACE {participant}. Une table qu'il
|
||||
* déclare et que la charge n'a plus, ou une table de la charge qu'il ne
|
||||
* déclare pas : TABLE_SUPPRIMEE, TABLE_AJOUTEE {table}. Une capacité
|
||||
* déclarée qui n'est plus la capacité courante de sa table :
|
||||
* CAPACITE_CHANGEE {table, avant, maintenant}. Un nombre de tours qui n'est
|
||||
* plus celui de l'événement : TOURS_CHANGES {avant, maintenant}.
|
||||
*
|
||||
* Triées par code, unité UTF-16 par unité, puis par identifiant ; [] quand
|
||||
* le placement décrit la configuration courante : son plan passe alors
|
||||
* indexerPlan du moteur sur elle. Noms, appartenances, numéros, positions,
|
||||
* formes de table et réglages n'entrent pas dans la dérive. Lève TypeError
|
||||
* quand p n'est pas cohérent (fautes).
|
||||
*
|
||||
* @param {import('./types.js').PropositionFichier|import('./types.js').Retenu} p
|
||||
* @param {import('./types.js').Charge} charge
|
||||
* @returns {Raison[]}
|
||||
*/
|
||||
export function derive(p, charge) {
|
||||
exigerCoherent(p, 'derive');
|
||||
return raisonsDeDerive(p, charge);
|
||||
}
|
||||
|
||||
// Ordre d'affichage des tables : numéro croissant, une table absente de la
|
||||
// charge — numéro null — après les autres ; à numéro égal, identifiant
|
||||
// croissant.
|
||||
const ordreDesTables = (a, b) =>
|
||||
(a.numero === null) - (b.numero === null) || a.numero - b.numero || a.id - b.id;
|
||||
|
||||
/**
|
||||
* Forme nommée d'un placement cohérent, proposition ou retenu (§ 8.9,
|
||||
* point 4) : une ligne par personne placée à un tour, { tour, table,
|
||||
* numero, siege, participant, nom, prenom }, pour la lecture et le
|
||||
* signalement d'un défaut. Les lignes se rangent par tour, puis par numéro
|
||||
* de table — une table que la charge n'a plus vient après les autres, sans
|
||||
* numéro, et l'identifiant départage deux numéros égaux —, puis par rang
|
||||
* dans la liste ; la réserve suit les tables de son tour, table et numéro
|
||||
* null. siege est le rang à partir de 1 quand le placement attribue les
|
||||
* sièges, null sinon et dans la réserve. Nom et prénom viennent de la
|
||||
* charge ; une personne qu'elle n'a plus les a null. Lève TypeError quand p
|
||||
* n'est pas cohérent (fautes).
|
||||
*
|
||||
* @param {import('./types.js').PropositionFichier|import('./types.js').Retenu} p
|
||||
* @param {import('./types.js').Charge} charge
|
||||
* @returns {Ligne[]}
|
||||
*/
|
||||
export function formeNommee(p, charge) {
|
||||
exigerCoherent(p, 'formeNommee');
|
||||
const personneDe = new Map(charge.participants.map((personne) => [personne.id, personne]));
|
||||
const numeroDe = new Map(charge.tables.map(({ id, numero }) => [id, numero]));
|
||||
const tables = p.tables
|
||||
.map((id, rang) => ({ id, rang, numero: numeroDe.get(id) ?? null }))
|
||||
.sort(ordreDesTables);
|
||||
const lignes = [];
|
||||
const ajouter = (tour, table, numero, siege, participant) => {
|
||||
const personne = personneDe.get(participant);
|
||||
lignes.push({
|
||||
tour,
|
||||
table,
|
||||
numero,
|
||||
siege,
|
||||
participant,
|
||||
nom: personne?.nom ?? null,
|
||||
prenom: personne?.prenom ?? null,
|
||||
});
|
||||
};
|
||||
p.placement.forEach(({ sieges, reserve }, rangDuTour) => {
|
||||
const tour = rangDuTour + 1;
|
||||
for (const { id, rang, numero } of tables) {
|
||||
sieges[rang].forEach((participant, position) => {
|
||||
ajouter(tour, id, numero, p.siegesAttribues ? position + 1 : null, participant);
|
||||
});
|
||||
}
|
||||
for (const participant of reserve) ajouter(tour, null, null, null, participant);
|
||||
});
|
||||
return lignes;
|
||||
}
|
||||
|
||||
// valeur quand elle est un identifiant de proposition, null sinon.
|
||||
const identifiantLisible = (valeur) => (premiereFaute(valeur, IDENTIFIANT) === null ? valeur : null);
|
||||
|
||||
// Compteur des propositions de la charge que rend examiner : celui de la
|
||||
// charge, relevé au-delà de plusGrand, le plus grand identifiant lu, 0 quand
|
||||
// aucun ne se lit ; il ne recule jamais. Le compteur est un identifiant, un
|
||||
// entier exact : relevé, il s'arrête à 2^53 − 1, et un identifiant de cette
|
||||
// valeur reste hors de lui.
|
||||
const compteurReleve = (charge, plusGrand) =>
|
||||
Math.max(charge.prochainsIds.proposition, Math.min(plusGrand + 1, Number.MAX_SAFE_INTEGER));
|
||||
|
||||
// Faute d'un identifiant de proposition que la charge a attribué au-delà de
|
||||
// son compteur, ou [] : prochainsIds.proposition dépasse tout identifiant
|
||||
// attribué (§ 5.7).
|
||||
const fautesDuCompteur = (id, charge) =>
|
||||
id >= charge.prochainsIds.proposition ? [{ code: 'IDENTIFIANT_HORS_COMPTEUR' }] : [];
|
||||
|
||||
// Fautes et raisons du retenu, examiné sous sa propre règle. Un retenu qui
|
||||
// se contredit n'a que ses fautes ; un retenu cohérent a ses raisons de
|
||||
// dérive, et la faute d'une proposition d'origine qui atteint le compteur ;
|
||||
// un retenu nul n'a ni l'un ni l'autre.
|
||||
function examinerRetenu(charge) {
|
||||
const { retenu } = charge;
|
||||
if (retenu === null) return { fautes: [], raisons: [] };
|
||||
const liste = fautesSelon(retenu, RETENU);
|
||||
if (liste.length > 0) return { fautes: liste, raisons: [] };
|
||||
return { fautes: fautesDuCompteur(retenu.proposition, charge), raisons: raisonsDeDerive(retenu, charge) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Partage les placements d'une charge lue (§ 8.9, point 3). L'analyse du
|
||||
* fichier admet toute proposition et tout retenu ; examiner les contrôle.
|
||||
*
|
||||
* Chaque élément de la liste est examiné dans son ordre, sous la règle
|
||||
* d'une proposition, même quand il porte la clé d'un retenu : fautif, il
|
||||
* est écarté et nommé dans fautives, { rang, id, fautes }, rang étant sa
|
||||
* place dans la liste et id son identifiant, null quand il n'en est pas un ;
|
||||
* cohérente, elle est gardée, et ses raisons de dérive, s'il y en a, vont
|
||||
* dans derives, { id, raisons }, rangées par identifiant. Une proposition
|
||||
* cohérente qui porte l'identifiant d'une proposition déjà gardée est
|
||||
* fautive, { code: 'IDENTIFIANT_REPETE' } ; celle dont l'identifiant atteint
|
||||
* prochainsIds.proposition aussi, { code: 'IDENTIFIANT_HORS_COMPTEUR' } :
|
||||
* l'identifiant d'une proposition est unique dans la charge rendue, et sous
|
||||
* le compteur, et le fichier s'ouvre sans elle. Un compteur abaissé écarte
|
||||
* ainsi les propositions qu'il ne couvre plus, qui se recalculent, sans
|
||||
* fermer la saisie. Une proposition écartée n'en écarte aucune autre : une
|
||||
* cohérente qui porte l'identifiant d'une fautive n'est pas une répétition,
|
||||
* et une fautive se nomme par ses propres fautes.
|
||||
*
|
||||
* Le retenu n'est jamais retiré : retenu porte ses fautes, sous la règle du
|
||||
* retenu. Cohérent, il porte aussi ses raisons de dérive, et pour faute
|
||||
* { code: 'IDENTIFIANT_HORS_COMPTEUR' } quand sa proposition d'origine
|
||||
* atteint prochainsIds.proposition. Un retenu nul n'a ni l'un ni l'autre.
|
||||
*
|
||||
* Ces fautes se jugent contre le compteur reçu. La charge rendue porte le
|
||||
* compteur relevé au-delà de chaque identifiant lu : prochainsIds.proposition
|
||||
* y vaut le plus grand du compteur reçu et du plus grand identifiant lu plus
|
||||
* un — celui de chaque élément de la liste, gardé ou écarté, quelle que soit
|
||||
* sa faute, et la proposition d'origine du retenu, fautif ou non ; une
|
||||
* valeur qui n'est pas un identifiant n'y entre pas. Le compteur ne recule
|
||||
* donc jamais, et une génération numérotée depuis lui, comme le dit
|
||||
* versFichier, ne réattribue ni l'identifiant d'une proposition écartée ni
|
||||
* celui de l'origine du retenu : le retenu ne désigne jamais une autre
|
||||
* proposition que la sienne. Le compteur reste un entier exact, qui s'arrête
|
||||
* à 2^53 − 1 ; un identifiant de cette valeur reste hors de lui, et
|
||||
* IDENTIFIANT_HORS_COMPTEUR le nomme tant que le fichier le porte.
|
||||
*
|
||||
* Rend une charge neuve, dont propositions ne porte que les gardées, dans
|
||||
* l'ordre de la liste, et prochainsIds un objet neuf, son compteur des
|
||||
* propositions relevé ; tout le reste, propositions gardées et retenu
|
||||
* compris, est celui de la charge reçue, qui n'est pas modifiée. Cette
|
||||
* charge s'écrit et se compare telle quelle : chaque proposition y suit sa
|
||||
* règle, et un retenu qui ne suit pas la sienne, canonique.js le recopie
|
||||
* hors du schéma (retenuHorsDeSaRegle).
|
||||
*
|
||||
* @param {import('./types.js').Charge} charge
|
||||
* @returns {Examen}
|
||||
*/
|
||||
export function examiner(charge) {
|
||||
const gardees = [];
|
||||
const fautives = [];
|
||||
const derives = [];
|
||||
const identifiants = new Set();
|
||||
let plusGrand = identifiantLisible(charge.retenu?.proposition) ?? 0;
|
||||
charge.propositions.forEach((proposition, rang) => {
|
||||
const id = identifiantLisible(proposition?.id);
|
||||
plusGrand = Math.max(plusGrand, id ?? 0);
|
||||
let liste = fautesSelon(proposition, PROPOSITION);
|
||||
if (liste.length === 0 && identifiants.has(proposition.id)) liste = [{ code: 'IDENTIFIANT_REPETE' }];
|
||||
if (liste.length === 0) liste = fautesDuCompteur(proposition.id, charge);
|
||||
if (liste.length > 0) {
|
||||
fautives.push({ rang, id, fautes: liste });
|
||||
return;
|
||||
}
|
||||
identifiants.add(proposition.id);
|
||||
gardees.push(proposition);
|
||||
const raisons = raisonsDeDerive(proposition, charge);
|
||||
if (raisons.length > 0) derives.push({ id: proposition.id, raisons });
|
||||
});
|
||||
derives.sort((a, b) => a.id - b.id);
|
||||
const prochainsIds = { ...charge.prochainsIds, proposition: compteurReleve(charge, plusGrand) };
|
||||
return {
|
||||
charge: { ...charge, prochainsIds, propositions: gardees },
|
||||
fautives,
|
||||
derives,
|
||||
retenu: examinerRetenu(charge),
|
||||
};
|
||||
}
|
||||
1282
src/stockage/placements.test.js
Normal file
1282
src/stockage/placements.test.js
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue