longevity: unico arrotondamento al pari nel motore, esatto sul valore binario
Sposta arrotonda1 (round-half-to-even come Python) da test-fisici.ts a curve.ts, esportata, cosi' entrambi i moduli condividono la stessa funzione invece di due copie che sui pareggi potrebbero divergere. Nel farlo, la versione precedente (moltiplica per 10, tolleranza 1e-9 sul pareggio) si e' rivelata sbagliata su un caso reale: 86.35 non e' un pareggio nel double che lo rappresenta (vale 86.34999999999999431..., Python arrotonda a 86.3 senza ambiguita'), ma 86.35*10 arrotonda ESATTAMENTE a 863.5 in virgola mobile, un pareggio fasullo creato dalla moltiplicazione. La versione naive dava 86.4. Sostituita con un confronto esatto: scompone il double in mantissa/esponente (bit IEEE 754) e confronta con aritmetica razionale su BigInt, senza mai passare per una moltiplicazione che possa introdurre o cancellare un pareggio. Aggiunti in motore-curve.test.ts i casi che dimostrano il pareggio vero (86.25 -> 86.2, 86.75 -> 86.8) e quello fasullo (86.35 -> 86.3, non 86.4: blocca la regressione appena descritta).
This commit is contained in:
@@ -33,9 +33,69 @@ export function lerp(x: number, x0: number, x1: number, y0: number, y1: number):
|
||||
return y0 + t * (y1 - y0);
|
||||
}
|
||||
|
||||
/** Arrotonda a una cifra decimale, come `round(x, 1)` in Python. */
|
||||
function arrotonda1(x: number): number {
|
||||
return Math.round(x * 10) / 10;
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Unica funzione di arrotondamento del motore: la usano sia `curve.ts` sia
|
||||
* `test-fisici.ts`, per non avere due implementazioni che sui pareggi
|
||||
* potrebbero divergere fra loro.
|
||||
*/
|
||||
export function arrotonda1(x: number): number {
|
||||
if (!Number.isFinite(x) || x === 0) return x === 0 ? 0 : x;
|
||||
const negativo = x < 0;
|
||||
const ax = Math.abs(x);
|
||||
|
||||
// Scompone il double nella sua rappresentazione esatta ax = mantissa * 2^exponente.
|
||||
const view = new DataView(new ArrayBuffer(8));
|
||||
view.setFloat64(0, ax);
|
||||
const hi = view.getUint32(0);
|
||||
const lo = view.getUint32(4);
|
||||
const expBits = (hi >>> 20) & 0x7ff;
|
||||
let mantissa = (BigInt(hi & 0xfffff) << 32n) | BigInt(lo);
|
||||
let exponent: number;
|
||||
if (expBits === 0) {
|
||||
exponent = -1074; // subnormale, non atteso su questi dati ma corretto comunque
|
||||
} else {
|
||||
mantissa |= 1n << 52n; // bit implicito
|
||||
exponent = expBits - 1075;
|
||||
}
|
||||
|
||||
// ax*10 = 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));
|
||||
den = 1n;
|
||||
} else {
|
||||
num = mantissa * 10n;
|
||||
den = 1n << BigInt(-exponent);
|
||||
}
|
||||
|
||||
const n = num / den; // floor(ax*10), 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;
|
||||
return negativo ? -valore : valore;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* nessun peso.
|
||||
*/
|
||||
|
||||
import { clamp, lerp } from './curve';
|
||||
import { clamp, lerp, arrotonda1 } from './curve';
|
||||
|
||||
export type Sesso = 'M' | 'F';
|
||||
|
||||
@@ -25,23 +25,6 @@ function eMaschio(sesso: Sesso): boolean {
|
||||
return sesso.trim().toLowerCase().startsWith('m');
|
||||
}
|
||||
|
||||
/**
|
||||
* Arrotonda a una cifra decimale, come `round(x, 1)` in Python: sui pareggi
|
||||
* esatti (x.x5) Python arrotonda al pari (banker's rounding), non sempre in
|
||||
* su come `Math.round`. Le curve a gradini dell'oracolo (plank, flamingo,
|
||||
* sollevamenti) producono pareggi reali — es. `lerp` di 190 su score_plank
|
||||
* dà 86.25 esatto, e l'oracolo lo porta a 86.2, non 86.3.
|
||||
*/
|
||||
function arrotonda1(x: number): number {
|
||||
const scaled = x * 10;
|
||||
const pavimento = Math.floor(scaled);
|
||||
const diff = scaled - pavimento;
|
||||
if (Math.abs(diff - 0.5) < 1e-9) {
|
||||
return (pavimento % 2 === 0 ? pavimento : pavimento + 1) / 10;
|
||||
}
|
||||
return Math.round(scaled) / 10;
|
||||
}
|
||||
|
||||
// --- Composizione Corporea -------------------------------------------------
|
||||
|
||||
/** Curva a campana ACE. Il picco è su Atleti/Fitness, non sul grasso più basso. Come `score_fat_percent`. */
|
||||
|
||||
Reference in New Issue
Block a user