Chienomi

Linuxで家計簿ユーティリティ

開発::util

2020年はちょっと大きなローンを組んだので、その都合で正確な家計簿をつける必要があった。

といっても要件は

  • 収入を記入する
  • 支出をカテゴリに分けて記入する
  • 何に対してどれくらい支出し、収支どれくらいだったかを数値とグラフで出す

というもので、エクセルマスターなみなさまならきっと必要ないようなものだ。

だが、私は表計算は使えないし、Excelに至っては使ったことがないので、無理だ。 そこで、簡単にできるようにスクリプトを書いた。

家計簿の生成

次のようなスクリプトを作った。

#!/usr/bin/ruby
# -*- mode: ruby; coding: UTF-8 -*-

require 'yaml'
require 'erb'

colors = %w:#C24E56 #6553C0 #74CDBF #89CD74 #F6894A #DA4DB7 #4D8DDA #E0DE52 #584272 #64D3D8 #AFEF7D #FD668C:

WIDTH_TOTAL_FACTOR = 0.001
WIDTH_ELEMENT_FACTOR = 0.003

TEMPLATE = <<EOF
<html>
  <head>
    <title>Balance Sheet</title>
    <style>
span {
  display: inline-block;
  margin: 0px;
  padding: 0px;
  border-width: 0px;
}
    </style>
  </head>
  <body>
    <h1>Summery</h1>
    <table>
      <caption>BALANCE</caption>
      <tbody>
        <tr><th>収入</th><td style="text-align:right;"><%= count[:intotal] %></td></tr>
        <tr><th>支出</th><td style="text-align:right;"><span style="color: #c03;"><%= count[:outtotal] %></span></td></tr>
        <tr><th></th><td style="text-align:right;"><span
<% if (count[:intotal] - count[:outtotal]) < 0 %>
  style="color: #c03;">▲
<% else %>
>
<% end %>
<%= count[:intotal] - count[:outtotal] %></span></td></tr>
      </tbody>
    </table>
    <%=  graph[:in] %>
<% graph[:in_keys].each_with_index do |k, index| %>
<div style="margin-top: 2px; margin-bottom: 2px;"><span style="width: 1em; hegiht: 1em; background-color: <%= colors[index % colors.length] %>"> </span><%= k %> (<%= sheet["in"][k] %>)</div>
<% end %>
    <%=  graph[:out] %>
<% graph[:out_keys].each_with_index do |k, index| %>
<div style="margin-top: 2px; margin-bottom: 2px;"><span style="width: 1em; hegiht: 1em; background-color: <%= colors[index % colors.length] %>"> </span><%= k %> (<%= (sheet["out"][k]&.values || [0]).sum %>)</div>
<% end %>
    <h1>Detail</h1>
<%
  nc = 0
  sheet["out"].each do |category, items|
    next unless items
%>
    <h2><%= category %></h2>
<%
  n = 0
  items.each do |k, v| %>
    <div><span style="<%= sprintf('background-color: %s; width: %d;', colors[n % colors.length], (v * WIDTH_ELEMENT_FACTOR)) %>"> </span><%= k %>(<%= v %>)</div>
<%
  n += 1
  end %>
  <div style="line-height: 1.8; vertical-align: middle; height: 2em; color: <%= colors[nc % colors.length] %>">TOTAL: <%= items.values.sum %></div>
<%
  nc += 1
  end %>
    <h1>Memo</h1>
    <ul>
<% sheet["memo"].each do |i| %>
      <li><%= i %></li>
<% end %>
    </ul>
  </body>
</html>
EOF

sheet = YAML.load ARGF

count = {}

count[] = sheet["in"].values.sum
count[] = sheet["out"].map {|k, v| v&.values || 0}.flatten.sum

graph = {}
keys = sheet["in"].keys
graph[] = keys
graph[] = keys.each_with_index.map {|i, index| sprintf('<span style="background-color: %s; width: %d; height:1em;"></span>', colors[index % colors.length], sheet["in"][i] * WIDTH_TOTAL_FACTOR) }.join
graph[] = {}

categories = sheet["out"].keys
graph[] = categories
graph[] = categories.each_with_index.map {|i, index| sprintf('<span style="background-color: %s; width: %d; height:1em;"></span>', colors[index % colors.length], (sheet["out"][i]&.values || [0]).sum * WIDTH_TOTAL_FACTOR) }.join

ERB.new(TEMPLATE).run(binding)

これを使うと単独HTMLでグラフなども確認できる。

出力されたHTMLの例

家計簿のフォーマット

先のスクリプトはARGFで会計YAMLファイルを与える必要がある。 収入にはカテゴリがなく、支出にはカテゴリがあるので階層がひとつ違う。

in:
  Company: 1000000
  MyJob: 500000 
out:
  食品:
    0101-おにぎり: 300
    0102-おにぎり: 300
    0103-カップラーメン: 150
    0104-ランチパック: 140
  パソコン:
    AMD Ryzen9 5950X: 120000
    SSD: 53500
  その他:
    現金: 100000
memo:
  - もっとちゃんとしたものを食べよう
  - 今月は5950Xを買ったので支出が多かった

割と簡単に書ける。そして、YAMLであるためにさらに楽をできるようになっている。

正確な値を出したいわけでもないので、現金についてはいくら財布に入れたかで計算し、注意すべき動きはmemo欄に書いている。 この場合重要なのは、「現金が決まった額で運用できているか」だからだ。

カードからの変換

実際のところこの家計簿は「現金を使うのは日常の額が小さな生活費だけで、カードで決済することを前提にカードの利用をカウントする」という前提に立っているから、カードからの変換作業というのがとても大事になる。 それを毎回手動でやるととてもめんどくさいので、半自動化したい。

まずはEPOSカードのCSVファイルからYAMLに変換するスクリプト。 請求元の名前以外に面倒な要素がなくて割と簡単。

#!/bin/ruby
require 'csv'
require 'yaml'

record = {}

csv = CSV.parse(ARGF.read.encode(Encoding::UTF_8, Encoding::CP932)).to_a
csv.shift
total = csv.pop

namemap = YAML.load(File.read("namemap.yaml"))
namemap.default_proc = ->(h, k) { k }

csv.sort_by {|i| i[1] }.each do |i|
  record[sprintf("%s%s-%s%s", i[1][5, 2], i[1][8, 2], namemap[i[2].tr("-", "ー").unicode_normalize().strip], (i[6].to_i == 1 ? "" : "(/#{i[6].to_i})"))] = i[5].to_i
end

YAML.dump(record, STDOUT)

これでYAML形式で出力される。

自動的にカテゴリ分類するのは難しいので、カテゴリは手動で分けてカット&ペーストする。 VSCodeならばCtrl+]でインデントできるから、後から揃えるのは難しくないのだが、一応他のエディタでもマージしやすいよう、高さは合うようにネストしてある。

EPOSカードのポイントは、請求元の名前が全角スペースで埋められていることだろう。 また、分割時に「全何回か」という情報はあるが、「何回目か」という情報がない。

namemapは非常によく使うところで、請求元表示がわかりにくいものをわかりやすく変換するためのもの。 YAMLで書かれた対応表だ。

次に示すのは三井住友カードのCSVからの変換スクリプト。

#!/usr/bin/ruby

require 'yaml'
require 'csv'

csvstr = ARGF.read.encode(Encoding::UTF_8, Encoding::CP932).unicode_normalize()

csv = CSV.parse(csvstr).to_a

headerline = csv.shift
totalline = csv.pop

namemap = YAML.load(File.read("namemap.yaml"))
namemap.default_proc = ->(h, k) { k }

record = {}

csv.each do |i|
  record[sprintf('%s-%s%s', i[0][5, 5], namemap[i[1]], (i[3].to_i == 1 ? "" : "(#{i[4]}/#{i[3]})") )] = i[5].to_i
end

YAML.dump({"out" => {"category" => record}}, STDOUT)

こっちは手動でやるのは難しい要素がある(分割回数などが全角になっているなど)ので、楽にするためNFKC正規化して対処している。 EPOSとは表記が違うのでnamemapが共有できないのが残念。

Lifecardは超めんどくさいフォーマットになっているので(CSVと言っているけど、データとして扱えるCSVではなく、単にExcelで整形表示するためのデータ)複雑。

#!/usr/bin/ruby
require 'yaml'
require 'csv'

normal_line = []
split_line = []

datafile = ARGF.read.encode(Encoding::UTF_8, Encoding::CP932).unicode_normalize().gsub("\r\n", "\n")

datafile.each_line do |line|
  if line =~ /^明細No\.,契約,/ ... line =~ /^$/
    normal_line.push(line)
  end
end

normal_line.shift
normal_line.pop

detail = CSV.parse(normal_line.join)

namemap = YAML.load(File.read("namemap.yaml"))
namemap.default_proc = ->(h, k) { k }

record = {}

detail.each do |i|
  splitter = if i[2] == "分割"
    i[9] =~ %r:(\d+)/(\d+):
    "(#$2/#$1)"
  else
    ""
  end

  record[ sprintf('%s-%s%s', i[3][5, 5], namemap[i[4]], splitter ) ] = i[10]
end

YAML.dump({"out" => {"category" => record}}, STDOUT)

flip-flopは以前一時期消滅の危機にあった機能だ。(無事復活した)

もうひとつ単純な計算

お金の変動自体は銀行口座を定点観測するのが一番早い。 クレジットカードは結局銀行のお金が動くし、手持ち現金はある程度一定になるようにしているから足りなくなればより多く口座から減るし、余れば口座から減る額が小さくなる。

そこで、それ用のディレクトリに

date: value
date: value

という形式の単純なファイルを各口座ごとに用意する。 dateは年月、valueはその月の給与振込時の残高。

定点観測するタイミングは「会社の給与振込時」にした。 ただし、「給与振込後振替をした場合は、振替をする前の残高」に固定しないと増減がわからないことになる。

そして

#!/usr/bin/ruby

total_count = Hash.new(0)
total_diff = Hash.new(0)

ARGV.each do |file|
  last_val = nil
  puts "\e[36m*** #{File.basename(file)}\e[0m"
  File.foreach(file) do |line|
    val = line.chomp.split(" ")
    i = val[1].to_i
    last_val ||= i
    d = i - last_val
    val.push(d < 0 ? "\e[35m" : "\e[32m")
    val.push(d)
    printf("\e[34m%s:\e[0m %8d %s(%+8d)\e[0m\n", *val)
    last_val = i
    total_count[val[0]] += i
    total_diff[val[0]] += d
  end
  puts
end

puts "\e[36m##### TOTAL COUNT #####\e[0m"
total_count.each_key do |k|
  v = total_count[k]
  d = total_diff[k]
  ansi = d < 0 ? "\e[35m" : "\e[32m"
  printf("%s: %8d (%s%+8d\e[0m)\n", k, v, ansi, d)
end

という感じで見ると(ANSIカラーを使っているので端末用)、色付きで

*** UFJ
202010: 1000000 (+500000)
202011: 500000 (-500000)
202012: 3000000 (+2500000)

って感じで見えるので良い。

家計簿のほうは「その月にどれだけお金が動いたか」の計算なので、直接口座の増減と連動しないのだけど、 こっちのほうは残高で見えるので「逼迫しそうかどうか」という観点で見えるから、補助的に使える。

口座残高表示

家計簿のほうはグラフも出るしどちらかというと分析用。

カラム表示バリエーション

やることは同じだが、端末に十分な幅があること(そして口座の数はあまり多くないこと)を前提として横並びで表示するバージョン。 こっちは色々と工夫が必要になっている。

#!/usr/bin/ruby

file_list = []
monthly = {}
total = {}

ARGV.each do |file|
  file_list.push(file)
  last_val = nil
  File.foreach(file) do |line|
    val = line.chomp.split(" ", 2)
    month = val.shift
    val[0] = last_val if val[0] == "-"
    i = val[0].to_i
    last_val ||= i
    d = i - last_val
    val.push(d < 0 ? "\e[35m" : "\e[32m")
    val.push(d)
    monthly[month] ||= {
      [],
      0,
      0
    }
    monthly[month][].push sprintf("%8d %s(%+8d)\e[0m", *val)
    monthly[month][] += i
    monthly[month][] += d
    last_val = i
  end
end

puts("Month   " + file_list.map {|i| sprintf("%19s", File.basename(i))}.join(" ") + "|" + sprintf("%19s", "TOTAL"))
puts("-" * (8 + 19 * file_list.length + (file_list.length - 1)) + "+" + "-"  * 19)

monthly.keys.sort.each do |k|
  v = monthly[k][]
  d = monthly[k][]
  ansi = d < 0 ? "\e[35m" : "\e[32m"
  printf("%s: %s|%8d %s(%+8d)\e[0m\n", k, monthly[k][].join(" "), v, ansi, d)
end

計算量にある程度気を使った設計にしているけど、別に愚直にループを回しても実際には問題はないだろう。

仕様上の差異として、行別バージョンでは問題のなかった「当該月のエントリが全ての口座にない」や「月の表記にゆれがある」が問題になるので、こちらのほうが少し繊細である。 ただ、利用可能な条件下ではこちらのほうが良いように見える。

口座残高表示 カラム版

ビギナー向けの話

プログラミングというのは単純に日常的にやっていればいいとか、勉強だとかそういうものじゃなくて、実用的な芸術なので、それを日常にいかに活かすかということが大事だし、自分の能力を日常にどんどん還元していったほうがいい。

その尺度において重要なのが、「いかにして問題を解決するか」だ。 別の言い方をすると、「どうなったとき、その状態は充足するか」である。

職業プログラマはその発想に至りにくかったりするかもしれないが、「要件定義」では手遅れな話である。 ハッカーたるもの、その方法は問わず解決に至らしめるものだ。

今回の場合、Excelが使えれば一発だろう、って話なので、コードを書くのでなく私が(MS Officeを使うのは無理な話なので、LibreOffice Calcを使うにしても)アプリの使い方を習得する、という選択肢もあった。 むしろそれが本筋だ。

今回の場合、記録の粒度はどの程度必要なのか? なんのための家計簿なのか? 何ができれば良いのか? どういうデータなら理解できるか? どういうデータなら未来につながるか? といろいろなことを考える必要がある。

実際、今回の場合は「ローンを組むにあたって、どの程度の額なら組めて、どの程度の余裕があるか」を把握することが第一で、収支を把握する必要がある。

そして、十分な収入があるということは分かっているので、それでローンに躊躇するような収支状況なら改善されなければならない。 なににどれくらいのお金が出ているという分析が必要だが、それはボリューム的な話と、そのボリューム構成を知る必要があるという意味だ。 だから詳細さはある程度参照・把握可能な程度であり、それ以上にボリュームとして把握できる必要があり、「食費が大きい」「ゲームに費やしすぎている」などざっくりした傾向を把握した上で、「どのようにすればどれくらい節約できるか」という判断をもとにそこが削れるかということを考え、削れる、削るべきだと思えばどれくらい削るかという額を出してその額を指標に消費を調整するということになる。

ボリュームを把握するにはグラフがほしいし、具体的な数字もほしい。 視覚的にぱっと見分けられることは必要で、そう考えるとディスク消費量と同じような表示がほしいんだよなー、となって、 グラフ生成の手間を考えるとHTMLだと簡単だなーとなってこの着地だ。

そして、その表から実際の口座の動きが見えず、どの程度使って大丈夫なのかが見えてこないので、別に計算できるように作った。

この機能は記帳しなければなんの意味もないので、楽に記帳できるようにユーティリティを書いた。 全体の収支ボリュームを知る必要があるものだから、原則クレジットカードで処理するようにすれば扱いが楽だ。

このようにしてできたプログラムで、そんなに時間はかけていないし、かなり楽をして書いたものだが、実生活への貢献度は高く、このプログラムによって念願だった買い物が実現した。

「こうだったら良い」を見つけ出し、「どうだったら良い?」を考え、それを成す道を考えることは、根幹をなすはずだ。