Diario
Tecnica3 min di lettura

Aritmetica del denaro in centesimi interi, e lo sconto che è sparito

Abbiamo applicato uno sconto di cinquanta centesimi e la contabilità non ne ha registrato nemmeno uno. La virgola mobile non c'entrava niente. Guida pratica ai conti sul denaro con i numeri interi: limiti, ripartizione e come far quadrare al centesimo una contabilità di prezzi unitari.

ITIl team tecnico di Bookatu

Ogni sviluppatore impara presto che 0.1 più 0.2 non fa 0.3, quindi salviamo tutti il denaro in centesimi interi e ci sentiamo al sicuro. Poi costruisci qualcosa di vero, per esempio uno sconto in percentuale su un carrello, e scopri che gli interi risolvono solo il primo problema. Il secondo è che alla divisione della tua contabilità non importa niente.

I cinquanta centesimi spariti

Ecco la situazione che ci ha morso, ridotta all'osso. Un carrello contiene 100 unità di un articolo da un dollaro. Il cliente ha cinquanta centesimi di sconto sull'ordine. Il subtotale è 10.000 centesimi, lo sconto è 50 centesimi e il totale scontato è 9.950 centesimi. Fin qui solo numeri interi.

Adesso registra quella vendita in una contabilità che salva un prezzo unitario per riga, come piace ai sistemi contabili. Il prezzo unitario scontato è 9.950 diviso 100, cioè 99,5 centesimi. Non è un numero intero, quindi viene arrotondato a 100. Adesso la contabilità dice 100 unità a 100 centesimi: una vendita a prezzo pieno. Lo sconto non si è ridotto. È sparito, mentre l'interfaccia prometteva al cliente 9.950 e i libri dicevano 10.000.

js
// The trap: one rounded unit price cannot represent every total.const subtotal = 100 * 100;              // 10000cconst discounted = subtotal - 50;        // 9950cconst unit = Math.round(discounted / 100); // 100c ... the discount is goneconsole.log(unit * 100);                 // 10000c, not 9950c // The fix: split the line into two prices one cent apart.const qty = 100;const base = Math.floor(discounted / qty);   // 99cconst rem = discounted - base * qty;         // 50 units get one extra cent// rem units at (base + 1), the rest at base:// 50 * 100 + 50 * 99 === 9950  ✓ exact, and every price is an integer

Spezzare la riga sembra una pignoleria finché non vedi cosa ti compra. Cinquanta unità a 100 centesimi più cinquanta unità a 99 centesimi fa esattamente 9.950. Ogni riga salva ancora un prezzo unitario intero e onesto, le quantità tornano ancora, e la somma della contabilità corrisponde al centesimo al numero sullo scontrino. Niente di tutto questo è possibile con un unico prezzo arrotondato.

La ripartizione ha bisogno di un passaggio di riconciliazione

La stessa malattia ricompare un livello più su, quando uno sconto sull'ordine si distribuisce su più righe. Dai a ogni riga la sua quota proporzionale e arrotonda, e le quote sommeranno a quasi lo sconto. A volte un centesimo in più, a volte uno in meno, ogni tanto esatte, che è il risultato peggiore di tutti perché nasconde il bug a un test superficiale.

js
// Prorate by share, then push the rounding leftover onto lines with room.let allocated = 0;for (const line of lines) {  line.discount = Math.min(    line.subtotal,    Math.round((discount * line.subtotal) / orderSubtotal),  );  allocated += line.discount;}let remainder = discount - allocated; // may be negativefor (const line of lines) {  if (remainder === 0) break;  const room = remainder > 0 ? line.subtotal - line.discount : line.discount;  const move = Math.min(Math.abs(remainder), room) * Math.sign(remainder);  line.discount += move;  remainder -= move;}// invariant: sum(line.discount) === discount, and no line goes negative

Due dettagli di quel ciclo se li sono guadagnati nel modo duro. Il resto si distribuisce su qualsiasi riga abbia capienza, non solo sull'ultima, perché l'ultima riga può essere un articolo da sei centesimi senza spazio per assorbire tre centesimi avanzati. E ogni quota è limitata al subtotale della sua riga, perché uno sconto più grande della riga implica un prezzo unitario negativo, ed è così che una contabilità comincia a mentire nell'altra direzione.

Una schermata di cassa con un subtotale, uno sconto del dieci per cento e il totale scontato esatto
Lo scontrino noioso è tutto il punto: subtotale, sconto e un totale che la contabilità riproduce al centesimo.

Metti un limite a ogni estremo

  • Gli sconti si limitano all'intervallo tra zero e il subtotale. Un centodieci per cento di sconto è un errore di battitura, non un rimborso.
  • Interpreta con prudenza gli importi digitati dall'utente: vuoto e non numerico valgono zero, mai NaN in mezzo a un calcolo.
  • Le quantità si limitano a quello che esiste. Il codice che gestisce il denaro eredita ogni bug di magazzino che sta a monte.
  • Prova i totali che non si dividono in modo esatto. Tre articoli da dieci dollari con un dollaro di sconto è un test migliore di qualsiasi numero tondo.

Niente di tutto questo è affascinante, ed è esattamente per questo che sfugge alla revisione. L'aritmetica sembra corretta perché è corretta, fino al momento in cui la divisione intera butta via in silenzio un resto che qualcuno doveva ricevere. Scrivi l'invariante, la somma delle parti è uguale al tutto, e metti un test a fare da guardia.

Gli interi tengono onesto il denaro. La riconciliazione tiene onesti gli interi.

denaro in centesimivirgola mobile e valuteripartizione dello scontoerrori di arrotondamentosviluppo dei pagamenti
Vuoi metterlo in pratica?

Bookatu ti dà una pagina di prenotazione con il tuo brand, acconti, abbonamenti, carte regalo e promemoria, con lo 0% di commissioni sulle tue prenotazioni.

Inizia gratis