Mert Özen Her Satırda Daha İleri
Frontend JavaScript Temel — #09

JavaScript Number ve Math: 0.1 + 0.2, Sayı Dönüşümü, Rastgele Sayı ve Biçimlendirme

Mert Özen 3 Kas 2026 10 dk 4 görüntülenme
JavaScript Number ve Math: 0.1 + 0.2, Sayı Dönüşümü, Rastgele Sayı ve Biçimlendirme

JavaScript'in tek number tipini, 0.1 + 0.2 hassasiyet sorununu, Number ve parseInt farkını, NaN ve bigint'i, Math ile yuvarlama ve rastgele sayıyı, Intl.NumberFormat ile para biçimlendirmeyi öğreniyoruz.

Sürüm notu: Bu yazıdaki kodlar ES2026 ve güncel tarayıcılar (Chrome, Edge, Firefox, Safari) içindir.

Önce Tahmin Et

Bir önceki yazıda string'lerle uğraştık. Şimdi sayılara geçiyoruz ve yine küçük bir soruyla başlıyoruz. Bu üç satır konsola ne yazdırır?

console.log(0.1 + 0.2 === 0.3);
console.log(parseInt("42px"));
console.log(Number("42px"));

Okumaya devam etmeden önce bir tahmin yap. İlk satır çoğu kişiyi şaşırtır, son ikisi ise neredeyse aynı işi yapan iki fonksiyonun aslında ne kadar farklı düşündüğünü gösterir. Cevapları yazının ilgili bölümlerinde tek tek açacağız.

Tek Bir Number Tipi

Birçok dilde tam sayılar ve ondalıklı sayılar ayrı tiplerdir. JavaScript'te böyle bir ayrım yok: 5 de 5.5 de aynı tipte, number tipinde yaşar. Bunu tarayıcının konsoluna yapıştırıp dene:

console.log(typeof 5);              // "number"
console.log(typeof 5.5);            // "number"
console.log(Number.isInteger(5.0)); // true
console.log(1_000_000);             // 1000000

5.0 ile 5 arasında motorun gözünde hiçbir fark yok, ikisi de tam sayı. Son satırdaki alt çizgiler ise ES2021 ile gelen sayısal ayırıcılar; yalnızca senin gözün için var, motor onları görmezden gelir. Büyük sayıları okurken işe yarar.

Perde arkasında her number, IEEE 754 standardına göre 64 bitlik bir kayan noktalı sayı (double) olarak tutulur. Bu cümlenin pratikteki anlamı iki şey: ondalıklı sayılarda küçük hassasiyet kayıpları olur ve tam sayıların güvenle temsil edilebildiği bir üst sınır vardır. İkisine de birazdan geleceğiz.

0.1 + 0.2 Neden 0.3 Etmiyor?

Baştaki ilk sorunun cevabı false. Sebebini görmek için toplamın kendisine bak:

console.log(0.1 + 0.2); // 0.30000000000000004
console.log(1.1 + 2.2); // 3.3000000000000003
console.log(0.1 * 3);   // 0.30000000000000004

Bu JavaScript'in bir hatası değil; aynı standardı kullanan neredeyse her dilde aynı sonucu alırsın. 1'i 3'e böldüğünü düşün. Ondalık sistemde sonucu tam olarak yazamazsın, 0.3333... diye bir yerde kesmek zorundasın. Bilgisayar sayıları ikilik sistemde tutar ve orada da 0.1 tam yazılamayan bir sayıdır. Motor onu en yakın değere yuvarlayarak saklar. İki yuvarlanmış değeri topladığında küçük hatalar birleşir ve sonuç 0.3'ün bir tık ötesine düşer.

Bununla yaşamanın üç yolu var. Ekrana yazdıracaksan toFixed ile istediğin basamağa yuvarla. İki ondalıklı sayıyı karşılaştıracaksan aralarındaki farkın çok küçük olup olmadığına bak. Para hesabı yapıyorsan tutarları en küçük birim (sent ya da kuruş) cinsinden tam sayı olarak tut, sadece gösterirken 100'e böl:

const sum = 0.1 + 0.2;
console.log(sum.toFixed(2));                     // "0.30"
console.log(Math.abs(sum - 0.3) < Number.EPSILON); // true

const priceInCents = 1999;               // $19.99
const totalInCents = priceInCents * 3;   // 5997
console.log(totalInCents / 100);         // 59.97

toFixed'in döndürdüğü şeyin bir string olduğuna dikkat et; bu ayrıntı tuzaklar bölümünde karşımıza çıkacak. Number.EPSILON ise 1 ile ondan büyük en yakın number arasındaki fark, yani yaklaşık 2.2e-16 gibi çok küçük bir sayı. Farkı bundan küçükse "bu ikisi eşit" kabul ediyoruz. Bu kalıp 1 civarındaki değerlerde iyi çalışır; çok büyük sayılarla çalışırken toleransı da büyütmen gerekir.

ES2026 ile gelen Math.sumPrecise

Uzun bir listeyi tek tek topladığında küçük hatalar her adımda birikir. ES2026 ile gelen Math.sumPrecise, listedeki sayıların toplamını tam hassasiyetle hesaplar ve yuvarlamayı en sonda yalnızca bir kez yapar:

let total = 0;
for (let i = 0; i < 10; i++) {
  total += 0.1;
}
console.log(total);                             // 0.9999999999999999
console.log(Math.sumPrecise([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1])); // 1

console.log(0.1 + 0.2 + 0.3);                   // 0.6000000000000001
console.log(Math.sumPrecise([0.1, 0.2, 0.3]));  // 0.6
console.log(Math.sumPrecise([0.1, 0.2]));       // 0.30000000000000004

Son satır bir mucize beklememen gerektiğini gösteriyor. 0.1 ve 0.2 daha yazıldıkları anda yuvarlanmış değerler; bu ikisinin tam toplamı bile en yakın 0.30000000000000004'e düşüyor. Math.sumPrecise toplama sırasında biriken hatayı önler, girdinin kendisindeki yuvarlamayı değil. Bir de Math.max'ın aksine sayıları tek tek değil, bir array içinde ister. Bu method bütün güncel tarayıcılarda var ama çok yeni; 2026 baharında Baseline oldu. Eski tarayıcıları da desteklemen gereken bir projede kullanmadan önce kontrol et.

String'den Sayıya

Formdan, URL'den ya da bir dosyadan gelen sayılar genelde string olarak gelir. Onları sayıya çevirmenin üç yaygın yolu var ve üçü farklı düşünür:

console.log(Number("42"));      // 42
console.log(Number(" 12 "));    // 12
console.log(Number("42px"));    // NaN
console.log(Number(""));        // 0
console.log(parseInt("42px"));  // 42
console.log(parseInt("3.9"));   // 3
console.log(parseInt("px42"));  // NaN
console.log(parseFloat("3.9kg")); // 3.9
console.log(parseInt("ff", 16));  // 255

İşte baştaki diğer iki sorunun cevabı. Number katı bir gümrük memuru gibidir: string'in tamamı geçerli bir sayı değilse kapıdan geçirmez ve NaN döner. Baştaki ve sondaki boşlukları ise sorun etmez. parseInt ve parseFloat daha hoşgörülüdür; soldan okumaya başlar, sayı olmayan ilk karakterde durur ve o ana kadar okuduğunu verir. "42px"'ten 42 çıkar, ama "px42" daha ilk harfte takıldığı için NaN olur.

Hangisini ne zaman kullanacaksın? Kullanıcının bir sayı girmesini bekliyorsan ve girdinin tamamen sayı olması gerekiyorsa Number kullan; "12abc"'yi sessizce 12'ye çevirmek çoğu zaman istediğin şey değildir. "16px" gibi birim taşıyan değerleri ayıklarken parseInt ya da parseFloat işini görür. parseInt'e ikinci argüman olarak sayı tabanını verebilirsin; ondalık için parseInt(value, 10) yazmak niyetini açıkça gösterir.

NaN ve Infinity

NaN, "Not a Number" demek ama tipi, ironik biçimde, number. Geçersiz bir matematik işleminin sonucunu temsil eder. En tuhaf özelliği, kendisine bile eşit olmaması:

console.log(typeof NaN);              // "number"
console.log(NaN === NaN);             // false
console.log(Number.isNaN(NaN));       // true
console.log(Number.isNaN("hello"));   // false
console.log(isNaN("hello"));          // true

Bir değerin NaN olup olmadığını === ile kontrol edemezsin. Bunun için Number.isNaN var. Eski, global isNaN fonksiyonu ise önce değeri sayıya çevirmeye çalışır; "hello" sayıya çevrilince NaN olduğu için true der. Yani "bu değer NaN mı?" sorusu yerine "bu değer sayıya çevrilince NaN mı oluyor?" sorusuna cevap verir. Karışıklık istemiyorsan Number.isNaN kullan.

Sıfıra böldüğünde ise hata almazsın, sonsuzluk elde edersin:

console.log(1 / 0);                    // Infinity
console.log(-1 / 0);                   // -Infinity
console.log(0 / 0);                    // NaN
console.log(Number.isFinite(1 / 0));   // false
console.log(Number.isFinite(42));      // true

Number.isFinite, bir değerin gerçek, sonlu bir sayı olup olmadığını tek seferde söyler. Kullanıcıdan gelen bir sayıyı doğrularken NaN ve Infinity'yi birlikte eler.

Güvenli Tam Sayı Sınırı ve bigint

64 bitlik bir double'ın bir kısmı sayının basamaklarına, bir kısmı ise büyüklüğüne ayrılmıştır. Bu yüzden tam sayıları kesin olarak tutabildiği bir sınır var:

console.log(Number.MAX_SAFE_INTEGER);          // 9007199254740991
console.log(2 ** 53);                          // 9007199254740992
console.log(2 ** 53 + 1);                      // 9007199254740992
console.log(Number.isSafeInteger(2 ** 53));    // false
console.log(9007199254740993n + 1n);           // 9007199254740994n

Sınırı aştığında motor hata vermez, sessizce en yakın temsil edilebilir sayıya yuvarlar. 2 ** 53 + 1'in 2 ** 53'e eşit çıkması bundan. Veritabanından gelen çok uzun ID'ler ya da kriptografik hesaplar gibi bu sınırı aşan tam sayılara ihtiyacın olursa, daha önce veri tipleri yazısında gördüğümüz bigint devreye girer. Sayının sonuna n koyduğunda o artık bir bigint olur ve istediğin kadar büyüyebilir. Yalnız bigint ile number'ı aynı işlemde karıştıramazsın; 10n + 1 bir TypeError fırlatır. Gündelik hesapların neredeyse hepsinde number yeterli.

Math Object'i

Math, matematik fonksiyonlarını bir arada tutan yerleşik bir object. Bir alet çantası gibi düşün; new ile oluşturmazsın, doğrudan Math.round(...) diye kullanırsın.

Yuvarlama

console.log(Math.round(4.5));  // 5
console.log(Math.round(4.4));  // 4
console.log(Math.floor(4.7));  // 4
console.log(Math.ceil(4.1));   // 5
console.log(Math.trunc(4.7));  // 4
console.log(Math.floor(-4.5)); // -5
console.log(Math.trunc(-4.5)); // -4

round en yakın tam sayıya, floor her zaman aşağıya, ceil her zaman yukarıya yuvarlar. trunc ise yuvarlamaz, virgülden sonrasını keser atar. Pozitif sayılarda floor ile trunc aynı sonucu verir; negatiflerde ayrılırlar, çünkü -4.5'in "aşağısı" -5'tir.

En büyük, en küçük, mutlak değer

console.log(Math.max(3, 9, 1)); // 9
console.log(Math.min(3, 9, 1)); // 1
console.log(Math.abs(-7));      // 7
console.log(2 ** 10);           // 1024

Üs almak için eskiden kullanılan Math.pow(2, 10) hâlâ çalışır, ama ** operatörü daha kısa ve okunur.

Rastgele tam sayı üretmek

Math.random() 0 ile 1 arasında rastgele bir ondalıklı sayı verir; 0 dahil, 1 hariç. Zar atmak gibi belli bir aralıkta tam sayı istiyorsan küçük bir formül gerekir:

const min = 1;
const max = 6;
const dice = Math.floor(Math.random() * (max - min + 1)) + min;
console.log(dice); // 1 ile 6 arasında bir tam sayı

Adım adım bakalım. Math.random() 0 ile 0.999... arasında bir şey verir. Bunu aralıktaki sayı adedi olan 6 ile çarpınca 0 ile 5.999... arasına yayılır. Math.floor bunu 0 ile 5 arasında bir tam sayıya indirir, min'i eklediğimizde de 1 ile 6 arasına kayar. Bu formülü tekrar tekrar kullanılabilir bir fonksiyona sarmayı ilerleyen yazılarda göreceğiz. Bir uyarı: Math.random oyunlar ve basit örnekler için yeterli, ama şifre ya da token gibi güvenlik gerektiren değerler için kullanılmaz. Onun için tarayıcının crypto.getRandomValues API'si var.

Intl.NumberFormat ile Biçimlendirme

Türkiye'de binlik ayırıcı nokta, ondalık ayırıcı virgüldür; ABD'de tam tersi. Bunu elle uğraşarak yapmak yerine tarayıcının yerleşik Intl.NumberFormat object'ini kullan:

const tr = new Intl.NumberFormat("tr-TR");
console.log(tr.format(1234567.891)); // "1.234.567,891"

const tl = new Intl.NumberFormat("tr-TR", { style: "currency", currency: "TRY" });
console.log(tl.format(1234.5));      // "₺1.234,50"

const usd = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
console.log(usd.format(1234.5));     // "$1,234.50"

const eur = new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" });
console.log(eur.format(1234.5));     // "1.234,50 €"

Aynı sayı, dile ve para birimine göre doğru ayırıcılarla, doğru yerde duran sembolle yazıldı. Birkaç kullanışlı seçenek daha:

const percent = new Intl.NumberFormat("tr-TR", { style: "percent" });
console.log(percent.format(0.256));  // "%26"

const compact = new Intl.NumberFormat("tr-TR", { notation: "compact" });
console.log(compact.format(1500000)); // "1,5 Mn"

const twoDigits = new Intl.NumberFormat("tr-TR", { maximumFractionDigits: 2 });
console.log(twoDigits.format(3.14159)); // "3,14"

console.log((1234.5).toLocaleString("tr-TR")); // "1.234,5"

Tek seferlik bir biçimlendirme için toLocaleString kısayolu yeterli. Aynı biçimi bir listede yüzlerce kez kullanacaksan bir kez Intl.NumberFormat oluşturup tekrar tekrar format çağırmak daha verimli.

Sık Düşülen Tuzaklar

toFixed string döner

const price = (1.5).toFixed(2);
console.log(price);     // "1.50"
console.log(price + 1); // "1.501"
console.log(Number(price) + 1); // 2.5

toFixed görüntüleme içindir. Sonucunu hesaba sokmadan önce sayıya çevir, ya da daha iyisi hesabı bitirip en son toFixed çağır. Bir de şunu bil: (1.005).toFixed(2) beklediğin gibi "1.01" değil "1.00" verir, çünkü 1.005 hafızada 1.00499999... olarak durur. Hassas parasal hesapta tam sayı sent yöntemine dön.

Boş string sıfırdır

Number("") ve Number(" ") NaN değil, 0 döner. Kullanıcı bir alanı boş bıraktığında bu yüzden sıfır girmiş gibi görünebilir. Sayıya çevirmeden önce değerin boş olup olmadığını ayrıca kontrol et.

Negatif sayılarda Math.round

Math.round(-2.5) -3 değil -2 verir. Tam ortada kalan değerleri her zaman pozitif sonsuzluğa doğru yuvarlar. Negatif değerlerle çalışıyorsan bunu test et.

Sayının hemen arkasındaki nokta

console.log((5).toFixed(2)); // "5.00"
console.log(5..toFixed(2));  // "5.00"

5.toFixed(2) yazarsan SyntaxError alırsın. Motor 5.'i ondalıklı bir sayının başlangıcı olarak okur ve arkasından gelen toFixed'i anlamlandıramaz. Sayıyı paranteze al.

Küçük Bir Deneme

★☆☆ Çalıştırmadan önce tahmin et: Number("3.14abc"), parseFloat("3.14abc"), parseInt(" 7 apples") ve Number(null) ne döner? Sonra konsolda kontrol et.

★★☆ Fiyatı "149,90" olarak gelen bir ürünün 3 adedinin toplamını hesapla ve Intl.NumberFormat ile "₺449,70" biçiminde yazdır. Virgülü önce noktaya çevirmen gerekecek; bir önceki yazıdaki replace hatırlarsan iş kolay.

★★☆ 10 ile 20 arasında (ikisi de dahil) rastgele tam sayı üreten satırı yaz. Daha önce gördüğümüz bir döngüyle bunu 1000 kez çalıştır ve en küçük ile en büyük değeri Math.min ve Math.max ile takip et; 10 ve 20'nin gerçekten çıktığını gör.

★★★ Bir alışveriş sepetindeki 19.99, 5.01 ve 0.1 değerlerini önce düz + ile, sonra Math.sumPrecise ile, sonra da sente çevirip tam sayı olarak topla. Üç sonucu karşılaştır ve hangisinin neden daha güvenilir olduğunu kendi cümlelerinle açıkla.

Bu yazıda tek tek sayılarla uğraştık. Peki elinde tek bir fiyat değil de bir sepet dolusu fiyat varsa? Bir sonraki yazıda değerleri sıralı bir liste hâlinde tutan array'lere geçiyoruz: element eklemeyi, çıkarmayı, kopyalamayı ve sort'un neden sayıları beklediğin gibi sıralamadığını göreceğiz.