Bildiğiniz
gibi onluk sistemde bazı kesirli sayılar tam olarak gösterilemez ve devirli
olarak ifade edilir. Örneğin; 1/3 = 0.33..... ya da 1/6 = 0.166..... gibi.Aynı durum bilgisayarın sayıları tuttuğu ikilik sistemde de geçerlidir.Örneğin onluk sistemdeki 7/10
sayısının ikilik sistemdeki karşılığı olan 111/1010 sayısını ele alalım. Bu
ikilik sistemdeki bölme işlemini gerçekleştirdiğimizde 0.1011001100... gibi
1100 sayı dizisinin devirli olarak devam ettiği bir sonuca varırız. Onluk sistemdeki bir kesirli sayının ikilik
sistemdeki karşılığının devirli olduğunu, bölme işlemi ile anlayabildiğimiz
gibi sayıyı a/b şeklinde yazıp,sadeleştirip, b'nin 2'nin kuvveti olup
olmadığına bakarakta anlayabiliriz. Eğer b 2'nin kuvveti değilse sayı devirli
olacaktır. Aynı sonuca bölme işlemi yerine asagidaki yöntemle de ulaşabiliriz.
0.7 * 2 = 1.4 ---> 1 (Ondalık ayracın solundaki sayıyı al)
0.4 * 2 = 0.8 ---> 0 (>1 ise 1 çıkararak çarpmaya devam et)
0.8 * 2 = 1.6 ---> 1
0.6 * 2 = 1.2 ---> 1
0.2 * 2 = 0.4 ---> 0
0.4 * 2 = 0.8 ---> 0
0.8 * 2 = 1.6 ---> 1 (Tekrar eden dizi sayısını belirledik)
Java Devirli Sayıları Nasıl Tutar ?
Şimdi, ikilik sistemde tam olarak
ifade edilemeyen 0.7 sayısının java'da nasıl gösterilmekte olduğuna bakalım.
Java dilinde kesirli sayıların gösteriminde IEEE 754 (IEEE Standard for
Floating-Point Arithmetic) standartları kullanılmaktadır.Bu standart çoğu
modern programlama dillerinde kullanılmaktadır.Float veri tipi 32 bit
tutabildiğinden IEEE 754 single precision formatını, Double veri tipi ise 64
bit tutabildiğinden IEEE 754 double precision formatını kullanır.Double
precision 1 bit'i işaret biti olarak, 11 bit'i üst olarak, 52 bit'i ise anlamlı
rakam(significant bits ; mantissa) olarak kullanırken single precision 1 bit'i
işaret biti, 8 bit'i üst , 23 bit'i ise anlamlı rakam(significant bits ;
mantissa) olarak kullanır.Ancak mantissada normalization için değer olarak
default 1 değerine sahip olan fazladan 1 bit daha kullanılmaktadır.Dolayısıyla
double-precision'da 53 bit, single-precision da ise 24 bit mantissa kullanılabilmektedir.(Şekil
2)
Burada Java'nın davranış şekli istenen
sonuca en yakın sonucu elde etmek için double-precision da 52 ya da 53 bit, single-precision da ise 23 ya
da 24 bit kullanmak ve gerekirse yuvarlama yapmak olacaktır.Bu en yakın sonuç
istenen sonuçtan büyük ya da küçük olabilir.
İşaret Üst Mantissa
1
|
11
|
52 + 1
|
64 bit double-precision format
1
|
8
|
23 + 1
|
32 bit single-precision format
Şekil 2
0.7 sayısını Double ve Float ile
nasıl tutabileceğimizi gösterelim.
double repeatingFractionNumber =
0.7;
ikilik sistemde;
0.1011001100110011001100110011001100110011001100110011(52.bit)...
Burada double-precision'da
mantissa için 52. bit sonrası truncate
edilir.IEEE 754 standartlarına göre 52.bit 1 ise olduğu gibi bırakılır. Bu
durumda 0.7 sayısını truncate ettiğimizden, 0.7 den çok az küçük bir sayı
edilmiş olur.
0.1011001100110011001100110011001100110011001100110011
sayısını tekrar onluk sisteme çevirirsek
0.6999999999999999555910790149937383830547332763671875 sayısını elde ederiz.
Bu sonuca aşağıdaki kod parçasını
çalıştırarak 0.7 nin aslında bu şekilde ifade edildiğini görebiliriz.
BigDecimal bd = new
BigDecimal(repeatingFractionNumber);
System.out.println("0.7 java'da
aslında bu sayıdır : " + bd);
52. ve 53. bit 1 ise ikiside kullanılır, 52.bit ve 53.bit 0 ise asıl sayıya yakınlık durumuna göre ya sayı olduğu gibi bırakılır ya da 53.bit 1 olarak yuvarlanır. 52.bit 0 53.bit 1 ise asıl sayıya yakınlığa göre ya sayı olduğu gibi bırakılır ya da 52.bit 1'e yuvarlanıp 53. bit kullanılmaz. 52.bit 1 ve 53.bit 0 ise asıl sayıya yakınlık durumuna göre ya sayı olduğu gibi bırakılır ya da 53.bit 1 e yuvarlanır.1'e yuvarlama yapılan her durumda asıl sayıdan daha büyük bir sayı , yapılmadığında ise daha küçük bir sayı elde edilir.(Aynı durum single-precision da 23 ve 24. bitler içinde geçerlidir.)
52. ve 53. bit 1 ise ikiside kullanılır, 52.bit ve 53.bit 0 ise asıl sayıya yakınlık durumuna göre ya sayı olduğu gibi bırakılır ya da 53.bit 1 olarak yuvarlanır. 52.bit 0 53.bit 1 ise asıl sayıya yakınlığa göre ya sayı olduğu gibi bırakılır ya da 52.bit 1'e yuvarlanıp 53. bit kullanılmaz. 52.bit 1 ve 53.bit 0 ise asıl sayıya yakınlık durumuna göre ya sayı olduğu gibi bırakılır ya da 53.bit 1 e yuvarlanır.1'e yuvarlama yapılan her durumda asıl sayıdan daha büyük bir sayı , yapılmadığında ise daha küçük bir sayı elde edilir.(Aynı durum single-precision da 23 ve 24. bitler içinde geçerlidir.)
Elde etmek istediğimiz sayıdan çok az
büyük bir sayı elde.Bu durumu göstermek için ondalık sistemdeki 0.1
sayısını ele alalım. 0.1'i Şekil 1 de gösterdiğimiz yöntemle ikilik sisteme
çevirirsek şu sonucu elde ederiz.
0.000110011... 0011 sayı dizisi
devirli olarak tekrar ediyor.
64 bit double-precision da bu sayı
aşağıdaki sayıya dönüşür.(Noktadan sonraki 3 sıfır anlamlı rakam değil, bu
yüzden 53 bit içinde sayılmıyor)
0.0001100110011001100110011001100110011001100110011001101(52.
bit 0 iken 1 yaptık ve 53 ve sonraki bitler için yuvarlama yaptık)
0.0001100110011001100110011001100110011001100110011001101
sayısını tekrar onluk sisteme çevirirsek
0.1000000000000000055511151231257827021181583404541015625 sayısını elde ederiz,
görüldüğü gibi 53. bit ve sonrasının 52. bit'in 1 yapılması sonucu yaptığımız
yuvarlama ile 0.1 den çok az büyük bir sayı elde ettik.
0.7 sayısını Float veri tipinde yani
32 bit single-precision da gösterimine bakalım.
float repeatingFractionNumber =
0.7f;
ikilik sistemde; 0.101100110011001100110011(24.bit)...
(Java burada 24 bit kullanmayı seçiyor çünkü 23 bit ile elde edeceğimiz sayı 0.69999992847442626953125
iken 24 bit ile elde edeceğimiz sayı 0.699999988079071044921875 olup 0.7 ye daha yakındır. )
24.bit 1 olduğundan yuvarlama
yapmadık. Yuvarlama yapılmıyorsa sonuç istenen sonuçtan daha küçük olmaktadır.
Yine aşağıdaki kod parçası ile 0.7
nin float veri tipi ile aslında nasıl ifade edildiğini görebiliriz.
BigDecimal bd = new
BigDecimal(repeatingFractionNumber);
System.out.println("0.7f
java'da aslında bu sayıdır : " + bd);
Buraya kadar java'da IEEE
Floating-Point standartlarına göre double ve single precision formatlarında
double ve float veri tipindeki sayıların nasıl tutulduğunu gördük.
Temel prensip istenen sonuca en
yakın sonucu elde etmek için double-precision da 52 ya da 53, single-precision
da 23 ya da 24 bit kullanmak ve istenen sonuçtan çok az büyük ya da çok az
küçük en yakınsak sayıyı elde etmek. Böyle bir yaklaşımla sürekli birbirine
eklenenen double ya da float sayıları düşünelim; sayılar eklendikçe
yuvarlamalar yapılacak ve toplam hata sürekli artacaktır. Double ve float
bilimsel hesaplamlar için uygunken finansal hesaplamalarda bu şekilde artan
toplam hata kabul edilemez. Bu yüzden toplam hatayı en aza indirecek yöntemleri
uygulamamız gerekmektedir. Şimdi bu yöntemleri inceleyelim.
BigDecimal kullanmak:
Double kullandığımız şu 2 örneği inceleyelim.
Örnek 1:
double d1 = 0.1, d2 =
0.2, d3 = 0.3;
if (d1 + d2 == d3)
System.out.println("0.1+0.2=0.3");
else
System.out.println("0.1+0.2=?");
Bu
program parçacığını çalıştırdığımızda ekrana 0.1+0.2 = ? basılmakta.Normalde
şaşırtıcı olacak bu sonuç ilk bölümde java'da double sayıların nasıl
tutulduğunu düşündümüzde gayet normal bir sonuç olmakta. Hatırlayalım,
0.1
= 0.1000000000000000055511151231257827021181583404541015625
0.2 = 0.200000000000000011102230246251565404236316680908203125
0.3 = 0.299999999999999988897769753748434595763683319091796875
Bu
duruma göre 0.1 + 0.2 0.3 'den büyük
olmakta.
Örnek 2:
double elimizdekiTutar
= 7.0;
int satinAlinanUrunSayisi = 0;
for (double urunTutar =
0.7; elimizdekiTutar >= urunTutar;) {
elimizdekiTutar -= urunTutar;
satinAlinanUrunSayisi++;
}
System.out.println("elimizdekiTutar="+elimizdekiTutar);
System.out.println("satinAlinanUrunSayisi="+satinAlinanUrunSayisi);
Bu
program parçacığını çalıştırdığımızda elimizdeki tutar'ın 0.6999999999999988,
satın alınan ürün sayısının ise 9
olduğunu görüyoruz. Burada beklenen sonuç elimizde hiç para kalmayıp 10 ürün
satılması idi.
Şimdi aynı örnekleri BigDecimal kullanarak
yapalım.
Örnek 1:
BigDecimal d1 = new BigDecimal("0.1");
BigDecimal d2 = new BigDecimal("0.2");
BigDecimal d3 = new BigDecimal("0.3");
if (d1.add(d2).equals(d3))
System.out.println("0.1+0.2=0.3");
else
System.out.println("0.1+0.2=?");
Bu durumda sonuç beklendiği gibi "0.1+0.2=0.3"
olacaktır.
Örnek 2(Yöntem 1):
BigDecimal elimizdekiTutar = new BigDecimal("7.0");
int satinAlinanUrunSayisi = 0;
final BigDecimal URUN_TUTAR = new BigDecimal("0.7");
for (BigDecimal
urunTutar = URUN_TUTAR;
elimizdekiTutar.compareTo(urunTutar) >= 0;)
{
elimizdekiTutar =
elimizdekiTutar.subtract(urunTutar);
satinAlinanUrunSayisi++;
}
System.out.println("elimizdekiTutar=" + elimizdekiTutar);
System.out.println("satinAlinanUrunSayisi=" +satinAlinanUrunSayisi);
Bu
durumda sonuç beklendiği gibi elimizde hiç para kalmayıp satın alınan ürün
sayısının 10 olması olacaktır.
Burada
dikkat edilmesi gereken önemli bir nokta, istenen sonuca ulaşabilmek için Big
Decimal kullanırken String parametre alan constructor'ı kullanmaktır.
BigDecimal'ın double
parametre
alan constructor'ı daha önce yaşadığımız sorunları ortadan kaldırmamaktadır.
Bu
teknikte scale'i
değiştirmek için setScale metodu ile roundlama yapabiliriz.
Örnek 2 (Yöntem 2):
double elimizdekiTutar = 7.0;
int
satinAlinanUrunSayisi = 0;
final int DECIMAL_PLACE = 2;
for (double urunTutar = 0.7;
elimizdekiTutar >= urunTutar;)
{
elimizdekiTutar = round(elimizdekiTutar
-= urunTutar);
satinAlinanUrunSayisi++;
}
System.out.println("elimizdekiTutar="+elimizdekiTutar);
System.out.println("satinAlinanUrunSayisi="+satinAlinanUrunSayisi);
double round(Double d)
{
BigDecimal bd = new
BigDecimal(d.toString());
bd = bd.setScale(DECIMAL_PLACE ,
BigDecimal.ROUND_HALF_EVEN);
d = bd.doubleValue();
return d;
}
Bu
yöntemi BigDecimal'ın daha yavaş olan add, substract gibi metotlarını kullanmak istemediğimizde kullanabiliriz. Burada yapılması gereken her ara işlemde elde
edilen tutarı BigDecimal'ın round metodunu kullanarak istenilen decimal
place'de roundlamak ve sonucu yine double olarak dönmek olacaktır.
round
metodunda BigDecimal constructor'ı olarak yine String parametreli olanın
kullanıldığına dikkat ediniz.
round metodunda scale 2 olarak verilirken round mode olarak BigDecimal.ROUND_HALF_EVEN
verilmiştir. Bu mode toplam hatayı en
aza indirgeyen roundlama modudur. Alt sınır ve üst sınıra eşit uzaklıkta
olunduğunda en son decimal place'in yanındaki sayı çift ise ROUND_DOWN tek ise
ROUND_UP gibi davranır.Örneğin;
0.685 2 decimal place ile 0.68 'e
yuvarlanırken, 0.675 2 decimal place ile yine 0.68 e
yuvarlanacaktır.
Ancak
bu davranışın tutarlı olması için yine BigDecimal'ın String constructor'ı
kullanılmalıdır.Aksi takdirde 0.685'in hafızada gösterildiği değer olan 0.685000000000000053290705182007513940334320068359375
değeri üzerinden yuvarlama yapılıp
0.685 0.69'a yuvarlanacaktır.
BigDecimal
kullanarak finansal hesaplamalarda ve tam sonuç gereken işlemlerde hata oranını
ortadan kaldırıp, istediğimiz yuvarlama tekniklerinden birini kullanma şansını
elde etmiş oluyoruz. Ancak primitive tipler yerine BigDecimal kullanmak hem
kullanım açısından daha karmaşıktır hem de performans açısından sorun teşkil edebilmektedir.Örneğin
benim bilgisayarımda Örnek 2 (Yöntem 1) , Örnek 2 ve Örnek 2 (Yöntem 2) ye göre
50 kat daha yavaş çalışmakta.(Bu testte farkı net görebilmek için elimizdekiTutar
değişkeni 7000000.0 olarak değiştirilmiştir. ) Bu durumda performans ihtiyacı
ön planda ise BigDecimal'a alternatif bir yöntem olarak kuruş hesabı üzerinden
giderek int ya da long kullanıp, decimal point yönetimini kendimiz yapabiliriz.
int ya da long kullanmak:
Örnek 2 'yi bu yöntemle tekrar
implement edelim.
int satinAlinanUrunSayisi = 0;
//0.7 tl 70 kuruş
for (int urunTutar = 70;
elimizdekiTutar >= urunTutar;) {
elimizdekiTutar -= urunTutar;
satinAlinanUrunSayisi++;
}
System.out.println("elimizdekiTutar(kuruş cinsinden)="+elimizdekiTutar);
System.out.println("satinAlinanUrunSayisi="+satinAlinanUrunSayisi);
Bu
yöntemle tutarların büyüklüğüne göre int(9-10 haneye kadar ) ya da daha büyük
tutarlar için long(18-19 haneye kadar) kullanarak kuruş hesabı yapıyoruz. Daha
fazla hane gerektiğinde BigDecimal kullanmak gerekmektedir.
Bu
teknikte BigDecimal'ın kullanımında olduğu gibi roundlamaya ihtiyaç
duymuyoruz.Ancak elde edilen sonuçta decimal point'i kullanmak gerektiğinde
decimal point yönetimini kendimiz yapmalıyız.
Yuvarlama Teknikleri:
BigDecimal kullanarak istediğimiz decimal place'de yuvarlama
yapabildiğimizden bahsetmiştik. BigDecimal ile ilgili olarak daha önce
bahsettiğimiz performans problemi yuvarlamada da karşımıza çıkmakta.
BigDecimal'ın round metodu yerine iki ayrı yöntem kullanabiliriz.
1- BigDecimal.setScale() yöntemi:
Örnek 2 (Yöntem 2) örneğinde kullandığımız şekilde BigDecimal'ın setScale metodunu kullanarak istenen seviyede yuvarlama yapabiliriz.Daha önce söylediğimiz gibi , BigDecimale'a parametre olarak String geçmeli ve setScale metodunda istediğimiz decimal place ve round mod'u vermeliyiz.
Double d = some double;
BigDecimal bd = new BigDecimal(bd.toString());
bd = bd.setScale(decimalPlace,
BigDecimal.ROUND_HALF_EVEN);
d = bd.doubleValue();
2- Math.round() yöntemi:
int
satinAlinanUrunSayisi = 0;
final double DECIMAL_PLACE_CARPANI = 100; // decimal place 2 ise 100, 3 ise // 1000 vb.
for (double urunTutar = 0.7;
elimizdekiTutar >= urunTutar;)
{
elimizdekiTutar = round(elimizdekiTutar
-= urunTutar);
satinAlinanUrunSayisi++;
}
System.out.println("elimizdekiTutar="+elimizdekiTutar);
System.out.println("satinAlinanUrunSayisi="+satinAlinanUrunSayisi);
double round(Double d)
{
d =
Math.round(d * DECIMAL_PLACE_CARPANI ) / DECIMAL_PLACE_CARPANI ;
return d;
}
Bu
yöntemle yuvarlama yaptığımızda benim bilgisayarımda 7000000 toplam tutar ile
yapılan testlerde BigDecimal round
metoduna göre yaklaşık 30 kat daha hızlı sonuç alabiliyoruz.
Ancak
burada BigDecimal'ın yuvarlama tekniklerini kullanamıyoruz ve alt ve üst sınıra
eşit uzaklıkta olan 0.685 gibi sayılar her durumda ROUND_UP gibi davranıyor.
3- Cast yöntemi:
double elimizdekiTutar = 7.0;
int
satinAlinanUrunSayisi = 0;
final double DECIMAL_PLACE_CARPANI = 100; // decimal place 2 ise 100, 3 ise // 1000 vb.
for (double urunTutar = 0.7;
elimizdekiTutar >= urunTutar;)
{
elimizdekiTutar = round(elimizdekiTutar
-= urunTutar, DECIMAL_PLACE_CARPANI);
satinAlinanUrunSayisi++;
}
System.out.println("elimizdekiTutar="+elimizdekiTutar);
System.out.println("satinAlinanUrunSayisi="+satinAlinanUrunSayisi);
long l =(long) (d < 0 ? d *
decimalPlaceCarpani - 0.5 : d * decimalPlaceCarpani + 0.5);
return l / decimalPlaceCarpani;
}
Bu
yöntemde temel prensip yine decimalPlaceCarpani değişkeni ile istediğimiz
decimal place'i elde etmek ve sonuca 0.5 ekleyerek alt ve üst sınıra eşit uzaklıkta olan durumunda yuvarlama işleminin ROUND_UP
gibi davranmasını sağlamak, alt sınıra yakın sayılarına alt'a, üst sınıra yakın sayıların ise üst'e yuvarlanmasını sağlamak.
Bu
yöntemle yuvarlama yaptığımızda benim bilgisayarımda 7000000 toplam tutar ile
yapılan testlerde BigDecimal round
metoduna göre yaklaşık 60 kat, Math.round() yöntemine göre 2 kat daha hızlı sonuç
alabiliyoruz.
Ancak
yine burada BigDecimal'ın yuvarlama tekniklerini kullanamıyoruz ve alt ve
sınıra eşit olan 0.685 gibi sayılar her durumda ROUND_UP gibi davranıyor.
Burada istersek kendi implementasyonumuzu geliştirerek BigDecimal'ın ROUND_HALF_EVEN metoduna benzer bir metot geliştirebiliriz.
Burada istersek kendi implementasyonumuzu geliştirerek BigDecimal'ın ROUND_HALF_EVEN metoduna benzer bir metot geliştirebiliriz.
Float ve Double Kullanımı Üzerine Bir Not: float ve double veri tipleri ile ilgili dikkat edilmesi
gereken bir diğer nokta, bu tipleri hashcode imaplementasyonunda kullanmak
istediğimizde NaN, Infinity gibi değerler üzerinde işlem yapılamayacağından double ve float değerlerini Double.doubleToLongBits()
ve Float.floatToIntBits() metotları ile, gösterilmiş
oldukları bit dizisinin karşılık geldiği long(double için 64 bit long) ve int(float
için 32 bit int) değerlerine çevirerek işleme tabi tutmak gerekmektedir.
Sonuç:
Java'da kesirli sayılarla işlem
yaparken finansal ya da tam sonuç gerektiren işlemlerde float ya da double veri tiplerini
doğrudan kullanmak hatalı sonuçlar doğurmaktadır.Bunun sebebi ikilik sistemde
devirli olan kesirli sayılardır.Çözüm için BigDecimal'ın String parametreli constructor'ını
ve ekleme, çıkarma gibi işlemler için BigDecimal'da tanımlı olan metotlar
kullanılabilir.Yine yuvarlama için de BigDecimal'ın setScale metodu yukarı, aşağı yuvarlama ya da
toplam hatayı azaltacak şekilde ROUND_HALF_EVEN modu ile yuvarlamaya imkan sağlamaktadır.Ancak
BigDecimal'ın kullanımı primitive tiplerin kullanımına nazaran daha karmaşıktır
ve daha önemlisi performans açısından soruna yol açabilmektedir. Önceliğimizin
performans olduğu durumlarda BigDecimal yerine int veya long kullanarak kuruş
hesabı yapıp, decimal point'i hesaplama
sonunda ayarlayabiliriz.Ancak long ile tutabileceğimiz 18-19 haneden fazlasında
yine BigDecimal kullanmamız gerekmektedir.Yine roundlama için BigDecimal'ın setScale
metodu yerine Math.round() ya da daha performanslı olan Cast yöntemlemlerinden
birini kullanabiliriz.
Scale olarak ihtiyacımızı doğru
belirleyip, hesaplamalarımız sonunda maximum oluşabilecek scale'e göre
tasarımımızı yaparsak yuvarlamadan dolayı kayıp yaşama şansımızı azaltmış oluruz.Ancak bu durumda dahi kesirli sayıların tutulma biçiminden dolayı roundlama gerekmektedir ve BigDecimal ROUND_HALF_EVEN toplam hatayı en aza indirmektedir.Uygulamamızın roundlama işleminde kullandığımız mod'u tüm hesaplamalarda aynı şekilde uygulamazsak farklı zamanlarda farklı modüllerden farklı sonuçlar alabiliriz.
Ayrıca eğer hiç roundlama yapmazsak toplamda elde ettiğimiz sayı, örneğin 2098.81999 olduğunda, veritabanına doğrudan insert sonucunda ve veritabanında scale 2 olduğunda sayı tabloda 2098.81 olarak tutulacaktır.Halbuki bu sayı yuvarlama sonunda 2098.82 olmalı idi.Aynı şekilde veritabanında sum() işlemi ile scale 2 olan kalemlerin toplamıyla elde edilen sayıya , java'da bu kalemleri topladığımızda en yakın ROUND_HALF_EVEN modu ile ulaşabiliriz.
Ayrıca eğer hiç roundlama yapmazsak toplamda elde ettiğimiz sayı, örneğin 2098.81999 olduğunda, veritabanına doğrudan insert sonucunda ve veritabanında scale 2 olduğunda sayı tabloda 2098.81 olarak tutulacaktır.Halbuki bu sayı yuvarlama sonunda 2098.82 olmalı idi.Aynı şekilde veritabanında sum() işlemi ile scale 2 olan kalemlerin toplamıyla elde edilen sayıya , java'da bu kalemleri topladığımızda en yakın ROUND_HALF_EVEN modu ile ulaşabiliriz.
Referanslar ve Kaynaklar:
Joshua
Bloch. Effective Java - Programming Language Guide
http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
http://mindprod.com/jgloss/floatingpoint.html
Kaynak kodu buradan indirebilirsiniz.
No comments:
Post a Comment