XSLT: attribute value used as numeric predicate - xslt

Given
<xsl:variable name="datePrecision" as="element()*">
<p>Year</p>
<p>Month</p>
<p>Day</p>
<p>Time</p>
<p>Timestamp</p>
</xsl:variable>
The expression
$datePrecision[5]
returns a nodeSet containing one text node with value "Timestamp", as expected.
Later in a template, with a context element having an attribute
#precision="5"
I try the following expressions but all return an empty string:
$datePrecision[#precision]
$datePrecision[number(#precision)]
$datePrecision[xs:decimal(#precision)]
However, the following sequence does what I want
<xsl:variable name="prec" select="number(#precision)"/>
... $datePrecision[$prec] ...
Using Oxygen/XML's debugger I've stepped to the point where the expression is about to be evaluated and display the following in the watch window:
Expression Value Nodes/Values Set
-------------------------- --------------- -----------------------
$datePrecision[5] Node Set(1) #text Timestamp
#precision Node Set(1) precision 5
$datePrecision[#precision]
number(#precision) 5
$datePrecision[number(#precision)]
$prec 5
$datePrecision[$prec] Node Set(1) #text Timestamp
Obviously I've missed something fundamental about how attribute nodes are atomized for use in a predicate, but can't find anything in the docs (Michael Kay's XSLT/XPATH 2.0, 4th ed) that would explain this difference.
Can someone explain why this is occurring, and point me to where, in either the XSLT 2.0 spec or Michael Kay's book, where this is described?
(the XSLT processor is Saxon-PE 9.2.0.3)

Obviously I've missed something
fundamental
Yes. The XPath expression:
$datePrecision[#precision]
means: all elements in $datePrecision that have an attribute named precision.
But you want #precision to mean the attribute named precision of the currnet node that is matched by the template.
XSLT provides the current() function exactly for this purpose. Use:
$datePrecision[current()/#precision]
UPDATE: As Martin Honnen hinted, the OP probably wants to get the 5th element out of $datePrecision -- something not immediately visible from the description of the problem. In this case, it may be necessary to use:
$datePrecision[position() = current()/#precision]

Related

XSLT - Key() function

I'm a bit confused about this key function:
<xsl:for-each select="article[count(. | key('idkey', #id)[1]) = 1]>
Is there anyone that can briefly explain whats happening in this for-each loop?
The key is: <xsl:key name="idkey" match="/newspapers/newspaper" use="#id"/>
#id is an attribute in newspaper.
Thanks.
The expression key('idkey', #id)[1] selects the first element whose idkey is equal to #id.
The expression count(A|B) = 1 is an insane XSLT 1.0 workaround for testing whether A and B are the same node. (You will also see people using generate-id(A)=generate-id(B) for this.)
Put these together and you are asking whether the current element is the first one in the document that has a particular id value.
This is the basis of the technique called Muenchian Grouping (which becomes redundant in XSLT 2.0).
There is something fishy about the code because the key seems to be matching newspaper id's, not article id's. But perhaps they are related in some way.
In this for-each element
<xsl:for-each select="article[count(. | key('idkey', #id)[1]) = 1]">
The for-each is being applied to the first article element for each #id attribute.
The call key('idkey', #id) is selecting all article elements with the same #id attribute as the current one.
key('idkey', #id)[1] selects the first of all article elements with the same #id.
Because a node cannot appear in a node set more than once, the union . | key('idkey', #id)[1] will contain one node if the current article is the same node as the first article with the same #id. Otherwise it will contain two.
Checking that the value of count() is one selects only the elements that are the first with any #id.
An alternative way of doing this, and the one I prefer, is to use generate-id like this
select="article[generate-id() = generate-id(key('idkey', #id)[1])]"
which checks directly whether the current element is the same one as the first element in the set by comparing their generated IDs.

XSLT 1.0 adding values together from multiple nodes

Say I have the following XML
<A>100</A>
<B>200</B>
<C>300</C>
and the following XSLT
<TEST>
<xsl:value-of select="A + B + C"/>
</TEST>
It produces the following output
<TEST>600</TEST>
however, when one of the nodes is blank
<A></A>
<B>200</B>
<C>300</C>
I get the following.
<TEST>NaN</TEST>
I only want to add the nodes that are valid numbers. I could do this if xsl allowed me to dynamically replace a variable value by adding to the already existing variable, but even that would be messy. I assume there is an easy way that I'm missing?
I want XSLT 1.0 answers only please.
Thanks!
<TEST>
<xsl:value-of select="sum((A | B | C)[number(.) = .])"/>
</TEST>
That is, sum the subset of the elements A,B,C consisting of those whose contents can be used as numbers.
Note that number('') yields NaN, and (NaN = NaN) is false, so this will not include elements without text content.
We test for numbers as discussed at https://stackoverflow.com/a/3854389/423105
A little variation of LarsH's answer:
sum((A|B|C)[number(.) = number(.)])
the expression in the predicate doesn't cause any type error in XPath 2.0, because both arguments of = are of the same type -- xs:double.
There is also:
For XSLT 1.0:
sum((A|B|C)[string(number())!='NaN'])
For XSLT 2.0:
sum((A,B,C)[string(number())!='NaN'])
For interest, here is another one. It works for both XSLT 1.0 and 2.0
sum((A|B|C)[number()>-99999999])
In the above, replace -99999999 with one less than the lower bound of the possible values. This works because NaN > any-thing always returns false. This is probably the most efficient solution yet, but it may be seen as a little ugly.
For example, if you know for a fact that none of A, B or C will be negative values, you could put:
sum((A|B|C)[number()>0])
...which looks a little cleaner and is still quiet readable.
<xsl:value-of select="sum(/root/*[self::A or self::B or self::C][.!=''])"/>
This will add values from A, B, and C under the "Root" element so long as the value isn't blank.

in XSLT, what does <xsl:if test=".[foo or #bar]"> mean and what is the cross-browser solution?

XSL:
<xsl:if test=".[foo or #bar]">
something
</xsl:if>
is it testing that if target is with 'foo' tag name or has a 'bar' attribute?
it works only in IE, I'm wondering if there is an error. What is the equivalent sentence but works cross-browser?
The syntax .[x] is not allowed in XSLT 1.0. It was allowed in early drafts of XSLT 1.0, and I suspect it was absent from the final version as a result of an oversight rather than being a deliberate design decision (and as already remarked, it was reinstated in XSLT 2.0). Because IE first shipped its XSLT processor before the 1.0 spec was finalized, it's possible that they support this syntax for backwards compatibility with those early drafts.
<xsl:if test=".[foo or #bar]">
something
</xsl:if>
In XSLT 1.0 this is syntactically invalid and produces an error.
In XSLT 2.0 this is equivalent to:
<xsl:if test="foo or #bar">
something
</xsl:if>
The expression:
foo or #bar
evaluates to true() exactly when the context node (current node) has a child element named foo or the context node has an attribute named bar (or both).
Otherwise this expression evaluates to false().
Therefore, the above code snippet means: If either of these conditions is true: the current node has a child element named foo or the current node has an attribute named bar -- then output the string "something"
It tests whether the current node has a child element named foo or has an attribute called bar. That is, if you change it to
<xsl:if test="foo or #bar">
The syntax you use is rejected in my xsl parser: Invalid XPath expression
Unexpected token - "[foo or #bar]"
The one I use should work cross-browser.
If you wish to select nodes that have name foo or a bar attribute, then use the following:
<xsl:if test="name()='foo' or #bar">

how to use two conditions in select conditions in xslt when using Apply template

<xsl:apply-templates mode="block2sequence" select="NewDataSet/Table[CTD_CTD_PKG_ID =$PackageId][position()=1] and NewDataSet/Table[CTD_SEQ_NUM =$strXSLMsgType][position()=1]"/>
why cant i use two conditions in above select condition, can any one suggest me
<xsl:apply-templates mode="block2"
select="NewDataSet/Table[CTD_CTD_PKG_ID =$PackageId][position()=1] "/>
why cant i use two conditions in above select condition
I guess this is to mean, "why can't the two conditions be specified in the same predicate?"
The answer is that the expression:
NewDataSet/Table[CTD_CTD_PKG_ID =$PackageId and position() = 1]
isn't equivalent at all to the 1st expression above.
The first expression selects the first Table child of NewDataSet such that the string value of its CTD_CTD_PKG_ID child is equal to the string value of $PackageId. In this case we don't know which child (at which position) of NewDataSet will be selected -- any child that happens to be the first with the specified properties, will be selected.
On the other side, the latter expression selects the first Table child of NewDataSet only if the string value of its CTD_CTD_PKG_ID child is equal to the string value of $PackageId. In this case, if anything is selected, it would be the first Table child.
If you want an equivalent expression to the first one, that has only one predicate, one such expression is:
NewDataSet/Table
[CTD_CTD_PKG_ID =$PackageId
and
not(preceding-sibling::Table[CTD_CTD_PKG_ID =$PackageId ])
]
Update: The OP has published a code snippet:
<xsl:apply-templates mode="block2sequence" select=
"NewDataSet/Table[CTD_CTD_PKG_ID =$PackageId][position()=1]
and
NewDataSet/Table[CTD_SEQ_NUM =$strXSLMsgType][position()=1]"/>
This code will cause an error thrown at compile time by the XSLT processor.
The value of the select attribute is a boolean (expr1 and expr2), however templates in XSLT 1.0 and XSLT 2.0 can only be applied on nodes. A boolean isn't a node -- hence the error.
Solution:
My first guess is that you want templates to be applied on both nodes. If this is so, then use:
<xsl:apply-templates mode="block2sequence" select=
"NewDataSet/Table[CTD_CTD_PKG_ID =$PackageId][1]
|
NewDataSet/Table[CTD_SEQ_NUM =$strXSLMsgType][1]"/>
My second guess is that you want templates applied only on the first of the two nodes. If this is so, then use:
<xsl:apply-templates mode="block2sequence" select=
"(NewDataSet/Table[CTD_CTD_PKG_ID =$PackageId]
|
NewDataSet/Table[CTD_SEQ_NUM =$strXSLMsgType]
)
[1]
"/>
Notes:
Please, learn how to ask a question -- provide all relevant data and explain -- in the question, not in subsequent comments.
Did you know that [1] is equivalent to [position()=1] and is shorter?
You can use two conditions and your expression looks perfectly correct. If it is failing with an error, please tell us the error. If it is not selecting what you want, then (a) show us your source document, and (b) tell us what you want to be selected.
(You know, your question gives so little information, you don't give the impression that you really want an answer.)

How can I use xsl stylesheet parameters to set a node name in XPATH expression?

I have the following XPATH expression:
select="catalog/product/$category_name = $category_value"
In the given example $category_name and $category_value are the XSL parameters that I receive from my servlet and I want to use them in XSL to filter the XML result based on category and its value.However, for some reason when,say, $category_name parameter equals 'price' attribute of the 'product' parent node and $category_value equals 40, the given expression does not return any result at all! Logically, the expression should be transformed to something like select="catalog/product/price = 40"....I guess there is some problem with specifying the node name which is the category in my case. Can anyone suggest the way to get around this problem?
You probably want:
catalog/product/*[name()=$category_name] [. = $category_value]
For variable xpath expressions, use dynamic xpath. See Is it possible to use a Dynamic xPath expression in a xslt style sheet?