JavaScript'te Number tipi, IEEE 754 standardındaki 64 bitlik çift duyarlıklı kayan noktalı sayıdır. Ayrı bir tam sayı tipi yoktur; 37 da 37.0 da aynı değerdir. Bu yüzden 0.1 + 0.2 tam olarak 0.3 etmez, 2^53 - 1 değerinden büyük tam sayılar kesinliğini kaybeder. Ondalık karşılaştırma için tolerans kullanırsınız, büyük tam sayılar için BigInt, kullanıcıya gösterim için Intl.NumberFormat.

Bu yazıda sayıların nasıl kodlandığını, 0.1 + 0.2 sonucunun nedenini, Number.EPSILON ve Number.MAX_SAFE_INTEGER sabitlerini, toFixed, parseInt ve parseFloat tuzaklarını, BigInt kurallarını ve Türkçe sayı ile para biçimlendirmeyi örneklerle görüyorsunuz.

IEEE 754: bir sayı 64 bitte nasıl tutulur?

IEEE 754: bir sayı 64 bitte nasıl tutulur?

Her Number değeri 64 bitten oluşur: 1 bit işaret, 11 bit üs ve 52 bit mantis (anlamlı basamaklar). Mantis başta gizli bir 1 ile birlikte 53 bit hassasiyet verir; bu da yaklaşık 15 ile 17 ondalık basamak demektir. Daha fazla basamak gerektiren işlemler yuvarlanır. Ondalık sistemin 1/3'ü tam yazamaması gibi, ikili sistem de 0.1 değerini tam olarak tutamaz.

javascript
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
console.log(0.1 * 3); // 0.30000000000000004

Number.EPSILON ile güvenli karşılaştırma

Number.EPSILON, 1 ile 1'den büyük ilk temsil edilebilir sayı arasındaki farktır (2 üzeri -52). Büyüklüğü 1 civarında olan sonuçları karşılaştırırken tolerans olarak işe yarar. Sayılar büyüdükçe hassasiyet düştüğü için toleransı büyüklükle ölçeklemeniz gerekir; MDN 2000 civarındaki değerler için 2000 * Number.EPSILON örneğini verir.

javascript
function yakin(a, b, tolerans = Number.EPSILON) {
  return Math.abs(a - b) < tolerans;
}
console.log(Number.EPSILON); // 2.220446049250313e-16
console.log(yakin(0.1 + 0.2, 0.3)); // true

console.log(1000.1 + 1000.2); // 2000.3000000000002
console.log(yakin(1000.1 + 1000.2, 2000.3)); // false
console.log(yakin(1000.1 + 1000.2, 2000.3, 2000 * Number.EPSILON)); // true

Güvenli tam sayı sınırı: MAX_SAFE_INTEGER

Tam sayılar yalnızca -(2^53 - 1) ile 2^53 - 1 arasında kayıpsız tutulur. Bu sınır Number.MAX_SAFE_INTEGER, yani 9007199254740991 değeridir. Sınırın ötesinde iki farklı tam sayı aynı değere düşebilir. Veritabanından gelen büyük kimlik numaraları ya da sosyal medya ID'leri bu yüzden genellikle metin olarak taşınır.

javascript
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2); // true
console.log(Number.isSafeInteger(2 ** 53)); // false
console.log(9007199254740993); // 9007199254740992

console.log(Number.MAX_VALUE); // 1.7976931348623157e+308
console.log(Number.MAX_VALUE * 2); // Infinity
console.log(0 / 0, 1 / 0); // NaN Infinity
console.log(Number.isNaN("abc"), isNaN("abc")); // false true

toFixed: yuvarlama ve metin dönüşü

toFixed(digits) sayıyı virgülden sonra tam olarak digits basamakla gösteren bir metin döndürür. digits 0 ile 100 arasında olmalıdır, aksi halde RangeError gelir. Yuvarlama, ondalık yazılışa değil sayının gerçek ikili değerine göre yapılır; bu yüzden 2.55 beklenenden farklı yuvarlanır.

javascript
const fiyat = 12345.6789;
console.log(fiyat.toFixed(2)); // 12345.68
console.log(typeof fiyat.toFixed(2)); // string
console.log((2.35).toFixed(1)); // 2.4
console.log((2.55).toFixed(1)); // 2.5  (2.55 ikili sistemde biraz küçük tutulur)
console.log((0.004).toFixed(2)); // 0.00
console.log(typeof -2.34.toFixed(1), typeof (-2.34).toFixed(1)); // number string
(1).toFixed(101);
// RangeError: toFixed() digits argument must be between 0 and 100

Son satırdaki öncelik tuzağına dikkat edin: özellik erişimi tekli eksiden önce çalıştığı için -2.34.toFixed(1) önce metin üretir, sonra eksi işareti onu tekrar sayıya çevirir.

parseInt, parseFloat ve Number() farkı

parseInt, parseFloat ve Number() farkı: "42px", "1.9", "0xFF", "1e3"

parseInt ve parseFloat metni baştan okur ve geçersiz ilk karakterde durur; o ana kadar okunanı döndürür. Number() ise metnin tamamının geçerli bir sayı olmasını ister. parseInt ondalık noktayı tanımaz, parseFloat 0x önekini tanımaz. Tip dönüşümlerinin genel kuralları tip dönüşümü yazısında.

javascript
console.log(parseInt("42px"), parseFloat("3.75 TL"), Number("42px")); // 42 3.75 NaN
console.log(parseInt("1.9"), parseInt("0xFF"), parseInt("ff", 16)); // 1 255 255
console.log(parseInt("1e3"), parseFloat("1e3")); // 1 1000
console.log(parseInt(""), Number(""), Number(null)); // NaN 0 0
console.log(parseFloat("1,5"), Number("1,5")); // 1 NaN
console.log(["1", "2", "3"].map(parseInt)); // [ 1, NaN, NaN ]
console.log(["1", "2", "3"].map(Number)); // [ 1, 2, 3 ]
console.log(parseInt(0.0000005)); // 5

map(parseInt) tuzağı, map'in callback'e ikinci argüman olarak indeksi vermesinden kaynaklanır; indeks parseInt'te taban (radix) olarak yorumlanır. parseInt(0.0000005) ise sayı önce "5e-7" metnine çevrildiği için 5 verir. MDN bu yüzden parseInt'in Math.trunc() yerine kullanılmamasını önerir. Türkçe yazılmış "1,5" gibi virgüllü değerleri ayrıştırmadan önce virgülü noktaya çevirmeniz gerekir.

GirdiparseIntparseFloatNumber
"42px"4242NaN
"1.9"11.91.9
"0xFF"2550255
"1e3"110001000
""NaNNaN0

Number() dönüşüm kuralları

Number(x) ve tekli artı (+x) aynı dönüşüm adımlarını izler. undefined NaN olur, null ve false 0, true 1 olur. Metinlerde baştaki ve sondaki boşluklar atılır, boş metin 0 verir ve sayı içindeki _ ayırıcıları kabul edilmez (kod içindeki 1_000 yazımı ise geçerlidir). Tek fark BigInt'tedir: +1n hata verirken Number(1n) çalışır, ama büyük değerlerde hassasiyet kaybolabilir. Nesneler önce ilkel değere çevrilir; bu adım Symbol.toPrimitive ile özelleştirilebilir.

javascript
console.log(Number(undefined), Number(null), Number(true)); // NaN 0 1
console.log(Number("  42  "), Number("1_000"), 1_000); // 42 NaN 1000
console.log(+"", +[], +{}); // 0 0 NaN
console.log(Number(12345678901234567890n)); // 12345678901234567000
+1n;
// TypeError: Cannot convert a BigInt value to a number

BigInt: sınırsız büyüklükte tam sayı

BigInt, MAX_SAFE_INTEGER sınırını aşan tam sayılar içindir. Sayının sonuna n ekleyerek ya da BigInt() fonksiyonunu (new olmadan) çağırarak oluşturulur. typeof sonucu "bigint"'tir. Bölme işlemi kesirli kısmı atar.

javascript
const buyuk = 9007199254740993n;
console.log(buyuk + 1n); // 9007199254740994n
console.log(BigInt("123456789012345678901234567890") * 2n); // 246913578024691357802469135780n
console.log(5n / 2n); // 2n
console.log(0n == 0, 0n === 0, 2n > 1); // true false true

1n + 1;
// TypeError: Cannot mix BigInt and other types, use explicit conversions
console.log(1n + BigInt(1), Number(10n) + 1); // 2n 11

Math.max(1n, 2n);
// TypeError: Cannot convert a BigInt value to a number
BigInt(1.5);
// RangeError: The number 1.5 cannot be converted to a BigInt because it is not an integer

BigInt ile Number aritmetik işlemlerde karıştırılamaz, Math metotlarıyla kullanılamaz ve tekli + desteklemez. Karşılaştırma operatörleri ise iki tipi karıştırmaya izin verir. MDN, BigInt işlemlerinin sabit sürede çalışmadığını ve bu yüzden zamanlama saldırılarına açık olduğunu, kriptografide dikkatli olunması gerektiğini de belirtir.

JSON.stringify BigInt değerleri varsayılan olarak serileştirmez. Bir replacer fonksiyonuyla metne çevirebilirsiniz:

javascript
JSON.stringify({ id: 1n });
// TypeError: Do not know how to serialize a BigInt

console.log(JSON.stringify({ id: 1n }, (k, v) => (typeof v === "bigint" ? v.toString() : v)));
// {"id":"1"}

Intl.NumberFormat ile Türkçe biçimlendirme

Kullanıcıya gösterilecek sayılar için toFixed yerine Intl.NumberFormat kullanın. "tr-TR" yerel ayarı binlik ayırıcı olarak nokta, ondalık ayırıcı olarak virgül kullanır. style seçeneği "decimal", "currency", "percent" ve "unit" değerlerini alır; "currency" için currency kodu zorunludur.

javascript
const tl = new Intl.NumberFormat("tr-TR", { style: "currency", currency: "TRY" });
console.log(tl.format(1234567.891)); // ₺1.234.567,89
console.log(new Intl.NumberFormat("tr-TR").format(1234567.891)); // 1.234.567,891
console.log(new Intl.NumberFormat("tr-TR", { style: "percent" }).format(0.256)); // %26
console.log(new Intl.NumberFormat("tr-TR", { maximumFractionDigits: 1 }).format(3.14159)); // 3,1
console.log(new Intl.NumberFormat("tr-TR", { style: "unit", unit: "kilobyte" }).format(512)); // 512 kB
console.log(tl.format(12345678901234567890n)); // ₺12.345.678.901.234.567.890,00

Para hesaplarında yaygın bir yaklaşım, tutarları kuruş cinsinden tam sayı olarak tutup yalnızca gösterirken 100'e bölmektir. Tam sayı toplamaları güvenli aralıkta kesin sonuç verir:

javascript
const kalemlerKurus = [1999, 2550, 10];
const toplam = kalemlerKurus.reduce((t, k) => t + k, 0);
console.log(toplam); // 4559
console.log(tl.format(toplam / 100)); // ₺45,59

Sık yapılan hatalar

Sık yapılan hatalar: Ondalık sonuçları === ile karşılaştırmak; tolerans kullanın., toFixed sonucunu sayı sanmak; dönen değer metindir, tekrar hesaplamaya sokmayın., Taban vermeden...
  • Ondalık sonuçları === ile karşılaştırmak; tolerans kullanın.
  • toFixed sonucunu sayı sanmak; dönen değer metindir, tekrar hesaplamaya sokmayın.
  • Taban vermeden parseInt kullanmak ve map(parseInt) yazmak; Number ya da açık bir callback kullanın.
  • Büyük kimlik numaralarını Number olarak ayrıştırmak; metin ya da BigInt olarak tutun.
  • BigInt ile Number değerlerini aritmetikte karıştırmak; önce açıkça dönüştürün ve hassasiyet kaybını göze alın.

Sık Sorulan Sorular

0.1 + 0.2 neden 0.30000000000000004 çıkıyor?

0.1 ve 0.2 ikili sistemde sonsuz basamaklı kesirlerdir ve 52 bitlik mantise sığmak için yuvarlanır. Toplam da yuvarlanmış iki değerin toplamı olduğu için 0.3'ün en yakın temsilinden biraz farklı çıkar.

Para hesabı için BigInt mi kullanmalıyım?

Tutarlar güvenli tam sayı aralığındaysa kuruş cinsinden normal tam sayılar yeterlidir. BigInt, 2^53 üzerindeki değerlerin makul ölçüde beklendiği durumlar için önerilir.

Number.isInteger ile Number.isSafeInteger farkı nedir?

isInteger değerin tam sayı olup olmadığına bakar; isSafeInteger ek olarak değerin güvenli aralıkta, yani kayıpsız temsil edilebilir olmasını ister.

toLocaleString ile Intl.NumberFormat farkı var mı?

toLocaleString tek seferlik biçimlendirme için uygundur. Çok sayıda değeri aynı ayarlarla biçimlendirecekseniz bir Intl.NumberFormat nesnesi oluşturup format metodunu tekrar kullanmak daha düzenlidir.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: JavaScript öğrenme rehberi · Veri tipleri · Tip dönüşümü · Eşitlik operatörleri · JSON.parse ve stringify