HTML文書作成日記の4月22日分、シリーズ! XML + XSLT 化までの道のり #3で挙げられていた、「同一の DD 属性値を持つものに 01 から順に番号を添え、 DD 属性値が変化したらまた 01 に戻
」るようにする、という問題に挑戦してみました。
問題を単純化するため、以下の入力文書:
<?xml version="1.0"?>
<document>
<sect DD="01">1-1</sect>
<sect DD="01">1-2</sect>
<sect DD="02">2-1</sect>
<sect DD="02">2-2</sect>
<sect DD="02">2-3</sect>
<sect DD="03">3-1</sect>
<sect DD="04">4-1</sect>
<sect DD="04">4-2</sect>
</document>
から、以下のような出力を得ることを想定します(勿論sect要素内の文字列を参照したりするのは無しで)。
<?xml version="1.0"?>
<document>
<p id="d01n01">1-1</p>
<p id="d01n02">1-2</p>
<p id="d02n01">2-1</p>
<p id="d02n02">2-2</p>
<p id="d02n03">2-3</p>
<p id="d03n01">3-1</p>
<p id="d04n01">4-1</p>
<p id="d04n02">4-2</p>
</document>
実のところ、最初にこの問題を見た際にも一度挑戦していまして、XSLTのnumber要素でなんとかできないかと思っていたのですが、うまく行かず挫折したことがありました。しかし前回の問題でXPath式の強力さを実感したので、同じようなアプローチでなんとかできるのではと思いまして、試行錯誤した結果、以下のスタイルシートができました。
<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:template match="/document">
<document>
<xsl:apply-templates select="
sect[@DD!=string(preceding-sibling::sect[1]/@DD)]
"/>
</document>
</xsl:template>
<xsl:template match="sect">
<xsl:for-each select="
. | following-sibling::sect[@DD=current()/@DD]
">
<p id="d{@DD}n{format-number(position(),'00')}">
<xsl:value-of select="."/>
</p>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
つまりapply-templates要素では「sect要素の中で、DD属性値がその一つ前のsect要素のDD属性値と異なるもの」にだけテンプレートを適用するようにし、テンプレートでは「カレントノードと、その後に続くsect要素でDD属性値がカレントノードのDD属性値と同じものを選んでカレントノードリストとし、その中でfor-eachループを回して番号を振る」というもの(本当に前回と同じような感じだ……)。
ひとつ良く分からなかった部分がありました。apply-templates要素のselect属性内で強調で示した部分、ここは当初、以下のようにstring関数を使わなくともよいのではと思っていました。
@DD!=preceding-sibling::sect[1]/@DD
しかし、これだと先頭のsect要素では望む結果が得られませんでした。
整理すると、入力文書において、コンテキストノードが先頭のsect要素だった場合、コンテキストノードのDD属性( @DD
)は"01"を返します。一方、「その一つ前の兄弟ノードであるsect要素のDD属性」をXPath式で書くと "preceding-sibling::sect[1]/@DD
" となりますが、実際には「一つ前の兄弟ノードであるsect要素」がそもそも存在しないため、比較の際には空文字列として評価されるものと考えました。なので両者を演算子 != で比較した場合にはtrueが返ってくるものと思っていたのです。
しかし実際に
boolean(@DD!=preceding-sibling::sect[1]/@DD)
を取ってみると、これはfalseを返します。また次のような比較:
boolean(@DD=preceding-sibling::sect[1]/@DD)
を取ってみても、同じくfalseが返されました。試してみた限りでは、XSLTプロセッサによる違いはなさそうです。
このあたりの根拠がXPathの仕様のどの部分に当たるのか、未だ見つけられずにいるのですが、取り敢えず以下のようにすれば望む結果を得ることができるようでした。
@DD!=string(preceding-sibling::sect[1]/@DD)
not(@DD=preceding-sibling::sect[1]/@DD)
上記の比較式について、XPathにおけるノード集合とその他のオブジェクトとの比較に追記しました。
(2004年5月4日)
北村曉 kits@akatsukinishisu.net