---
title: "Počítání peněz v celých centech a sleva, která zmizela"
description: "Dali jsme slevu 50 centů a účetní kniha z ní nezaznamenala nic. Plovoucí desetinná čárka v tom ani nebyla. Praktický průvodce počítáním peněz v celých číslech: ořezávání, rozpočítání a účetní kniha jednotkových cen, jejíž součet sedí přesně."
url: "https://bookatu.com/cs-cz/blog/money-math-in-integer-cents"
language: "cs"
section: "Blog"
category: "Technika"
author: "Technický tým Bookatu"
published: "2026-07-13"
updated: "2026-07-13"
tags: ["peníze v centech", "měna a plovoucí desetinná čárka", "rozpočítání slevy", "chyby zaokrouhlení", "vývoj plateb"]
---

# Počítání peněz v celých centech a sleva, která zmizela

Dali jsme slevu 50 centů a účetní kniha z ní nezaznamenala nic. Plovoucí desetinná čárka v tom ani nebyla. Praktický průvodce počítáním peněz v celých číslech: ořezávání, rozpočítání a účetní kniha jednotkových cen, jejíž součet sedí přesně.

> **Ve zkratce**
>
> - Peníze ukládejte a počítejte jako celé centy. Floaty jsou ta slavná chyba, ta skutečná je pravidlo zaokrouhlování.
> - Rozpočítaná sleva musí sednout: po zaokrouhlení podílu každého řádku rozhoďte zbylé centy na řádky, kde je místo, dokud součet nesedí přesně.
> - Když účetní kniha ukládá jednu jednotkovou cenu na řádek, zlevněný součet, který nejde beze zbytku vydělit, se nedá zapsat. Rozdělte řádek na dvě ceny lišící se o jeden cent.
> - Ořezávejte všechno na okrajích: slevy na mezisoučet, počty na sklad, součty na nulu.

Každý vývojář se brzy naučí, že 0.1 plus 0.2 není 0.3, takže všichni ukládáme peníze jako celé centy a cítíme se v bezpečí. Pak postavíte něco skutečného, třeba procentní slevu přes celý košík, a zjistíte, že celá čísla řeší jen první problém. Ten druhý je, že dělení se o vaši účetní knihu nestará.

## Padesát centů, které zmizely

Tady je situace, která nás kousla, osekaná na kost. V košíku je 100 kusů zboží po jednom dolaru. Zákazník dostane z objednávky slevu 50 centů. Mezisoučet je 10 000 centů, sleva 50 centů a zlevněný součet 9 950 centů. Zatím samá celá čísla.

Teď ten prodej zapište do účetní knihy, která ukládá jednotkovou cenu na řádek, jak to účetní systémy mají rády. Zlevněná jednotková cena je 9 950 děleno 100, tedy 99,5 centu. To není celé číslo, takže se zaokrouhlí na 100. Kniha teď říká 100 kusů po 100 centech: prodej za plnou cenu. Sleva se nezmenšila. Zmizela, zatímco rozhraní slíbilo zákazníkovi 9 950 a knihy říkaly 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
```

Rozdělení vypadá puntičkářsky, dokud neuvidíte, co za to dostanete. Padesát kusů po 100 centech plus padesát kusů po 99 centech se rovná přesně 9 950. Každý řádek pořád ukládá poctivou celočíselnou jednotkovou cenu, počty pořád sedí a součet knihy se rovná číslu na účtence do centu. Nic z toho není s jedinou zaokrouhlenou cenou možné.

## Rozpočítání potřebuje dorovnávací průchod

Stejná nemoc se objeví o patro výš, když se sleva na úrovni objednávky rozprostře přes několik řádků. Dejte každému řádku jeho poměrný podíl a zaokrouhlete, a součet podílů bude skoro roven slevě. Někdy o cent výš, někdy o cent níž, občas přesně, což je ze všeho nejhorší, protože to chybu před zběžným testováním schová.

```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
```

Dva detaily v té smyčce si své místo vydřely. Zbytek se rozprostírá na kterýkoli řádek s kapacitou, ne jen na poslední, protože poslední řádek může být šesticentová položka, do které tři zbylé centy nevejdou. A každý podíl je omezený mezisoučtem svého řádku, protože sleva větší než řádek znamená zápornou jednotkovou cenu, a záporné jednotkové ceny jsou způsob, jak kniha začne lhát na druhou stranu.

![Obrazovka pokladny s mezisoučtem, desetiprocentní slevou a přesným zlevněným součtem](https://bookatu.com/blog/shots/pos-discount-light.png)

*Nudná účtenka je celý smysl: mezisoučet, sleva a součet, který kniha zopakuje do centu.*

## Ořezávejte na každém okraji

- Slevy ořezávejte do rozsahu od nuly po mezisoučet. Sto deset procent dolů je překlep, ne vratka.
- Částky zadané uživatelem parsujte obezřetně: prázdné a nečíselné znamená nula, nikdy NaN uprostřed výpočtu.
- Počty ořezávejte na to, co existuje. Kód okolo peněz dědí každou chybu skladu nad sebou.
- Testujte součty, které nejdou vydělit beze zbytku. Tři položky po deseti dolarech s dolarem dolů jsou lepší test než jakékoli kulaté číslo.

Nic z toho není žádná sláva, a přesně proto to proklouzne revizí. Aritmetika vypadá správně, protože správná je, až do chvíle, kdy celočíselné dělení potichu zahodí zbytek, na který měl někdo nárok. Zapište si tu podmínku, součet částí se rovná celku, a nechte ji hlídat test.

> Celá čísla drží peníze poctivé. Dorovnání drží poctivá celá čísla.
