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

Podobne dokumenty