Para birimini temsil etmek için neden Double veya Float kullanmıyorsunuz?


939

Her zaman asla parayla doubleveya floattürlerle para temsil etmem söylendi ve bu sefer size soruyorum: neden?

Eminim çok iyi bir sebep vardır, ne olduğunu bilmiyorum.


4
Şu SO sorusuna bakın: Yuvarlama Hataları?
Jeff Ogata

80
Sadece açık olmak gerekirse, sadece para birimi değil, doğruluk gerektiren hiçbir şey için kullanılmamalıdır.
Jeff

152
Onlar gerektiren tüm durumlar için kullanılmamalıdır doğruluk . Ancak çiftin 53 önemli biti (~ 16 ondalık basamak) genellikle doğruluk gerektiren şeyler için yeterlidir .
dan04

21
@jeff Yorumunuz, ikili kayan noktanın neyin iyi olduğunu ve neyin iyi olmadığını yanlış anlar. Aşağıdaki zneak tarafından verilen cevabı okuyun ve lütfen yanıltıcı yorumunuzu silin.
Pascal Cuoq

Yanıtlar:


985

Çünkü şamandıralar ve çiftler, para için kullandığımız temel 10 katını doğru bir şekilde temsil edemez. Bu sorun yalnızca Java için değil, temel 2 kayan nokta türlerini kullanan herhangi bir programlama dili için de geçerlidir.

Temel 10'da, 10.25'i 1025 * 10 -2 ( 10'luk bir tam sayı çarpı) olarak yazabilirsiniz . IEEE-754 kayan nokta sayıları farklıdır, ancak bunları düşünmenin çok basit bir yolu, bunun yerine iki güçle çarpmaktır. Örneğin , aynı zamanda 10.25'e eşit olan 164 * 2 -4'e (tamsayı çarpı iki güç) bakabilirsiniz. Sayılar bellekte bu şekilde temsil edilmez, ancak matematik sonuçları aynıdır.

Taban 10'da bile, bu gösterim çoğu basit kesri doğru bir şekilde temsil edemez. Örneğin, 1/3'ü temsil edemezsiniz: ondalık gösterim tekrarlanır (0.3333 ...), bu nedenle 1/3 elde etmek için 10'luk bir güçle çarpabileceğiniz sonlu bir tamsayı yoktur. Sen 333333333 * 10 gibi 3 yılların uzun dizisinin ve küçük bir üs üzerinde yerleşmek olabilir -10 , ancak doğru değildir: 3 ile bu sen çarpın, sen 1 almazsınız eğer.

Bununla birlikte, para sayma amacıyla, en azından parası ABD doları büyüklüğünde bir değere sahip olan ülkeler için, genellikle tek ihtiyacınız olan 10 -2 katlarını saklayabilmektir , bu yüzden gerçekten önemli değil 1/3 temsil edilemez.

Şamandıralar ve çiftler ile ilgili sorun , para benzeri sayıların büyük çoğunluğunun 2 tamsayı çarpı olarak tam bir temsili olmamasıdır. tam IEEE-754 ikili kayan noktalı sayı olarak temsil edilebilecekleri için 0, 0,25, 0,5, 0,75 ve 1'dir. Diğerleri küçük bir miktar kapalıdır. 0.333333 örneğine benzer bir şekilde, kayan nokta değerini 0.1 olarak alırsanız ve 10 ile çarparsanız, 1 elde edemezsiniz.

Parayı bir olarak temsil etmek doubleveya floatyazılım küçük hataları yuvarlarken ilk başta muhtemelen iyi görünecektir, ancak hatalı sayılar üzerinde daha fazla toplama, çıkarma, çarpma ve bölme gerçekleştirdiğinizde, hatalar birleşecek ve görünür olan değerlerle sonuçlanacaksınız kesin değil. Bu, yüzer ve çiftleri, temel 10 güçlerin katları için mükemmel doğruluk gerektiren para ile başa çıkmak için yetersiz kılar.

Hemen hemen her dilde çalışan bir çözüm, bunun yerine tamsayıları kullanmak ve sentleri saymaktır. Örneğin, 1025 $ 10.25 olurdu. Birçok dilde parayla başa çıkmak için yerleşik türler vardır. Diğerlerinin yanı sıra, Java BigDecimalsınıfı ve C # decimaltürü vardır.


3
@Fran Yuvarlama hataları alacaksınız ve bazı durumlarda büyük miktarlarda para biriminin kullanıldığı durumlarda, faiz hesaplamaları tamamen kapalı olabilir
linuxuser27

5
... çoğu temel 10 kesir, yani. Örneğin, 0.1 tam bir ikili kayan nokta gösterimine sahip değildir. Yani, 1.0 / 10 * 101.0 ile aynı olmayabilir.
Chris Jester-Young

6
Sanırım Fran komik olmaya çalışıyordu. Her neyse, zneak'ın cevabı gördüğüm en iyisi, Bloch'un klasik versiyonundan bile daha iyi.
Isaac Rabinovitch

5
Tabii ki hassasiyeti biliyorsanız, her zaman sonucu yuvarlayabilir ve böylece tüm sorunu önleyebilirsiniz. Bu, BigDecimal kullanmaktan çok daha hızlı ve basittir. Başka bir alternatif int veya long sabit hassasiyet kullanmaktır.
Peter Lawrey

2
@zneak, rasyonel sayıların gerçek sayıların bir alt kümesi olduğunu biliyorsunuz , değil mi? IEEE-754 gerçek sayılar olan gerçek sayılar. Onlar sadece ne de rasyonel.
Tim Seguine

314

Bloch, J., Etkili Java, 2. baskı, Madde 48:

floatVe doublebunun 0.1 (ya da on başka negatif güç) bir şekilde temsil etmek için mümkün değildir, çünkü tip özellikle para hesaplamalar için kötü uygundur floatya da doubletam olarak.

Örneğin, 1.03 $ 'ınız olduğunu ve 42c harcadığınızı varsayalım. Ne kadar paranız kaldı?

System.out.println(1.03 - .42);

yazdırır 0.6100000000000001.

Bu sorunu çözmek için doğru yol kullanmaktır BigDecimal, intya long parasal hesaplamalar için.

BigDecimalBazı uyarılar olsa da (lütfen şu anda kabul edilen cevaba bakınız).


6
Parasal hesaplamalar için int veya long kullanma önerisiyle biraz kafam karıştı. 1.03'ü int veya long olarak nasıl temsil edersiniz? Ben denedim "uzun a = 1.04;" ve "uzun a = 104/100;" boşuna.
Peter

49
@Peter, long a = 104dolar yerine sent kullanır ve sayarsınız .
Mart'ta zneak

@zneak Bileşik faiz veya benzeri bir yüzdenin ne zaman uygulanması gerektiğine ne dersiniz?
trusktr

3
@trusktr, platformunuzun ondalık türüyle giderdim. Java'da bu kadar BigDecimal.
Mart'ta zneak

13
@maaartinus ... ve böyle şeyler için double kullanmanın hataya açık olduğunu düşünmüyor musunuz? Ben şamandıra yuvarlama sorunu gerçek sistemlerini vurmak gördüm sert . Bankacılıkta bile. Lütfen bunu önermeyin ya da bunu yaparsanız, ayrı bir cevap olarak sağlayın (böylece aşağıya doğru değerlendirebiliriz: P)
eis

75

Ne doğruluk meselesi ne de kesinlik meselesi. Temel 2 yerine hesaplamalar için taban 10'u kullanan insanların beklentilerini karşılama meselesidir. Örneğin, finansal hesaplamalar için iki katını kullanmak, matematiksel anlamda "yanlış" cevaplar üretmez, ancak finansal anlamda beklenen şey değil.

Sonuçlarınızı çıktıdan önceki son dakikada tamamlasanız bile, beklentileri karşılamayan çiftleri kullanarak ara sıra sonuç alabilirsiniz.

Bir hesap makinesi kullanarak veya sonuçları elle hesaplayarak, tam olarak 1.40 * 165 = 231. Ancak, dahili olarak çiftler kullanarak, derleyici / işletim sistemi ortamımda, 230.99999'a yakın bir ikili sayı olarak saklanır ... böylece sayıyı kısaltırsanız, 231 yerine 230 alırsınız. Kesmek yerine yuvarlamanın 231 istenen sonucu vermiştir. Bu doğrudur, ancak yuvarlama her zaman kesmeyi içerir. Hangi yuvarlama tekniğini kullanırsanız kullanın, yuvarlanmasını beklediğinizde yuvarlanacak olan bunun gibi hala sınır koşulları vardır. Sıradan testler veya gözlem yoluyla sıklıkla bulunamayacak kadar nadirdirler. Beklendiği gibi davranmayan sonuçları gösteren örnekleri aramak için bazı kodlar yazmanız gerekebilir.

Bir şeyi en yakın kuruşa yuvarlamak istediğinizi varsayın. Böylece nihai sonucunuzu alın, 100 ile çarpın, 0,5 ekleyin, kısaltın ve ardından kuruşlara geri dönmek için sonucu 100'e bölün. Kaydettiğiniz dahili numara 3.465 yerine 3.46499999 .... ise, numarayı en yakın kuruşa yuvarladığınızda 3.47 yerine 3.46 elde edersiniz. Ancak, temel 10 hesaplamalarınız cevabın tam olarak 3.465 olması gerektiğini belirtmiş olabilir, bu da 3.46'ya değil, 3.47'ye yuvarlanmalıdır. Bu tür şeyler, finansal hesaplamalar için iki katını kullandığınızda gerçek hayatta bazen meydana gelir. Nadirdir, bu nedenle genellikle bir sorun olarak fark edilmez, ancak olur.

Dahili hesaplarınız için iki kat yerine taban 10 kullanırsanız, yanıtlar her zaman tam olarak insanlar tarafından beklenen, kodunuzda başka bir hata olmadığı varsayılır.


2
İlgili, ilginç: Chrome js konsolumda: Math.round (.4999999999999999): 0 Math.round (.49999999999999999): 1
Curtis Yallop

16
Bu cevap yanıltıcı. Tam olarak 231 dışındaki 1.40 * 165 = 231. Herhangi bir sayıda olan bir matematiksel anlamda (ve diğer tüm duyular) yanlış.
Karu

2
@Karu Bence Randy bu yüzden yüzerlerin kötü olduğunu söylüyor ... Chrome JS konsolum sonuç olarak 230.99999999999997 gösteriyor. Yani bir cevap yapılan nokta olan yanlış.
trusktr

6
@Karu: Cevabın cevabı matematiksel olarak yanlış değil. Sadece bir soru cevaplanan 2 soru vardır ki bu sorulan soru değildir. Derleyicinizin cevapladığı soru 1.39999999 * 164.99999999 ve bunun gibi matematiksel olarak doğru 230.99999'a eşittir .... Açıkçası ilk etapta sorulan soru bu değildir ....
markus

2
@CurtisYallop çünkü 0,499999999999999999 değerine çift kapanış değeri 0,5 olur Neden Math.round(0.49999999999999994)1 döndürür?
phuclv

53

Bu yanıtların bazıları beni rahatsız ediyor. Finansal hesaplamalarda çiftlerin ve yüzenlerin bir yeri olduğunu düşünüyorum. Kesinlikle, kesirli olmayan parasal tutarları eklerken ve çıkarırken, tamsayı sınıfları veya BigDecimal sınıfları kullanılırken hassasiyet kaybı olmaz. Ancak daha karmaşık işlemler gerçekleştirirken, sayıları nasıl saklarsanız saklayın, genellikle birkaç veya çok sayıda ondalık basamaktan çıkan sonuçlar elde edersiniz. Sorun sonucu nasıl sunacağınızdır.

Sonucunuz yuvarlama ve yuvarlama arasındaki sınırda ise ve son kuruş gerçekten önemliyse, muhtemelen izleyiciye cevabın neredeyse ortada olduğunu söylemelisiniz - daha fazla ondalık basamak göstererek.

Çiftler ve daha çok şamandıralar ile ilgili problem, büyük sayıları ve küçük sayıları birleştirmek için kullanıldıklarıdır. Java’da,

System.out.println(1000000.0f + 1.2f - 1000000.0f);

sonuç

1.1875

16
BU!!!! Bu İLGİLİ GERÇEK bulmak için tüm cevapları arıyordum !!! Normal hesaplamalarda hiç kimse bir kuruşluk bir orana sahip değilseniz umursamaz, ancak burada yüksek sayılarla kolayca işlem başına bazı dolarlar kaybolur!
Falco

22
Ve şimdi birisinin 1 Milyon dolarlık günlük% 0,01 gelir elde ettiğini hayal edin - her gün hiçbir şey elde edemez - ve bir yıl sonra 1000 Dolar kazanmadı, BU ÖNEMLİ OLACAK
Falco

6
Sorun doğruluk değil, şamandıra size yanlış olduğunu söylemiyor. Bir tamsayı yalnızca 10 basamağa kadar çıkabilir, bir şamandıra yanlış hale gelmeden 6'ya kadar tutabilir (buna göre kestiğinizde). Bir tamsayı taşma olur ve java gibi bir dil sizi uyarır veya izin vermez. Bir çift kullandığınızda, birçok kullanım durumu için yeterli olan 16 basamağa kadar gidebilirsiniz.
sigi

39

Şamandıralar ve çiftler yaklaşık değerlerdir. Bir BigDecimal oluşturursanız ve bir şamandırayı kurucuya geçirirseniz şamandıranın gerçekte neye eşit olduğunu görürsünüz:

groovy:000> new BigDecimal(1.0F)
===> 1
groovy:000> new BigDecimal(1.01F)
===> 1.0099999904632568359375

bu muhtemelen 1,01 doları temsil etmek istemezsiniz.

Sorun, IEEE spesifikasyonunun tüm kesirleri tam olarak temsil etmenin bir yolu olmamasıdır, bazıları da kesirleri tekrarlamakla sonuçlanır, böylece yaklaşık hatalarla sonuçlanırsınız. Muhasebeciler şeyleri tam olarak kuruşa çıkmayı sevdiğinden ve müşteriler faturalarını öderlerse sinirlenirler ve ödeme işlendikten sonra 0,01 borçludurlar ve ücret alırlar veya hesaplarını kapatamazlar, kullanmak daha iyidir ondalık (C # olarak) veya Java'daki java.math.BigDecimal gibi tam türler.

Yuvarlarsanız hata kontrol edilemez: Peter Lawrey'in bu makalesine bakın . İlk etapta yuvarlamak zorunda değilsiniz. Parayı işleyen çoğu uygulama çok fazla matematik gerektirmez, işlemler bir şeyler eklemekten veya farklı kovalara miktar tahsis etmekten oluşur. Kayan noktaya ve yuvarlamaya başlamak işleri karmaşıklaştırır.


5
float, doubleve kesin değerleri BigDecimaltemsil eder . Koddan nesneye dönüştürme, diğer işlemlerin yanı sıra tam değildir. Türlerin kendileri tam değildir.
chux - Monica

1
@chux: bunu tekrar okurken, ifadelerimin geliştirilebileceğine dair bir fikriniz olduğunu düşünüyorum. Bunu düzenleyip yeniden yazacağım.
Nathan Hughes

28

Düşürülme riskiyle karşı karşıya kalacağım, ancak kayan nokta sayılarının para birimi hesaplamaları için uygunluğunun abartıldığını düşünüyorum. Cent yuvarlamayı doğru yaptığınızdan ve zneak tarafından açıklanan ikili ondalık gösterim uyumsuzluğuna karşı koymak için çalışmak için yeterince önemli basamaklara sahip olduğunuzdan emin olun, sorun olmayacaktır.

Excel'de para birimi ile hesaplanan insanlar her zaman çift duyarlıklı şamandıralar kullanmıştır (Excel'de para birimi türü yoktur) ve henüz yuvarlama hataları hakkında şikayet eden birini görmedim.

Tabii ki, mantık içinde kalmak zorundasınız; Örneğin, basit bir web mağazası muhtemelen çift duyarlıklı kayan reklamlarla ilgili herhangi bir sorun yaşamaz, ancak örneğin muhasebe veya çok sayıda (sınırsız) sayı eklemeyi gerektiren başka bir şey yaparsanız, kayan nokta numaralarına on ayağı dokunmak istemezsiniz. kutup.


3
Bu aslında oldukça iyi bir cevap. Çoğu durumda bunları kullanmak mükemmeldir.
Vahid Amiri

2
Çoğu yatırım bankasının çoğu C ++ programında olduğu gibi iki katını kullandığına dikkat edilmelidir. Bazıları uzun süre kullanılır, ancak kendi ölçek ölçeği sorunu vardır.
Peter Lawrey

20

Kayan nokta türünün yalnızca yaklaşık ondalık verileri temsil edebileceği doğru olsa da, bir kişi sayıları sunmadan önce gerekli hassasiyete yuvarlarsa, doğru sonucu elde eder. Genelde.

Genellikle çift tip 16 rakamdan daha az bir hassasiyete sahip olduğundan. Daha iyi bir hassasiyete ihtiyacınız varsa, bu uygun bir tip değildir. Ayrıca yaklaşımlar birikebilir.

Sabit nokta aritmetiği kullansanız bile, sayıları yuvarlamak zorunda olmanız gerektiği söylenmelidir, periyodik ondalık sayılar elde ederseniz BigInteger ve BigDecimal'ın hata vermesi değil. Yani burada da bir yaklaşım var.

Örneğin, geçmişte finansal hesaplamalar için kullanılan COBOL, maksimum 18 rakam hassasiyetine sahiptir. Dolayısıyla genellikle örtük bir yuvarlama vardır.

Sonuç olarak, bence çift yaklaşık 16 basamaklı hassasiyeti için uygun değildir, ki bu yaklaşık değil, yetersiz olabilir.

Sonraki programın aşağıdaki çıktısını göz önünde bulundurun. Bu, çift yuvarlamadan sonra hassasiyet 16'ya kadar BigDecimal ile aynı sonucu verdiğini gösterir.

Precision 14
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.000051110111115611
Double                        : 56789.012345 / 1111111111 = 0.000051110111115611

Precision 15
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.0000511101111156110
Double                        : 56789.012345 / 1111111111 = 0.0000511101111156110

Precision 16
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.00005111011111561101
Double                        : 56789.012345 / 1111111111 = 0.00005111011111561101

Precision 17
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.000051110111115611011
Double                        : 56789.012345 / 1111111111 = 0.000051110111115611013

Precision 18
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.0000511101111156110111
Double                        : 56789.012345 / 1111111111 = 0.0000511101111156110125

Precision 19
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.00005111011111561101111
Double                        : 56789.012345 / 1111111111 = 0.00005111011111561101252

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.MathContext;

public class Exercise {
    public static void main(String[] args) throws IllegalArgumentException,
            SecurityException, IllegalAccessException,
            InvocationTargetException, NoSuchMethodException {
        String amount = "56789.012345";
        String quantity = "1111111111";
        int [] precisions = new int [] {14, 15, 16, 17, 18, 19};
        for (int i = 0; i < precisions.length; i++) {
            int precision = precisions[i];
            System.out.println(String.format("Precision %d", precision));
            System.out.println("------------------------------------------------------");
            execute("BigDecimalNoRound", amount, quantity, precision);
            execute("DoubleNoRound", amount, quantity, precision);
            execute("BigDecimal", amount, quantity, precision);
            execute("Double", amount, quantity, precision);
            System.out.println();
        }
    }

    private static void execute(String test, String amount, String quantity,
            int precision) throws IllegalArgumentException, SecurityException,
            IllegalAccessException, InvocationTargetException,
            NoSuchMethodException {
        Method impl = Exercise.class.getMethod("divideUsing" + test, String.class,
                String.class, int.class);
        String price;
        try {
            price = (String) impl.invoke(null, amount, quantity, precision);
        } catch (InvocationTargetException e) {
            price = e.getTargetException().getMessage();
        }
        System.out.println(String.format("%-30s: %s / %s = %s", test, amount,
                quantity, price));
    }

    public static String divideUsingDoubleNoRound(String amount,
            String quantity, int precision) {
        // acceptance
        double amount0 = Double.parseDouble(amount);
        double quantity0 = Double.parseDouble(quantity);

        //calculation
        double price0 = amount0 / quantity0;

        // presentation
        String price = Double.toString(price0);
        return price;
    }

    public static String divideUsingDouble(String amount, String quantity,
            int precision) {
        // acceptance
        double amount0 = Double.parseDouble(amount);
        double quantity0 = Double.parseDouble(quantity);

        //calculation
        double price0 = amount0 / quantity0;

        // presentation
        MathContext precision0 = new MathContext(precision);
        String price = new BigDecimal(price0, precision0)
                .toString();
        return price;
    }

    public static String divideUsingBigDecimal(String amount, String quantity,
            int precision) {
        // acceptance
        BigDecimal amount0 = new BigDecimal(amount);
        BigDecimal quantity0 = new BigDecimal(quantity);
        MathContext precision0 = new MathContext(precision);

        //calculation
        BigDecimal price0 = amount0.divide(quantity0, precision0);

        // presentation
        String price = price0.toString();
        return price;
    }

    public static String divideUsingBigDecimalNoRound(String amount, String quantity,
            int precision) {
        // acceptance
        BigDecimal amount0 = new BigDecimal(amount);
        BigDecimal quantity0 = new BigDecimal(quantity);

        //calculation
        BigDecimal price0 = amount0.divide(quantity0);

        // presentation
        String price = price0.toString();
        return price;
    }
}

15

Kayan nokta sayısının sonucu kesin değildir, bu da onları kesin sonuç gerektiren ve yaklaşık olarak tahmin edilmeyen herhangi bir finansal hesaplama için uygun hale getirmez. float ve double mühendislik ve bilimsel hesaplama için tasarlanmıştır ve çoğu zaman kesin sonuç vermez ayrıca kayan nokta hesaplamasının sonucu JVM'den JVM'ye kadar değişebilir. Aşağıda, para değerini temsil etmek için kullanılan BigDecimal ve double ilkel örneğine bakın, kayan nokta hesaplamasının kesin olmayabileceği ve finansal hesaplamalar için BigDecimal kullanması gerektiği açıktır.

    // floating point calculation
    final double amount1 = 2.0;
    final double amount2 = 1.1;
    System.out.println("difference between 2.0 and 1.1 using double is: " + (amount1 - amount2));

    // Use BigDecimal for financial calculation
    final BigDecimal amount3 = new BigDecimal("2.0");
    final BigDecimal amount4 = new BigDecimal("1.1");
    System.out.println("difference between 2.0 and 1.1 using BigDecimal is: " + (amount3.subtract(amount4)));

Çıktı:

difference between 2.0 and 1.1 using double is: 0.8999999999999999
difference between 2.0 and 1.1 using BigDecimal is: 0.9

3
Önemsiz toplama / çıkarma ve tamsayı mutplicaiton dışında bir şey deneyelim, Kod% 7 kredinin aylık oranını hesapladıysa, her iki türün de tam bir değer sağlayamaması ve en yakın 0.01'e yuvarlanması gerekir. En düşük para birimine yuvarlama, para hesaplamalarının bir parçasıdır, Ondalık türlerin kullanılması toplama / çıkarma ile ihtiyaçtan kaçınır - ancak başka bir şey değildir.
chux - Monica'yı eski durumuna getir

@ chux-ReinstateMonica: Eğer faiz aylık olarak birleştirilecekse, günlük bakiyeyi toplayarak faizi her ay hesaplayın, bunu 7 (faiz oranı) ile çarpın ve en yakın kuruşa yuvarlayarak gün sayısına bölün. yıl. En son adımda ayda bir kez dışında hiçbir yerde yuvarlama yapılmaz.
supercat

@supercat Benim yorumum , en küçük para biriminin ikili FP'sini ya da ondalık FP'nin her ikisini de benzer yuvarlama sorunlarına neden olduğunu vurgulamaktadır. Bir taban 2 veya taban 10 FP kullanmak, senaryoda her iki şekilde de bir avantaj sağlamaz.
chux - Monica'yı eski haline getirin

@ chux-ReinstateMonica: Yukarıdaki senaryoda, eğer matematik ilginin tam olarak yarım sente eşit olması gerektiği sonucuna varırsa, doğru bir finansal program tam olarak belirtilen şekilde yuvarlanmalıdır. Kayan nokta hesaplamaları örneğin 1.23499941 dolarlık bir faiz değeri veriyorsa, ancak yuvarlamadan önce matematiksel olarak kesin değerin 1.235 $ olması ve yuvarlamanın "en yakın çift" olarak belirtilmesi durumunda, bu tür kayan nokta hesaplamalarının kullanılması sonuca neden olmaz 0,000059 $ 'a kapalı olmak yerine, muhasebe amaçları için Just Plain Wrong olan 0,01 $' a kadar.
supercat

@supercat İkili FP'yi sent olarak kullanmak double, ondalık FP değerinde olduğu gibi 0,5 sent'i hesaplamakta sorun yaşamayacaktı. Kayan nokta hesaplamaları ikili FP veya ondalık FP aracılığıyla örneğin 123.499941 ¢ faiz değeri verirse, çift yuvarlama sorunu aynıdır - her iki durumda da avantaj yoktur. Öncülünüz matematiksel olarak kesin değeri varsayar ve ondalık FP aynıdır - ondalık FP bile bir şey garanti etmez. 0.5 / 7.0 * 7.0 ikili ve deikmal FP için bir problemdir. IAC, C'nin bir sonraki versiyonunun ondalık FP sağlamasını beklediğimden çoğu tartışmalı olacak.
chux - Monica'yı eski haline getirin

11

Daha önce de belirtildiği gibi "Parayı bir çift veya kayan nokta olarak temsil etmek, yazılım küçük hataları yuvarlarken ilk başta muhtemelen iyi görünecektir, ancak hatalı sayılar üzerinde daha fazla toplama, çıkarma, çarpma ve bölme gerçekleştirdikçe, daha fazla hassasiyet kaybedeceksiniz Bu, yüzer ve çiftleri, temel 10 güçlerin katları için mükemmel doğruluk gerektiren para ile başa çıkmak için yetersiz kılar. "

Sonunda Java'nın Para ve Para ile çalışmak için standart bir yolu var!

JSR 354: Para ve Para Birimi API'sı

JSR 354, Para ve Para Birimi ile kapsamlı hesaplamaları temsil etmek, taşımak ve gerçekleştirmek için bir API sağlar. Bu bağlantıdan indirebilirsiniz:

JSR 354: Para ve Para Birimi API'sı İndir

Şartname aşağıdaki şeylerden oluşur:

  1. Parasal tutarlar ve para birimlerini işlemek için bir API
  2. Değiştirilebilir uygulamaları destekleyen API'ler
  3. Uygulama sınıflarının örneklerini oluşturan fabrikalar
  4. Parasal tutarların hesaplanması, dönüştürülmesi ve biçimlendirilmesi için işlevsellik
  5. Java 9'a eklenmesi planlanan Para ve Para Birimleri ile çalışmak için Java API.
  6. Tüm spesifikasyon sınıfları ve arayüzler javax.money. * Paketinde bulunur.

JSR 354 Örneği: Para ve Para Birimi API'sı:

Bir MonetaryAmount oluşturma ve konsola yazdırma örneği şuna benzer:

MonetaryAmountFactory<?> amountFactory = Monetary.getDefaultAmountFactory();
MonetaryAmount monetaryAmount = amountFactory.setCurrency(Monetary.getCurrency("EUR")).setNumber(12345.67).create();
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault());
System.out.println(format.format(monetaryAmount));

Referans uygulama API'sini kullanırken, gerekli kod çok daha basittir:

MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault());
System.out.println(format.format(monetaryAmount));

API ayrıca MonetaryAmounts ile hesaplamaları da destekler:

MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmount otherMonetaryAmount = monetaryAmount.divide(2).add(Money.of(5, "EUR"));

Para Birimi ve Parasal Miktar

// getting CurrencyUnits by locale
CurrencyUnit yen = MonetaryCurrencies.getCurrency(Locale.JAPAN);
CurrencyUnit canadianDollar = MonetaryCurrencies.getCurrency(Locale.CANADA);

MonetaryAmount, atanan para birimine, sayısal tutara, kesinliğine ve daha fazlasına erişmeyi sağlayan çeşitli yöntemlere sahiptir:

MonetaryAmount monetaryAmount = Money.of(123.45, euro);
CurrencyUnit currency = monetaryAmount.getCurrency();
NumberValue numberValue = monetaryAmount.getNumber();

int intValue = numberValue.intValue(); // 123
double doubleValue = numberValue.doubleValue(); // 123.45
long fractionDenominator = numberValue.getAmountFractionDenominator(); // 100
long fractionNumerator = numberValue.getAmountFractionNumerator(); // 45
int precision = numberValue.getPrecision(); // 5

// NumberValue extends java.lang.Number. 
// So we assign numberValue to a variable of type Number
Number number = numberValue;

MonetaryAmount'lar bir yuvarlama operatörü kullanılarak yuvarlanabilir:

CurrencyUnit usd = MonetaryCurrencies.getCurrency("USD");
MonetaryAmount dollars = Money.of(12.34567, usd);
MonetaryOperator roundingOperator = MonetaryRoundings.getRounding(usd);
MonetaryAmount roundedDollars = dollars.with(roundingOperator); // USD 12.35

MonetaryAmounts koleksiyonlarıyla çalışırken, filtreleme, sıralama ve gruplama için bazı güzel yardımcı yöntemler mevcuttur.

List<MonetaryAmount> amounts = new ArrayList<>();
amounts.add(Money.of(2, "EUR"));
amounts.add(Money.of(42, "USD"));
amounts.add(Money.of(7, "USD"));
amounts.add(Money.of(13.37, "JPY"));
amounts.add(Money.of(18, "USD"));

Özel Parasal Montaj işlemleri

// A monetary operator that returns 10% of the input MonetaryAmount
// Implemented using Java 8 Lambdas
MonetaryOperator tenPercentOperator = (MonetaryAmount amount) -> {
  BigDecimal baseAmount = amount.getNumber().numberValue(BigDecimal.class);
  BigDecimal tenPercent = baseAmount.multiply(new BigDecimal("0.1"));
  return Money.of(tenPercent, amount.getCurrency());
};

MonetaryAmount dollars = Money.of(12.34567, "USD");

// apply tenPercentOperator to MonetaryAmount
MonetaryAmount tenPercentDollars = dollars.with(tenPercentOperator); // USD 1.234567

Kaynaklar:

JSR 354 ile Java'da para ve para birimlerini işleme

Java 9 Para ve Para Birimi API'sini inceleme (JSR 354)

Ayrıca Bakınız: JSR 354 - Para Birimi ve Para


5

Hesaplamanız çeşitli adımlar içeriyorsa, keyfi hassas aritmetik% 100'ü kapsamaz.

Sonuçların mükemmel bir temsilini kullanmanın tek güvenilir yolu (Bölme işlemlerini son adıma toplu yapacak özel bir Kesir veri türü kullanın) ve yalnızca son adımda ondalık gösterime dönüştürün.

Keyfi kesinlik yardımcı olmaz çünkü her zaman çok fazla ondalık basamağa sahip sayılar olabilir veya gibi bazı sonuçlar olabilir 0.6666666. Böylece her adımda küçük hatalarınız olacaktır.

Bu hatalar eklenir, sonunda artık göz ardı edilmesi kolay olmayabilir. Buna Hata Yayılımı denir .


4

Çoğu cevap, kişinin para ve para birimi hesaplamaları için iki katını kullanmama nedenlerini vurgulamıştır. Onlara tamamen katılıyorum.

Bu, çiftlerin asla bu amaç için kullanılamayacağı anlamına gelmez.

Çok düşük gc gereksinimleri olan bir dizi proje üzerinde çalıştım ve BigDecimal nesnelerine sahip olmak bu yüke büyük bir katkıda bulundu.

Bu, akıllıca bir öneri getiren doğruluk ve kesinlik ile ilgili çifte temsil anlayışı ve deneyim eksikliğidir.

Hangi çift değer aralığının ele alındığına bağlı olarak yapılması gereken projenizin hassasiyet ve doğruluk gereksinimlerini karşılayabiliyorsanız, çalışmasını sağlayabilirsiniz.

Daha fazla fikir edinmek için guava'nın FuzzyCompare yöntemine başvurabilirsiniz. Parametre toleransı anahtardır. Bir menkul kıymet alım satım uygulaması için bu sorunu ele aldık ve farklı aralıklarda farklı sayısal değerler için hangi toleransların kullanılacağı konusunda kapsamlı bir araştırma yaptık.

Ayrıca, karma haritayı uygulama olarak çift anahtarları bir harita anahtarı olarak kullanmaya çalıştığınız durumlar olabilir. Çok risklidir çünkü Double.equals ve karma kodu örneğin "0,5" ve "0,6 - 0,1" değerleri büyük karışıklığa neden olur.


2

Bu soruya gönderilen cevapların çoğu IEEE ve kayan noktalı aritmetiği çevreleyen standartları tartışmaktadır.

Bilgisayar dışı bilim geçmişinden (fizik ve mühendislik) geliyor, problemlere farklı bir bakış açısıyla bakma eğilimindeyim. Benim için, matematiksel bir hesaplamada bir çift ya da şamandıra kullanmamamın nedeni, çok fazla bilgi kaybetmem.

Alternatifleri nelerdir? Çok var (ve daha pek çoğu farkında değilim!).

Java'daki BigDecimal, Java diline özgüdür. Apfloat, Java için bir başka keyfi hassas kütüphanedir.

C # 'daki ondalık veri türü, 28 önemli rakam için Microsoft'un .NET alternatifidir.

SciPy (Scientific Python) muhtemelen finansal hesaplamaları da yapabilir (denemedim, ama şüpheliyim).

GNU Çoklu Hassas Kütüphane (GMP) ve GNU MFPR Kütüphanesi, C ve C ++ için iki ücretsiz ve açık kaynak kaynaktır.

JavaScript (!) İçin sayısal hassas kütüphaneler de var ve bence finansal hesaplamaları yapabilen PHP.

Ayrıca birçok bilgisayar dili için özel (özellikle Fortran için) ve açık kaynaklı çözümler de vardır.

Eğitim alarak bilgisayar bilimcisi değilim. Ancak, Java BigDecimal veya C # ondalık eğilimi eğilimindedir. Listelediğim diğer çözümleri denemedim, ancak muhtemelen de çok iyi.

Benim için desteklediği yöntemler nedeniyle BigDecimal'ı seviyorum. C # 'ın ondalığı çok güzel, ama istediğim kadar onunla çalışma şansım olmadı. Boş zamanlarımda ilgimi çeken bilimsel hesaplamalar yapıyorum ve BigDecimal çok iyi çalışıyor gibi görünüyor çünkü kayan nokta sayılarımın hassasiyetini ayarlayabiliyorum. BigDecimal'ın dezavantajı? Özellikle bölme yöntemini kullanıyorsanız, bazen yavaş olabilir.

Hız için C, C ++ ve Fortran'daki ücretsiz ve tescilli kütüphanelere bakabilirsiniz.


1
SciPy / Numpy ile ilgili olarak, sabit hassasiyet (yani Python'un ondalık.Decimal ) desteklenmez ( docs.scipy.org/doc/numpy-dev/user/basics.types.html ). Bazı işlevler Ondalık ile düzgün çalışmaz (örneğin isnan). Pandalar Numpy'ye dayanır ve büyük bir nicel hedge fonu olan AQR'de başlatıldı. Yani mali hesaplamalar (bakkal muhasebe değil) ile ilgili cevabınız var.
comte

2

Önceki yanıtları eklemek için Joda-Money uygulama seçeneği de vardır , soruda ele alınan sorunla uğraşırken, BigDecimal'in yanı sıra Java'da . Java modül adı org.joda.money'dir.

Java SE 8 veya üstünü gerektirir ve bağımlılığı yoktur.

Daha kesin olmak gerekirse, derleme zamanı bağımlılığı vardır, ancak zorunlu değildir.

<dependency>
  <groupId>org.joda</groupId>
  <artifactId>joda-money</artifactId>
  <version>1.0.1</version>
</dependency>

Joda Money kullanımına örnekler:

  // create a monetary value
  Money money = Money.parse("USD 23.87");

  // add another amount with safe double conversion
  CurrencyUnit usd = CurrencyUnit.of("USD");
  money = money.plus(Money.of(usd, 12.43d));

  // subtracts an amount in dollars
  money = money.minusMajor(2);

  // multiplies by 3.5 with rounding
  money = money.multipliedBy(3.5d, RoundingMode.DOWN);

  // compare two amounts
  boolean bigAmount = money.isGreaterThan(dailyWage);

  // convert to GBP using a supplied rate
  BigDecimal conversionRate = ...;  // obtained from code outside Joda-Money
  Money moneyGBP = money.convertedTo(CurrencyUnit.GBP, conversionRate, RoundingMode.HALF_UP);

  // use a BigMoney for more complex calculations where scale matters
  BigMoney moneyCalc = money.toBigMoney();

Belgeler: http://joda-money.sourceforge.net/apidocs/org/joda/money/Money.html

Uygulama örnekleri: https://www.programcreek.com/java-api-examples/?api=org.joda.money.Money


0

Bazı örnek ... bu (aslında beklendiği gibi çalışmaz), hemen hemen her programlama dilinde ... Delphi, VBScript, Visual Basic, JavaScript ve şimdi Java / Android ile denedim:

    double total = 0.0;

    // do 10 adds of 10 cents
    for (int i = 0; i < 10; i++) {
        total += 0.1;  // adds 10 cents
    }

    Log.d("round problems?", "current total: " + total);

    // looks like total equals to 1.0, don't?

    // now, do reverse
    for (int i = 0; i < 10; i++) {
        total -= 0.1;  // removes 10 cents
    }

    // looks like total equals to 0.0, don't?
    Log.d("round problems?", "current total: " + total);
    if (total == 0.0) {
        Log.d("round problems?", "is total equal to ZERO? YES, of course!!");
    } else {
        Log.d("round problems?", "is total equal to ZERO? NO... thats why you should not use Double for some math!!!");
    }

ÇIKTI:

round problems?: current total: 0.9999999999999999 round problems?: current total: 2.7755575615628914E-17 round problems?: is total equal to ZERO? NO... thats why you should not use Double for some math!!!


3
Sorun, yuvarlama hatasının gerçekleşmemesi değil, bununla ilgilenmemenizdir. Sonucu iki ondalık basamağa yuvarlayın (sent istiyorsanız) ve işiniz bitti.
maaartinus

0

Şamandıra, farklı tasarımlı Ondalık'ın ikili biçimidir; onlar iki farklı şeydir. Birbirine dönüştürüldüğünde iki tür arasında çok az hata vardır. Ayrıca, şamandıra bilimsel için sonsuz sayıda değeri temsil edecek şekilde tasarlanmıştır. Bu, sabit sayıda bayt ile aşırı küçük ve aşırı büyük sayıya karşı hassasiyeti kaybetmek için tasarlandığı anlamına gelir. Ondalık sonsuz sayıda değeri temsil edemez, sadece bu ondalık basamak sayısına bağlanır. Yani Float ve Ondalık farklı amaçlar içindir.

Para birimi değeri hatasını yönetmenin bazı yolları vardır:

  1. Uzun tamsayı kullanın ve bunun yerine sent cinsinden sayın.

  2. Çift hassasiyet kullanın, önemli basamaklarınızı yalnızca 15 olarak tutun, böylece ondalık sayı tam olarak simüle edilebilir. Değerleri sunmadan önce yuvarlayın; Hesaplamalar yaparken sık sık yuvarlayın.

  3. Java BigDecimal gibi bir ondalık kütüphane kullanın, böylece ondalığı simüle etmek için çift kullanmanız gerekmez.

ps el bilimsel hesap makineleri markaların çoğu float yerine ondalık üzerinde çalıştığını bilmek ilginç. Yani hiç kimse şamandıra dönüşüm hataları şikayet.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.