무게를 재서 파는 가게는 언젠가 마감이 안 맞는다.
라벨 뭉치를 더한 값과 포스 합계가 몇백 원 차이가 나고, 어디서 났는지 못 찾는다. 그리고 매일 조금씩 난다.
왜 나는가
값이 두 번 계산되기 때문이다.
- 라벨을 찍을 때 — 손님이 낼 수 있는 단위로 반올림한다. 3,444.5원짜리를 3,440원이라고 찍는다.
- 장부에 넣을 때 — 무게에서 다시 계산한다. 3,444.5원.
둘 다 틀리지 않았다. 그런데 다르다. 그리고 그 차이는 건마다 4~5원이라 하루가 지나야 보인다.
값을 만드는 자리를 하나로
이 샘플의 답은 함수 하나다.
/// The only place a price is made.
///
/// Rounds to what somebody can pay, once. Every other number in this server
/// — the label, the ledger, the day total — comes from this and never from
/// a second pass over the weight.
static int priceFor(Product p, int grams) {
final exact = p.wonPer100g * grams / 100;
return (exact / _payableStep).round() * _payableStep;
}
그리고 라벨을 찍을 때 그 한 번의 결과가 장부로 그대로 들어간다.
// One call, one number. The label and the ledger row are the same
// integer, so there is nothing for them to disagree about.
final won = priceFor(_product, _grams);
_sales.add(Sale(_product.code, _product.name, _grams, won));
손님이 낸 금액이 곧 장부에 남는 금액이다. 장부가 "원래는 3,444.5원인데"라고 말할 이유가 없다 — 손님은 그걸 안 냈다.
저울 위

라벨에 반올림이 얼마였는지가 적혀 있다.
pan: Pork belly 347 g — exact 10340.6 won, label 10340 won (rounded -0.6 won to the nearest 10)
// What the rounding actually did, in won, said out loud. A shop
// that rounds without showing it is a shop that argues later.
이건 손님을 위한 게 아니라 가게를 위한 것이다. 반올림을 안 보이게 하면 나중에 "왜 계산이 이렇게 나오냐"는 질문에 아무도 답을 못 한다. 보이면 그 자리에서 끝난다.
다섯 건, 그리고 마감
label 1: 347 g -> 10340 won (exact 10340.6, rounded -0.6)
label 2: 512 g -> 21250 won (exact 21248.0, rounded +2.0)
label 3: 210 g -> 2600 won (exact 2604.0, rounded -4.0)
label 4: 1000 g -> 29800 won (exact 29800.0, rounded 0.0)
label 5: 83 g -> 3440 won (exact 3444.5, rounded -4.5)
올림도 있고 내림도 있고 딱 떨어지는 것도 있다. 일부러 그렇게 골랐다 — 전부 같은 방향으로 반올림되는 데이터로 시험하면 이 종류의 버그는 안 잡힌다.

ledger says 5 labels, total 67430 won
harness added the labels itself: 67430 won
ledger total minus watched labels: 0 won