XSLTで並立の要素を2つずつグループ化

「HTML鳩丸倶楽部」更新履歴の2001-10-03で提示され、agendaのカレントノードについて(XSLT)で言及されている問題について、別解を求めるべく挑戦してみました。一応問題を書いておくと、

<?xml version="1.0"?>
<document>
 <foo>1</foo>
 <foo>2</foo>
 <foo>3</foo>
 <foo>4</foo>
 <foo>5</foo>
</document>

という文書を、

<?xml version="1.0"?>
<document>
 <bar>
  <foo>1</foo>
  <foo>2</foo>
 </bar>
 <bar>
  <foo>3</foo>
  <foo>4</foo>
 </bar>
 <bar>
  <foo>5</foo>
 </bar>
</document>

とするにはどうすればよいか、というものです。

最初にXSLTスタイルシートを作った時は「やはりfor-eachを使うべきだろうか」と思ってそのようにしてみたのですが、できてみるとjintrickさん作成のものと殆ど同じになってしまったので、どうにかして何とか別のやり方ができないかと頭を捻ったところ、以下のスタイルシートができました。

<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:output indent="yes"/>
<xsl:param name="group" select="2"/>

<xsl:template match="/document">
 <document>
  <xsl:apply-templates select="
   foo[(position() mod $group)=1 or $group=1]
  "/>
 </document>
</xsl:template>

<xsl:template match="foo">
 <bar>
  <xsl:copy-of select="
   . | following-sibling::foo[position()&lt;$group]
  "/>
 </bar>
</xsl:template>

</xsl:stylesheet>

つまりapply-template要素ではfoo要素の中で1, 3, 5, ... 番目のものだけにテンプレートを適用するようにして、テンプレートでは「カレントノードと、その後に続くfoo要素のうちカレントノートから数えて2番目未満のもの」をコピーしてbar要素の中に入れる、という方法。言わば諸々の条件分岐をすべてXPath式(select属性)に押し込んだ形であり、書いているうちはあまり深く考えていなかったのですが、実際できてみると「XPathって便利だなあ」と実感したものでした。

「幾つずつグループ化するか」というのはgroupパラメタで指定しているので、外から2以外の数を指定することもできます。例えばxsltprocを使ってgroupパラメタに3を渡すと、このように。

$ xsltproc --param group 3 [XSL file] [input XML file]
<?xml version="1.0"?>
<document>
  <bar>
    <foo>1</foo>
    <foo>2</foo>
    <foo>3</foo>
  </bar>
  <bar>
    <foo>4</foo>
    <foo>5</foo>
  </bar>
</document>

一応 group=1 でも動作するようになってます。apply-templates要素のselect属性に or $group=1 という条件が入っているのはそのためで、本来の目標からすると無くてもよい部分だったり。

* * *

for-each要素を使わずにapply-templates要素とtemplate要素でなんとかできてみると、そこはかとない満足感を覚えるのは自分だけでしょうか。

(2004年5月4日)

北村曉 kits@akatsukinishisu.net