[ADD] storage: empty chair in a table list when seats are assigned

The operator plans, the room is not tracked live: removing a person from
the retained plan must free her chair and leave every other seat where it
is. With seats assigned, a table list may now hold null before its last
occupant, seat 1 included, and never end on it; without assignment no
list holds null. Validation, canonical form, patches and the named form
handle it, and the delivered demos keep their bytes.
Checked: 1549 node and 58 node-long tests; depot.js, journal.js at 100 %.

--- FR ---

[ADD] stockage : chaise vide dans une liste de table aux sièges attribués

L'opérateur planifie, la salle ne se suit pas en direct : retirer une
personne du plan retenu doit libérer sa chaise et laisser tout autre siège
à sa place. Sièges attribués, une liste de table peut porter null avant son
dernier occupant, siège 1 compris, jamais à la fin ; sans attribution,
aucune liste ne porte null. Contrôle, forme canonique, correctifs et forme
nommée le traitent, et les démonstrations livrées gardent leurs octets.
Vérifié : 1549 node et 58 node-long ; depot.js, journal.js à 100 %.

Assisted-by: Claude Opus 5.5
This commit is contained in:
Mathieu Benoit 2026-10-07 02:31:26 -04:00
parent 63efdb21a1
commit 03c04bdf16
7 changed files with 366 additions and 45 deletions

View file

@ -1565,7 +1565,12 @@ régénérable.
Puis, **par tour** : la liste des tables, chacune étant la liste des
**identifiants entiers** de ses occupants **dans l'ordre des sièges** ; et la
**réserve**, la liste des identifiants assis nulle part à ce tour. Le numéro de
siège n'est pas stocké : il est la position dans la liste.
siège n'est pas stocké : il est la position dans la liste. Quand les sièges sont
attribués, une chaise vide avant le dernier occupant, siège 1 compris, s'écrit
`null` à sa position, et une liste ne finit jamais par `null` : retirer une
personne libère sa chaise, et aucun autre siège ne change — le placement se
prépare, il ne suit pas la salle.
Sans attribution, aucune liste ne porte `null`.
**Quand les sièges ne sont pas attribués** (§ 5.3), l'ordre à l'intérieur d'une
table ne porte aucune information : le logiciel l'écrit **trié par identifiant

View file

@ -24,7 +24,9 @@
// d'une proposition, la place d'une autre à une autre table touche les deux
// listes de ces tables et rien d'autre : une entrée de chacune quand chaque
// personne prend le rang de l'autre, ce que garde l'ordre des sièges quand
// ils sont attribués. Sans attribution, chaque liste se trie par
// ils sont attribués ; une personne qui quitte sa chaise sans être
// remplacée y laisse null, le marqueur d'une chaise vide, une pose au rang
// de la chaise. Sans attribution, chaque liste se trie par
// identifiant, et l'entrée qui change de rang décale celles qu'elle
// franchit ; une liste d'identifiants de même longueur se pose alors
// entière dès que ses poses rang par rang pèsent plus que cette pose, et
@ -64,12 +66,15 @@ import { ErreurStockage } from './erreurs.js';
* @typedef {OperationPoser|OperationRetirer} Operation
*/
// Règles de la charge, du retenu et d'un identifiant dans le schéma du
// fichier d'état.
// Règles de la charge, du retenu, d'un identifiant et de l'occupant d'une
// liste de table — un identifiant, ou null pour une chaise vide — dans le
// schéma du fichier d'état.
const regleDuChamp = (regle, cle) => regle.champs.find(([nom]) => nom === cle)[1];
const CHARGE = regleDuChamp(SCHEMA, 'charge');
const RETENU = regleDuChamp(CHARGE, 'retenu');
const IDENTIFIANT = regleDuChamp(regleDuChamp(CHARGE, 'propositions').element, 'id');
const PROPOSITION = regleDuChamp(CHARGE, 'propositions').element;
const IDENTIFIANT = regleDuChamp(PROPOSITION, 'id');
const OCCUPANT = regleDuChamp(regleDuChamp(PROPOSITION, 'placement').element, 'sieges').element.element;
// Vrai pour un objet qui n'est ni null ni une liste.
const estObjet = (valeur) => typeof valeur === 'object' && valeur !== null && !Array.isArray(valeur);
@ -103,9 +108,9 @@ const poids = (operations) => JSON.stringify(operations).length;
// objets se comparent champ par champ, dans l'ordre du schéma. Deux listes
// d'enregistrements à identifiants distincts se comparent par identifiant ;
// deux autres listes, de longueurs différentes, posent b entière, et de même
// longueur se comparent rang par rang — deux listes d'identifiants
// exceptées, qui posent b entière quand leurs poses rang par rang pèsent
// plus que la sienne.
// longueur se comparent rang par rang — deux listes d'identifiants ou
// d'occupants exceptées, qui posent b entière quand leurs poses rang par
// rang pèsent plus que la sienne.
function comparer(a, b, regle, chemin, operations) {
if (a === null || b === null || (regle.genre !== 'objet' && regle.genre !== 'liste')) {
if (a !== b) operations.push({ op: 'poser', chemin, valeur: b });
@ -117,23 +122,25 @@ function comparer(a, b, regle, chemin, operations) {
comparerParIdentifiant(a, b, regle.element, chemin, operations);
} else if (a.length !== b.length) {
operations.push({ op: 'poser', chemin, valeur: b });
} else if (regle.element === IDENTIFIANT) {
comparerIdentifiants(a, b, chemin, operations);
} else if (regle.element === IDENTIFIANT || regle.element === OCCUPANT) {
comparerIdentifiants(a, b, regle.element, chemin, operations);
} else {
a.forEach((element, rang) => comparer(element, b[rang], regle.element, [...chemin, rang], operations));
}
}
// Ajoute à operations ce qui mène de a à b, deux listes d'identifiants de
// même longueur, au bout de chemin : une pose par rang qui change, ou b
// entière quand ces poses pèsent plus que la sienne. Une liste triée par
// identifiant — une liste de table sans attribution, une réserve, les
// participants d'un plan — décale d'un rang chaque entrée qu'un identifiant
// remplacé franchit, et ses poses rang par rang se multiplient ; ce qui
// s'en écrit ne pèse jamais plus que la liste posée entière.
function comparerIdentifiants(a, b, chemin, operations) {
// même longueur, d'élément de règle regleElement, au bout de chemin : une
// pose par rang qui change, ou b entière quand ces poses pèsent plus que la
// sienne. Une liste triée par identifiant — une liste de table sans
// attribution, une réserve, les participants d'un plan — décale d'un rang
// chaque entrée qu'un identifiant remplacé franchit, et ses poses rang par
// rang se multiplient ; ce qui s'en écrit ne pèse jamais plus que la liste
// posée entière. Sous des sièges attribués, une chaise libérée ou reprise
// ne change que son rang : une pose, de valeur null ou de l'occupant.
function comparerIdentifiants(a, b, regleElement, chemin, operations) {
const parRang = [];
a.forEach((id, rang) => comparer(id, b[rang], IDENTIFIANT, [...chemin, rang], parRang));
a.forEach((id, rang) => comparer(id, b[rang], regleElement, [...chemin, rang], parRang));
const entiere = { op: 'poser', chemin, valeur: b };
operations.push(...(poids(parRang) > poids([entiere]) ? [entiere] : parRang));
}

View file

@ -1662,3 +1662,90 @@ describe("appliquer : un correctif qui ne s'applique pas lève CORRECTIF avec le
}
});
});
describe('le marqueur de chaise vide : correctifs et forme canonique (§ 8.9)', () => {
// La petite démonstration, sa proposition 1 à sièges attribués, retenue
// telle quelle : au tour 1, la table 1 assied 9, 5 et 1, dans cet ordre
// de sièges. Une charge neuve à chaque appel.
function retenuAttribue() {
const charge = chargeDe('petite', [{ id: 1, siegesAttribues: true }]);
charge.propositions[0].placement[0].sieges[0] = [9, 5, 1];
charge.retenu = retenuDe(charge.propositions[0]);
charge.evenement.etat = 'retenu';
return charge;
}
// retenuAttribue, la personne 5 retirée du tour 1 du retenu : sa chaise,
// le siège 2 de la table 1, est libre, et elle attend en réserve.
function chaiseLiberee() {
const charge = retenuAttribue();
charge.retenu.placement[0].sieges[0] = [9, null, 1];
charge.retenu.placement[0].reserve = [5];
return charge;
}
test("libérer une chaise du retenu : une seule pose dans la liste, valeur null, au rang de la chaise ; appliquer rend la charge d'après, que canoniser garde dans l'ordre des sièges", () => {
const a = geler(retenuAttribue());
const b = geler(chaiseLiberee());
assert.deepStrictEqual(fautes(b.retenu), []);
const correctif = difference(a, b);
assert.deepStrictEqual(correctif, [
{ op: 'poser', chemin: ['retenu', 'placement', 0, 'sieges', 0, 1], valeur: null },
{ op: 'poser', chemin: ['retenu', 'placement', 0, 'reserve'], valeur: [5] },
]);
const rendue = appliquer(a, correctif);
assert.deepStrictEqual(rendue, canoniser(b));
assert.deepStrictEqual(rendue.retenu.placement[0].sieges[0], [9, null, 1]);
assert.equal(serialiserCharge(rendue), serialiserCharge(b));
assert.deepStrictEqual(analyser(serialiser(rendue, ENTETE)).charge.retenu, rendue.retenu);
// Le retour : la chaise reprend son occupant, par une pose au même rang.
const retour = difference(b, a);
assert.deepStrictEqual(retour, [
{ op: 'poser', chemin: ['retenu', 'placement', 0, 'sieges', 0, 1], valeur: 5 },
{ op: 'poser', chemin: ['retenu', 'placement', 0, 'reserve'], valeur: [] },
]);
assert.deepStrictEqual(appliquer(b, retour), canoniser(a));
});
test("dans une proposition aussi, une chaise libérée est une pose au rang de la chaise ; une liste qui change à chaque rang se pose entière, chaises vides comprises", () => {
const a = geler(retenuAttribue());
const b = retenuAttribue();
b.propositions[0].placement[0].sieges[0] = [9, null, 1];
b.propositions[0].placement[0].reserve = [5];
assert.deepStrictEqual(difference(a, geler(b)), [
{ op: 'poser', chemin: ['propositions', { id: 1 }, 'placement', 0, 'sieges', 0, 1], valeur: null },
{ op: 'poser', chemin: ['propositions', { id: 1 }, 'placement', 0, 'reserve'], valeur: [5] },
]);
const c = chaiseLiberee();
c.retenu.placement[0].sieges[0] = [1, null, 9];
const correctif = difference(a, geler(c));
assert.deepStrictEqual(correctif, [
{ op: 'poser', chemin: ['retenu', 'placement', 0, 'sieges', 0], valeur: [1, null, 9] },
{ op: 'poser', chemin: ['retenu', 'placement', 0, 'reserve'], valeur: [5] },
]);
assert.deepStrictEqual(appliquer(a, correctif), canoniser(c));
});
test("appliquer admet null dans une liste de table, comme la règle, et le refuse ailleurs dans un placement : réserve, tables, participants, capacités", () => {
const base = geler(chaiseLiberee());
const admises = [
['retenu', 'placement', 1, 'sieges', 2, 1],
['propositions', { id: 1 }, 'placement', 2, 'sieges', 0, 1],
];
for (const chemin of admises) {
assert.equal(refusDe(base, [{ op: 'poser', chemin, valeur: null }]), 'aucun refus', JSON.stringify(chemin));
}
const refusees = [
['retenu', 'placement', 0, 'reserve', 0],
['retenu', 'tables', 0],
['retenu', 'participants', 0],
['retenu', 'capacites', 0],
['propositions', { id: 1 }, 'placement', 1, 'sieges', 0],
['propositions', { id: 1 }, 'participants', 3],
];
assert.ok(refusees.length > 0);
for (const chemin of refusees) {
assert.deepEqual(refusDe(base, [{ op: 'poser', chemin, valeur: null }]), { rang: 0 }, JSON.stringify(chemin));
}
});
});

View file

@ -82,6 +82,12 @@ const NOMBRE = regle('nombre');
// reste ainsi loin de 2^53, où l'analyse ne le relirait plus. Au-delà, la
// valeur sort de sa règle.
const IDENTIFIANT = entier(1, 2 ** 31 - 1);
// L'occupant d'un siège dans une liste de table d'un placement :
// l'identifiant d'une personne, ou null, le marqueur d'une chaise vide
// (§ 8.9). La règle admet null à toute place d'une telle liste ; le
// contrôle des placements (placements.js) ne l'admet que sous des sièges
// attribués, et jamais en fin de liste.
const OCCUPANT = entier(1, 2 ** 31 - 1, { nul: true });
const DATE_OU_NUL = regle('date', { nul: true });
// Ordres des listes de la charge (§ 8.8, § 8.9). canonique.js range les
@ -196,15 +202,18 @@ const TITRE = objet([
]);
// Le placement d'une proposition ou du retenu (§ 8.9) : un tour par élément,
// chacun sur sa ligne. sieges porte une liste d'identifiants par table
// chacun sur sa ligne. sieges porte une liste d'occupants par table
// déclarée, dans l'ordre des sièges. Quand l'objet qui porte le placement a
// siegesAttribues faux, cet ordre ne porte rien, et chaque liste se trie par
// identifiant croissant ; vrai, il est celui des sièges et se garde. reserve
// identifiant croissant ; vrai, il est celui des sièges et se garde : le
// siège est le rang dans la liste, et une chaise vide avant le dernier
// occupant, siège 1 compris, y porte null. Une liste qui ne finit jamais
// par null a ainsi une seule écriture, que le tri ne touche pas. reserve
// porte ceux qui ne sont assis nulle part à ce tour, un ensemble : elle se
// trie par identifiant croissant, quel que soit le drapeau.
const PLACEMENT = liste(
objet([
['sieges', liste(liste(IDENTIFIANT, { triSansAttribution: croissant }))],
['sieges', liste(liste(OCCUPANT, { triSansAttribution: croissant }))],
['reserve', liste(IDENTIFIANT, { tri: croissant })],
]),
{ mise: 'lignes' },

View file

@ -1026,3 +1026,48 @@ describe('premiereFaute : le parcours de forme, sans lever (§ 8.8, § 8.9)', ()
assert.equal(premiereFauteEnPlace({ ...evenement, nom: '' }, EVENEMENT, 'charge.evenement'), 'charge.evenement.nom');
});
});
describe('le marqueur de chaise vide : la règle d\'une liste de table (§ 8.9)', () => {
// La proposition du document valide, sièges attribués, dont le tour 2
// laisse vide le siège 2 de la table 3 : sa liste vaut [1, null, 5].
const propositionAChaiseVide = () => {
const proposition = documentValide().charge.propositions[0];
proposition.siegesAttribues = true;
proposition.placement[1].sieges[1] = [1, null, 5];
return proposition;
};
test("premiereFaute admet null dans une liste de table, d'une proposition comme du retenu, et nulle part ailleurs dans un placement", () => {
const regleDe = (regle, cle) => regle.champs.find(([nom]) => nom === cle)[1];
const CHARGE = regleDe(SCHEMA, 'charge');
const PROPOSITION = regleDe(CHARGE, 'propositions').element;
const RETENU = regleDe(CHARGE, 'retenu');
const proposition = propositionAChaiseVide();
const { id, graine, arret, historique, produitVersion, ...plan } = proposition;
const retenu = { proposition: id, ...plan };
assert.equal(premiereFaute(proposition, PROPOSITION), null);
assert.equal(premiereFaute(retenu, RETENU), null);
const cas = [
['dans une réserve', (p) => { p.placement[0].reserve = [null]; }, 'placement[0].reserve[0]'],
['parmi les participants', (p) => { p.participants[1] = null; }, 'participants[1]'],
['parmi les tables', (p) => { p.tables[0] = null; }, 'tables[0]'],
['parmi les capacités', (p) => { p.capacites[1] = null; }, 'capacites[1]'],
['à la place d\'une liste de table', (p) => { p.placement[0].sieges[0] = null; }, 'placement[0].sieges[0]'],
];
assert.ok(cas.length > 0);
for (const [libelle, abimer, chemin] of cas) {
const abimee = propositionAChaiseVide();
abimer(abimee);
assert.equal(premiereFaute(abimee, PROPOSITION), chemin, libelle);
}
});
test("l'analyse lit un fichier dont une proposition porte une chaise vide, et sa relecture rend la liste telle qu'écrite", () => {
const documentLu = documentValide();
documentLu.charge.propositions = [propositionAChaiseVide()];
const { charge } = analyser(JSON.stringify(documentLu));
assert.deepStrictEqual(charge.propositions[0].placement[1].sieges[1], [1, null, 5]);
const relue = analyser(serialiser(charge, { revision: 13, produitVersion: VERSION.affichee })).charge;
assert.deepStrictEqual(relue.propositions[0], propositionAChaiseVide());
});
});

View file

@ -23,14 +23,18 @@
// 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.
// manière. Une liste de table porte null à la place d'une chaise vide
// (§ 8.9) : la règle l'admet à toute place, et fautes ne le garde que sous
// des sièges attribués, avant le dernier occupant, siège 1 compris. 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';
import { ErreurStockage } from './erreurs.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
* une valeur sort de sa règle, la déclaration se contredit, ou une chaise
* vide est mal placée — sans attribution des sièges, ou en fin de liste ; 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
@ -66,8 +70,9 @@ import { ErreurStockage } from './erreurs.js';
* @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|null} siege rang dans la liste, à partir de 1,
* chaises vides comprises, 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
@ -93,8 +98,9 @@ import { ErreurStockage } from './erreurs.js';
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.
// contiennent n'admet null, hors de l'occupant d'une liste de table, la
// chaise vide ; 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');
@ -127,14 +133,32 @@ function fauteDeDeclaration({ tables, capacites, participants }) {
);
}
// Chemin de la première chaise vide mal placée d'un placement de forme et
// de déclaration conformes, ou null, dans l'ordre des tours, des listes et
// des rangs : un null sous siegesAttribues faux, où l'ordre d'une liste ne
// dit aucun siège, ou au dernier rang d'une liste, qu'une écriture sans
// lui dit pareil. Le chemin suit la convention de premiereFaute.
function fauteDeChaiseVide({ siegesAttribues, placement }) {
for (let tour = 0; tour < placement.length; tour += 1) {
const { sieges } = placement[tour];
for (let table = 0; table < sieges.length; table += 1) {
const liste = sieges[table];
const rang = siegesAttribues ? liste.length - 1 : liste.indexOf(null);
if (rang >= 0 && liste[rang] === null) return `placement[${tour}].sieges[${table}][${rang}]`;
}
}
return null;
}
// 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.
// déclarée, dans l'ordre déclaré, contre sa capacité, chaises vides
// comprises ; 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. Une chaise vide n'y entre pas.
//
// 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
@ -167,6 +191,7 @@ function fautesDeCoherence({ tables, capacites, tours, participants, placement }
const inconnus = [];
for (const ids of [...sieges, reserve]) {
for (const id of ids) {
if (id === null) continue;
const deja = occurrences.get(id) ?? 0;
occurrences.set(id, deja + 1);
if (deja === 0 && !estDeclare.has(id)) inconnus.push(id);
@ -183,11 +208,11 @@ function fautesDeCoherence({ tables, capacites, tours, participants, placement }
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.
// Fautes de valeur sous regle : la première faute de forme, de
// déclaration ou de chaise vide seule, qui rend la cohérence illisible ;
// sinon les fautes de cohérence.
function fautesSelon(valeur, regle) {
const chemin = premiereFaute(valeur, regle) ?? fauteDeDeclaration(valeur);
const chemin = premiereFaute(valeur, regle) ?? fauteDeDeclaration(valeur) ?? fauteDeChaiseVide(valeur);
return chemin === null ? fautesDeCoherence(valeur) : [{ code: 'FORME', chemin }];
}
@ -197,7 +222,9 @@ function fautesSelon(valeur, regle) {
* 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,
* déclaration, puis les chaises vides : null, admis par la règle d'une
* liste de table, n'y est cohérent que sous des sièges attribués et avant
* le dernier rang. La première de ces fautes 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
@ -248,8 +275,9 @@ function exigerCoherent(p, fonction) {
* 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
* identifiant croissant ; avec, elle garde son ordre, celui des sièges, et
* ses chaises vides, null. 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.
*
@ -263,7 +291,8 @@ function exigerCoherent(p, fonction) {
* 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é.
* nombre de tours, personne non placée, liste au-delà d'une capacité,
* chaise vide sans attribution ou en fin de liste.
*
* @param {import('../moteur/recherche.js').Proposition} proposition
* @param {import('../moteur/types.js').Configuration} configuration
@ -309,9 +338,9 @@ export function versFichier(proposition, configuration, { produitVersion, attrib
/**
* 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
* tables dans l'ordre déclaré, chaque liste de table sans ses chaises
* vides, 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
@ -321,7 +350,7 @@ export function planDepuisFichier(p) {
exigerCoherent(p, 'planDepuisFichier');
return {
tables: [...p.tables],
tours: p.placement.map(({ sieges }) => sieges.map((liste) => [...liste].sort(croissant))),
tours: p.placement.map(({ sieges }) => sieges.map((liste) => liste.filter((id) => id !== null).sort(croissant))),
reserves: p.placement.map(({ reserve }) => [...reserve].sort(croissant)),
};
}
@ -411,7 +440,8 @@ const ordreDesTables = (a, b) =>
* 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
* null sinon et dans la réserve ; une chaise vide, null dans la liste,
* garde son rang et n'a pas de ligne. Nom et prénom viennent de la charge ; une
* personne qu'elle n'a plus, ou qu'elle n'a jamais eue, les a null.
*
* Un placement incohérent (fautes : LONGUEUR, DOUBLON, INCONNU, MANQUANT)
@ -454,6 +484,7 @@ export function formeNommee(p, charge) {
// au-delà des tables déclarées.
const ajouterListe = (tour, ids, table, numero) => {
ids.forEach((participant, position) => {
if (participant === null) return;
ajouter(tour, table, numero, p.siegesAttribues ? position + 1 : null, participant, false);
});
};

View file

@ -1495,3 +1495,140 @@ describe('déterminisme (§ 8.8, § 14.7)', () => {
assert.deepStrictEqual(fautes(alphaPuisZeta), [{ code: 'FORME', chemin: 'alpha' }]);
});
});
// fichierParfait, sièges attribués, dont le tour 1 laisse vide le siège 2
// de la table 1 : sa liste vaut [12, null, 7], et la personne 2 attend en
// réserve. Une copie neuve à chaque appel.
function fichierAChaiseVide() {
const p = fichierParfait();
p.siegesAttribues = true;
p.placement[0] = { sieges: [[12, null, 7], [4, 5, 6], [1, 8, 9], [10, 11, 3]], reserve: [2] };
return p;
}
describe('le marqueur de chaise vide : null avant le dernier occupant, sièges attribués (§ 8.9)', () => {
test("une liste [12, null, 7] est cohérente, proposition comme retenu : le null compte dans la longueur, n'est ni DOUBLON ni INCONNU, et planDepuisFichier le retire", () => {
const p = fichierAChaiseVide();
assert.deepStrictEqual(fautes(p), []);
assert.deepStrictEqual(fautes(retenuDe(p)), []);
assert.deepStrictEqual(planDepuisFichier(p).tours[0], [[7, 12], [4, 5, 6], [1, 8, 9], [3, 10, 11]]);
assert.deepStrictEqual(planDepuisFichier(retenuDe(p)).tours[0][0], [7, 12]);
// Deux chaises vides au même tour, à deux tables : aucune n'est un
// doublon de l'autre.
const deux = fichierAChaiseVide();
deux.placement[0] = { sieges: [[12, null, 7], [4, null, 6], [1, 8, 9], [10, 11, 3]], reserve: [2, 5] };
assert.deepStrictEqual(fautes(deux), []);
assert.deepStrictEqual(planDepuisFichier(deux).tours[0].slice(0, 2), [[7, 12], [4, 6]]);
// Le null occupe un siège : quatre éléments sur une capacité de 3.
const longue = fichierAChaiseVide();
longue.placement[0] = { sieges: [[12, null, 7, 9], [4, 5, 6], [1, 8], [10, 11, 3]], reserve: [2] };
assert.deepStrictEqual(fautes(longue), [{ code: 'LONGUEUR', tour: 1, table: 1, declare: 3, ecrit: 4 }]);
});
test("une chaise vide au siège 1, [null, 12, 7] ou [null, null, 7], est cohérente, proposition comme retenu : planDepuisFichier la retire, et formeNommee garde le siège de chaque occupant", () => {
const cas = [
[[null, 12, 7], [2], [7, 12], [ligne(1, 1, 1, 2, 12, 'Mélisse', 'Mathurin'), ligne(1, 1, 1, 3, 7, 'Giroflée', 'Gisèle')]],
[[null, null, 7], [2, 12], [7], [ligne(1, 1, 1, 3, 7, 'Giroflée', 'Gisèle')]],
];
assert.ok(cas.length > 0);
for (const [liste, reserve, occupants, assis] of cas) {
const p = fichierAChaiseVide();
p.placement[0] = { sieges: [liste, [4, 5, 6], [1, 8, 9], [10, 11, 3]], reserve };
assert.deepStrictEqual(fautes(p), [], JSON.stringify(liste));
assert.deepStrictEqual(fautes(retenuDe(p)), [], JSON.stringify(liste));
assert.deepStrictEqual(planDepuisFichier(p).tours[0][0], occupants);
assert.deepStrictEqual(planDepuisFichier(retenuDe(p)).tours[0][0], occupants);
const lignes = formeNommee(p, chargePetite());
assert.deepStrictEqual(lignes.filter(({ tour, table: id }) => tour === 1 && id === 1), assis);
assert.deepStrictEqual(
lignes.filter(({ tour, reserve: enReserve }) => tour === 1 && enReserve).map(({ participant }) => participant),
reserve,
);
}
});
test("sans attribution, ou en fin de liste, null est une faute FORME qui nomme la chaise ; seul sous sa règle, il ne s'admet nulle part ailleurs dans un placement", () => {
const cas = [
['sièges non attribués', (p) => { p.siegesAttribues = false; }, 'placement[0].sieges[0][1]'],
['une liste qui finit par null', (p) => {
p.placement[0] = { sieges: [[12, null], [4, 5, 6], [1, 8, 9], [10, 11, 3]], reserve: [2, 7] };
}, 'placement[0].sieges[0][1]'],
['une liste réduite à null', (p) => {
p.placement[1].sieges[3] = [null];
p.placement[1].reserve = [1, 4, 7];
}, 'placement[1].sieges[3][0]'],
['une chaise vide admise au tour 1, puis une fin nulle au tour 3', (p) => {
p.placement[2].sieges[0] = [4, 8, null];
p.placement[2].reserve = [10];
}, 'placement[2].sieges[0][2]'],
['une fin nulle au dernier tour, à la dernière table', (p) => {
p.placement[3].sieges[3] = [2, 6, null];
p.placement[3].reserve = [8];
}, 'placement[3].sieges[3][2]'],
['sièges non attribués, une chaise vide au siège 1', (p) => {
p.siegesAttribues = false;
p.placement[0] = { sieges: [[null, 7, 12], [4, 5, 6], [1, 8, 9], [3, 10, 11]], reserve: [2] };
}, 'placement[0].sieges[0][0]'],
['null dans la réserve', (p) => { p.placement[0].reserve = [2, null]; }, 'placement[0].reserve[1]'],
['null parmi les participants', (p) => { p.participants[11] = null; }, 'participants[11]'],
['null parmi les tables', (p) => { p.tables[3] = null; }, 'tables[3]'],
];
assert.ok(cas.length > 0);
const ecarts = cas.flatMap(([libelle, abimer, chemin]) =>
[fichierAChaiseVide(), retenuDe(fichierAChaiseVide())].flatMap((p) => {
abimer(p);
const rendu = fautes(p);
const attendu = [{ code: 'FORME', chemin }];
return JSON.stringify(rendu) === JSON.stringify(attendu) ? [] : [`${libelle} : ${JSON.stringify(rendu)}`];
}),
);
assert.deepEqual(ecarts, []);
leveTypeError(() => planDepuisFichier({ ...fichierAChaiseVide(), siegesAttribues: false }), /FORME/);
});
test('formeNommee assied 12 au siège 1 et 7 au siège 3, sans ligne pour la chaise vide', () => {
const lignes = formeNommee(fichierAChaiseVide(), chargePetite());
assert.equal(lignes.length, 48);
assert.ok(lignes.every(({ participant }) => participant !== null));
assert.deepStrictEqual(lignes.filter(({ tour, table: id }) => tour === 1 && id === 1), [
ligne(1, 1, 1, 1, 12, 'Mélisse', 'Mathurin'),
ligne(1, 1, 1, 3, 7, 'Giroflée', 'Gisèle'),
]);
assert.deepStrictEqual(
lignes.filter(({ tour, reserve }) => tour === 1 && reserve),
[ligne(1, null, null, null, 2, 'Bruyère', 'Basile')],
);
});
test('versFichier garde les chaises vides du plan reçu quand les sièges sont attribués, et lève sans attribution', () => {
const plan = {
tables: [1, 2, 3, 4],
tours: [[[12, null, 7], [4, 5, 6], [1, 8, 9], [10, 11, 3]], ...PLAN_PARFAIT_PETITE.tours.slice(1)],
reserves: [[2], [], [], []],
};
const proposition = { ...PARFAITE, plan };
const options = { produitVersion: VERSION.affichee, decalage: 0 };
const fichier = versFichier(proposition, petite(), { ...options, attribuerSieges: true });
assert.deepStrictEqual(fichier.placement[0].sieges, [[12, null, 7], [4, 5, 6], [1, 8, 9], [10, 11, 3]]);
assert.deepStrictEqual(fichier, fichierAChaiseVide());
leveTypeError(() => versFichier(proposition, petite(), { ...options, attribuerSieges: false }), /FORME/);
});
test("le fichier relu porte la liste telle quelle : examiner garde la proposition et le retenu sans faute, et écarte la proposition sans attribution qui porte null", () => {
const charge = chargePetite();
const proposition = fichierAChaiseVide();
charge.propositions = [proposition, { ...fichierAChaiseVide(), id: 2, siegesAttribues: false }];
charge.retenu = retenuDe(proposition);
charge.prochainsIds.proposition = 3;
charge.evenement.etat = 'retenu';
const relue = relire(charge);
assert.deepStrictEqual(relue.retenu.placement[0].sieges[0], [12, null, 7]);
assert.deepStrictEqual(relue.propositions[0].placement[0].sieges[0], [12, null, 7]);
const examen = examiner(relue);
assert.deepStrictEqual(examen.retenu, { fautes: [], raisons: [] });
assert.deepStrictEqual(examen.charge.propositions, [fichierAChaiseVide()]);
assert.deepStrictEqual(examen.fautives, [
{ rang: 1, id: 2, fautes: [{ code: 'FORME', chemin: 'placement[0].sieges[0][0]' }] },
]);
});
});