XPath 3で、複数トークンをORでfn:contains-token()判定したい
2024-08-24追記。
fn:contains-token()
はDITAやHTMLの@class
の値を判定するときに使える便利関数です。この文脈での@class
の値というのは次のようなものです。
<div class="col-1 col-2-md col-4-lg" ...>...</div>
fn:contains()
が単純な一致判定のために入力全体を走査するのに対し、fn:contains-token()
は入力をトークンに分けた上で文字列の一致を見てくれます。
今回の問題は「幾つかの候補文字列のうち、どれかに合致するかを判定したい」というものです。どういうときに使うかというと、たとえば「トークンAまたはトークンBが入力@class
に含まれるときtemplatelABを呼ぶ」という処理を行うときですね。XSLT 3.0のパッケージ使わなければこんなん書かない気がしますが。
<xsl:template match="*[トークンAまたはトークンBを含む@classにいい感じにマッチする述部]">
<xsl:call-template name="templateAB"/>
</xsl:template>
単純には「入力に対しトークンAの有無を判定し、trueならその時点で終了し、falseならトークンBの有無を判定し……」という処理ですね。
fn:contains-token()
はどんな関数か
さて、トークンに対し文字列の一致を判定するfn:contains-token()
の引数と返り値の型をみてみましょう。
fn:contains-token($input as xs:string*, $token as xs:string) as xs:boolean
入力は1個以上の(*
は付いていても省略はできない)xs:string
でよいのに対し、
判定するトークンを示す引数は単一の文字列となっています。複数のトークンを一度に判定はできない、ということですね。
ところで、XPath 3系には便利演算子!
があります。
!
マップ演算子マップ演算子!
は左辺のシーケンスに対し、右辺をマップで適用します。左辺のシーケンスには複数のitem()
が想定されます。
これはパス指定での返り値としてのノードセットなどが分かりやすい対象ですが、XPathで直接、(A,B,...)
のようにしても記述できます。
マップされるそれぞれの対象item()
は右辺中ではコンテキストアイテム.
として記述できます。
左辺がxs:string*
であれば、!
を挟んだ右辺ではそれぞれのxs:string
が.
となるわけですね。
式 2023-10-20 追記
先の話を踏まえ、次のようなパターンを書くとよいわけです。
*[let $class := @class return ('トークンA','トークンB')!fn:contains-token($class, .) => fn:fold-left(false(),function($head,$rest){$head or $rest})]
let構文はlet $variable := return ...
といった記法で、XSLTで<xsl:variable>
を使わずともXPathだけで変数を用意可能にします。XPath 3.0からのlet構文で@class
を取得しておき、コンテキストアイテムがxs:string
になっても判定したい値を読めるようにしておきます。
2023-10-23 よく考えたらfold-left内で判定すればいいよね。これなら素で書いてもいいかもしれない。
*[let $class := @class return
('tokenA','tokenB','tokenC'...) => fold-left(false(),
function($head, $rest){
$head or fn:contains-token($class, $rest)
})
]
おまけ AND条件の場合
*[@class => fn:contains-token('トークンA')][@class => fn:contains-token('トークンB')]
述部は左から評価して残った結果を次の述部で……という挙動なので、ANDの場合これでOK。同じ述部内でAND取るのとどっちが速いかはよく分かっていません。=>
演算子は左のitem()
右の第一引数として渡す、はず。
2024-08-24 someでまあまあ解決ということにする
ふとsomeのことを思い出した(あるいは記憶していなかった)ので追記。
some $item in $sequence satisfies $function then(A) else(B)
で$sequence
の中のある$item
で$function
がtrue()
を返すようなときAを、まったく無ければBを返すという構文。$sequence
中の全$item
が$function
でtrue()
を返すケースでAを返すような操作ではsome
ではなくevery
がある。XSLT 2.0からある文。
some .. in ... satisfies .. then..
と、かなりテキスト的な並びなのでシーケンスや函数部分が長くならなければそれなりに見易さを保てるのがポイント。
で、真偽値のみが必要な場合、then() else()はなくとも条件判定部分は機能するので、次のように書ける。
*[some $tk in ('トークンA','トークンB') satisfies fn:contains-token(@class, $tk)]
Discussion
fn:contains-token()
便利ですよね。わたしもそれをor
したい場面に多々出くわします。これですと、
fn:contains-token()
の第 1 パラメーター評価時も'トークンA'
と'トークンB'
が各コンテキスト アイテムであるがゆえに、@class
の評価でエラー (err:XPTY0020
) となるのではないでしょうか。別の処理で使っていたものを「
@class
処理したい」となってガッと書いたものでした。let文によって元の
./@class
を保持するように修正。まだ期待の結果になっていないので検討します。これはboolean()*で述部としてfalseになっている気がする
当初の式のように
!
を適用しただけだと、結果が複数の値になり、最終的な effective boolean value を得られなかったということではないかと。@class
の内容にある程度の行儀良さを期待できる場面では、tokenize(@class) = ('トークンA', 'トークンB')
やxs:NMTOKENS(@class) = ('トークンA', 'トークンB')
でもよいかもしれませんね。はい。
当初の方針をやりつつ修正したのでfold-left()が登場しました。ここまで長くなると
!
の手軽さはないですね。