[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:
parent
63efdb21a1
commit
03c04bdf16
7 changed files with 366 additions and 45 deletions
7
spec.md
7
spec.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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]' }] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue