XSLT 2.0で便利になった機能(50) every式をつかう.(その2)

以前every式を使う例を紹介しましたが、今回もう少し効果的なところで使用する例がありましたので紹介します.以下のXMLをご覧ください.
 
<?xml version="1.0" encoding="UTF-8" ?>
<section>
    <div group="フルーツ" id="idりんご">
        <span>りんご</span>
    </div>   
    <div group="フルーツ" id="idみかん">
        <span>みかん</span>
    </div>   
    <div group="フルーツ" id="idバナナ">
        <span>バナナ</span>
    </div>
    <div group="文房具" id="idえんぴつ">
        <span>えんぴつ</span>
    </div>   
    <div group="文房具" id="id消しゴム">
        <span>消しゴム</span>
    </div>
    <div group="文房具" id="idボールペン">
        <span>ボールペン</span>
    </div>
    <div group="文房具" id="id筆入れ">
        <span>筆入れ</span>
    </div>   
    <div group="文房具" id="idノート">
        <span>ノート</span>
    </div>   
</section>
 
このようなXMLから、div/@groupで分類して、最初のdiv/@spanにだけ"【" + div/@group + "】"と出してやりたいという要望がありました.この場合だと、
 
<span>【フルーツ】りんご</span>
...
<span>【文房具】えんぴつ</span>
 
としたい訳です.
本当は、divをコントロールするレベルでxsl:for-each-groupを使って出現順にグルーピングすればよいのですが、今回は単純にdivにxsl:apply-templatesがかかっている場合という前提でやってみます.
 
この場合、spanのテンプレートでで親のdivが、同じ@groupの値を持っているdivのグループで最初のものであるかを判定する必要があります.そしてそうするためには、divが与えられたときに、その前後で同じ@groupの値を持つelement()*のグループを得る必要があります.
 
ところがこれは意外と簡単ではありません.divが与えられたとき前後の要素は$div/preceding-sibling::*と$div/follwoing-sibling::*で参照できます.そうすると、同じ@groupを持つものを集めたければ、$div/preceding-sibling::*[string(@group) eq string($div/@group)]で済むように思えます.ところがこの書き方だと、
 
<div group="フルーツ" id="idメロン">
<div group="やさい"   id="idきゅうり">
<div group="フルーツ" id="idりんご">
<div group="フルーツ" id="idみかん">
<div group="フルーツ" id="idバナナ">
 
で現在が<div group="フルーツ" id="idバナナ">の場合、途中に<div group="やさい"   id="idきゅうり">と異種のグループが入っていても、<div group="フルーツ" id="idメロン">までグルーピングしてしまいます.つまり@groupの値が「連続」していなければならないのですが、この書き方だとその制約条件が書けていないのです.
 
そうするとdiv/preceding-sibling::*で「自分から開始divに向かってすべてのdivが開始divの@groupと同じ」という制約条件を書くが必要になります.こんなことが簡単にできるのでしょうか?はい、できます.ここで登場するのがevery式です.ある$divから前の同じ@groupを持つdivのグループは
 
<xsl:variable name="precedingDiv" as="element()*" select="$div/preceding-sibling::*[every $divPre in (.|./following-sibling::*)[. &lt;&lt; $div] satisfies ($divPre[self::div] and (string($divPre/@group) eq $group))]"/>
 
と書けます.評価される$div/preceding-sibling::*の要素に対して、それより後ろのdivが同じ@groupであるかをevery式でチェックしています.ちょっと複雑ですが、every式のおかげで一発でこの条件が記述できるのです.
スタイルシートのコア部分は次のようになります.following-sibling::*の側も同じように求めることができます.
 
<xsl:template match="div">
    <xsl:apply-templates/>
</xsl:template>
 
<xsl:template match="span">
    <xsl:copy>
        <xsl:if test="parent::div and tmf:isFirstSpanInDivGroup(.)">
            <xsl:value-of select="'【'"/>
            <xsl:value-of select="parent::div/@group"/>
            <xsl:value-of select="'】'"/>
        </xsl:if>
        <xsl:apply-templates/>
    </xsl:copy>
</xsl:template>
 
<xsl:function name="tmf:isFirstSpanInDivGroup" as="xs:boolean">
    <xsl:param name="prmSpan" as="element(span)"/>
    <xsl:variable name="div" as="element()" select="$prmSpan/parent::div"/>
    <xsl:variable name="group" as="xs:string" select="string($div/@group)"/>
    <xsl:variable name="precedingDiv" as="element()*" select="$div/preceding-sibling::*[every $divPre in (.|./following-sibling::*)[. &lt;&lt; $div] satisfies ($divPre[self::div] and (string($divPre/@group) eq $group))]"/>
    <xsl:variable name="followingDiv" as="element()*" select="$div/following-sibling::*[every $divFol in (.|./preceding-sibling::*)[. &gt;&gt; $div] satisfies ($divFol[self::div] and (string($divFol/@group) eq $group))]"/>
    <xsl:variable name="divGroup" as="element()*" select="$precedingDiv|$div|$followingDiv"/>
    <xsl:variable name="firstSpan" as="element()" select="($divGroup/span)[1]"/>
    <xsl:sequence select="$prmSpan is $firstSpan"/>
</xsl:function>
 
結果は次のとおりです.
 
<section>
    <span>【フルーツ】りんご</span>
    <span>みかん</span>
    <span>バナナ</span>
    <span>【文房具】えんぴつ</span>
    <span>消しゴム</span>
    <span>ボールペン</span>
    <span>筆入れ</span>
    <span>ノート</span>
</section>
 
every式は大変強力です.もし「一発でノードの条件をチェックできないか?」と考えたときに候補に上げて間違いはないでしょう.