Ark SiteColumn & Report Arkアーキテクチャ
TOP XMLアカデミー XMLで作る「iモード家計簿」

 XMLで何かできないか、と思いながら日々を暮らしていたら、格好のネタがありました。そう、家計簿です。このページでは、大の家計簿好きの筆者がiモード対応XML家計簿を3日で開発した経緯と、開発で培ったXSLTプログラミングのコツとをご紹介します。
※XML家計簿については、関連記事が『JavaWorld』2月号に掲載されています。
今回のこの記事は、Arkプロダクトチームには参戦していないながらも、XMLを得意とするニューフェイス:Yが担当します。



   XMLアカデミー XMLで作る「iモード家計簿」 
 
'理想の家計簿'を求めて
Web、そしてXMLの出番がやってきた
月データXMLの設計
CGIによるXMLファイルの生成
月データを一覧表示させるXSLTの開発
SVGによる円グラフの描画
おわりに



 筆者は、2年ほど前から、断続的にではあるが家計簿をつけていた。自宅のPCにインストールした一般的な家計簿ソフトを使用していたのだが、次第にそれでは飽き足らなくなってきた。最大の不満は、「インストールしたPCでしか家計簿をつけられない」という点だ。もちろん、家庭内LANを使えば、ほかのPC上でも家計簿をつけることはできるのだが、結局は家に居るときにしかつけられない。これでは、例えば旅行などで家を長く空けたようなときに、帰宅してから溜まった家計簿をつけるのが大変だ。そうなると、ついついサボりがちになってしまう。一度サボってしまうと、ときには2週間くらいほったらかしてしまうことも……。

 幾度かそうした苦い経験をするうちに、いつしか筆者は「いつでもどこでもつけられる家計簿」を夢見るようになった。そして、「ならばいっそのこと、自分で理想の家計簿を作ってみようじゃないか」と思い立ったわけである。


 「いつでもどこでもつけられる家計簿」を、具体的にどう実現するか。最初に考えたのは、データを簡単に受け渡しできるような家計簿ソフトウェアを作り、家のPC、ノートPC、会社のPCでそのソフトを使うという方式だった。だが、実際にC++のプログラムを作りかけてみて、すぐに挫折した。その程度ではまだ不便だし、開発も大変である。抜本的な解決を図るには、扱うデータを1つにして、それを共有するような方法をとることが望ましい。

 「1つのデータにさまざまな場所からアクセスしたい」ということなら、手軽なのはWebである。インターネットにつながったWebブラウザのある環境なら、どこからでもデータにアクセスできる。しかも、iモード対応携帯電話から入力が行えるので、ノートPCを持ち歩く必要もない。携帯電話なら、レジで買い物を終えたその場で家計簿をつけることも十分に可能だ。“クリック&モルタル”ならぬ、“モルタル&クリック”というわけである※1。この図式を頭に描きはじめた頃は、筆者はまだiモードの携帯を持っていなかったのだが、ある日、美しいボディが気に入って衝動買いしてしまった。あとは、開発を始めるばかりだ。

 プロトタイプには、最低限の基本機能として、データ入力/表示の機能を実装することにした。早速、入力用CGIの設計に取りかかったが、そこで、当然ながら家計簿データのフォーマットを検討する必要が生じてきた。「バイナリにするか、それともテキストにするか、区切りはカンマで行うか……」などと考えているうちに、少し憂鬱になってきた。独自のフォーマットには限界がある。いくら、将来の変更に備えた設計、処理ルーチンの作成などに意を尽くしても、“記憶の風化”とともにメンテナンスがしづらくなるだろうし、作った処理ルーチンを他の目的に応用するのは難しいだろう。

 そこで思い当たったのが、XMLの利用だ。XMLなら、低レベルのフォーマット(シンタックス)を独自に定める必要がないので、データに持たせる意味的な構造の設計に専念することができる。また、拡張性も高いし、標準規格なので、他のシステムとの連携でも威力を発揮するだろう。パーサなど処理系ライブラリもすでに多く出回っている。しかも、WebでXMLを利用するなら、表示部分はWebブラウザとXSLTに任せることによって開発効率を高めることができる。グラフの表示も、XSLTによってSVGに変換してしまえば簡単に行えそうだ。

 以上のコンセプトに基づき、プロトタイプの構成は、ひとまず図1のように定めた。なお、PerlのCGIを採用した理由は、たまたま契約しているプロバイダーで簡単に実現可能だったためだ。

図1
※1 クリック&モルタルとは、無店舗型のビジネス手法に対して、実際の店舗とインターネットを組み合わせたビジネス手法を指す。国内では、「TSUTAYA Online」の試みなどが知られている。


 家計簿データを格納するXMLファイルは、月単位で作成することとした(リスト1)。具体的には、要素monthlyの下に、その月の全入力内容を格納する。入力内容は、1件につき1つの要素lineとした。要素lineの属性typeには、その入力が収入(income)、支出(outgo)、移動(move)、残高入力(check)のいずれであるかを記述する。また、出金元口座は属性fromに、入金先口座は属性toに、そして残高入力した口座は属性accountに記述する(ここで言う「口座」には、銀行口座に限らず、現金、カード、金券なども含める。いわば“財布単位”というわけである)。さらに、金額は属性amountに、費目は属性groupに、そして摘要を要素の値としてそれぞれ記述することにした。

 ちなみに、残高入力には、ある時点におけるある口座の残高を自由に入力できるようにし、それ以前の帳簿上の残高との差分があれば自動的に不明金として扱うようにしている。これは、以前に使っていた家計簿ソフトからの改良点の1つだ。

 月データXMLがこのかたちに落ち着くまでには、多少の試行錯誤があった。例えば、残高入力した口座は、当初は出金元を意味する属性from(支出/移動で使用)を用いて表現していた。だが、後にXSLTを作成する段階で、属性fromを一律に出金元口座として取り扱えないと、口座残高の算出処理で余計なコードを書かなくてはならないことが判明した。そこで、残高入力では属性accountを使用するように変更した。

リスト1 :月データX ML ファイル「2001-01.xml
001
002
003
004
005
006
007
008
009
010
011
012
<?xml version="1.0" encoding="Shift_JIS" ?>
<!-- Copyright (c) 2000 Yasuko Yokota -->
<monthly year="2001" month="1">
 <line type="check" day="1" account="現金" amount="10061" group=""/>
 <line type="check" day="1" account="XML銀行" amount="128606" group=""/>
 <line type="outgo" day="1" from="現金" amount="230" group="交通費">地下鉄</line>
 <line type="outgo" day="1" from="現金" amount="500" group="趣味・娯楽">雑誌</line>
 <line type="move" day="2" from="XML銀行" to="現金" amount="10000" group=""/>
 <line type="outgo" day="2" from="現金" amount="672" group="食費">お弁当</line>
 <line type="check" day="4" account="現金" amount="18109" group=""/>
 <line type="income" day="4" to="現金" amount="2000" group="贈答"/>
</monthly>


 月データXMLは、PerlによるCGIプログラムで生成する。CGI内部で行う処理は、HTMLのフォームから入力データを受け取って上述のXMLファイルを出力し、処理結果を通知するという単純なものだ。むろん、本格的な運用に際しては、ユーザー認証、月が変わった際の初期データの生成、過去のデータの変更など、さまざまな機能が必要となるだろう。

 XMLファイルの生成に関しては、DOMを使用するかどうかで少し迷った。DOMを使うことには、整形式(well-formed)のXMLファイルを確実に生成できるというメリットがあるからだ。だが、CGIではサーバへの負荷を軽減することが重要なので、結局、単純にprint命令によってタグを出力させることにした。


 次に必要となるのが、月別のデータを一覧にするためのXSLTスタイルシートである。

 まず、XSLTで処理するのは、月ごとのデータ・ファイルそのものではなく、それを外部参照として含んだ一覧表示ラッパXMLファイル(リスト2)とした。後ほど詳しく説明するが、このようにした理由は、別ファイルで設定した口座情報XMLファイル(リスト3)を利用しながら表示させたいがためである。ラッパXMLを、XSLTでXHTML形式に変換した結果は、画面1のようになる。ちなみに、この画面では、JavaScriptなどは使用していない。XSLTとXPATHの規格だけでもそれなりの計算処理を行うことができるのだ。

 ところで、XSLTにはかなり高度なシステム・ロジックを記述することができるが、それには相応のテクニックが必要となる。そこで以下に、一覧表示XSLTのソースを例にとり、実践的なテクニックを5つほど紹介しよう。なお、以下に説明するXSLTスタイルシートを「Internet Explorer(以下、IE) 5」上で画面1のように表示させるには、XSLT 1.0に対応したモジュール(本稿執筆時点での最新版は「MSXML3.dll」)を別途インストールする必要がある。

画面1

リスト2 :一覧表示ラッパXML「monthlyList.xml
001
002
003
004
005
006
007
008
009
010
011
012
<?xml version="1.0" encoding="Shift_JIS" ?>
<!-- Copyright (c) 2000 Yasuko Yokota --> 
<?xml-stylesheet href="viewMonthly.xsl" type="text/xsl"?>
<!DOCTYPE doc
[
<!ENTITY list SYSTEM "2001-01.xml">
<!ENTITY accounts SYSTEM "accounts.xml">
]>
<target>
 &list;
 &accounts;
</target>

リスト3 :口座情報XML「accounts.xml
001
002
003
004
005
006
<?xml version="1.0" encoding="Shift_JIS" ?>
<!-- Copyright (c) 2000 Yasuko Yokota -->
 <accounts>
 <account name="現金"/>
<account name="XML銀行"/>
</accounts>


■ポイント1−同じ変換結果を複数個所で利用する
 ルート要素に対する処理では、「2001年1月の家計簿」といったタイトル文字列を2カ所で使用する。そこで、タイトル文字生成部分を別テンプレートにして、<xsl:apply-templates>をつかって呼び出すことで、サブルーチンのように動作させることができる(リスト4)。

 ちなみに、要素monthlyに対して処理を行うのはタイトル用のテンプレートだけではない。一覧表、収支計算に関するテンプレート類でもmonthly要素を対象としているため、mode属性によってそれらを区別している。

リスト4:文書ルートに対するテンプレート(「viewMonthly.xsl」から抜粋)
001
002
003
004
005
006
007
008
009
010
011
012
013
014
<xsl:template match="/">
 <html>
  <head>
   <title>
    <xsl:apply-templates mode="title"/>
   </title>
  </head>
  <body>
   <h2><xsl:apply-templates mode="title"/></h2>
   <xsl:apply-templates mode="list"/>
   <xsl:apply-templates select="//accounts"/>
  </body>
 </html>
</xsl:template>


■ポイント2−数字を3桁ごとに区切る
 数字を3桁ごとにコンマを入れて表示するには、以下のように、XSLTのformat-number関数を使う。

 <xsl:value-of select="format-number( @amount, '###,##0' )"/>


■ポイント3−パラメータを利用する
 リスト5では、各口座の収入合計を得るために、mode属性の値にsumIncomeを指定してテンプレートを呼び出しているが、その際、呼び出し先のテンプレートに対してパラメータtargetAccountを渡している。テンプレートにパラメータを渡すようにすることで、異なる口座に対する合計処理を同じテンプレートで行うことができるのだ。

 パラメータを指定してテンプレートを呼び出すには、以下のように記述する。($accountには、for-eachで回している口座要素が入る。)

<xsl:apply-templates mode="sumIncome" select="//monthly">
  <xsl:with-param name="targetAccount" select="$account"/>
</xsl:apply-templates>


 templateを呼び出す方法には、<xsl:apply-templates>を使う方法のほかに、<xsl:call-template>を使う方法もあるが、どちらの方法でも<xsl:with-param>を使ってパラメータを渡すことが可能だ。<call-template>では、カレントノードに関係なく名前付きテンプレートを呼び出すので、カレントノードに依存したテンプレートは<xsl:apply-templates>を、そうでないものは<call-template>を使って呼び出すようにすればわかりやすいだろう。

 ちなみに、リスト5の後半では別の再帰テンプレート(リスト6)を呼び出しているが、これについてはポイント5で説明する。

リスト5:収支合計表への変換テンプレート(「viewMonthly.xsl」から抜粋)
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
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
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
<!-- 各口座ごとの収入・支出・不明金・収支・残高と合計 -->
<xsl:template match="accounts">

 <table border="2">
  <tr><th colspan="8">【口座情報】</th></tr>
  <!-- 項目欄 -->
  <tr>
   <th> </th>
   <th>収入</th>
   <th>支出</th>
   <th>不明金</th>
   <th>収支</th>
   <th>移入</th>
   <th>移出</th>
   <th>残高</th>
  </tr>

  <!-- 口座ごとに -->
  <xsl:for-each select="account">
   <xsl:variable name="account" select="@name"/>
   <tr>
    <!-- 口座名 -->
    <th><xsl:value-of select="$account"/></th>
    <!-- 収入計算 -->
    <xsl:variable name="incomeSum">
     <xsl:apply-templates mode="sumIncome" select="//monthly">
      <xsl:with-param name="targetAccount" select="$account"/>
     </xsl:apply-templates>
    </xsl:variable>
    <xsl:variable name="outgoSum">
     <xsl:apply-templates mode="sumOutgo" select="//monthly">
      <xsl:with-param name="targetAccount" select="$account"/>
     </xsl:apply-templates>
    </xsl:variable>

    <!-- 収入表示 -->
    <td align="right">
     <xsl:value-of select="format-number( $incomeSum, '###,##0' )"/>
    </td>
    <!-- 支出表示 -->
    <td align="right">
     <xsl:value-of select="format-number( $outgoSum, '###,##0' )"/>
    </td>

    <!-- 現実の残高を計算 -->
    <xsl:variable name="realBalance">
     <xsl:apply-templates mode="sumAccount" select="//line
[@account=$account and @type='check'][position()=last()]"/>
    </xsl:variable>
    <!-- 仮想的な残高を計算 -->
    <xsl:variable name="virtualBalance">
     <xsl:apply-templates mode="sumAccount" select="//line
[@account=$account and @type='check'][position()=1]"/>
    </xsl:variable>

    <!-- 不明金表示 -->
    <td align="right">
     <xsl:value-of select="format-number( $realBalance - 
$virtualBalance, '###,##0' )"/>
    </td>
    <!-- 収支表示 -->
    <td align="right">
     <xsl:value-of select="format-number( $incomeSum - $outgoSum + 
$realBalance - $virtualBalance, '###,##0' )"/>
    </td>
    <!-- 移入計算 -->
    <xsl:variable name="moveInSum">
     <xsl:apply-templates mode="sumMoveIn" select="//monthly">
      <xsl:with-param name="targetAccount" select="$account"/>
     </xsl:apply-templates>
    </xsl:variable>
    <!-- 移出計算 -->
    <xsl:variable name="moveOutSum">
     <xsl:apply-templates mode="sumMoveOut" select="//monthly">
      <xsl:with-param name="targetAccount" select="$account"/>
     </xsl:apply-templates>
    </xsl:variable>
    <!-- 移入表示 -->
    <td align="right">
     <xsl:value-of select="format-number( $moveInSum, '###,##0' 
)"/>
    </td>
    <!-- 移出表示 -->
    <td align="right">
     <xsl:value-of select="format-number( $moveOutSum, '###,##0' 
)"/>
    </td>
    <!-- 残高表示 -->
    <td align="right">
     <xsl:value-of select="format-number( $realBalance, '###,##0' 
)"/>
    </td>
   </tr>
 </xsl:for-each>

 <!-- 各口座合計を求めるための再帰計算をして結果を表示 -->
 <tr>
  <th>合計</th>
   <!-- 収入合計計算 -->
   <xsl:variable name="incomeSum">
    <xsl:apply-templates select="account[position()=1]">
     <xsl:with-param name="type">income</xsl:with-param>
    </xsl:apply-templates>
   </xsl:variable>
   <!-- 支出合計計算 -->
   <xsl:variable name="outgoSum">
    <xsl:apply-templates select="account[position()=1]">
     <xsl:with-param name="type">outgo</xsl:with-param>
    </xsl:apply-templates>
   </xsl:variable>

   <!-- 収入合計表示 -->
   <td align="right">
    <xsl:value-of select="format-number($incomeSum, '###,##0')"/>
   </td>
   <!-- 支出合計表示 -->
   <td align="right">
    <xsl:value-of select="format-number($outgoSum, '###,##0')"/>
   </td>

   <!-- 現実の残高合計を計算 -->
   <xsl:variable name="realBalance">
    <xsl:apply-templates select="account[position()=1]">
     <xsl:with-param name="type">realBalance</xsl:with-param>
    </xsl:apply-templates>
   </xsl:variable>
   <!-- 仮想的な残高合計を計算 -->
   <xsl:variable name="virtualBalance">
    <xsl:apply-templates select="account[position()=1]">
     <xsl:with-param name="type">virtualBalance</xsl:with-param>
    </xsl:apply-templates>
   </xsl:variable>

   <!-- 不明金合計表示 -->
   <td align="right">
    <xsl:value-of select="format-number($realBalance - 
$virtualBalance, '###,##0')"/>
   </td>
   <!-- 収支合計表示 -->
   <td align="right">
    <xsl:value-of select="format-number($incomeSum - $outgoSum + 
$realBalance - $virtualBalance, '###,##0')"/>
   </td>

   <!-- 移入・移出合計はスキップ-->
   <td colspan="2"><hr/></td>

   <!-- 残高合計表示 -->
   <td align="right">
    <xsl:value-of select="format-number($realBalance, '###,##0')"/>
   </td>
  </tr>
 </table>

 </xsl:template>


■ポイント4−処理対象ノードの選択条件が複雑になる例
 4つ目のポイントは、残高入力を利用した口座残高集計の方法である。口座残高(現実の残高)の集計は、最も日付の新しい残高入力の金額から、それ以降のその口座への入出金を加減算していく(その前提として、データ入力CGIでは時系列順に要素listを出力し、月始めには、前月の集計から自動的に全口座の残高入力行を出力する)。

 それにはまず、データ中の最新の残高入力要素を抜き出し、その要素よりも順序が後になる要素lineを集計する。一般に、あるノードの集合に対して一括してある処理を行いたい場合は、処理の部分を別テンプレートにして、対象となる集合を<xsl:apply-template>の属性selectで限定してやると処理コードが書きやすい※2。ここでは、集計部分を別テンプレートにして、それを最新の残高入力要素に対して適用している。最新の残高入力要素を選択するには、以下のように記述すればよい。

<xsl:apply-templates mode="sumAccount"
select="//line[@account=$account and @type='check'][position()=
last()]"/>


 なお、ここで注意が必要なのは、以下のように書くのは誤りであるという点だ。

<xsl:apply-templates mode="sumAccount"
select="//line[@account=$account and @type='check' and position()=
last()]" />


 このように記述すると、last()の対象となるノード集合が、要素lineすべてとなってしまう。だが、last()の対象は、指定した口座に関する残高入力要素lineの集合でなくてはならないのだ。

 ちなみに、集計テンプレートの内部では、カレントノード(最新の残高入力要素line)よりも順序が後の要素lineを集計対象とするために、following-siblingを利用している(以下参照)。

<xsl:variable name="outgoSum"
select="sum(following-sibling::line[@from=$account]/@amount)"
/>


 同じ方法で、仮想的な残高も計算している。仮想的な残高とは、もっとも古い残高入力のデータ以降の収支を計算し、それより新しい残高入力を無視して計算する残高であり、これと現実の残高の差分が不明金となる。仮想的な残高を計算するには、同じテンプレートを呼び出すが、以下のように、対象(select)をもっとも文書順が若い残高入力行にする。

<xsl:apply-templates mode="sumAccount"
select="//line[@account=$account and @type='check'][position()=
1]"/>


■ポイント5−テンプレートの再帰を利用した'変数'演算
 XSLTのクセの一つとして、変数(xsl:variable)にいったん値を代入した後は、値を変更することができない点がある。しかし、for-eachなどのループ内で、値を順々に加算して合計を求めたいといった要求はしばしば生じる。このような目的は、テンプレートを再帰的に呼び出すことで、ある程度、果たすことができる。

 一覧表示XSLTでは、口座情報で、各口座の収入、支出、不明金、収支、残高を合計するのに、テンプレートの再帰(リスト6)を使っている。このテンプレートは、あるaccount要素に対して、そのaccount要素に関する値を算出し、その値と次のaccount要素に対してこのテンプレートを再帰的に適用した結果の値とを加算して返す。これは、for-eachで各account要素に関する値を算出して順次足していくのと同様の結果になる。

 なお、このテンプレートはポイント3で紹介したリスト5から呼び出される。

リスト6:収入合計の算出テンプレート(「viewMonthly.xsl」から抜粋)
001
002
003
004
005
006
007
008
009
010
011
012
013
<!-- 特定の口座の収入を合計 -->
<xsl:template match="monthly" mode="sumIncome">
 <xsl:param name="targetAccount"/>
 <xsl:choose>
  <!-- 該当入力データがあれば合計値、なければ0-->
  <xsl:when test="line/@amount[../@type='income' and ../@to=
$targetAccount]">
   <xsl:value-of select="sum(line/@amount[../@type='income' and ../@to=
$targetAccount])"/>
  </xsl:when>
  <xsl:otherwise>0</xsl:otherwise>
 </xsl:choose>
</xsl:template>


 以上で、ひとまずプロトタイプが完成した。このプロトタイプでは、PC上のブラウザやiモード対応携帯電話(画面2)から入力を行い、その結果をIE上で表示させることが可能である。

※2 ほかに、<xsl:for-each>を利用する方法もある。別テンプレート化する方法と比較すると、<xsl:for-each>を利用する方法には、「ソースを読む際に、それを入力側XMLの構造と見比べる必要性が少ない」、「テンプレートの競合や意図せぬ暗黙の呼び出しを防止できる」といった長所がある。ただし反面、短所として、「処理を再利用できない」、「ループ内のコード量が増えると全体の流れが見えにくくなる」といった点が挙げられる。どちらの方法を採用するかは、状況と目的に応じて選択するとよいだろう。なお、<xsl:for-each>は、カレントノードの子を順次選択するようなケースには最適だが、「ある条件のときにカレントノードを再選択したい」といったケースには向いていない。なぜなら、<xsl:for-each>の主機能は複数の対象の順次処理であるため、対象が1つであることがわかっているような状況で強引に使うと、ソースの可読性が落ちてしまうのだ。よって、このような場合には、別テンプレート化をお勧めする。

画面2



 さて、これで一応は動作するプログラムが出来上がったわけだが、いかんせんまだ機能が少ない。特に、筆者にとって家計簿をつける楽しみの1つともなっている「支出内訳を表す円グラフの表示機能」はぜひ欲しいところだ。そこで、SVGを利用した、支出内訳の円グラフの生成に挑戦することにした。基本方針は、月データをXSLTでSVGに変換し、SVGプラグイン(アドビ システムズの「SVG Viwer」)によってWebブラウザ上で見られるようにすることである。

 まず、変換のゴールとなるSVGのソースは、リスト7のようになる。このプログラムでは、「200×200」のビュー・ポートの中に、(100,100)を中心点とする「半径80」の円グラフの描画を行う。

リスト7:円グラフのSVGソース
001
002
003
004
005
006
<?xml version="1.0" encoding="iso-8859-1"?>
<svg width="200px" height="200px" viewBox="0 0 200 200">
 <path d="M100,100 L100,20 a80,80 0 0,1 68,38 z" style="fill:red"/>
 <path d="M100,100 L168,58 a80,80 0 0,1 -77,121 z" style="fill:green"/>
 <path d="M100,100 L91,179 a80,80 0 0,1 9,-159 z" style="fill:purple"/>
</svg>


 1つの扇形に対応するのが、1つの要素pathである(以下参照)。

<path d="M100,100 L100,20 a80,80 0 0,1 68,38 z" style="fill:red"/>


 この要素pathは、「M100,100」で中心点(100,100)に移動し、「L100,20」で弧の開始点まで線を引く。「a80,80 0 0, 1 6,0 z」は、「半径80」の正円の円弧を点(68,38)まで描画する。そして、最後の「z」では、pathの開始位置(つまり円の中心)までこの図形を閉じる線を引く。

 なお、すでにお気づきの方もおられるだろうが、ここで1つ問題が生じる。月データからXSLTで変換する場合、データ内の各収支の割合を計算し、それを各扇形に対応させることになる。当然、比率は扇形の角度に対応する。ところが、上記のコードでは、扇形の角度を指定することができない。角度ではなく、円周上の終了点を求めなければならないのだ。

 終了点は、図2に示すように、角度からsin/cosを用いて求めることができる。だが困ったことに、XSLTとXPathには、sin/cos関数が用意されていない。

 ひとつの解決策として、図3に示すように、角度指定によって円グラフを表したXML形式の中間データ(リスト8)をXSLTで出力し、最後にそれをJavaの変換モジュール(ソースはSvgCircleGraph.java)で処理して完全なSVGに変換する方法が考えられる。月データを中間データに変換するXSLTでは、支出を費目ごとに集計し、支出合計に占める各費目の支出を角度に変換して出力している(リスト9)。

図2

図3


リスト8:XML形式の中間データ
001
002
003
004
005
006
007
008
<?xml version="1.0" encoding="Shift_JIS"?>
<svg xmlns:svgcg="http://www.myDomain/svg/cg" width="200" height="200">
 <svgcg:circle cx="100" cy="100" r="80">
  <svgcg:arc angle="59" color="blue"/>
  <svgcg:arc angle="128" color="red"/>
  <svgcg:arc color="green"/>
 </svgcg:circle>
</svg>


リスト9:月データを中間形に変換するXSLTファイル「toOutgoGraph.xsl
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
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
075
076
<?xml version="1.0" encoding="Shift_JIS" ?>

<xsl:stylesheet
 version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:svgcg="http://www.myDomain/svg/cg">

<xsl:variable name="width">200px</xsl:variable>
<xsl:variable name="height">200px</xsl:variable>
<xsl:variable name="viewBox">0 0 200 200</xsl:variable>
<xsl:variable name="cx">100</xsl:variable>
<xsl:variable name="cy">100</xsl:variable>
<xsl:variable name="r">80</xsl:variable>

<!--支出項目を、費目をキーにして取り出しておく-->
<xsl:key name="outgo" match="//line[@type='outgo']" 
use="@group"/>

<xsl:template match="/">
 <svg width="{$width}" height="{$height}" viewBox="
$viewBox">

 <!--全支出合計-->
 <xsl:variable name="sum" select="sum(//@amount
[../@type='outgo'])"/>

 <svgcg:circle cx="{$cx}" cy="{$cy}" r="{$r}">
  <!--費目ごとに-->
  <xsl:for-each select=
   "//line[generate-id(.)=generate-id( key('outgo', @group ) )]">
  <!-- この費目の支出合計を計算 -->
   <xsl:variable name="group" select="@group"/>
   <xsl:variable name="group-sum"
    select="sum( ../line/@amount[../@type='outgo'
    and ../@group=$group] )"/>
   <xsl:element name="svgcg:arc">
    <xsl:if
     test="position()!=last()"
    >
     <xsl:attribute name="angle">
      <xsl:value-of
       select="round($group-sum * 360 div $sum)"/>
      </xsl:attribute>
     </xsl:if>
     <xsl:attribute name="color">
      <xsl:choose>
       <xsl:when test="
        position()=last()"
       >
        purple
       </xsl:when>
       <xsl:when test=
       "position() mod 4 = 0">
        blue
       </xsl:when>
       <xsl:when test=
       "position() mod 4 = 1">
        red
       </xsl:when>
       <xsl:when test=
       "position() mod 4 = 2">
        green
       </xsl:when>
       <xsl:when test=
       "position() mod 4 = 3">
        yellow
       </xsl:when>
      </xsl:choose>
     </xsl:attribute>
    </xsl:element>
   </xsl:for-each>
  </svgcg:circle>
 </svg>
</xsl:template>

</xsl:stylesheet>




 また、XSLTプロセッサの中には、外部メソッドを拡張関数としてXSLTスタイルシート内から呼び出す機能を備えているものもある。例えば、「XT」や「Xalan」では、Javaのメソッドを呼び出すことが可能だ。そういった機能を利用すれば、XSLT変換一発で目的のSVGソースが得られる。特定のXSLTプロセッサの使用を決めている場合には、このアプローチのほうが、より少ない開発コストでXSLTに独自処理を追加できて良い。リスト10に、XalanのJAVAメソッド呼び出し拡張機能を使った、家計簿の月データを直接SVGソースに変換するXSLTスタイルシートを示す。(前者のJava変換モジュールとリスト10のXSLTスタイルシートはほぼ同じ働きをするので、2つのソースを比較しながら読むと、JavaとXSLTのプログラミング作法の違いがわかって面白いかもしれない。)

リスト10:Xalanの拡張機能をつかって円グラフSVGに変換するXSLTスタイルシート「toOutgoGraph2.xsl
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
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
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
<?xml version="1.0" encoding="Shift_JIS" ?>

<xsl:stylesheet
 version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:java="http://xml.apache.org/xslt/java"
 xmlns:svgcg="http://www.myDomain/svg/cg"
 exclude-result-prefixes="java">

<xsl:variable name="width">200px</xsl:variable>
<xsl:variable name="height">200px</xsl:variable>
<xsl:variable name="viewBox">0 0 200 200</xsl:variable>
<xsl:variable name="cx">100</xsl:variable>
<xsl:variable name="cy">100</xsl:variable>
<xsl:variable name="r">80</xsl:variable>
<!-- 初期開始点 -->
<xsl:variable name="x0">100</xsl:variable>
<xsl:variable name="y0">20</xsl:variable>

<!--支出項目を、費目をキーにして取り出しておく-->
<xsl:key name="outgo" match="//line[@type='outgo']" 
use="@group"/>

<xsl:template match="/">
 <svg width="{$width}" height="{$height}" 
viewBox="{$viewBox}">

  <!--全支出合計-->
  <xsl:variable name="sum" select="sum(//@amount
[../@type='outgo'])"/>

  <!--費目ごとに-->
  <xsl:apply-templates mode="arc" select="//line
[generate-id(.)=generate-id( key('outgo', @group ) )][position()=1]">
   <xsl:with-param name="prevColor"/>
   <xsl:with-param name="angleSum">0</xsl:with-param>
   <xsl:with-param name="x" select="$x0"/>
   <xsl:with-param name="y" select="$y0"/>
   <xsl:with-param name="sum" select="$sum"/>
  </xsl:apply-templates>
 </svg>
</xsl:template>

<!-- 角度と開始点を再帰的に取り扱いながら次の扇形を描いていく -->
<xsl:template mode="arc" match="line">
 <xsl:param name="angleSum"/><!-- 前までの角度合計 -->
 <xsl:param name="prevColor"/><!-- 前の色 -->
 <xsl:param name="x"/><!-- 開始点x座標 -->
 <xsl:param name="y"/><!-- 開始点y座標 -->
 <xsl:param name="sum"/>

 <!-- 角度の算出 -->
 <xsl:variable name="group" select="@group"/>


 <xsl:variable name="group-sum"
  select="sum( ../line/@amount[../@type='outgo'
  and ../@group=$group] )"/>

 <xsl:variable name="angle">
  <xsl:choose>
   <!-- 次があるかで角度計算方法を変える -->
   <xsl:when test="following-sibling::line[generate-id(.)=generate-id( key
('outgo', @group ) )]" >
    <xsl:value-of select="round($group-sum * 360 div $sum)"/>
   </xsl:when>
   <xsl:otherwise><xsl:value-of select="360 - 
$angleSum"/></xsl:otherwise>
  </xsl:choose>
 </xsl:variable>

 <!-- 色の決定 -->
 <xsl:variable name="color">
  <xsl:choose>
   <xsl:when test="not(following-sibling::line[generate-id(.)=generate-id( key
('outgo', @group ) )])">purple</xsl:when>
   <xsl:when test="$prevColor='blue'">red</xsl:when>
   <xsl:when test="$prevColor='red'">green</xsl:when>
   <xsl:when test="$prevColor='green'">yellow</xsl:when>
   <xsl:otherwise>blue</xsl:otherwise>
  </xsl:choose>
 </xsl:variable>

 <!-- 開始点の、初期開始点からの相対座標を求める -->
  <xsl:variable name="sx" select="$x - $x0"/>
  <xsl:variable name="sy" select="$y - $y0"/>

 <!-- 終了点の、初期開始点からの相対座標を求める -->
 <xsl:variable name="ex">
  <xsl:choose>
   <xsl:when test="following-sibling::line[generate-id(.)=generate-id( key
('outgo', @group ) )]">
    <xsl:value-of select="floor( $r * java:java.lang.Math.sin( 
java:java.lang.Math.toRadians( number($angle) + number($angleSum) ) ) )"/>
   </xsl:when>
   <xsl:otherwise>0</xsl:otherwise>
  </xsl:choose>
 </xsl:variable>
 <xsl:variable name="ey">
  <xsl:choose>
   <xsl:when test="following-sibling::line[generate-id(.)=generate-id( key
('outgo', @group ) )]">
    <xsl:value-of select="floor( $r * ( 1 - java:java.lang.Math.cos( 
java:java.lang.Math.toRadians( number($angle) + number($angleSum) ) ) ))"/>
   </xsl:when>
   <xsl:otherwise>0</xsl:otherwise>
  </xsl:choose>
 </xsl:variable>

 <!-- 終了点を、開始点からの相対座標に変換する -->
 <xsl:variable name="rex" select="$ex - $sx"/>
 <xsl:variable name="rey" select="$ey - $sy"/>

 <!-- largeArcFlag -->
 <xsl:variable name="largeArcFlag">
  <xsl:choose>
   <xsl:when test="$angle &lt; '180'">0</xsl:when>
   <xsl:otherwise>1</xsl:otherwise>
  </xsl:choose>
 </xsl:variable>

 <!-- 自分自身の描画 -->
 <xsl:choose>
  <!-- 終了点が開始点なら、円を描く -->
  <xsl:when test="$ex = '0' and $ey = '0' and $angle &gt; '180'">
   <circle cx="{$cx}" cy="{$cy}" r="{$r}" 
style="fill:{$color}"/>
  </xsl:when>
 <!-- それ以外は扇形を描く -->
 <xsl:otherwise>
  <path
   d="M{$cx},{$cy} L{$x},{$y} a{$r},{$r} 0 {$largeArcFlag},1 {$rex},{$rey} z"
   style="fill:{$color}"/>
  </xsl:otherwise>
 </xsl:choose>

 <!-- 足して次を呼ぶ -->
 <xsl:if test="following-sibling::line[generate-id(.)=generate-id( key
('outgo', @group ) )]">
  <xsl:apply-templates mode="arc" select="following-sibling::line
[generate-id(.)=generate-id( key('outgo', @group ) )][position()=1]">
   <xsl:with-param name="angleSum" select="$angle + 
$angleSum"/>
   <xsl:with-param name="prevColor" select="$color"/>
   <xsl:with-param name="x" select="$x + $rex"/>
   <xsl:with-param name="y" select="$y + $rey"/>
   <xsl:with-param name="sum" select="$sum"/>
  </xsl:apply-templates>
 </xsl:if>
</xsl:template>

</xsl:stylesheet>


 ところで、前出の一覧表示XSLTで、口座別残高を集計するために、口座設定ファイル(account.xml)を入力ファイルと“合体”させたことを思い出していただきたい。これは、各口座を<xsl:for-each>で順次処理するために行っていた。支出のあった各費目の集計でも同様のことが行えるのだが、その場合に、いちいち全費目名を一覧形式で入力XMLファイルに含ませるのは面倒である。実は、登場した費目を重複なくピックアップする方法があるので、以下にそれを紹介しておこう。
まず、XSLTのトップレベル(要素stylesheetの子)に対し、以下のように記述する。


<xsl:key name="outgo" match="//line[@type='outgo']" use="@group"/>


 これで、属性typeがoutgoである要素line(支出データ)を、属性groupの値(費目)に関連づけて記憶させることができる。
 記憶された要素lineを取り出すには、属性selectなどでkey関数を使用する。例えば、「key('outgo', '交通費' )」とした場合は、交通費の支出データの集合が返される。

 これとgenerate-id()関数とを組み合わせると、グルーピングが行える。generate-id()は引数に指定したノードを識別するための一意なIDを返す関数で、引数がノード・リストであれば、文書内で最も先頭に近いノードのIDを返す。したがって、「generate-id( key( 'outgo', '交通費' ) )」とした場合は、文書内に最初に現れた交通費の支出データの要素lineのIDが返される。よって、以下のようなfor-each文により、登場した全費目を重複なくループで回すことができる。

<xsl:for-each select=
"//line[generate-id(.)=generate-id( key('outgo', @group ) )]"
>
	...
</xsl:for-each>


 各要素lineのうち、その費目が文書中に最初に現れる要素だけが選択されるわけである。
以上で、月データXMLファイルから円グラフのSVGデータを生成することができるようになった。DOMパーサ、XSLTプロセッサ、円グラフ変換モジュールをサーブレットから操作するようにすれば、Webブラウザ上に、月データと動的に連携した支出内訳グラフを表示指させることができる(画面3)。


 なお、XSLTやSVGを使った拡張例としては、ほかにも、支出や残高の推移グラフ、予算と実情の比較グラフなどが考えられる。

画面3:サーブレットによるSVG円
グラフの表示



 以上、XML関連技術とCGI、Javaを用いた家計簿アプリケーションの作成方法を紹介した。XMLとその関連技術を活用することで、Webベースの家計簿という身近なアプリケーションの開発が容易になることが実感していただけただろうか。これらの技術を用いれば、本稿で紹介したような比較的小規模なプロトタイプの開発においても、データ設計、実装、将来の拡張など、さまざまな段階で開発コストの削減を図ることが可能となる。また、データが標準規格であることから、例えば料理レシピのデータベースを家計簿と連携させ、購入した食材から自動的に料理を提案する、などといったサービスも実現できるだろう。

 さて、こうして完成したiモード対応家計簿のプロトタイプであるが、実際に携帯電話でデータを入力したり、一覧を眺めたりするのは非常に楽しい。まさに“理想の家計簿”に一歩近づいたという感じなのだが、実は1つだけ困った点がある。それは、携帯電話の使用料により、「こまめに家計簿をつければつけるほど、お金が減ってしまう」という点だ。さすがにこれは、「XMLで解決」とはいきそうもないが……。

■付録

2001_01.xml
monthlyList.xml
accounts.xml
viewMonthly.xsl
toOutgoGraph.xsl
toOutgoGraph2.xsl
SvgCircleGraph.java
 



TOP XMLアカデミー XMLで作る「iモード家計簿」