
Ett belopp kan bli fel på tre olika sätt: decimalen representeras ungefärligt, avrundningen sker vid fel steg eller en rest försvinner när summan delas. Börja med ett kontrakt för enhet och precision. Kontrollera sedan att samma värde överlever indata, beräkning, fördelning, lagring och API-svar. Två snygga decimaler i gränssnittet bevisar bara hur texten ser ut.
Följ beloppet innan du ändrar avrundningen
Anta att en genererad funktion ska dela 100 kr lika mellan tre mottagare. Den räknar fram 33,33 kr per mottagare och sparar tre rader. Summan är 99,99 kr. Det saknade öret beror här på fördelningsregeln: tre lika stora belopp med två decimaler kan inte summera till 100 kr. Om samma fel upprepas i 100 sådana fördelningar saknas 1 kr. Det är ett konstruerat räkneexempel, ingen mätning av hur ofta AI-kod gör fel.
Logga ett reproducerbart exempel i utvecklingsmiljön: originalsträngen, dess tolkade värde, enheten, mellanresultatet före avrundning, de sparade delbeloppen och svaret som klienten faktiskt läser. Använd exempeldata. Om summan redan är fel före databasens anrop behöver du börja där. Om den ändras först vid hämtning ska du granska kolumntyp och drivrutinens konvertering.
Den här felsökningen gäller även ett enda anrop. Om två parallella anrop skriver över varandras saldon behöver du också kontrollera samtidighetsbuggar i AI-genererad kod. En korrekt pengatyp löser inte en förlorad uppdatering.
Formatering reparerar inte beräkningen
JavaScripts Number bygger på IEEE 754 binary64, enligt ECMAScript-specifikationens definition av Number. Decimalbråken i följande exempel får därför ungefärliga representationer. Kör uttrycken var för sig:
const summa = 0.1 + 0.2;
console.log(summa); // 0.30000000000000004
console.log(summa.toFixed(2)); // "0.30"
console.log(summa === 0.3); // false
Texten visar två decimaler, men variabeln innehåller samma värde som före formateringen. Att använda den formaterade strängen i nästa beräkning inför dessutom en ny konverteringsgräns. I exemplet nedan väljer vi i stället heltalsören redan vid inläsningen. För andra beräkningar kan en decimaltyp passa bättre; välj efter den precision som faktiskt behövs, inte efter vilken typ modellen råkade använda.
Skriv representationsregeln före koden
Vårt exempel gäller SEK med 100 öre per krona. API-indata är en decimalsträng med punkt, högst två decimaler och högst nio siffror före punkten. Svensk kommainmatning normaliseras och valideras i ett separat formulärsteg. Strängar med tusentalsavskiljare, exponentnotation eller fler decimaler avvisas här; de avrundas inte tyst.
Internt summerar vi heltalsören. En beräknad kvot avrundas till närmaste öre, med exakt halva ören bort från noll. Fördelning mellan lika andelar använder däremot heltalskvot och rest: de första mottagarna i en sparad ordning får ett extra öre i beloppets teckenriktning. API-svaret skickar heltalsören som sträng tillsammans med valuta och skala. Det här är ett valt tekniskt kontrakt för exemplet, inte en regel för skatt eller redovisning.
Behöver du räntor eller enhetspriser med fler decimaler kan du behålla en högre decimalprecision fram till den beslutade avrundningspunkten. Pythons decimal-dokumentation beskriver strängkonstruktion, inställbar precision och avrundningslägen. Konstruktion från ett redan avrundat flyttal återskapar inte den ursprungliga decimalsträngen. Ange både precision och läge uttryckligen; en decimaltyp beslutar inte själv när ett belopp ska avrundas.
Ett körbart kontrakt i Python
Koden skiljer på att läsa ett färdigt belopp och att avrunda ett beräknat bråk. Funktionen ore accepterar bara kontraktets strängformat. Funktionen avrunda tar en täljare i öre och en positiv heltalsnämnare. Den använder absolutbelopp vid halva ören och återställer sedan tecknet. Därmed får 100,5 öre resultatet 101 öre och −100,5 öre resultatet −101 öre.
import re
import json
def ore(text):
if not isinstance(text, str):
raise ValueError("Belopp ska vara en decimalsträng")
if not re.fullmatch(r"-?[0-9]{1,9}(?:\.[0-9]{1,2})?", text):
raise ValueError("Ogiltigt belopp")
tecken = -1 if text.startswith("-") else 1
kronor, _, decimaler = text.lstrip("-").partition(".")
return tecken * (int(kronor) * 100 + int(decimaler.ljust(2, "0")))
def avrunda(taljare, namnare):
if type(taljare) is not int or type(namnare) is not int or namnare <= 0:
raise ValueError("Heltal och positiv nämnare krävs")
kvot, rest = divmod(abs(taljare), namnare)
return (-1 if taljare < 0 else 1) * (kvot + (2 * rest >= namnare))
def fordela(total, antal):
if type(total) is not int or type(antal) is not int or not 1 <= antal <= 10000:
raise ValueError("Heltalsbelopp och 1–10000 mottagare krävs")
kvot, rest = divmod(abs(total), antal)
tecken = -1 if total < 0 else 1
return [tecken * (kvot + (i < rest)) for i in range(antal)]
def till_api(total):
if type(total) is not int:
raise ValueError("Heltalsören krävs")
return json.dumps({"currency": "SEK", "scale": 2, "amountMinor": str(total)})
Heltalsrepresentationen gör inte lika fördelning exakt delbar. fordela(10000, 3) ger 3334, 3333 och 3333 öre, med summan 10000 öre. Vid en kreditering av samma fördelning ger −10000 öre delarna −3334, −3333 och −3333 öre. Spara mottagarnas ordning eller använd stabilt sorterade identifierare. Om ordningen växlar kan en annan mottagare få restöret nästa gång. För viktade andelar behövs en annan, dokumenterad fördelningsalgoritm.
Rader först eller total först ger olika svar
Två beräknade rader på 100,5 öre vardera blir 101 + 101 = 202 öre om varje rad avrundas först med vår regel. Om du summerar bråken först blir resultatet 201 öre. Båda beräkningarna följer sin respektive ordning. Felet uppstår när specifikationen förutsätter den ena och implementationen använder den andra.
Dokumentera därför ett beslut som går att testa: exempelvis ”beräkna varje rad med bevarad precision, avrunda varje slutlig rad, summera sedan radbeloppen”. Om verksamheten kräver totalavrundning måste du i stället bestämma hur differensen kopplas till raderna. Att lägga till en liten konstant före avrundning förklarar varken tidpunkten eller vem som får restöret.
Behåll kontraktet genom databas och API
I PostgreSQL kan du lagra vårt exempel som bigint i en kolumn som heter amount_minor, tillsammans med valuta. Kontrollera att både varje delbelopp och den största tillåtna totalsumman ryms. Databasens dokumentation om numeriska typer anger heltalstypernas intervall. Den beskriver också numeric som exakt decimalrepresentation och att en deklarerad skala avrundar inkommande värden till den skalan. För numeric går halva steg bort från noll.
En kolumn med två decimaler kan alltså införa en avrundningspunkt som applikationen inte räknat med. Kontrollera drivrutinens verkliga returtyp med ett integrationstest. Testet ska skriva ett känt belopp, läsa tillbaka det genom samma kod som produktionen och jämföra värde, valuta och skala. En lokal JSON-rundresa bevisar inte databasvägen.
I vårt API är amountMinor en sträng även när beloppet är litet. Mottagaren ska tolka den enligt kontraktet, utan mellansteget flyttal. Annars flyttar du bara precisionstappet till nästa tjänst. Kontrollera också att klienten visar 10000 öre som 100,00 kr och inte som 10000 kr; exakt lagring med fel enhet ger fortfarande fel belopp.
Tester som bevakar besluten
Lägg följande efter funktionerna och kör filen. Testerna täcker decimalindata, summering, negativa värden, avrundningspunkt, restfördelning och serialisering. De förväntade värdena är uttryckliga exempel på kontraktet. Intervallslingan kontrollerar dessutom att varje fördelning bevarar summan och att största och minsta andel skiljer högst ett öre.
assert ore("0.10") + ore("0.20") == 30
assert ore("-1.05") == -105
assert ore("0") == 0
for fel in ["1.005", "1,00", "1e2", "NaN", "1000000000", 0.1]:
try:
ore(fel)
except ValueError:
pass
else:
raise AssertionError(fel)
assert avrunda(201, 2) == 101
assert avrunda(-201, 2) == -101
assert avrunda(200, 2) == 100
assert sum(avrunda(x, 2) for x in [201, 201]) == 202
assert avrunda(sum([201, 201]), 2) == 201
assert fordela(10000, 3) == [3334, 3333, 3333]
assert fordela(-10000, 3) == [-3334, -3333, -3333]
for total in range(-101, 102):
for antal in range(1, 12):
delar = fordela(total, antal)
assert sum(delar) == total
assert max(delar) - min(delar) <= 1
assert fordela(-total, antal) == [-x for x in delar]
for total in [0, -105, 10000, 99999999999]:
svar = json.loads(till_api(total))
assert svar == {"currency": "SEK", "scale": 2, "amountMinor": str(total)}
assert int(svar["amountMinor"]) == total
print("Beloppskontraktet passerar")
Be därefter modellen granska den verkliga diffen med samma kriterier: ”Följ beloppet från originalsträngen till lagring och API-svar. Markera varje typkonvertering, enhetsbyte och avrundning. Visa vilket test som verifierar varje regel och redovisa databasvägen separat.” Använd guiden till kodgranskning för att sätta in kontrollen i din vanliga granskning. Godkänn först när både beräkningsregeln och gränserna mot omvärlden går att peka ut i koden.
[1] ECMAScript, definitionen av Number — dubbel precision, som förklarar varför ett belopp inte alltid går jämnt ut. [2] Python, decimal-dokumentationen — exakt decimalaritmetik och kvantisering. [3] PostgreSQL, dokumentationen om numeriska typer — numeric mot flyttalstyper. Samtliga kontrollerade 8 sep 2026. Beloppen och fördelningsregeln är artikelns egna pedagogiska exempel. Testerna verifierar Python-kontraktet; databas- och klientintegration måste provas i din egen miljö.