← Zpět na články

Od kódu k pravidlům

V prvním díle jsem popsal, proč bankovní výpis v PDF nečtu jako text, ale jako stránku plnou souřadnic. Dnes bych se rád podíval na to, co se stane, když takových výpisů nemáte jeden, ale desítky. A každý od jiné banky, v jiné verzi a s jinými nápady, jak zarovnat částku. A občas to není jen ta částka, která není zarovnaná jinak.

Extraktor za extraktorem

První extraktor jsem napsal pro jednu českou banku, protože jsem měl po ruce vlastní výpisy. Byl to kus kódu, který prošel stránky, našel hlavičku, přečetl z ní potřebné údaje a potom řádek po řádku skládal transakce. Druhá banka byla podobná, tak jsem kus kódu zkopíroval a upravil. U třetí jsem už vytáhl společné věci do základní třídy. Klasika.

Každý extraktor se skládá ze čtyř částí, které se od začátku moc nezměnily:

  • rozpoznání — patří tenhle dokument mně? Obvykle podle textu a jeho polohy na první stránce;
  • šablona pro ověření — jak vypadá pravý výpis této banky (kdo ho vygeneroval, jaká písma používá, jestli je podepsaný);
  • hlavička — účet, majitel, období, zůstatky;
  • transakce — tabulka, která se táhne přes několik stránek.

Nová banka tedy znamenala napsat čtyři třídy. Po pár měsících jsem měl kolem dvaceti českých extraktorů a všechno fungovalo. Pak přišlo Slovensko a během pár týdnů přibylo dalších patnáct. A tam jsem narazil.

Pořád stejný kód, jen jiná čísla

Když jsem procházel extraktory vedle sebe, viděl jsem pořád stejný vzor. Najdi řádek, který začíná datem. Pokud je vpravo částka, je to nejspíš nová transakce. Jinak je to pokračování předchozí. Pokud narazíš na text „Konečný zůstatek“, skonči. Pokud už čteš „Strana 2 z 5“, přeskoč na další stránku. Rozdíl mezi bankami byl často jen v tom, jaký text hledám a kde.

Jenže každý extraktor to měl napsané trochu jinak. Jeden hledal konec stránky podle čísla stránky, druhý podle patičky, třetí podle toho, že došly řádky. Když jsem opravil chybu v jednom, v ostatních zůstala. A když jsem chtěl pochopit, co přesně extraktor pro jednu banku dělá, musel jsem číst sto řádků kódu místo deseti řádků popisu.

A tak jsem se rozhodl pro větší refaktoring. Místo toho, aby extraktor dělal kroky, bude popisovat, jak jeho výpis vypadá. Ty kroky už pak udělá jeden společný kód.

Deklarovat, ne programovat

Postupně vznikly čtyři stavební kameny, které dnes používá většina extraktorů:

Hlavička jako seznam polí. Každý údaj hlavičky má svůj zdroj: text na určité pozici, hodnota vpravo od popisku, hodnota v řádku, kde je popisek „Konečný zůstatek“ (který se posouvá podle počtu transakcí). K tomu záložní varianta pro starší podobu výpisu a případně úprava hodnoty. Převod měny, parsování období a zůstatků už extraktor neřeší — to dělá jedno společné místo pro všechny banky.

Sekce. Kde tabulka transakcí začíná na první stránce, kde na dalších, co značí konec stránky a co konec celého výpisu.

Sloupce a řádky. Sloupce jsou prostě souřadnice s informací o zarovnání. A pravidla pro řádky říkají, kdy začíná nová transakce — třeba „ve sloupci datum je datum a ve sloupci částka je částka“. Některé banky oddělují transakce graficky, takže i čára nebo šedý obdélník se dají použít jako podmínka.

Formáty buněk. Tohle je moje oblíbená část. Každý formát (částka, datum, částka se znaménkem na konci, částka v eurech…) obsahuje jak vzor, podle kterého hodnotu poznám, tak parser, který ji přečte. Plus příklady, které se automaticky testují.

Zjednodušeně vypadá popis transakční tabulky nějak takto:

var date   = Column.AlignedLeft(40);
var amount = Column.AlignedRight(560);

rows.NewTransactionWhen(date.Is(Dates.Short), amount.Is(Amounts.Czech));

A na konci už jen jedno mapování — z buněk transakce do výsledného objektu.

Co mi pravidla přinesla

Největší přínos jsem nečekal tam, kde jsem ho hledal. Čekal jsem méně kódu. Toho je opravdu méně, ale hlavní je, že se extraktory dají číst. Když dnes otevřu extraktor slovenské banky, kterou jsem neviděl půl roku, během minuty vím, kde hledá hlavičku a podle čeho dělí transakce.

Druhý přínos je v chybách. Když opravím, jak se hledá konec stránky, opravím to pro všechny. A když něco rozbiju, rozbiju to taky pro všechny — což zní hůř, ale díky testům, o kterých bude příští díl, se to dozvím okamžitě, a ne za měsíc od klienta.

Třetí přínos je trochu filozofický. Pravidlo „co rozpozná, to i přečte“ u formátů buněk odstranilo celou kategorii chyb, kdy regulární výraz řekl „ano, tohle je částka“, a parser pak řekl „ne, tohle neumím“. Když jsou vzor i parser na jednom místě, nemůžou se rozejít.

Kde pravidla nestačí

Neříkám, že deklarace vyřeší všechno. Některé výpisy jsou prostě zvláštní. Jeden má tabulku, kde se začátek transakce pozná jen podle toho, jak vypadala ta předchozí. Jiný má na jedné stránce několik měn a pro každou vlastní sekci. Pro tyhle případy zůstávají v základních třídách „háčky“, které může extraktor přepsat. Důležité je, že jsou výjimkou a že na první pohled vidíte, že tady se děje něco nestandardního.

A ještě jedno pravidlo, které jsem si nastavil a které se osvědčilo: když banka změní vzhled výpisu, vzniká nový extraktor, třeba s příponou V2. Neupravuji starý, aby uměl oba formáty. Staré výpisy klienti posílají ještě dlouho a nechci riskovat, že kvůli nové verzi přestanou fungovat. Výjimkou jsou drobnosti, kdy se jen posune blok nebo změní text v záhlaví — tam stačí záložní varianta.

Poučení

Kdybych začínal znovu, šel bych k pravidlům dřív. Ale upřímně, bez těch prvních dvaceti extraktorů psaných ručně bych nevěděl, jaká pravidla potřebuji. Abstrakce vymyšlená předem by nejspíš byla špatně. Tahle vznikla z opakování, které jsem viděl na vlastní oči — a to je podle mě ta správná cesta. A právě v této fázi mi hodně pomohla AI, ale o tom až v posledním díle.

Aplikace dnes běží v rámci GoodVeri a každý nový layout výpisu už je spíš o popisu než o programování.

Jak to máte vy? Píšete abstrakce dopředu, nebo až když vás opakování začne bolet?