Prakticke problemy pri konvertovani mien pomocou datoveho typu Double
Zacnime vtipom:
"Rada nad zlato.
Chcete zvacsit svoj kapital?
Pozerajte si vyplatu pod lupou."
Skippova poznamka: "najma po 1. januari 2009" :-)
Kazdy programator sa skor ci neskor dostane do kontaktu s aplikaciou, ktora naraba s peniazmi. Peniaze su citliva tema, najma ak sa dostane na ich reprezentaciu v pocitaci, ktory ma obmedzenu pamat (-:jedina nekonecna vec by mala byt ludska hlupost:-). Nedavno som opravoval par problemom suvisiacich s konverziou penazi do roznych mien a teraz sa mi naskytlo trosku casu to nejako zosumarizovat. Niektore veci su trivialne, ine trosku menej, nic-menej vsetko, co suvisi s peniazmi (klientov) je dolezite. Ked som problem videl prvy krat na obrazovke, hned som si spomenul na duracellkov post o problemoch datovych typov s pohyblivou desatinnou ciarkou a bol som na spravnej stope. Dufam, ze tento prispevok pomoze niekomu dalsiemu v luskani otazok suvisiacich s peniazmi.
Situacia
Ste programator znamej online 3D hry 2nd Life. Vasi klienti si mozu do systemu naliat peniaze a potom obchodovat a fungovat ako v realnom svete. Pre jednoduchost predpokladajme, ze tu mame fantasticky slovensky trh a specialne prenho sme vytvorili specialnu x-mas verziu, kde moze klient nahrat peniaze len v SKK mene (EUR este nestihlo prist).
Vasi klienti mozu virtualne peniaze, ktore vlozili do systemu menit za rozne veci (oblecenie, zetony do kasina, nehnutelnosti a pod.). Idu vianoce a preto budu obchodovat ako divy :-)
Problemy
Hned na uvod by som rad zdoraznil, ze dane problemy sa nesnazia byt vseobecne pre vsetky typy aplikacie, skor chcu nabadat citatela, aby sa nad nimi zamyslel v kontexte tej jeho aplikacie.
1. Zobrazenie penazi
Na prvy pohlad jasna vec, no nemusi to tak byt. Na kolko desatinnych miest zobrazime aktualny stav uctu klienta? Pri transakciach penazi tam a spat nam moze stav uctu nadobudnut velmi rozne hodnoty. Kedze pocitac nedokaze niektore cisla s nekonecnym desatinny rozvojom drzat v pamati, pracujeme vzdy s najlepsim priblizenim - aproximaciou. Kolko mieste je OK? V nasej hre by sme pokojne mohli rozhodnut, ze 2. Teda klient po urcitom case hrania a niekolkych trasakciach bude mat na svojom urcte 100,126 SKK. Zobrazime hodnotu na 2 desatinne miesta ale ako - 100,12 alebo 100,13 ?
2. Zaokruhlovanie
Zaokruhlovanie suvisi aj s bodom c.1 - zobrazenu sumu zaokruhlovat alebo orezavat? Vacsinou chceme byt ku klientom dobri a pri istych transakciach zaokruhlujeme v prospech klienta. Tu chcem zdoraznit jeden bezpecnostny problem, ak budeme na klienta prilis dobry, moze sa nam to vypomstit. Predstavme si, ze vojdeme do virtualneho kasina, kupime si za cely nas ucet zetony (nech jeden zeton == 1EUR). Ak zaokruhlujeme aj v tomto pripade v prospech klienta a nechame na strane klienta istu desatinnu cast, moze sa nam lahko stat, ze parta polsko-ruskych nadsencov nam zacne prelievat peniaze tam a spat a za jeden den nas okradne par sto az tisic korun. Obrana pred takymito utokmi vsak patri do ineho clanku.
Co ale s vypisom jeho stavu uctu. Nuz tu treba byt opatrny. Este raz, nech klient ma na svojom ucte realnych 100,126 SKK a nech padlo rozhodnutie zobrazovat stav na 2 desatinne miesta. Ako - orezat? -zaokruhlit? Nuz obe moznosti sa v nasom pripade 2nd Life daju aplikovat, s roznymi dosledkami.
Ak orezeme, klient moze mat pocit, ze mu zobrazujeme nespravne informacie (ludia nie su hlupi), nebodaj, ze sme ho okradli. To moze vazne poskodit nasu rokmi budovanu znacku a znicit cely biznis, ak je klient nespokojny a hlavne nam neveri.
Zaokruhlime? V poriadku, klient nemoze namietat, davame mu viac ako od nas ocakava (a ktomu odpovedajuci vtip: "pride chlapik na konkurz na policajta a dostane otazku: kolko je dva plus dva. Muz odpoveda: dvanast. Policajti: vyborne, berieme vas, povedali ste nam viac, ako sme od vas ocakavali"). Toto rozhodnutie nam vsak moze poriadne skomplikovat zivot, ak na inom mieste v aplikacii ponukame funkcnost typu 'zamen vsetky peniaze za zetony' a uzivatel do textoveho policka napise sumu 100,13 v domienke, ze tolko penazi naozaj ma. Po odklepnuti tlacidka vsak dostane hlasku, ze na svojom ucte nema tolko penazi, a ze si moze zamenit o nieco menej zetonov. To znamena robit dotatocnu kontrolu a rozne cesticky, ktore v konecnom dosledu a pri velkosti hry 2nd Life mozu mat negativny dopad na udrzbu, zmenu a vyvoj aplikacie. Urcite vas hned napada n-dalsich problemov.
3. Implementacia v .NET
V .NETe a C# mame 3 datove typy pracujuce podla standardu pre aritmetiku s pohyblivou desatinnou ciarkou: double, float, decimal. Ktory typ pouzit. Decimal je urceny na pracu s penaznymi hodnotami a mal by sa na to pouzivat. Je vsak asi 40x pomalsi ako double. Interny popis double a floatu najdete v napr v duracellkovom clanku alebo na wikipedii.
Niekedy situaciu nemame vo svojich rukach a dostaneme pod ruky kod, ktory pracuje s double. Tiez zdoraznim jednu podstatnu vec, realna hodnota double a jeho textova reprezentacia .ToString() nie su rovnake. Nie som expert v implementacii aritmetiky s pohyblivou desatinnou ciarkou v .NET, no docital som sa, ze nie je uplna podla standardov. Mozno by sa niekto mohol vyjadrit v komentaroch, aky je momentalny stav v .NET 3.5, teda CLR 2.0 a BCL.
Jeden lahko prehliadnutelny dosledok tohto faktu je, ak pouzivame ASP.NET ViewState. Ak potrebujeme preniest stav-double cislo vo viewstate, jednoduche priradenie ViewState["mojeCislo"] = cislo; sposobi (predpokladam) zavolanie overrided .ToString() metody na vstupnom objekte. A v dalsom volani stranky a ziskani hodnoty z viewstate cez double.Parse nam vrati inu hodnotu, ako sme povodne vlozili. Tym padom dostavame neocakavane vysledky v dalsich vypoctoch.
Riesenim je ulozit cislo vo formate round-trip ViewState["mojeCislo"] = cislo.ToString("r"); Ako pise MSDN, takto ziskana reprezentacia cisla zaruci rovnake vysledny po opatovnom parsovani.
Zaver
Pri praci s aproximaciou realnych cisel je dolezite vediet, ako dana platforma pracuje a implementuje system pre aritmetiku cisel s pohyblivou desatinnou ciarkou. O to viac, ak robime porovnavania a cisla reprezentuju peniaze. Chyby v kode sa opravia lahko, tazko sa opat nadobudne dovera zakaznikov.
Dufam, ze vas mozno trosku odlahceny styl s prikladom z praxe a zaujal. Chcem dodat, ze som sa nesnazil o ziadnu reklamu spominanej hry. Jednoducho mi to padlo ako dobry programatorsko-managersky priklad, pre mnohych z vas mozno nie celkom typickej aplikacie, na ktorych denne pracujete.
Kviz
Pre naruziveho citatela ponukame v skutku jednoduchu otazku: aka hodnota bude ulozena v booleovskej premennej b po vykonani nasledujuceho riadku (C#):
bool b = double.NaN == double.NaN
Tesim sa na Vase odpovede.