Download: ProgramowaniePerl
Transkrypt
Download: ProgramowaniePerl
PROGRAMOWANIE Perl: Finanse osobiste Rozbudowywalna przeglądarka finansów osobistych OSTATNIA LINIJKA Dzisiejszy skrypt Perla beancounter pozwala uzyskać natychmiastowy pogląd na nasz status finansowy, dodawać salda wielu kont i wkładów akcji. Można nawet pisać własne wtyczki. MICHAEL SCHILLI W brew temu, co się powszechnie uważa, ludzie bogaci nie są mniej szczęśliwi od tych bez grosza przy duszy. Zewsząd słychać o pomocy dla biednych – minęły czasy chciwości napędzanej pieniędzmi. I po raz pierwszy od lat ludzie znów mają odwagę sprawdzać stan swoich oszczędności. Czytaj dalej, a dowiesz się, w jaki sposób. Nie licząc paru ekscentryków wolących bunkrować majątek pod podłogą w swoich willach, ludzie coraz częściej korzystają z programów takich jak Gnucash do zarządzania kontami i wkładami. Dzięki takim programom uporządkujesz swoje konta, otrzymasz schludnie sformatowane lub nawet graficzne wyniki. Jednak narzędzia Open Source wciąż są w duchu programów Quicken czy Microsoft Money i wymagają dużo zdyscyplinowanej pracy. Właściciele kont – amatorzy zwykle nie mają czasu starannie wypełniać tych wszystkich pól, nie mówiąc już o skomplikowanej instalacji wymaganej przez Gnucash. Innymi słowy, nie jest to proste narzędzie i nie jest łatwo rozszerzalne. W przeciwieństwie do tego, nasz skrypt w Perlu zaprojektowany jest dla reszty z nas, która woli nie spędzać więcej niż dziesięć minut miesięcznie na uaktualnianie sald 80 NUMER 15 KWIECIEŃ 2005 bankowych, ale nadal chciałaby na co dzień sprawdzić ostatnią linijkę pakietu akcji. System obsługuje modułowe rozszerzenia w postaci wtyczek, pozwalając użytkownikom na uwzględnianie podatku bądź przewalutowania, nie tworząc nadmiernie rozbudowanej aplikacji. Łatwo jest z czymś przesadzić. Na przykład nie ma sensu pisanie modułu do podziału akcji, co się zdarza raz na kilka lat; do jego obsłużenia wystarczy ręcznie przeprowadzić kilka kroków (mówiąc inaczej, nie próbuj wszystkich na siłę uszczęśliwiać). Nasz skrypt beancounter próbuje znaleźć złoty środek. Posiada podstawową funkcję podawania sumy sald wielu kont i pakietów akcji, ale pozostawia użytkownikom wystarczające pole do spełniania ich specjalnych wymagań. finicje kont, sprawdza ceny akcji oraz sumuje zyski i straty, dając oświadczenie finansowe. Skrypt uruchamia się, wpisując beancounter money w linii poleceń, ale jest jeszcze prostszy sposób. Uczyń plik konfiguracyjny money wykonywalnym i dodaj interpreter beancounter do pierwszej linii. Teraz nie perl będzie interpreterem money, a beancounter. Skrypt interpretera Użytkownicy mogą określić dane konta w pliku o nazwie money, jak na Rysunku 1. Nowe konto definiowane jest słowem kluczowym keyword. Akcje rozpoczynają się od stock, a słowa cash wyjaśniać nie trzeba. Interpreterem tych danych finansowych jest skrypt beancounter z Listingu 1. Analizuje de- WWW.LINUX-MAGAZINE.PL Rysunek 1: Posiadaczy kont definiuje plik konfiguracyjny; jest on jednocześnie wykonywalnym skryptem. Perl: Finanse osobiste PROGRAMOWANIE Jeśli nie używasz hipernowoczesnej powłoki Zsh, tylko starego dobrego Basha, nie będziesz mógł dodać skryptu do pierwszej linijki. Potrzebujesz opakowani napisanego C, za który posłuży nam następujący program, beancount. c: main (int argc, char **argv) { execv ("/usr/bin/U beancounter", argv); } Skompiluj teraz beancount.c wydając poniższe polecenie cc -o beancount beancount.c Rysunek 2: Licznik pieniędzy beancounter w akcji. Wywołany z linii poleceń daje kolorowe podsumowanie kont i ogólnego stanu finansowego. Dzięki temu będzie można użyć pliku wykonywalnego beancount jako interpretera danych finansowych w pierwszej linii: #!/usr/bin/beancount account Barclays ################################ # ticker shares at stock VOD.L 10 120.17 # ... Jeśli plik z tym kodem, money, jest wykonywalny, można prosto wpisać money, by uruchomić nasz licznik pieniędzy. Chociaż wygląda jak plik konfiguracyjny, naprawdę mamy wykonywalny skrypt. Na Rysunku 2 widzimy rezultat. Jakie to praktyczne! Listing 1: beancounter 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 #!/usr/bin/perl -w ############################# # beancounter - Money # Counting Interpreter # Mike Schilli, 2004 # ([email protected]) ############################# use strict; Rozszerzanie interpretera za pomocą wtyczek Interpreter z Listingu 1 jest dosyć rozsiany: tworzy instancję obiektu typu Plugger, wywołuje init(), by zainicjalizować architekturę wtyczek i przekazuje do metody parse () systemu wtyczek plik konfiguracyjny wczytany wcześniej ze standardowego wejścia przy użyciu <>. Zrąb Plugger z Listingu 2 interpretuje pierwsze słowo w każdej linii jako polecenie. Ale bez wtyczek nie może niczego zinterpretować. Jedyne, co robi, to ignoruje linie komentarzy w pliku money, wszystkie zaczynające się #. Plugger.pm podczas kompilacji automatycznie wczytuje moduły dodane do katalogu Plugger/. Linia 7 dołącza moduł CPAN Module::Pluggable, który się tym zajmuje. Linie 8 i 9 ustawiają flagę require i ścieżkę wyszukiwania wtyczek względem katalogu bieżącego albo ścieżki @INC. Wtyczki nie mają konstruktora new() jak w typowo obiektowym podejściu, tyl- ko funkcję init(), wywoływaną po kolei dla każdej znalezionej wtyczki przez Plugger.pm – pana wszystkich pluginów. Module::Pluggable automatycznie dodaje metodę plugins() do swojego gospodarza, klasy Plugger.plugins() zwraca listę nazw wszystkich wykrytych wtyczek. Mechanizm ten jest wykorzystany w liniach 31 i 32, które literują po metodach init() wszystkich wtyczek. Aby wtyczka znała wywołujący ją obiekt i mogła w razie potrzeby odwoływać się do jego metod, Plugger.pm przekazuje do metody init() każdej wtyczki odnośnik $ctx(kontekst). Jest to referencja do jedynego istniejącego obiektu typu Plugger, menedżera wtyczek. Pozwala ona wtyczkom wydawać instrukcje menedżerowi Plugger. Ponieważ Plugger interpretuje polecenia pliku konfiguracyjnego, wtyczka wywołuje metodę administracyjną register_cmd(), by zarejestrować nowe polecenia. use lib '/some/where/in/module/land'; use Log::Log4perl qw(:easy); Log::Log4perl->easy_init( $ERROR); use Plugger; my $string = join '', <>; my $plugger = Plugger->new(); $plugger->init(); $plugger->parse($string); Rysunek 3: Menedżer pluginów Plugger.pm używa Module::Pluggable do wczytywania wtyczek Plugger:: i wywołuje ich funkcje init (). Wtyczki następnie wywołują register_cmd() do zarejestrowania polecenia w programie. WWW.LINUX-MAGAZINE.PL NUMER 15 KWIECIEŃ 2005 81 PROGRAMOWANIE Perl: Finanse osobiste Wsparcie argumentowe dla wtyczki Account Na Listingu 3 mamy przykładową wtyczkę z katalogu Plugger/: Account.pm używając a wyżej omówionego mechanizmu register_cmd() do nauczenia menedżera wtyczek polecenia account: $ctx->register_cmd("account", \&start, \&process, U \&finish); Te dwie linijki w zrębie określają, że Plugger podczas interpretowania w pliku konfiguracyjnym słowa kluczowego account powinien wywołać funkcję process() z Plugger/Account.pm i przekazać jej rozdzielone elementy pliku konfiguracyjnego jako argumenty. Ponadto Plugger.pm przed rozpoczęciem interpretacji pliku konfiguracyjnego uruchamia funkcję start(), co widać na Listingu 3 w linii 21, a na końcu wywołuje funkcję finish() (linia 84). Wtyczka account używa tego mechanizmu przed rozpoczęciem analizy do ustawienia na wartość zero przechowywanej w zmiennej globalnej account_total sumy ze wszystkich zdefiniowanych kont. Nadal musimy zdecydować, gdzie umieścić definicję tego rodzaju licznika, do którego musi mieć dostęp Account.pm i inne wtyczki. Plugger.pm tworzy do tego celu wartość hash o nazwie %MEM. Moduł przekazuje referencję do wartości hash do wszystkiego, co używa akcesora mem() pytającego o referencję. Na przykład plugin taki jak Account.pm może użyć $ctx->mem()->U {account_total} = 0; by ustawić zmienną, do której będą miały dostęp inne wtyczki mające dzięki $ctx refe- rencję do menedżera wtyczek Plugger. W taki właśnie sposób komunikują się wtyczki Account.pm i Position.pm: Account.pm najpierw ustawia wartość account_total na zero. Position.pm używany jest przez każdą definicję stock i cash, którą oblicza i dodaje do account_total. Dodawanie koloru Załóżmy, że chcesz, by Account.pm wyświetlał górną linię konta i saldo na niebiesko pogrubioną czcionką. Zajmie się tym moduł CPAN Term::ANSIColor. Dodanie do wyrażenia use znacznika constants eksportuje do przestrzeni nazw wywołującego skryptu stałe atrybutów tekstu, takie jak BLUE, BOLD i RESET (przywróć krój standardowy). Dzięki temu możemy używać wyrażenia print w taki sposób: print BLUE, BOLD, Listing 2: Plugger.pm 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 82 ############################# package Plugger; ############################# use strict; use warnings; use Module::Pluggable require => 1, search_path => [qw(Plugger)]; our %DISPATCH = (); our %MEM = (); ############################# sub new { ############################# my ($class) = @_; bless my $self = {}, $class; return $self; } ############################# sub init { ############################# my ($self) = @_; $_->init($self) NUMER 15 KWIECIEŃ 2005 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 for $self->plugins(); } ############################# sub mem { return \%MEM; } ############################# ############################# sub parse { ############################# my ($self, $string) = @_; for (sort keys %DISPATCH) { $DISPATCH{$_}->{start} ->($self) if $DISPATCH{$_} ->{start}; } for (split /\n/, $string) { s/#.*//; next if /^\s*$/; last if /^__END__/; chomp; my ($cmd, @args) = split ' ', $_; die "Unknown command: $cmd" WWW.LINUX-MAGAZINE.PL 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 unless exists $DISPATCH{$cmd}; $DISPATCH{$cmd} ->{process} ->($self, $cmd, @args); } for (sort keys %DISPATCH) { $DISPATCH{$_}->{finish} ->($self) if $DISPATCH{$_} ->{finish}; } } ############################# sub register_cmd { ############################# my ($self, $cmd, $start, $process, $finish) = @_; $DISPATCH{$cmd} = { start => $start, process => $process, finish => $finish, }; } 1; Perl: Finanse osobiste PROGRAMOWANIE Listing 3: Account.pm 001 ######################## ##### 002 package Plugger::Account; 003 ######################## ##### 004 use strict; 005 use warnings; 006 use Term::ANSIColor 007 qw(:constants); 008 009 ######################## ##### 010 sub init { 011 ######################## ##### 012 my ($class, $ctx) = @_; 013 014 $ctx->register_cmd( 015 "account", \&start, 016 \&process, \&finish 017 ); 018 } 019 020 ######################## ##### 021 sub start { 022 ######################## ##### 023 my ($ctx) = @_; 024 025 $ctx->mem() 026 ->{account_total} = 0; 027 } 028 029 ######################## ##### 030 sub account_start { 031 ######################## ##### 032 my ($ctx, $name) = @_; 033 034 print BOLD, BLUE, 035 "Account: $name\n", 036 RESET; 037 038 $ctx->mem() 039 ->{account_subtotal} = 0; 040 $ctx->mem() 041 ->{account_current} = 042 $name; 043 } 044 045 ######################## ##### 046 sub account_end { 047 ######################## ##### 048 my ($ctx, $name) = @_; 049 050 print BOLD, BLUE; 051 printf "%-47s %9.2f\n\n", 052 "Subtotal:", $ctx->mem() 053 ->{account_subtotal}; 054 print RESET; 055 } 056 057 ######################## ##### 058 sub account_end_all { 059 ######################## ##### 060 my ($ctx) = @_; 061 062 print BOLD, BLUE; 063 printf "%-47s %9.2f\n\n", 064 "Total:", $ctx->mem() 065 ->{account_total}; 066 print RESET; 067 } 068 069 ######################## ##### 070 sub process { 071 ######################## ##### 072 my ($ctx, @args) = @_; 073 074 my $c = 075 $ctx->mem() 076 ->{account_current}; 077 account_end($ctx, $c) 078 if $c; 079 account_start($ctx, 080 $args[1]); 081 } 082 083 ######################## ##### 084 sub finish { 085 ######################## ##### 086 my ($ctx) = @_; 087 088 my $c = 089 $ctx->mem() 090 ->{account_current}; 091 account_end($ctx, $c) 092 if $c; 093 account_end_all($ctx); 094 } 095 096 ######################## ##### 097 sub position { 098 ######################## ##### 099 my ( 100 $type, $ticker, 101 $n, $at, 102 $price, $value, 103 $gain 104 ) 105 = @_; 106 107 unless (defined $ticker) { 108 printf "%-47s %9.2f\n", 109 $type, $value; 110 return; 111 } 112 113 my $clr = 114 $gain > 0 ? GREEN: RED; 115 116 printf 117 "%-8s %-10s %9.3f %9.3f" 118 . " %7.2f %9.2f" 119 . " %s(%+9.2f)%s\n", 120 $type, $ticker, $n, $at, 121 $price, $value, $clr, 122 $gain, RESET; 123 } 124 125 1; WWW.LINUX-MAGAZINE.PL NUMER 15 KWIECIEŃ 2005 83 PROGRAMOWANIE Perl: Finanse osobiste "Pogrubione na niebiesko!",U RESET; które wyrzuci sekwencje ANSI wyświetlające na niebiesko pogrubiony tekst w bieżącym terminalu, i przywróci tekst normalny dla następnych wyrażeń print. Ceny akcji online z buforem tymczasowym Wtyczka Position z Listingu 4 pobiera aktualne (tj. opóźnione o 20 minut) ceny akcji ze strony finansowej Yahoo przy użyciu kolejnego modułu CPAN, Finance::YahooQuote. Wyeksportowana funkcja getonequote() zwraca skrótowe symbole jak VOD.L dla ak- cji Vodafone na londyńskiej giełdzie albo EBAY dla udziałów Ebay na Nasdaq. Przydatna lista symboli używanych w Wielkiej Brytanii znajduje się pod adresem [3]. Ponieważ beancounter może wielokrotnie potrzebować tej samej ceny akcji, Position przechowuje cenę w pamięci podręcznej przez 10 minut. Moduł CPAN Cache::Cache ma bardzo prosty interfejs: set() zachowuje pozycję w pamięci, a get() ją później odczytuje. Istnieje implementacja w pamięci nazwana Cache::MemoryCache i trwały bufor plikowy Cache::FileCache. Position.pm używa { namespace =>U 'Beancount', default_expires_in U => 600, }); do stworzenia obiektu pamięci podręcznej i zajmuje się wszystkimi szczegółami, jak wydajne składowanie w plikach tymczasowych bez kolizji z innymi aplikacjami. Użytkownicy prosto wywołują $cache->set () i $cache->get (). Bogaty kontra biedny my $cache = Cache::U FileCache->new( Beancounter jest przesadnie rozbudowany jako narzędzie jedynie podsumowujące konta Listing 4: Position.pm 001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 84 ############################# package Plugger::Position; ############################# use strict; use warnings; use Log::Log4perl qw(:easy); use Finance::YahooQuote; use Term::ANSIColor; ############################# sub init { ############################# my ($class, $ctx) = @_; DEBUG "Registering @_"; $ctx->register_cmd( "stock", undef, \&process, undef ); $ctx->register_cmd("cash", undef, \&process_cash, undef); } ############################# sub process { ############################# my ($ctx, $cmd, @args) = @_; my $value = price($args[0]) * $args[1]; my $gain = $value - $args[2] * $args[1]; NUMER 15 KWIECIEŃ 2005 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 Plugger::Account::position( ucfirst($cmd), @args[ 0 .. 2 ], price($args[0]), $value, $gain ); my $mem = $ctx->mem(); $mem->{account_subtotal} += $value; $mem->{account_total} += $value; } ############################# sub process_cash { ############################# my ($ctx, $cmd, @args) = @_; my $mem = $ctx->mem(); $mem->{account_subtotal} += $args[0]; $mem->{account_total} += $args[0]; Plugger::Account::position( ucfirst($cmd), (undef) x 4, $args[0], undef); } use Cache::FileCache; my $cache = WWW.LINUX-MAGAZINE.PL 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 Cache::FileCache->new( { namespace => 'Beancount', default_expires_in => 600, } ); ############################# sub price { ############################# my ($stock) = @_; DEBUG "Fetching $stock quote"; my $cached = $cache->get($stock); if (defined $cached) { DEBUG "Cached: $cached"; return $cached; } my @quote = getonequote $stock; die "$stock failed" unless @quote; $cache->set($stock, $quote[2]); return $quote[2]; } 1; Perl: Finanse osobiste – oczywista robota astronauty architektury; podziękowania dla Joela Spolskyego za trafienie w samo sedno rzeczy w [4]. Zrąb pluggera zaczyna spełniać zadanie, gdy daje się dodawać funkcjonalność bez modyfikacji oryginalnego kodu. Przykładem jest wtyczka Plugger/TaxedPosition.pm z Listingu 5. Odejmuje 50 procent podatku od (domniemanych) zysków zdefiniowanych przez txstock. Ten tryb „marzenia o bezludnej wyspie” podlicza bilans, gdybyś chciał spieniężyć swoje akcje i zapłacić 50procentowy podatek. TaxedPosition nie odejmuje niczego, gdy akcje przyniosą straty, tylko zwraca wartość nominalną akcji po zamknięciu przegranych. Zależnie od potrzeb, użytkownicy mogą pisać wtyczki do nowych słów, dodawać je do szkieletu i modyfikować system. Ponieważ TaxedPosition.pm odnosi się do funkcji price() zdefiniowanej w Position. pm, dobrze byłoby użyć mechanizmu dziedziczenia lub interfejsu do połączenia TaxedPosition.pm i Position.pm. Ponieważ jednak zrąb pluggera nie posiada klas, w linii 9 Listingu 5 TaxedPosition.pm definiuje funkcję obsługi AUTOLOAD, która przekazuje wywołania nieznanych funkcji do Position.pm. Żeby uprościć wyprowadzanie danych i zapewnić łatwość zarządzania, całe wyjście na ekran obsługiwane jest we wtyczce Position. pm. Funkcja position() przyjmuje dane jednej pozycji: typ, symbol, numer, cenę zakupu, obecną cenę, obecną wartość całkowitą, zysk i stratę, i drukuje je ładnie sformatowane na wyjściu. Pozycje pieniężne potrzebują tylko lewą i prawą kolumnę. Twoje własne wtyczki powinny korzystać z funkcji print skryptu Position.pm, tak jak to robi TaxedPosition.pm. Funkcja price() z Position.pm również powinna być przydatna w pisanych wtyczkach. Instalacja Zarówno skrypt beancounter (Listing 1), jak i skompilowana otoczka w C beancount powinny się znaleźć w katalogu /usr/bin oraz być wykonywalne. Plugger.pm (Listing 2) i wszystkie wtyczki z katalogu Plugger/ powinny być w jednej ze ścieżek @INC środowiska Perla. Jeśli tak nie jest, do opublikowa- PROGRAMOWANIE nia ścieżki można w skrypcie Perla beancounter użyć linii use lib '/home/mschilli/U perl-modules'; zakładając, że Plugger i reszta rzeczy znajdują się w podanym katalogu. Moduły Module::Pluggable, Finance::YahooQuote i Term::ANSIColor dostępne są z CPAN. Najprostszym sposobem ich instalacji jest użycie powłoki CPAN. I nic już Perlowi nie zabroni rządzić Twoimi pieniędzmi! ■ INFO [1] Listingi: ftp://www.linux-magazine.com/Magazine/Downloads/53/Perl [2] Samouczek Module::Pluggable: http://www.perladvent.org/2004/6th [3] Oznaczenia popularnych akcji w Wielkiej Brytanii: http://uk.biz.yahoo.com/p/uk/cpi/cpia0.html [4] Joel Spolsky, "Don't let Architecture Astronauts scare you" w Joel on Software: Apress 2004. Listing 5: TaxedPosition.pm 01 ######################## ##### 02 package 03 Plugger::TaxedPosition; 04 ######################## ##### 05 use strict; 06 use warnings; 07 use Log::Log4perl qw(:easy); 08 09 ######################## ##### 10 sub AUTOLOAD { 11 ######################## ##### 12 no strict qw(vars refs); 13 14 (my $func = $AUTOLOAD) =~ 15 s/.*::/Plugger::Position::/; 16 $func->(@_); 17 } 18 19 ######################## ##### 20 sub init { 21 ######################### #### 22 my ($class, $ctx) = @_; 23 24 $ctx->register_cmd( 25 "txstock", undef, 26 \&process, undef 27 ); 28 } 29 30 ######################### #### 31 sub process { 32 ######################### #### 33 my ($ctx, $cmd, @args) = 34 @_; 35 36 my $value = 37 price($args[0]) * 38 $args[1]; WWW.LINUX-MAGAZINE.PL 39 my $gain = 40 $value - $args[2] * 41 $args[1]; 42 43 my $tax = $gain / 2; 44 45 $value -= $tax 46 if $gain > 0; 47 $gain -= $tax if $gain > 0; 48 49 Plugger::Account::position( 50 ucfirst($cmd), 51 @args[ 0 .. 2 ], 52 price($args[0]), 53 $value, 54 $gain 55 ); 56 57 my $mem = $ctx->mem(); 58 $mem->{account_subtotal} += 59 $value; 60 $mem->{account_total} += 61 $value; 62 } 63 64 1; NUMER 15 KWIECIEŃ 2005 85