Spis treści:

Kategoria:C#


Zamiana kwoty liczbowej na kwotę słowną w C#.NET

Problemy na styku cyfrowym

Problem zamiany kwoty liczbowej na kwotę pisaną słownie nie jest szczególnie nowy, a poszukiwania w Internecie nie doprowadziły mnie do prostego i satysfakcjonującego rozwiązania. Język, któym posługują się komputery dość mocno odbiega od języka żywego. Żywy język kształtował się na przestrzeni wieków i wiele w nim, czasem tylko pozornie, nielogicznych konstrukcji. Z komputerowego punktu widzenia wiele konstrukcji wydaje się niepotrzebnych, przegadanych. Jak bowiem wyjaśnić istnienie synonimów? Dlaczego posługujemy się własnymi rękami i rękomaDawniej, w języku staropolskim, oprócz znanej dziś liczby mnogiej używano liczby podwójnej. Była zatem liczba pojedyncza, liczba podwójna i liczba mnoga. Liczba podwójna łączona była z wszystkimi obiektami występującymi w parach, a pary takie to między innymi ręce i oczy. Stąd mamy: rękami i rękoma, oczami i oczyma. Z czasem forma liczby podwójnej zaczęła zanikać i prawie jej się to udało. Przyzwyczajenie i częstotliwość użycia wspomnianych wyżej form podwójnych sprawiły, że istnieją do dziś. Więcej - są poprawne!? Uznałem, że wypadałoby taką funkcję napisać samemu.

Przykładowa implementacja

Zastosuję następującą technikę: najpierw kod, potem opis. Jedno z rozwiązań przedstawionego problemu znajduje się na poniższym listingu:

public class Formatowanie
{
    private static string zero = "zero";
    private static string[] jednosci = { "", " jeden ", " dwa ", " trzy ",
        " cztery ", " pięć ", " sześć ", " siedem ", " osiem ", " dziewięć " };
    private static string[] dziesiatki = { "", " dziesięć ", " dwadzieścia ",
        " trzydzieści ", " czterdzieści ", " pięćdziesiąt ",
        " sześćdziesiąt ", " siedemdziesiąt ", " osiemdziesiąt ",
        " dziewięćdziesiąt "};
    private static string[] nascie = { "dziesięć", " jedenaście ", " dwanaście ",
        " trzynaście ", " czternaście ", " piętnaście ", " szesnaście ",
        " siedemnaście ", " osiemnaście ", " dziewiętnaście "};
    private static string[] setki = { "", " sto ", " dwieście ", " trzysta ",
        " czterysta ", " pięćset ", " sześćset ",
        " siedemset ", " osiemset ", " dziewięćset " };
    private static string[,] rzedy = {{" tysiąc ", " tysiące ", " tysięcy "},
                            {" milion ", " miliony ", " milionów "},
                            {" miliard ", " miliardy ", " miliardów "}};

    private static Dictionary<string, string[]> Waluty = new Dictionary<string, string[]>() {
        //Formy podawane według wzorca: jeden-dwa-pięć, np.
        //(jeden) złoty, (dwa) złote, (pięć) złotych
        { "PLN", new string[]{ "złoty", "złote", "złotych" } },
        { "RUB", new string[]{ "rubel", "ruble", "rubli" } },
        { "USD", new string[]{ "dolar", "dolary", "dolarów" } }
    };

    public static string LiczbaSlownie(int liczba)
    {
        return LiczbaSlownieBase(liczba).Replace("  ", " ").Trim();
    }

    public static string WalutaSlownie(int liczba, string kodWaluty)
    {
        var key = Waluty[kodWaluty];
        return key[DeklinacjaWalutyIndex(liczba)];
    }

    private static string LiczbaSlownieBase(int wartosc)
    {
        StringBuilder sb = new StringBuilder();
        //0-999
        if (wartosc == 0)
            return zero;
        int jednosc = wartosc % 10;
        int para = wartosc % 100;
        int set = (wartosc % 1000) / 100;
        if (para > 10 && para < 20)
            sb.Insert(0, nascie[jednosc]);
        else
        {
            sb.Insert(0, jednosci[jednosc]);
            sb.Insert(0, dziesiatki[para / 10]);
        }
        sb.Insert(0, setki[set]);

        //Pozostałe rzędy wielkości
        wartosc = wartosc / 1000;
        int rzad = 0;
        while (wartosc > 0)
        {
            jednosc = wartosc % 10;
            para = wartosc % 100;
            set = (wartosc % 1000) / 100;
            bool rzadIstnieje = wartosc % 1000 != 0;
            if ((wartosc % 1000) / 10 == 0)
            {
                //nie ma dziesiątek i setek
                if (jednosc == 1)
                    sb.Insert(0, rzedy[rzad, 0]);
                else if (jednosc >= 2 && jednosc <= 4)
                    sb.Insert(0, rzedy[rzad, 1]);
                else if (rzadIstnieje)
                    sb.Insert(0, rzedy[rzad, 2]);
                if (jednosc > 1)
                    sb.Insert(0, jednosci[jednosc]);
            }
            else
            {
                if (para >= 10 && para < 20)
                {
                    sb.Insert(0, rzedy[rzad, 2]);
                    sb.Insert(0, nascie[para % 10]);
                }
                else
                {
                    if (jednosc >= 2 && jednosc <= 4)
                        sb.Insert(0, rzedy[rzad, 1]);
                    else if (rzadIstnieje)
                        sb.Insert(0, rzedy[rzad, 2]);
                    sb.Insert(0, jednosci[jednosc]);
                    sb.Insert(0, dziesiatki[para / 10]);
                }
                sb.Insert(0, setki[set]);
            }
            rzad++;
            wartosc = wartosc / 1000;
        }
        return sb.ToString();
    }

    private static int DeklinacjaWalutyIndex(int liczba)
    {
        if (liczba == 1)
            return 0;

        int para = liczba % 100;
        if (para >= 10 && para < 20)
            return 2;

        int jednosc = liczba % 10;
        if (jednosc >= 2 && jednosc <= 4)
            return 1;

        return 2;
    }
}

Użycie klasy

Dla użytkownika końcowego, czyli programisty-użytkownika klasy, najważniejsze są metody publiczne. Są dwie, ale wystarczają do większości zastosowań. Na bazie tych dwóch metod można wykonać większość operacji, które przyszły mi do głowy podczas pracy nad klasą. Jakie to są operacje?

Wypisywanie liczby słownie

To najbardziej podstawowa operacja. Wszystkie pozostałe są pochodną tego podstawowego mechanizmu. Przyjrzyjmy się przykładowemu wywołaniu realizującemu omawianą funkcję zaprezentowanemu poniżej:

//Zwykła wartość liczbowa
int kwota = 2424;
Console.WriteLine(Formatowanie.LiczbaSlownie(kwota));
//Wynik: dwa tysiące czterysta dwadzieścia cztery

Wypisywanie liczby z walutą

Problem jest nieco bardziej złożony. Tym razem oprócz słownej reprezentacji liczby, potrzebujemy słownej reprezentacji waluty. Nie trzeba przypominać, że walut jest dużo i zaszycie złotówek w kodzie nie jest dobrym rozwiązaniem. Lepiej udostępnić inną metodę, bardziej uniwersalną. W pokazanym przykładzie kod waluty podawany jest przy pomocy parametru, a sam kod przyjmuje wartość zdefiniowaną normą ISO 4217. Dla złotówki będzie to PLN, dla dolara USD, dla rubla RUB. Przykład konwersji kwoty w wybranej walucie na słowny odpowiednik pokazany jest poniżej:

//Kwota z walutą
Console.WriteLine(String.Format("{0} {1}",
    Formatowanie.LiczbaSlownie(kwota),
    Formatowanie.WalutaSlownie(kwota, "PLN")));
//dwa tysiące czterysta dwadzieścia cztery złote

Wypisywanie kwoty z częścią zdawkową (groszową)

Kwoty w aplikacjach przechowywane są najczęściej w postaci typu decimal. Dlaczego? Bo wiele kwot nie da się łatwo (w sposób naturalny, oczywisty, jednoznaczny) wyrazić za pomocą liczby całkowitej. W Polsce mamy przecież grosze, w USA są centy. Czy nie lepiej byłoby napisać metodę przyjmującą decimal zamiast int? Nie! Z dwóch powodów. Po pierwsze, nie zawsze mamy do czynienia z kwotami. Po drugie, nie wszystkie waluty posiadają monetę zdawkowąPrzypadek nie jest taki prosty jak się wydaje. Wiele krajów nie używa w obiegu monet zdawkowych, co nie znaczy, że ich nie ma. Japoński system finansowy posługuje się wyłącznie jenami, co nie znaczy, że monety zdawkowej nie posiada (są to sen i rin). Procesy inflacyjne i zmiana wartości waluty sprawiły, że te monety zdawkowe wyszły z obiegu.. Jak sobie poradzić z groszami, centami i drobnymi z innych krajów? Po pierwsze, należy dodać odpowiednie formy do słownika Waluty w klasie Formatowanie. Więcej o słowniku Waluty w dalszej części. Na tym etapie wystarczy taka oto linijka kodu:

{ ".PLN", new string[]{ "grosz", "grosze", "groszy" } },

Kluczem słownika jest wartość .PLN, co oznacza dziesiątą część (kropka) danej waluty (PLN). Trzymając się tego nazewnictwa amerykańskie centy miałyby postać .USD, a eurocenty .EUR.

Tak przygotowani możemy już przedstawić pełne rozwiązanie. Przyjrzyjmy się poniższemu listingowi:

//Wartość walutowa
decimal kwotaDec = 852.94M;
int zlote = (int)kwotaDec;
int grosze = (int)(100 * kwotaDec) % 100;
Console.WriteLine(String.Format("{0} {1}, {2} {3}",
    Formatowanie.LiczbaSlownie(zlote),
    Formatowanie.WalutaSlownie(zlote, "PLN"),
    Formatowanie.LiczbaSlownie(grosze),
    Formatowanie.WalutaSlownie(grosze, ".PLN")));
//osiemset pięćdziesiąt dwa złote, dziewięćdziesiąt cztery grosze

Forma nie jest tak zwięzła, jak mogłaby być, ale jest to cena płacona za elastyczność. Zawsze można napisać kilka dodatkowych metod, które uproszczą najczęściej stosowane konstrukcje opakowując tego typu wywołania.

Waluty, typ decimal i kolejna metoda

Waluty w kodzie C# przechowuje się najczęściej w typie decimal. To dlatego postanowiłem uzupełnić wpis o jeszcze jedną sekcję. Pokażę metodę, która będzie przyjmowała wartość typu decimal oraz kod waluty, zgodny z pokazanym wcześniej wzorcem. Słownikowanie części zdawkowych pod kluczem zbudowanym z klucza waluty głównej i poprzedzonej kropką pozwala na dość zwięzłą implementację. To zalety konwencji. Popatrzmy na przykład:

public static string KwotaSlownie(decimal kwota, string kodWaluty)
{
    int calosc = (int)kwota;
    int ulamki = (int)(kwota * 100) % 100;
    return string.Format("{0} {1}, {2} {3}",
        Formatowanie.LiczbaSlownie(calosc),
        Formatowanie.WalutaSlownie(calosc, kodWaluty),
        Formatowanie.LiczbaSlownie(ulamki),
        Formatowanie.WalutaSlownie(ulamki, "." + kodWaluty));
}

//dwa tysiące sześćset sześćdziesiąt jeden złotych, siedemdziesiąt osiem groszy
Console.WriteLine(Formatowanie.KwotaSlownie(2661.78M, "PLN"));

Do pracy z transakcjami finansowymi, kwotami słownymi na fakturach taka klasa będzie najwygodniejsza. Jeżeli operujemy tylko jedną walutą, można pozbyć się drugiego parametru.

Słownik walut

Aby uzupełnić temat, należałoby jeszcze wspomnieć o walutach. Zaproponowane rozwiązanie, korzystające ze statycznej reprezentacji danych, działa sprawnie i doskonale nadaje się do pokazania. Obsługa kolejnych walut sprowadza się do dodania kolejnego elementu słownika, tj. symbolu waluty będącego kluczem, oraz trzech form wyrazowych właściwych dla danej waluty. Problem polega na tym, że każdą nową walutę należy umieścić w kodzie i konieczna jest ponowna kompilacja całego modułu. Istnieje szereg innych możliwości, które można rozważyć. Dobrym pomysłem jest umieszczenie danych w oddzielnym pliku, czy nawet w bazie danych. W ten sposób dodanie kolejnych walut może być znacznie łatwiejsze, a zmiana nie będzie wymagać kompilacji/ponownego wdrożenia. Warto też rozważyć udostępnienie odpowiednich metod, które pozwalają na dodanie elementów słownika walut w sposób dynamiczny. W wielu przypadkach rozsądne wydaje się zastosowanie mechanizmu opóźnionego ładowania danych tak, aby dane związane z formami wyrazowymi nie były wczytywane za każdym razem podczas startu aplikacji, jeżeli nie ma takiej potrzeby. Walut może być dużo i nie zawsze wszystkie są nam potrzebne. Te zagadnienia pozostawiam do przemyślenia.

Podsumowanie

Nie jest to może szczyt możliwości algorytmiki, ale wydaje się, że swoją funkcję klasa spełnia. Rozbudowanie jej o kolejne rzędy wielkości i typ long nie powinno nastręczać problemów. W jednym z kolejnych artykułów być może napiszę, jak poradzić sobie z takim problemem w SQL Server. Zachęcam do dzielenia się swoimi spostrzeżeniami i sugestiami w komentarzach.

Aktualizacja: Zachęcam do zapoznania się z podobnym wpisem i rozważaniami na temat części groszowej i wyższych rzędów wielkości tutaj: Kwota słownie w SQL.

Kategoria:C#

, 2013-12-20

Komentarze:

Robert (2014-01-09 09:41:43)
Potwierdzam - kod działa bez zarzutu. Świetny tekst!
Jacek Gzel (JGPr (2013-10-10 14:53:12)
Bardzo dobry artykuł, pełne wyczerpanie tematu. Fajnie że znalazł się również opis części zdawkowej. Kod napisany czytelnie i zwięźle, co najważniejsze działa bez zarzutu!
Biz (2014-02-06 07:44:04)
Świetny tekst ... i ta liczba podwójna - ciekawe :).
Adam (2014-02-07 10:27:23)
Przed chwilą sprawdziłem i nie działa dla np. 1000004. Wyświetla się "tysięcy cztery"...
PD: Rzeczywiście, w przykładzie był błąd. W poprzednim kodzie pojawiły się fragmenty testowe, których tam nie powinno być. Teraz powinno być dobrze.
Krzysztof Radzimski (2014-03-25 11:39:00)
Działa bardzo dobrze ... szacun
Piotr (2015-04-09 16:33:56)
Na dokumentach finansowo-księgowych kwota 1.000,00 powinna być zapisana jako "jeden tysiąc" a nie "tysiąc". Aby to uzyskać usunąłem linię "if (jednosc > 1)".
Jarek (2015-06-24 18:46:27)
Bardzo dobry artykuł, jedyny drobiazg który wg. mnie wymaga korekty, to poprawa algorytmu pod kątem obsługi wartości ujemnych.

Dodaj komentarz
Wyślij
Ostatnie komentarze
Dzieki za rozjasnienie zagadnienia upsert. wlasnie sie ucze programowania :).
Co się stanie gdy spróbuję wyszukać:
SELECT * FROM NV_Airport WHERE Code='SVO'
SELECT * FROM V_Airport WHERE Code=N'SVO'
(odwrotnie są te N-ki)
Będzie konwersja czy nie znajdzie żadnego rekordu?