longevity: la copertura si confronta grezza, non arrotondata
Bug in aggrega(): la soglia veniva confrontata con la copertura gia' arrotondata a due decimali, quindi 0.396 (< 0.40) passava per 'ok' perche' arrotondava a 0.40. Confronto ora sulla copertura grezza; l'arrotondamento resta solo sul valore esposto. Generalizzata arrotonda1 in curve.ts a un numero qualsiasi di decimali (bit-exact, banker's rounding) e riusata qui a due decimali, invece di tenere una seconda logica di arrotondamento nel motore.
This commit is contained in:
@@ -31,27 +31,31 @@ export type Punteggio =
|
||||
/** Una voce da aggregare: punteggio 0-100 (o null se il dato manca) e il suo peso nominale. */
|
||||
export type VocePesata = { punteggio: number | null; peso: number };
|
||||
|
||||
/** Arrotonda a due cifre decimali, come `round(x, 2)` per la copertura nell'oracolo. */
|
||||
function arrotonda2(x: number): number {
|
||||
return Math.round(x * 100) / 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Media pesata delle voci disponibili, con i pesi delle voci mancanti
|
||||
* ridistribuiti su quelle presenti. Come `aggregate` nell'oracolo, con la
|
||||
* differenza di tipo descritta sopra.
|
||||
*
|
||||
* ATTENZIONE (bug corretto, trovato in revisione): il confronto con la soglia
|
||||
* usa la copertura GREZZA (`pesoDisponibile / pesoTotale`), non quella
|
||||
* arrotondata. L'arrotondamento a due decimali (`arrotonda1(.., 2)`) serve
|
||||
* solo al valore esposto nel campo `copertura` del risultato. Confrontare la
|
||||
* copertura gia' arrotondata avrebbe fatto passare per 'ok' una copertura
|
||||
* grezza appena sotto 0.40 che arrotonda a 0.40 (es. 0.396) - esattamente il
|
||||
* numero pieno fasullo che il tipo `Punteggio` esiste per impedire.
|
||||
*/
|
||||
export function aggrega(voci: VocePesata[]): Punteggio {
|
||||
const pesoTotale = voci.reduce((s, v) => s + v.peso, 0);
|
||||
const disponibili = voci.filter((v) => v.punteggio !== null);
|
||||
const pesoDisponibile = disponibili.reduce((s, v) => s + v.peso, 0);
|
||||
const copertura = pesoTotale ? arrotonda2(pesoDisponibile / pesoTotale) : 0;
|
||||
const coperturaGrezza = pesoTotale ? pesoDisponibile / pesoTotale : 0;
|
||||
const copertura = arrotonda1(coperturaGrezza, 2);
|
||||
|
||||
if (disponibili.length === 0) {
|
||||
return { stato: 'insufficiente', copertura: 0 };
|
||||
}
|
||||
|
||||
if (copertura < COPERTURA_MINIMA) {
|
||||
if (coperturaGrezza < COPERTURA_MINIMA) {
|
||||
return { stato: 'insufficiente', copertura };
|
||||
}
|
||||
|
||||
|
||||
@@ -34,31 +34,34 @@ export function lerp(x: number, x0: number, x1: number, y0: number, y1: number):
|
||||
}
|
||||
|
||||
/**
|
||||
* Arrotonda a una cifra decimale, come `round(x, 1)` in Python: sui pareggi
|
||||
* ESATTI (il valore binario del double è esattamente a metà, es. 86.25)
|
||||
* arrotonda al pari (banker's rounding), non sempre in su come farebbe
|
||||
* `Math.round`. Alcune curve a gradini producono pareggi veri — `lerp` che
|
||||
* restituisce 86.25 esatto va a 86.2, non 86.3.
|
||||
* Arrotonda a `decimali` cifre decimali (default 1), come `round(x, decimali)`
|
||||
* in Python: sui pareggi ESATTI (il valore binario del double è esattamente a
|
||||
* metà, es. 86.25 per un decimale) arrotonda al pari (banker's rounding), non
|
||||
* sempre in su come farebbe `Math.round`. Alcune curve a gradini producono
|
||||
* pareggi veri — `lerp` che restituisce 86.25 esatto va a 86.2, non 86.3.
|
||||
*
|
||||
* Non basta moltiplicare per 10 e confrontare con una tolleranza: un valore
|
||||
* come 86.35 NON è un pareggio vero (il double che gli sta dietro è
|
||||
* 86.34999999999999431..., quindi Python arrotonda a 86.3, non a metà), ma
|
||||
* `86.35 * 10` in virgola mobile arrotonda esattamente a 863.5 — un pareggio
|
||||
* fasullo creato dalla moltiplicazione, non presente nel valore originale.
|
||||
* Una prima versione con tolleranza ci cadeva (dava 86.4). Per evitarlo si
|
||||
* scompone il double nella sua mantissa ed esponente esatti (bit IEEE 754,
|
||||
* via `DataView`) e si confrontano `ax*10` e il pareggio con aritmetica
|
||||
* razionale su `BigInt`, senza mai passare per una moltiplicazione in
|
||||
* virgola mobile che potrebbe introdurre o cancellare un pareggio.
|
||||
* Non basta moltiplicare per 10^decimali e confrontare con una tolleranza: un
|
||||
* valore come 86.35 NON è un pareggio vero a un decimale (il double che gli
|
||||
* sta dietro è 86.34999999999999431..., quindi Python arrotonda a 86.3, non
|
||||
* a metà), ma `86.35 * 10` in virgola mobile arrotonda esattamente a 863.5 —
|
||||
* un pareggio fasullo creato dalla moltiplicazione, non presente nel valore
|
||||
* originale. Una prima versione con tolleranza ci cadeva (dava 86.4). Per
|
||||
* evitarlo si scompone il double nella sua mantissa ed esponente esatti (bit
|
||||
* IEEE 754, via `DataView`) e si confrontano `ax*10^decimali` e il pareggio
|
||||
* con aritmetica razionale su `BigInt`, senza mai passare per una
|
||||
* moltiplicazione in virgola mobile che potrebbe introdurre o cancellare un
|
||||
* pareggio.
|
||||
*
|
||||
* Unica funzione di arrotondamento del motore: la usano sia `curve.ts` sia
|
||||
* `test-fisici.ts`, per non avere due implementazioni che sui pareggi
|
||||
* Unica funzione di arrotondamento del motore: la usano `curve.ts`,
|
||||
* `test-fisici.ts` (a un decimale, i punteggi) e `cascata.ts` (anche a due
|
||||
* decimali, la copertura), per non avere due implementazioni che sui pareggi
|
||||
* potrebbero divergere fra loro.
|
||||
*/
|
||||
export function arrotonda1(x: number): number {
|
||||
export function arrotonda1(x: number, decimali = 1): number {
|
||||
if (!Number.isFinite(x) || x === 0) return x === 0 ? 0 : x;
|
||||
const negativo = x < 0;
|
||||
const ax = Math.abs(x);
|
||||
const potenza = 10n ** BigInt(decimali);
|
||||
|
||||
// Scompone il double nella sua rappresentazione esatta ax = mantissa * 2^exponente.
|
||||
const view = new DataView(new ArrayBuffer(8));
|
||||
@@ -75,26 +78,27 @@ export function arrotonda1(x: number): number {
|
||||
exponent = expBits - 1075;
|
||||
}
|
||||
|
||||
// ax*10 = num/den, con num e den interi esatti: nessuna moltiplicazione
|
||||
// in virgola mobile, quindi nessun pareggio fasullo introdotto qui.
|
||||
// ax*10^decimali = num/den, con num e den interi esatti: nessuna
|
||||
// moltiplicazione in virgola mobile, quindi nessun pareggio fasullo
|
||||
// introdotto qui.
|
||||
let num: bigint;
|
||||
let den: bigint;
|
||||
if (exponent >= 0) {
|
||||
num = mantissa * (10n << BigInt(exponent));
|
||||
num = mantissa * (potenza << BigInt(exponent));
|
||||
den = 1n;
|
||||
} else {
|
||||
num = mantissa * 10n;
|
||||
num = mantissa * potenza;
|
||||
den = 1n << BigInt(-exponent);
|
||||
}
|
||||
|
||||
const n = num / den; // floor(ax*10), esatto
|
||||
const n = num / den; // floor(ax*10^decimali), esatto
|
||||
const resto2 = (num % den) * 2n; // confronta il resto con 1/2 di den, esatto
|
||||
let risultato: bigint;
|
||||
if (resto2 < den) risultato = n;
|
||||
else if (resto2 > den) risultato = n + 1n;
|
||||
else risultato = n % 2n === 0n ? n : n + 1n; // pareggio vero: arrotonda al pari
|
||||
|
||||
const valore = Number(risultato) / 10;
|
||||
const valore = Number(risultato) / Number(potenza);
|
||||
return negativo ? -valore : valore;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user