---
title: "Rahalaskenta kokonaisina sentteinä, ja alennus joka katosi"
description: "Annoimme viidenkymmenen sentin alennuksen, eikä kirjanpito kirjannut siitä mitään. Liukuluku ei ollut edes syyllinen. Kenttäopas rahalaskentaan kokonaisluvuilla: rajaus, alennuksen jakaminen riveille ja se, miten yksikköhintojen kirjanpito summautuu tarkalleen oikeaan loppusummaan."
url: "https://bookatu.com/fi-fi/blog/money-math-in-integer-cents"
language: "fi"
section: "Blogi"
category: "Tekniikka"
author: "Bookatun tekniikkatiimi"
published: "2026-07-13"
updated: "2026-07-13"
tags: ["raha sentteinä", "liukuluku ja valuutta", "alennuksen jakaminen riveille", "pyöristysvirheet", "maksujen tekniikka"]
---

# Rahalaskenta kokonaisina sentteinä, ja alennus joka katosi

Annoimme viidenkymmenen sentin alennuksen, eikä kirjanpito kirjannut siitä mitään. Liukuluku ei ollut edes syyllinen. Kenttäopas rahalaskentaan kokonaisluvuilla: rajaus, alennuksen jakaminen riveille ja se, miten yksikköhintojen kirjanpito summautuu tarkalleen oikeaan loppusummaan.

> **Lyhyesti**
>
> - Tallenna ja laske raha kokonaisina sentteinä. Liukuluvut ovat se kuuluisa bugi, pyöristyskäytäntö on se oikea.
> - Riveille jaetun alennuksen on täsmättävä: kun jokaisen rivin osuus on pyöristetty, työnnä ylijäävät sentit niille riveille joilla on tilaa, kunnes summa on tarkalleen oikea.
> - Jos kirjanpito tallentaa yhden yksikköhinnan riviä kohden, alennettua loppusummaa joka ei jakaudu tasan ei voi esittää. Jaa rivi kahdeksi hinnaksi, joiden ero on yksi sentti.
> - Rajaa kaikki reunoilla: alennukset välisummaan, määrät varastoon, loppusummat nollaan.

Jokainen ohjelmoija oppii varhain, ettei 0,1 plus 0,2 ole 0,3, joten tallennamme rahan kokonaisina sentteinä ja tunnemme olomme turvalliseksi. Sitten rakennat jotain oikeaa, kuten prosenttialennuksen koko ostoskorille, ja huomaat että kokonaisluvut ratkaisevat vain ensimmäisen ongelman. Toinen ongelma on se, ettei jakolasku välitä sinun kirjanpidostasi.

## Ne viisikymmentä senttiä, jotka katosivat

Tässä on asetelma joka puri meitä, luurangoksi riisuttuna. Ostoskorissa on 100 kappaletta yhden dollarin tuotetta. Asiakas saa tilauksesta viisikymmentä senttiä alennusta. Välisumma on 10 000 senttiä, alennus 50 senttiä ja alennettu loppusumma 9 950 senttiä. Tähän asti pelkkiä kokonaislukuja.

Kirjaa nyt tuo myynti kirjanpitoon, joka tallentaa yksikköhinnan riviä kohden, niin kuin kirjanpitojärjestelmät haluavat. Alennettu yksikköhinta on 9 950 jaettuna 100:lla, eli 99,5 senttiä. Se ei ole kokonaisluku, joten se pyöristyy sataan. Kirjanpito sanoo nyt 100 kappaletta hintaan 100 senttiä: täyshintainen myynti. Alennus ei kutistunut. Se katosi, ja samaan aikaan käyttöliittymä lupasi asiakkaalle 9 950 ja kirjat sanoivat 10 000.

```js
// The trap: one rounded unit price cannot represent every total.
const subtotal = 100 * 100;              // 10000c
const discounted = subtotal - 50;        // 9950c
const unit = Math.round(discounted / 100); // 100c ... the discount is gone
console.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);   // 99c
const 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
```

Jako näyttää nipottavalta, kunnes näet mitä sillä saa. Viisikymmentä kappaletta hintaan 100 senttiä plus viisikymmentä kappaletta hintaan 99 senttiä on tarkalleen 9 950. Jokainen rivi tallentaa yhä rehellisen kokonaislukuisen yksikköhinnan, määrät täsmäävät edelleen, ja kirjanpidon summa vastaa kuitin lukua senttiä myöten. Mikään tästä ei ole mahdollista yhdellä pyöristetyllä hinnalla.

## Riveille jakaminen tarvitsee täsmäytyskierroksen

Sama tauti ilmestyy tasoa ylemmäs, kun tilaustason alennus leviää useammalle riville. Anna jokaiselle riville sen suhteellinen osuus ja pyöristä, niin osuudet summautuvat melkein alennukseen. Joskus sentti yli, joskus sentti alle, silloin tällöin tarkalleen oikein, mikä on kaikista huonoin lopputulos, koska se piilottaa bugin pinnalliselta testaamiselta.

```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 negative
for (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
```

Kaksi yksityiskohtaa tuossa silmukassa ansaitsivat paikkansa kantapään kautta. Ylijäämä leviää mille tahansa riville jolla on kapasiteettia, ei vain viimeiselle riville, koska viimeinen rivi voi olla kuuden sentin tuote jolla ei ole tilaa imeä kolmea ylijäävää senttiä. Ja jokainen osuus rajataan oman rivinsä välisummaan, koska riviä suurempi alennus tarkoittaa negatiivista yksikköhintaa, ja negatiiviset yksikköhinnat ovat se tapa jolla kirjanpito alkaa valehdella toiseen suuntaan.

![Kassanäyttö, jolla näkyy välisumma, kymmenen prosentin alennus ja tarkka alennettu loppusumma](https://bookatu.com/blog/shots/pos-discount-light.png)

*Tylsä kuitti on koko pointti: välisumma, alennus ja loppusumma jonka kirjanpito toistaa senttiä myöten.*

## Rajaa jokaisella reunalla

- Alennukset rajataan välille nollasta välisummaan. Sadankymmenen prosentin alennus on kirjoitusvirhe, ei hyvitys.
- Jäsennä käyttäjän syöttämät summat varovasti: tyhjä ja ei-numeerinen tarkoittavat nollaa, ei koskaan NaN:ia kesken laskutoimituksen.
- Määrät rajataan siihen mitä on olemassa. Rahakoodi perii jokaisen varastobugin ylävirrastaan.
- Testaa ne loppusummat jotka eivät jakaudu tasan. Kolme kymmenen dollarin tuotetta yhden dollarin alennuksella on parempi testi kuin mikään tasaluku.

Mikään tästä ei ole hohdokasta, ja juuri siksi se lipsahtaa katselmoinnin ohi. Laskutoimitus näyttää oikealta koska se on oikea, aina siihen asti kun kokonaislukujako hiljaa heittää pois jäännöksen joka kuului jollekulle. Kirjoita invariantti muistiin, osien summa on kokonaisuus, ja laita testi vartioimaan ovea.

> Kokonaisluvut pitävät rahan rehellisenä. Täsmäytys pitää kokonaisluvut rehellisinä.
