Para hesabında kayan nokta kullanmamak: nasıl saklamalı
Para alanını float tutan sistem er ya da geç bir kuruş kaybeder. Tam sayı ve ondalık tip seçenekleri, bölme kalanının dağıtımı ve yuvarlama kuralının kodda yazılması.
Para tutan bir alanın tipi float ya da double ise, o sistem er ya da geç bir kuruş kaybeder. Bu bir olasılık değil, ikili tabanda kesir saklamanın doğrudan sonucu.
Sebep basit: 0.1 ikili tabanda sonlu basamakla yazılamaz. Onlukta 1/3'ün yazılamaması gibi. IEEE 754 en yakın değeri saklar, o değer de tam olarak 0.1 değildir.
0.1 + 0.2 // 0.30000000000000004
0.1 + 0.2 === 0.3 // false
(1.005).toFixed(2) // "1.00" — 1.005 aslında 1.00499999...
Son satır özellikle sinsi. Yuvarlama fonksiyonu doğru çalışıyor; yuvarlanan sayı zaten beklediğiniz sayı değil.
İki geçerli yol var
1. Tam sayı olarak, en küçük birimde sakla
Tutarı kuruş cinsinden integer olarak tut. 149,90 TL veritabanında 14990 olur.
const fiyat = 14990; // 149,90 TL
const adet = 3;
const ara = fiyat * adet; // 44970 — tamamen kesin
Uygulama sınırında biçimlendir, içeride asla ondalığa dönme:
const bicimle = (kurus) =>
new Intl.NumberFormat("tr-TR", { style: "currency", currency: "TRY" })
.format(kurus / 100);
bicimle(44970); // "₺449,70"
JavaScript'te güvenli tam sayı sınırı Number.MAX_SAFE_INTEGER, yani 9.007.199.254.740.991. Kuruş cinsinden bu 90 trilyon liranın üzerine denk gelir; tekil tutarlar için fazlasıyla yeterli. Toplamların bu sınıra yaklaşabileceği bir raporlama yazıyorsanız BigInt kullanın.
2. Ondalık tip kullan
Veritabanı tarafında karşılığı NUMERIC / DECIMAL. Bunlar onluk tabanda sabit basamakla çalışır, kayan nokta hatası üretmez.
-- PostgreSQL
tutar NUMERIC(14, 2) NOT NULL
-- MySQL / MariaDB
tutar DECIMAL(14, 2) NOT NULL
Uyarı: NUMERIC bir sürücüyle okunduğunda dilin float tipine dönüştürülebilir ve kazanç orada kaybolur. Node'da pg sürücüsü NUMERIC'i varsayılan olarak string döndürür — bunu parseFloat ile karşılamak, veritabanında kazandığınız kesinliği uygulama katmanında geri vermek demektir. String'i olduğu gibi bir ondalık kütüphanesine verin.
Python'da karşılığı decimal.Decimal, ve Decimal her zaman string'den kurulur:
from decimal import Decimal
Decimal("0.1") + Decimal("0.2") # Decimal('0.3')
Decimal(0.1) # Decimal('0.1000000000000000055511151231257827')
İkinci satır, float'ın hatasını Decimal'in içine taşımanın nasıl göründüğü.
Asıl zor kısım: bölme
Toplama ve çarpma tam sayıda sorunsuz. Bölme sorunlu, çünkü kuruş bölünemez.
Klasik durum: 100,00 TL'yi üç kişiye paylaştırın. 33,33 × 3 = 99,99. Bir kuruş ortada kalır. Bunu yuvarlayarak çözemezsiniz; toplam tutmak zorundadır.
Doğru yöntem, kalanı dağıtmak:
function paylastir(toplamKurus, pay) {
const temel = Math.floor(toplamKurus / pay);
const kalan = toplamKurus - temel * pay;
return Array.from({ length: pay }, (_, i) => temel + (i < kalan ? 1 : 0));
}
paylastir(10000, 3); // [3334, 3333, 3333] — toplamı 10000
Aynı sorun KDV hesabında da çıkar. Fatura toplamının KDV'si ile satır satır hesaplanan KDV'lerin toplamı, yuvarlama nedeniyle birbirini tutmayabilir. Hangisinin esas alınacağına karar verip bunu tek bir yerde uygulayın; iki farklı modülde iki farklı sıra kullanılırsa fark rapor zamanında ortaya çıkar.
Yuvarlama kuralını açıkça seç
Çoğu dilin varsayılan yuvarlaması "yarımı yukarı" değildir. Python 3'te round(2.5) sonucu 2 verir — bankacı yuvarlaması, yarımı en yakın çift sayıya götürür. Bu bilinçli bir tercihtir ve toplamda sapmayı azaltır, ama mali müşavirin beklediği kural bu olmayabilir.
Kuralı kodda açıkça yazın:
from decimal import Decimal, ROUND_HALF_UP
def kurusa(d: Decimal) -> Decimal:
return d.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
kurusa(Decimal("2.675")) # Decimal('2.68')
Para birimi tutardan ayrılmaz
Bir tam sayı tek başına para değildir. 14990 sayısı, kuruş mu yoksa sent mi olduğu bilinmeden anlamsız. İkisini bir arada taşıyın:
type Para = { tutar: number; kur: "TRY" | "USD" | "EUR" };
Ayrıca birimlerin ondalık basamak sayısı sabit değil. Japon yeninde alt birim yok, bazı para birimlerinde üç basamak var. "Yüze böl" varsayımı çok para birimli bir sistemde tutmaz.
Kur çevriminde de ham sonucu saklamayın. Çevrimin yapıldığı kuru, tarihi ve kaynağı kaydedin; aksi halde altı ay sonra "bu satır neden bu tutar" sorusuna yanıt veremezsiniz.
Serileştirme sınırı
JSON'un sayı tipi kayan noktadır. {"tutar": 149.90} yazan bir API, alıcı tarafta hatayı yeniden üretir. İki güvenli seçenek var: tutarı string olarak gönderin ("149.90"), ya da en küçük birimde tam sayı gönderip alanın adında bunu belirtin (tutar_kurus: 14990).
Özet karar
Yeni bir alan açarken sırayla sorun: bu alan para mı, hangi birimde, kaç basamak, yuvarlama kuralı ne. Dördü de yazılmadan tabloya float koymayın. Var olan bir sistemde float alan varsa, göç sırasında string üzerinden dönüştürün — float'tan Decimal'e doğrudan geçiş, kaybolmuş basamağı geri getirmez, yalnızca kalıcı hale getirir.
Bu kararı bir teste bağlamak da işe yarıyor. Para hesabı yapan modülün testine, toplamın korunduğunu doğrulayan tek bir kontrol koyun: satırların toplamı, belgenin toplamına eşit mi. Yuvarlama hatası kodda hangi noktadan girerse girsin bu kontrol kırmızıya döner, ve hatayı üretimde muhasebe fark etmeden önce yakalarsınız.
- veri tipleri
- para
- yuvarlama
- veritabanı