Načítání kurzů měn z různých bank - malý exkurz do základů objektového programování

Do jednoho systému, který právě píšu, potřebuji automatické přepočítávání měn pomocí právě platného kurzu, a to navíc hned několika bank. Což je výborná možnost jak ukázat zase trochu opravdového programování, aby mi na cizích blozích v komentářích nevyčítali, že ukazuji jenom klikání uživatelského rozhraní.

Základní idea je následující: napsat komponentu, která umožní automatizovaně načítat z webů několika různých bank informace o právě platném směnném kurzu koruny. Rozhraní by mělo být jednotné a univerzální, abych nebyl vázán na to, kterou banku právě používám. Základní úkol není nijak složitý, neboť banky dávají tyto informace na web ve formátu, který je (podle nich) vhodný pro automatické zpracování, což je typicky nějaký textový formát s oddělovačem, nebo v případě pokročilejších ústavů XML soubor. Proparsovat tato data není nikterak obtížné, takže se zaměříme na jejich vhodnou reprezentaci.

Struktura

Základní strukturu tříd si můžete prohlédnout na následujícím diagramu:

Diagram tříd

Informace o kurzu konkrétní měny jsou reprezentovány strukturou ExchangeRate. Ta obsahuje informace o mezinárodním trojpísmenném kódu měny (např. USD, pole Code). Pole Country a Currency mohou obsahovat "lidsky čitelné" označení země a měny, nicméně jejich obsah závisí na libovůli banky. Measure je nominální množství měny, pro které je kurz stanoven a konečně Rate je vlastní kurz.

Typicky se kurz stanovuje za jednotku cizí měny (1 USD = 22,445 CZK), ale pokud je kurz za jednotku nižší než kurz domácí měny, z důvodu přehlednosti se používá cena za 100 nebo 1000 nominálních jednotek (takže kurz se uvádí jako 100 SKK = 76,097 CZK, nikoliv 1 SKK = 0,76097 CZK). Tato reprezentace je vhodná pro lidské bytosti, ale ne pro automatizované přepočty. Proto má tato struktura též vlastnost NormalizedRate, což je automaticky přepočítaná cena za jednotku.

Zdrojový kód vypadá takto:

public struct ExchangeRate {

    public string Code, Country, Currency;

    public int Measure;

    public decimal Rate;

 

    public decimal NormalizedRate {

        get { return this.Rate / this.Measure; }

    }

}

Banka poskytuje seznam kurzů všech měn, se kterými běžně obchoduje. Kurzy pro všechny takové měny jsou reprezentovány kolekcí ExchangeRateCollection. Využívám zde novinky .NET 2.0 v podobě generické třídy System.Collections.ObjectModel.KeyedCollection, u které se zastavím poněkud podrobněji.

Kolekcí se v případě .NET obecně rozumí sbírka prvků téhož typu (v našem případě jde o strukturu ExchangeRate). Prvky v kolekci lze procházet buďto sekvenčně pomocí cyklu For Each a nebo k nim adresně přistupovat pomocí klíče. Typickým příkladem použití kolekce je například přístup k parametrům předaným stránce metodou GET, kdy se zpravidla používá konstrukce Request.QueryString("NázevParametru") (resp. v C# Request.QueryString["NázevParametru"]).

U měnových kurzů předpokládám, že se bude k hodnotám přistupovat pomocí klíče, který je obsažen v hodnotě samé, konkrétně jako její pole Code. Žádoucí je tedy možnost napsat věci jako cenaUSD = cenaCZK / kurzy["USD"].NormalizedRate;. Přesně k tomuto účelu slouží nová generická třída KeyedCollection. Ta umožňuje skladovat libovolné typy a určit, která že z jejích vlastností je klíčem. Vytvoříme si tedy novou třídu ExchangeRateCollection, kterou od shora uvedené podědíme. Přepíšeme metodu GetKeyForItem, v niž stanovíme pole Code jako klíč.

Zdrojový kód vypadá takto:

public class ExchangeRateCollection : System.Collections.ObjectModel.KeyedCollection<string, ExchangeRate> {

 

    public ExchangeRateCollection() : base() { }

 

    protected override string GetKeyForItem(ExchangeRate item) {

        return item.Code;

    }

}

Poslední část páteře aplikace představuje rozhraní IExchangeRateSource, které reprezentuje obecný kurzovní lístek libovolné banky či podobné instituce. Z hlediska objektově orientovaného programování jsou třídy obecně pokládány za "černé skříňky", o jejichž obsah se zbytek aplikace nezajímá a komunikuje přes definované rozhraní (angl. interface), tedy sbírku vlastností, metod a podobně.

V řadě případů, včetně našeho, vyvstává potřeba mít několik různých tříd, s různým obsahem (protože každá banka zveřejňuje kurzy jiným způsobem), ale se stejným rozhraním, aby bylo možno k nim přistupovat systemizovaně stejným způsobem. V objektově orientovaném programování se taková věc zajišťuje zpravidla prostřednictvím konstrukce, která se v zájmu zmatení programátorů též nazývá interface, neboli česky rozhraní. Jedná se v podstatě o prázdnou obálku třídy, která definuje, jaké má mít metody a vlastnosti, ale sama o sobě neobsahuje žádnou funkčnost. Konvence praví, že název rozhraní by měl začínat velkým písmenem I - a v zájmu zachování zdravého rozumu vám doporučuji se této konvence držet.

Zdrojový kód našeho interface IExchangeRateSource vypadá takto:

public interface IExchangeRateSource {

 

    void Load();

 

    void Load(DateTime date);

 

    DateTime Date { get; }

 

    ExchangeRateCollection ExchangeRates { get; }

 

    string Name { get; }

 

}

Kód praví, že každý zdroj kurzových informací musí implementovat metodu Load, a to ve dvou inkarnacích. Zavolána bez parametrů vrátí aktuální (poslední dostupný) kurz. Obdařena parametrem date vrátí kurz který byl aktuální pro definované datum. Ne všechny banky zveřejňují ve strojově čitelné podobě historické kurzy, může se tedy stát, že při druhém způsobu volání třída vyhodí výjimku NotSupportedException.

Dále máme k dispozici tři vlastnosti, všechny jsou pouze pro čtení:

  • Name vrací název zdroje (typicky tedy název banky).
  • ExchangeRates vrací shora popsanou kolekci informací o kurzu jednotlivých měn.
  • Date vrací datum (a event. čas, pokud ho banka uvádí) kdy byl kurz vydán. Banky kurzy nevydávají každý den, ale obvykle pouze ve dnech pracovních. V sobotu a neděli tedy platí kurz z pátku a hodnota Date se tedy může lišit od data zadaného jako parametr metody Load.

Využití dat

Shora uvedená architektura nám dává možnost napsat metodu, která dokáže získat a zobrazit kurzovní lístek libovolné banky, pro kterou máme k dispozici odpovídající třídu. Následující kód vypíše získané informace na konzoli:

private static void Process(IExchangeRateSource s) {

    // Download data

    Console.Write("Downloading data from {0}...", s.Name);

    s.Load();

    Console.WriteLine("OK");

 

    // Show data

    Console.WriteLine("Exchange rates for {0:g}:", s.Date);

    foreach (ExchangeRate er in s.ExchangeRates) {

        Console.WriteLine("  {2} {0} {1} = {3} CZK", er.Measure.ToString().PadLeft(6), er.Code, er.Country.PadRight(20), er.Rate.ToString("N3").PadLeft(7));

    }

    Console.WriteLine();

}

Získání údajů z konkrétní banky

Zdrojový kód, který si můžete stáhnout, obsahuje čtyči třídy, které implementují rozhraní IExchangeRateSource a umožňují načítat data několika českých bank:

Zdrojový kód třídy pro načítání dat z ČNB vypadá takto:

public class CnbExchangeRateSource : IExchangeRateSource {

    private const string DataLinePattern = @"^[^\|]+\|[^\|]+\|\d+\|[A-Z]{3}\|\d+(,\d+)?$";

 

    private string dataUrl = @"http://www.cnb.cz/cz/financni_trhy/devizovy_trh/kurzy_devizoveho_trhu/denni_kurz.txt?date={0:dd\.MM\.yyyy}";

    private DateTime date;

    private ExchangeRateCollection exchangeRates;

 

    public string DataUrl {

        get { return dataUrl; }

        set {

            if (string.IsNullOrEmpty(value)) throw new ArgumentNullException();

            dataUrl = value;

        }

    }

 

    public CnbExchangeRateSource() { }

 

    public CnbExchangeRateSource(string url) {

        this.DataUrl = url;

    }

 

    public void Load() {

        this.Load(DateTime.Now);

    }

 

    public void Load(DateTime date) {

        // Download data

        string data = string.Empty;

        using (System.Net.WebClient c = new System.Net.WebClient()) {

            c.Encoding = System.Text.Encoding.UTF8;

            data = c.DownloadString(string.Format(this.DataUrl, date));

        }

 

        // Parse data

        string[] lines = data.Split(new string[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries);

        if (lines.Length < 3) throw new FormatException("Downloaded data are invalid -- not enough lines.");

        this.date = DateTime.ParseExact(lines[0].Substring(0, 10), @"dd\.MM\.yyyy", null);

        this.exchangeRates = new ExchangeRateCollection();

        for (int i = 2; i < lines.Length; i++) {

            if (!System.Text.RegularExpressions.Regex.IsMatch(lines[i], DataLinePattern)) throw new FormatException("Unexpected format of string - '" + lines[i] + "'.");

            string[] sa = lines[i].Split('|');

            ExchangeRate r = new ExchangeRate();

            r.Country = sa[0];

            r.Currency = sa[1];

            r.Measure = int.Parse(sa[2]);

            r.Code = sa[3];

            r.Rate = decimal.Parse(sa[4], new CultureInfo("cs-CZ"));

            this.exchangeRates.Add(r);

        }

    }

 

    public DateTime Date {

        get { return this.date; }

    }

 

    public ExchangeRateCollection ExchangeRates {

        get { return this.exchangeRates; }

    }

 

    public string Name {

        get { return "?eská národní banka"; }

    }

 

Závěr

  • Altairis
  • Nemesis
  • Microsoft MVP
  • IIS
  • ASP.NET