さて、前回の続き、今回もPerlによる超手抜き家計簿の実装法を紹介する。

前回は、

 2012-03-12 09:00「残高設定 5000円」
 2012-03-12 12:00「ココイチ カレー 900円」
 2012-03-12 10:00「ドトール コーヒー 150円」
 2012-03-12 13:00「使途不明 100円」

というツイート列を、

 日付    時刻  店舗   品目   金額  残高
 2012-03-12 13:00 ?    -     100円 3850円
 2012-03-12 12:00 ココイチ カレー   900円 3950円
 2012-03-12 10:00 ドトール コーヒー  150円 4850円
 2012-03-12 09:00 残高設定 -    5000円 5000円

という家計簿に組み立てる仕様を実装するプログラムに突入した。
先週はある程度まとまったプログラムをドバーンと公開して、上から説明していたが、途中で時間と紙数が尽きてしまった。


#!/usr/bin/perl -w
# mailHTML -- ツイッター家計簿アルファ版

use strict;
use Encode qw(from_to encode);
use Mail::Mailer;

use lib '/home/????????/local/lib';
use lib '/home/????????/local/lib/perl5';
use lib '/home/????????/local/lib/perl5/site_perl';

use Net::Twitter;
use utf8;
use DateTime::Format::Strptime;
use HTML::Template;

my $handle = Net::Twitter->new({
traits => [qw/OAuth API::REST API::Search/],
consumer_key => "????????",
consumer_secret  => "????????",
access_token  => "????????",
access_token_secret => "????????",
});

my $dt = DateTime->now( time_zone => 'Asia/Tokyo' );
$dt->subtract( days => 1 );

my $screen_name = '????????'; # アカウントのscreen name
my $statuses =
$handle->user_timeline({ id => $screen_name, count => 200, since => $dt });

my @out = ();

for my $status ( @$statuses ) {
my ($date, $time, $shop, $item, $cash);
my @tw = split / /, $status->{text};
for (@tw) {
 if (/(\d\d\d\d-\d\d-\d\d)/) {
  $date = $1;
 } elsif (/(\d\d:\d\d)/) {
  $time = $1.':00';
 } elsif (/(\d+)円$/) {
  $cash = $1;
 } elsif (defined $shop) {
  $item = $_;
 } else {
  $shop = $_;
 }
}

next unless defined $cash;

my ($nowdate, $nowtime) = &tzChange($status->{created_at});
$date = $nowdate unless defined $date;
$time = $nowtime unless defined $time;

$item = "-" unless defined $item;
$shop = "?" unless defined $shop;

push @out, { date => $date, time => $time, shop => $shop, item => $item, cash => $cash }; # ★イマココ
}

my $zan;
my @sort;
for my $out (sort {$a->{date}.$a->{time} cmp $b->{date}.$b->{time}} @out) {
 if ($out->{shop} eq '残高設定') {
  $zan = $out->{cash};
 } elsif (defined $zan) {
  $zan -= $out->{cash};
 } else {
  next;
 }
 $out->{hand} = $zan;
 push @sort, $out;
}

@sort = reverse @sort;

my $tmpl_file = "kakeibo.tmpl";

my $tmpl;
open (my $template, "<:utf8", $tmpl_file );
$tmpl = HTML::Template->new(filehandle => *$template);
close $template;

$tmpl->param( { Table => \@sort } );

my $from = 'from@example.com';
my $to = '?????@example.com';
my $subject = "家計簿";

my $mailer = Mail::Mailer->new("sendmail");
$mailer->open({ From => $from,
  To => $to,
  Subject => $subject,
  'MIME-Version' => '1.0',
  'Content-Type' => "text/html; charset=UTF-8",
  'Content-Transfer-Encoding' => '8bit',
}) or die "Can't open $!\n";

print $mailer $tmpl->output;
$mailer->close();

sub tzChange ($) {
my $utc = shift;

#Sat Feb 11 03:54:53 +0000 2012
my $strp = DateTime::Format::Strptime->new(
 pattern => '%a %b %d %H:%M:%S %z %Y'
);

my $dt = $strp->parse_datetime($utc);
$dt->set_time_zone('Asia/Tokyo');

return ($dt->strftime("%Y-%m-%d"), $dt->strftime("%H:%M:%S"));
}


上のリストの★イマココというところまで到達している。
処理としては、@out という配列に

 2012-03-12 09:00「残高設定 5000円」
 2012-03-12 12:00「ココイチ カレー 900円」
 2012-03-12 10:00「ドトール コーヒー 150円」
 2012-03-12 13:00「使途不明 100円」

というツイートを受けて、

 2012-03-12 09:00 残高設定 -    5000
 2012-03-12 12:00 ココイチ カレー   900
 2012-03-12 10:00 ドトール コーヒー  150
 2012-03-12 13:00 ?    -     100

というハッシュ配列が入っているはずだ。

ハッシュ配列というのは、配列の1要素1要素が無名ハッシュへの参照になっている状態のことだ。
いや、そういうPerl用語はないが、分かるのでいまオンザフライで作って使ってみたけどどうですか。

push @out, { date => $date, time => $time, shop => $shop, item => $item, cash => $cash }; # ★イマココ

今、@outはこうなっている。

@out = (
 {
  date => "2012-03-12",
  time => "09:00",
  shop => "残高設定",
  item => "-",
  cash => "5000",
 },
 {
  date => "2012-03-12",
  time => "12:00",
  shop => "ココイチ",
  item => "カレー",
  cash => "900",
 },
 ...
);

ではプログラムの続きである。

まず各明細を日付>時刻の順でソートする。
ツイートは入力時刻順になっているが、この仕様では削除して修正入力が出来るので、date、timeの順でソートする必要がある。
また、残高設定をしたら残高はそのcashの値になり、それ以外はcashの値を引いていく。

my $zan;
my @sort;
for my $out (sort {$a->{date}.$a->{time} cmp $b->{date}.$b->{time}} @out) {
 if ($out->{shop} eq '残高設定') {
  $zan = $out->{cash};
 } elsif (defined $zan) {
  $zan -= $out->{cash};
 } else {
  next;
 }
 $out->{hand} = $zan;
 push @sort, $out;
}

$zanは現在の残高の金額を格納するスカラー変数である。
@sortはソート後の明細を格納する配列だ。

sort {$a->{date}.$a->{time} cmp $b->{date}.$b->{time}} @out

は@outを各要素をデリファレンスしたもの(無名ハッシュ)のハッシュキーがdate、timeの要素をつないで文字列比較してソートしている。
これをforループで各要素を$outに入れながら回す。
ちなみに、foreachのことをforとも書ける。

foreach変数の$outと、配列@outを同じ名前にしている。
Perlでは配列とスカラー変数に同じ名前が付けられる(同じ名前でも違うものになる)からだ。
また、@sortとかPerlのキーワードをバンバン変数名に使えるのもPerlの特徴だ。
これは$、@のようなシジルがついているおかげである。
キーワードを避けて変な名前を思いつかなくて良いのである。

 if ($out->{shop} eq '残高設定') {
  $zan = $out->{cash};

は店舗名が「残高照会」であるツイートは、金額を残高に設定している。

 } elsif (defined $zan) {
  $zan -= $out->{cash};

は、店舗名が「残高照会」でなくて、かつ、もし残高が設定されていた場合は、残高を買い物の分だけ減らす、という処理である。
もし残高が決まっていない場合は、残高設定より前に普通の買い物が来ている場合だ。
その場合は、金額を決めようがない(マイナスにするのも変である)ので、ここでは処理しない。

 } else {
  next;
 }

最後が、店舗名が残高照会でもなく、残高がまだ設定されていない場合の処理である。
これはさっき書いた、残高設定より前の普通の買い物に当たる。
この場合は本明細を無視して次のfor周回に進む。

 $out->{hand} = $zan;

ここでは、各明細の残高を、handというハッシュ要素に入れている。

 push @sort, $out;
}

で、配列@sortに$outを追加する。

forに入る前は、@outはこうなっていた。

 日付    時刻 店舗   品目   金額
 2012-03-12 09:00 残高設定 -    5000
 2012-03-12 12:00 ココイチ カレー   900
 2012-03-12 10:00 ドトール コーヒー  150
 2012-03-12 13:00 ?    -     100

で、forループを経て、@sortはこうなっている。

 日付    時刻 店舗   品目   金額  残高
 2012-03-12 09:00 残高設定 -    5000  5000
 2012-03-12 10:00 ドトール コーヒー  150  4850
 2012-03-12 12:00 ココイチ カレー   900  4150
 2012-03-12 13:00 ?    -     100  4050

で、家計簿としてはこれでカンペキだが、実際に携帯端末でちょこちょっと見るときは時刻で降順(最新の明細がトップ)にしたいので、配列を逆転してみた。

@sort = reverse @sort;

あっでもさっきforじゃなくてpushじゃなくてunshiftを使えば配列を逆転できたのね。
いま気づいた。

ということでHTML::Templateに入るが、すみませんまた来週・・・。

Subscribe with livedoor Reader

Add to Google

RSS