空要素(NaN)を計算に使う
はじめに
空要素(タグだけの要素)があると,計算に工夫が必要になる.空要素がNaN(not a number)となるためだ.以下のxmlをサンプルとして色々試してみよう.
<!-- sam.xml -->
<?xml version="1.0" encoding="utf-8"?>
<root>
<product>aaa</product>
<qty>10</qty>
<cost>100</cost>
<product>bbb</product>
<qty>20</qty>
<cost>150</cost>
<product>ccc</product>
<qty/> <!-- 空要素 -->
<cost>200</cost>
</root>
それぞれのタグの意味は
- product
- 商品.aaa,bbb,cccの三つ.
- qty
- 数量.商品cccの数量は空要素.
- cost
- 価格.
実行環境は以下の通り.
- Microsoft Windows XP SP2+セキュリティパッチたくさん
- Cygwin 1.5.24(uname -r で確認)
- Saxon-B 9.0.0.2J(インストールしたディレクトリを${saxon_home}と以下表記)
合計
まずは数量(qty)を合計しよう.空要素がなければ,以下のようなxslt(summary.xsl)で合計できる.
<!-- summary.xsl -->
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output
method="text"
encoding="UTF-8"/>
<xsl:template match="/root">
<xsl:value-of select="sum(qty)"/> <!-- qtyの値を合計 -->
</xsl:template>
</xsl:stylesheet>
空要素がある場合に使うとエラーとなる.
$ java -jar "${saxon_home}/saxon9.jar" -versionmsg:off -s:sam.xml -xsl:summary.xsl
Validation error
FORG0001: Cannot convert string "" to a double
Transformation failed: Run-time errors were reported
空要素の値("")を数値に変換できないからだ.関数(number())を使って明示的に変換しても合計は計算できない.
<!-- summary.xsl -->
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output
method="text"
encoding="UTF-8"/>
<xsl:template match="/root">
<xsl:value-of select="sum(number(qty))"/> <!-- qtyをnumberに明示的に変換してを合計 -->
</xsl:template>
</xsl:stylesheet>
合計ではなく,一番最初のqtyの値を表示する.
$ java -jar "${saxon_home}/saxon9.jar" -versionmsg:off -s:sam.xml -xsl:summary.xsl
10
空要素の値を数値に変換できないためにエラーになるのなら,空要素のqtyは計算対象から外す(値を持っているqtyのみで計算する)ことにしよう.述語[.!='']
を指定する.
<!-- summary.xsl -->
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output
method="text"
encoding="UTF-8"/>
<xsl:template match="/root">
<xsl:value-of select="sum(qty[.!=''])"/> <!-- 値を持っているqtyのみを計算 -->
</xsl:template>
</xsl:stylesheet>
実行結果:
$ java -jar "${saxon_home}/saxon9.jar" -versionmsg:off -s:sam.xml -xsl:summary.xsl
30
合計を計算できた.
変数に入れて計算
価格(cost)と数量(qty)から金額(cost*qty)を計算しよう.以下のsam.xmlを入力として,要素totalを追加xsltを考えよう.
入力:
<!-- sam.xml -->
<?xml version="1.0" encoding="utf-8"?>
<root>
<product>aaa</product>
<qty>10</qty>
<cost>100</cost>
<product>bbb</product>
<qty>20</qty>
<cost>150</cost>
<product>ccc</product>
<qty/>
<cost>200</cost>
</root>
出力:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<product>aaa</product>
<qty>10</qty>
<cost>100</cost>
<total>1000</total> <!-- total追加 -->
<product>bbb</product>
<qty>20</qty>
<cost>150</cost>
<total>3000</total> <!-- total追加 -->
<product>ccc</product>
<qty/>
<cost>200</cost>
<total>0</total> <!-- total追加 -->
</root>
商品cccの数量がnullだが,とりあえずそれを考えずに計算してみた.
<!-- total.xsl -->
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output
method="xml"
encoding="UTF-8"
indent="yes"/>
<xsl:template match="/root">
<root>
<xsl:apply-templates/>
</root>
</xsl:template>
<xsl:template match="cost">
<xsl:copy-of select="."/>
<total>
<xsl:value-of select=". * preceding::qty[1]"/> <!-- 一つ前のqtyとカレントのcostの積 -->
</total>
</xsl:template>
<xsl:template match="*">
<xsl:copy-of select="."/>
</xsl:template>
</xsl:stylesheet>
実行結果:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<product>aaa</product>
<qty>10</qty>
<cost>100</cost>
<total>1000</total>
<product>bbb</product>
<qty>20</qty>
<cost>150</cost>
<total>3000</total>
<product>ccc</product>
<qty/>
<cost>200</cost>
<total>NaN</total> <!-- qtyがnullなのでNaN -->
</root>
商品cccの金額はNaN.200*nullを計算できなかったということだろう.数量のnullは0(ゼロ)として計算しよう.変数qtyを定義して,nullだったら0に置き換える.
<!-- total.xsl -->
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output
method="xml"
encoding="UTF-8"
indent="yes"/>
<xsl:template match="/root">
<root>
<xsl:apply-templates/>
</root>
</xsl:template>
<xsl:template match="cost">
<xsl:copy-of select="."/>
<!-- 変数qtyはnull以外だったらその値,nullだったら0 -->
<xsl:variable name="qty">
<xsl:variable name="qty" select="number(preceding::qty[1])"/>
<xsl:choose>
<xsl:when test="$qty">
<xsl:value-of select="$qty"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="0"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<total>
<xsl:value-of select=". * $qty"/>
</total>
</xsl:template>
<xsl:template match="*">
<xsl:copy-of select="."/>
</xsl:template>
</xsl:stylesheet>
実行結果:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<product>aaa</product>
<qty>10</qty>
<cost>100</cost>
<total>1000</total>
<product>bbb</product>
<qty>20</qty>
<cost>150</cost>
<total>3000</total>
<product>ccc</product>
<qty/> <!-- qtyはnull -->
<cost>200</cost>
<total>0</total> <!-- NaNではなく0 -->
</root>
qtyがnullかどうかを判定しないで,先頭に0を付けると言うやり方もある.先頭に0をつけると10は010,20は020,nullは0となる.
<!-- total.xsl -->
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output
method="xml"
encoding="UTF-8"
indent="yes"/>
<xsl:template match="/root">
<root>
<xsl:apply-templates/>
</root>
</xsl:template>
<xsl:template match="cost">
<xsl:copy-of select="."/>
<xsl:variable name="qty" select="number(concat('0',preceding::qty[1]))"/> <!-- 先頭に0をつける -->
<total>
<xsl:value-of select=". * $qty"/>
</total>
</xsl:template>
<xsl:template match="*">
<xsl:copy-of select="."/>
</xsl:template>
</xsl:stylesheet>
実行結果:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<product>aaa</product>
<qty>10</qty>
<cost>100</cost>
<total>1000</total>
<product>bbb</product>
<qty>20</qty>
<cost>150</cost>
<total>3000</total>
<product>ccc</product>
<qty/> <!-- qtyはnull -->
<cost>200</cost>
<total>0</total> <!-- NaNではなく0 -->
</root>
出力
合計や計算に使わず,出力時にNaNを0に変えるのであれば,decimal-format
とformat-number
を使う.
- format-numberでnullを数値として表示
- nullは数値ではないのでNaN
- decimal-formatでNaNを0に変換
と言う手順となる.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output
method="text"
encoding="UTF-8"/>
<xsl:decimal-format NaN="0"/> <!-- NaNを0と出力 -->
<xsl:template match="/root">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="qty">
<xsl:value-of select="format-number(.,0)"/> <!-- format-numberで書式を指定.format-numberがなければnullを表示 -->
</xsl:template>
</xsl:stylesheet>
実行結果:
aaa
10
100
bbb
20
150
ccc
0
200
decimal-format
とformat-number
には名前を付けることもできる.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output
method="text"
encoding="UTF-8"/>
<xsl:decimal-format NaN="0" name="foo"/> <!-- 名前fooとする -->
<xsl:template match="/root">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="qty">
<xsl:value-of select="format-number(.,0)"/>
<xsl:value-of select="format-number(.,0,'foo')"/> <!-- 名前fooを指定 -->
</xsl:template>
</xsl:stylesheet>
実行結果:
aaa
10 <!-- 名前のないformat-numberの出力結果 -->
10 <!-- 名前fooを指定したformat-numberの出力結果 -->
100
bbb
20 <!-- 名前のないformat-numberの出力結果 -->
20 <!-- 名前fooを指定したformat-numberの出力結果 -->
150
ccc
NaN <!-- 名前のないformat-numberの出力結果 -->
0 <!-- 名前fooを指定したformat-numberの出力結果 -->
200