空要素(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-formatformat-numberを使う.

  1. format-numberでnullを数値として表示
  2. nullは数値ではないのでNaN
  3. 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-formatformat-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