XSLT2.0の感覚

Written byがいすと

6 グルーピング

表計算ソフトの表のように同じようなデータが並んでおり、これをグルーピングして出力する場合を考えます。私のウェブページは蛾とプログラミングを扱っていますので、たまには少し蛾っぽい例として、以下の例を挙げます:

<moth>
  <species family="メイガ">フタスジシマメイガ</species>
  <species family="シャクガ">ウスバミスジエダシャク</species>
  <species family="シャクガ">ソトシロオビナミシャク</species>
  <species family="ヤガ">クロクモヤガ</species>
  <species family="メイガ">ツヅリガ</species>
  <species family="メイガ">マツノマダラメイガ</species>
  <species family="シャクガ">ウスキヒメシャク</species>
  <species family="ヤガ">キボシアツバ</species>
  <species family="ヤガ">ムラサキシタバ</species>
</moth>

各列は1種(species)のデータであり、それぞれに科名(family)のデータが付加されています。そこで、科名でグルーピングして以下のような出力を得る場合を考えてみましょう。

<output>
  <family name="メイガ">フタスジシマメイガ, ツヅリガ, マツノマダラメイガ</family>
  <family name="シャクガ">ウスバミスジエダシャク, ソトシロオビナミシャク, ウスキヒメシャク</family>
  <family name="ヤガ">クロクモヤガ, キボシアツバ, ムラサキシタバ</family>
</output>

XSLT1.0でこれをやるのは以外に面倒くさいです。1)どうやって科名ごとのデータをまとめるのか、2)どうやって種名を区切るカンマを出力するのか、の二点に注目して以下のソースを見てください:

<xsl:template match="moth">
  <output>
    <xsl:for-each select="species">
      <xsl:variable name="family" select="@family"/>
      <!-- 自分自身と同じ科名が前にあればすでに出力済み -->
      <xsl:if test="not(preceding-sibling::species[@family=$family])">
        <family name="{$family}">
          <!-- 同じ科名の要素をすべて出力 -->
          <xsl:for-each select="../species[@family=$family]">
            <xsl:value-of select="."/>
            <!-- 句切り文字のカンマを出力 -->
            <xsl:if test="position()!=last()">
              <xsl:text>, </xsl:text>
            </xsl:if>
          </xsl:for-each>
        </family>
      </xsl:if>
    </xsl:for-each>
  </output>
</xsl:template>

科名ごとのデータは、以前の兄弟ノードをpreceding-siblingで検索して新しい科名かどうかを判断し、新しい科名と判明した時点でそれ以降の同じ科名の全ノードを出力しています。ここでは同じ科名の全要素を得るのに親要素から検索していますが、これは「自分自身とそれ以降の兄弟ノード」に相当する基準点(following-sibling-or-selfみたいなやつ)の指定方法がないためです。また、カンマは、処理しているノードが最後の要素でない、つまりlast()でない場合に出力しています。ともかく、面倒くさい処理ですね。もっとスマートにできないものでしょうか。

XSLT2.0では、xsl:for-each-groupという新しい要素によって、このようなグルーピングが楽にできます:

<xsl:template match="moth">
  <output>
    <xsl:for-each-group select="species" group-by="@family">
      <family name="{@family}">
        <xsl:value-of select="current-group()" separator=", "/>
      </family>
    </xsl:for-each-group>
  </output>
</xsl:template>

ソースが1/3位になりました。重要なのは、グルーピングのために使用したxsl:for-each-groupと、カンマ出力で使用したvalue-ofseparator属性です。どちらもXSLT2.0で新たに加わった機能です。

順に見ていきましょう。xsl:for-each-groupは、select属性で指定された要素を、group-by属性で指定された要素の内容によってグルーピングします。そして、グルーピングされたノードリストごとに繰り返し処理をします。xsl:for-eachの繰り返し単位は各ノードですが、xsl:for-each-groupの繰り返し単位は複数ノードですので注意が必要です。各繰り返し単位のノードリストはcurrent-group()でアクセスできます。例えば、1回目のループのcurrent-group()は、以下のようになります:

<species family="メイガ">フタスジシマメイガ</species>
<species family="メイガ">ツヅリガ</species>
<species family="メイガ">マツノマダラメイガ</species>

次に、実際に出力する部分の実装です。value-ofseparator属性は、select属性で指定されたノードリストを区切るための文字列を指定するのに用います(Perlのjoin関数みたいなものです)。上の例ではcurrent-group()を指定しています。例として再び1回目のループを考えます。この時、current-group()は上記のように「フタスジシマメイガ」「ツヅリガ」「マツノマダラメイガ」の3つのノードを含みます。ですから、そのまま出力すれば「フタスジシマメイガツヅリガマツノマダラメイガ」となりますが、separator属性によって「フタスジシマメイガ, ツヅリガ, マツノマダラメイガ」という出力が得られます。

current-group()の各ノードを操作するには、例えば<xsl:for-each select="current-group()">などとします。