XSLT functions

Functions

1. element-available() question
2. How do I obtain the content of a linked element
3. Strange problem with string-replace
4. how to count equal nodes into one result element
5. How do I insert an element count into a document.
6. What does position function do
7. substring-after syntax error
8. How to show the number of nodes of the same name in a subtree
9. How to test that a node is the last children in the xsl file
10. How to format numbers and Dates
11. How to isolate delimited text
12. xsl: descendent count
13. Normalise question
14. Substring on telephone numbers
15. How to use position function
16. How to count the number of Choice nodes whose text is true.
17. Combining two tests using the and operator
18. normlise-space function
19. How to implement a for loop in XSLT
20. Logical and function in XSL Transformations
21. How to use the not function
22. Character Translation
23. How to get the position of an element with descendant y with value z
24. Sum function
25. position function
26. Using concat

1.

element-available() question

Michael Kay

The specification says that element-available() should return true if an instruction of the given name is available. Saxon is taking the specification literally: "xsl:template" is not an instruction. Other processors are taking the name "element available" at its face value, and returning true because there is an element available with that name.

2.

How do I obtain the content of a linked element

Sebastian R:

I was delighted to find that id() function. 
It meant that from  <code id="GB">Great Britain</code>
 ..
 <nat idref="GB"/>

I could get the <nat> to generate "Great Britain" with just
   <xsl:apply-templates select="id(@idref)"/>


            

3.

Strange problem with string-replace

David Carlisle


> But when I want to remove
> all &#13; (line feed) or &#32; (space)

line endings are normalised to 10 not 13, and since this is
working at the level of single characters you can use
translate() rather than string-replace.


<xsl:template match="a">
  <xsl:value-of select="translate(.,'&#10; ','')"/>
</xsl:template>
</xsl:stylesheet>

converts


<a>
1 2
3 4
5 6
</a>

to

123456

4.

how to count equal nodes into one result element

Juliane Harbarth

Expansion of Question:
This is the source tree:
<count_this>
      <tag>apple</tag>
      <tag>apple</tag>
<tag>apple</tag>
<tag>banana</tag>
<tag>banana</tag>
<tag>cherry</tag>
<tag>cherry</tag>
<tag>cherry</tag>
<tag>cherry</tag>
</count_this>

The result tree should be something like this:
<result>
     <tag><value>apple</value>
             <occurred>3</occurred></tag>
     <tag><value>banana</value>
             <occurred>2</occurred></tag>
     <tag><value>cherry</value>
             <occurred>4</occurred></tag>
</result>

<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Tranform"     version="1.0">

  <xsl:template match="/">
  <result>
  <xsl:apply-templates/> 
  </result>
  </xsl:template>

    <xsl:template match="tag">
<xsl:variable name="currentFruit"><xsl:value-of
select="."/></xsl:variable>
<xsl:variable name="others">
  <xsl:number
expr="count(preceding-siblings::tag[self::*=$currentFruit]"/>
</xsl:variable>
<xsl:if test="$others=0">
  <tag>
   <value><xsl:value-of select="."/></value>
   <occurred>
    <xsl:number
expr="count(following-sibling::tag[self::*=$currentFruit]) + 1"/>
   </occurred>
  </tag>
</xsl:if>
    </xsl:template>

</xsl:stylesheet>



            

5.

How do I insert an element count into a document.

Steve Muench

 For instance, I have an xml file that lists people; 
I want to be able to count how many 
<person></person> records are in the file and output a total.

If the current context is right, then you can simply do: 
 
<xsl:value-of select="count(person)"/> 
 
or you might have to augment the "person" pattern 
to fit your situation... 

            

6.

What does position function do

Ken Holman

It produces the context node's position amongst the nodes of the context node list ... which varies depending on what you are doing (which is probably why you've had problems understanding what exactly it is doing). In my example below, I'm explicitly setting the context node list to the children of <list> and then relying on that in my calculations with position().

xml file:

<list type="gloss">
  <label>
    <gi>front</gi>
  </label>
  <item>contains any prefatory matter 
   </item>
 </list>

stylesheet:

<xsl:template match="list">   <!--document element-->
    <xsl:apply-templates/> <!--set node list to children-->
</xsl:template>                          
<xsl:template match="item"> <!--node list = siblings-->
<xsl:variable name="this-pos" 
         expr="position()"/> <!--sibling #-->
Label = <xsl:value-of 
        select="../*[$this-pos - 1]"/> <!--prev sibling-->
</xsl:template>

output:
    Label = front


            

From Oren Ben-Kiki

it is the position in "the context node list" of the node matched by the pattern, not of the node matched by the <xsl:template> containing the pattern. You can work around this by using a variable:

<xsl:variable name="position-before-me" expr="position() - 1"/>
<xsl:??? select="../*[position() = $position-before-me]"/>

But it is better to:

<xsl:??? select="preceding-sibling::*[1]"/>

See section 6.1.1 (Axes).

            

from: David Carlisle

position() returns the position of the node currently under consideration so things like [position()=3] gets the third thing in the node list. [3] is short for [position()=3] so I suppose (without checking or trying it out) that [position()] is short for [position()=position()] which is always true.

7.

substring-after syntax error

Michael Kay

E.g.
the xslprocessor always gives me an error at the line
   

VALUE="{substring-after("abcd","b")"} />
What's wrong in it?


It's not well-formed. I think you mean
VALUE="{substring-after("abcd","b")}" />

            

David Carlisle

> Wrong: VALUE="{substring-after("abcd","b")"} />
> Right: VALUE="{substring-after("abcd","b")}" />

Wrong actually, you can't use " inside an XML attribute value delimited by " so you have to have "{substring-after('abcd','b')}" or '{substring-after("abcd","b")}'

 '{substring-after("{
                     ^

If you read the xsl draft it'll explain that you can not nest { constructs, and you don't need to.

8.

How to show the number of nodes of the same name in a subtree

Linda van den Brink

You can use the count function, 

<xsl:number value="count(article)"/>
            

9.

How to test that a node is the last children in the xsl file

David Carlisle

You can test if you are at the last by
<xsl:if test="position()=last()"/>

but probably that isn't what you want to do, normally you would do something like

....stuff to put before the list
<xsl:apply-templates select="animal"/>
....stuff to put after the list

ie rather than process the list one at a time asking each time if you are at the end, it is usually more convenent to specify what you want to put around that branch of the tree, and inbetween, select the elements you want in that branch.

10.

How to format numbers and Dates

Ric Emery

Formatting numbers can be achieved with the format-number function of XSL. This function is described in section 14.3 of the latest XSL spec.

Here is an example that formats a variable to 2 decimal places.

<xsl:value-of select="format-number ($SOMENUMBER, '0.00')" />

11.

How to isolate delimited text

David Carlisle

Q: Expansion.

I have a whole mess of semicolon 
delimited input that looks like this:

<field>thing1; thing2;...thingn</field>

I'm trying to transform it to this:

<field>thing1</field>
<field>thing2</field>
etc...
    

Loops are evil. Recursion is your friend.

<xsl:stylesheet 
  xmlns:xsl="http://www.w3.org/1999/XSL/Tranform"     version="1.0"


<xsl:template match="field">
<xsl:call-template name="split">
  <xsl:with-param name="text" select="."/>
</xsl:call-template>
</xsl:template>


<xsl:template name="split">
<xsl:param  name="text" select="''"/>

<xsl:choose>
<xsl:when test="contains($text,';')">
 <field>
  <xsl:value-of select="substring-before($text,';')"/>
 </field>
<xsl:call-template name="split">
  <xsl:with-param name="text" 
         select="substring-after($text,';')"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
 <field>
<xsl:value-of select="$text"/>
 </field>
</xsl:otherwise>
</xsl:choose>

</xsl:template>

</xsl:stylesheet>

12.

xsl: descendent count

David Carlisle


>  How can I get CHILDREN count of an element.
e.g.
Source Tree
<CHAPTER>
     <PARA>
     <PARA>
     <PARA>
</CHAPTER>

<xsl:template match="CHAPTER>
<xsl:value-of select="count(*)"/>
for all children, or
<xsl:value-of select="count(PARA)"/>
to tell you how many PARA children the current node has.

To use this in a test,

<xsl:if test="count(*) > 5">
    true condition
</xsl:if>

            

13.

Normalise question

Mike Brown

Q: Expansion

I modified David Carlisle's example to use normalize-space() since whitespace distinctions are not desired. However, when I add normalize-space(), the stylesheet stops returning the expected "XYZ" and instead gives "XXXXYZZ"

What am I doing wrong here?

... the stylesheet ...

<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Tranform"     version="1.0"
                xmlns:xt="http://www.jclark.com/xt"
                extension-element-prefixes="xt">
  <xsl:template match="root">
    <xsl:for-each select="//c[not(normalize-space(text())
                          =normalize-space(following::c/text()))]" >
      <xsl:sort order="ascending" select="." />
      <xsl:value-of select="." />
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

.... the input file ...

<?xml version="1.0"?>
<root>
  <a>   
    <b id="101" >
      <c>X</c>
    </b>
    <b id="102" >
      <c>Y</c>
    </b>
    <b id="103" >
      <c>X</c>   
    </b>
    
    <b id="104" >
      <c>Z</c>
    </b>
      
    <b id="105" > 
      <c>Z</c> 
    </b>
    
    <b id="106" >
      <c>Z</c>
    </b>
  </a>
    
  <a>
    <b id="201" >
      <c>X</c>
    </b>
      
    <b id="202" >
      <c>Z</c>
    </b>
      
   </a> 
      
   <a>
     <b id="301" >
       <c>X</c>
     </b>
    
     <b id="302">
       <c>X</c>
     </b>
   </a>
</root>

Answer

I can explain why you're getting these results, but I don't have a way to solve your problem. What you are doing wrong is trying to normalize up to 9 text nodes at a time.

http://www.w3.org/TR/xpath#axes: "the following axis contains all nodes in the same document as the context node that are after the context node in document order, excluding any descendants and excluding attribute nodes and namespace nodes."

First, why do you get 'XYZ' without the attempt at normalization?

file://c[not(text()=following::c/text())]

file://c will test all the "c" element nodes in document order. Only those for which [...] is true will be selected. The sort order you specified will be applied to these selected nodes for purposes of iterating through your xsl:for-each.

For each node being tested, text() is a node-set with just one member: the 'X', 'Y', or 'Z' text node child, as expected. following::c/text() is a node-set with every text node child of every "c" element node from that point in the document onward, (not counting descendants of the node being tested).

http://www.w3.org/TR/xpath#booleans: "If both objects to be compared are node-sets, then the comparison will be true if and only if there is a node in the first node-set and a node in the second node-set such that the result of performing the comparison on the string-values of the two nodes is true"

http://www.w3.org/TR/xpath#section-Text-Nodes: "The string-value of a text node is the character data"

So then, is going through the file://c elements, is the "text()" node-set equal to the "following::c/text()" node-set? The answer, in the fourth column, is true (i.e., yes, they are equal) if the item in the second column **can be found in** the third.

file://c:     text(): following::c/text():               result:
<c>X</c> 'X'    'Y','X','Z','Z','Z','X','Z','X','X' true
<c>Y</c> 'Y'    'X','Z','Z','Z','X','Z','X','X'    false
<c>X</c> 'X'    'Z','Z','Z','X','Z','X','X'        true
<c>Z</c> 'Z'    'Z','Z','X','Z','X','X'            true
<c>Z</c> 'Z'    'Z','X','Z','X','X'                true
<c>Z</c> 'Z'    'X','Z','X','X'                    true
<c>X</c> 'X'    'Z','X','X'                       true
<c>Z</c> 'Z'    'X','X'                           false
<c>X</c> 'X'    'X'                               true
<c>X</c> 'X'    (empty)                            false

Therefore, file://c[not(text()=following::c/text())] will select the file://c items that are not true, which just happened to be these elements:

<c>Y</c>
<c>Z</c>
<c>X</c>

...which you then sorted in ascending order and looked at the string values of to produce 'XYZ'.

Second, why did you get 'XXXXYZZ' when you applied normalize-space() to the node-sets in the second and third columns?

http://www.w3.org/TR/xpath#section-String-Functions: "The normalize-space function returns the argument string with white space normalized ..." [and] "A node-set is converted to a string by returning the string-value of the node in the node-set that is first in document order. If the node-set is empty, an empty string is returned."

file://c:     text(): following::c/text(): result:
<c>X</c> 'X'    'Y' (and others)    false
<c>Y</c> 'Y'    'X' (and others)    false
<c>X</c> 'X'    'Z' (and others)    false
<c>Z</c> 'Z'    'Z' (and others)    true
<c>Z</c> 'Z'    'Z' (and others)    true
<c>Z</c> 'Z'    'X' (and others)    false
<c>X</c> 'X'    'Z' (and others)    false
<c>Z</c> 'Z'    'X' (and others)    false
<c>X</c> 'X'    'X'                 true
<c>X</c> 'X'    (empty)             false

Thus, file://c[not(normalize-space(text())=normalize-space(following::c/text()))]
selects:

<c>X</c>
<c>Y</c>
<c>X</c>
<c>Z</c>
<c>X</c>
<c>Z</c>
<c>X</c>
...which, when sorted and so on produces 'XXXXYZZ'.

The solution is a little beyond me, though. I'd assume that you'd have to do it with recursive template calls that mimic the XPath evaluation above, but with normalize-space() thrown in. It wouldn't be efficient at all. Why don't you just normalize your source data first :)

14.

Substring on telephone numbers

Steve Muench

Q: Expansion If I have an XML document containing the following tag:

<TELEPHONE>316-855-2145</TELEPHONE>

I would like to transform the above tag into the following 3 tags:

<AREACODE>316</AREACODE>
<PREFIX>855</PREFIX>
<NUMBER>2145</NUMBER>

No need for scripting to accomplish this particular example, you can do:

  <xsl:template match="TELEPHONE">
    <AREACODE><xsl:value-of select="substring(.,1,3)"/></AREACODE>
    <PREFIX><xsl:value-of select="substring(.,5,3)"/></PREFIX>
    <NUMBER><xsl:value-of select="substring(.,9,4)"/></NUMBER>
  </xsl:template>

Where the "." dot represents the current element, TELEPHONE inside the template.

Eric van der Vlist added

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Tranform"     
    version="1.0">

<xsl:template match="TELEPHONE">

<AREACODE>
  <xsl:value-of select="substring-before(string(), '-')"/>
</AREACODE>
<PREFIX>
 <xsl:value-of 
  select="substring-before
   substring-after(string(), '-'),'-')"/>
</PREFIX>
<NUMBER>
 <xsl:value-of select=
   "substring-after(substring-after(string(),'-'), '-')"/>
</NUMBER>

</xsl:template>
</xsl:stylesheet>

These functions are documented in the XPATH recommendation (http://www.w3.org/TR/xpath).

15.

How to use position function

Natalie Rooney

I have the following xml structure:

    <USERS>
          <USER>bunch of attributes</USER>
          <USER>bunch of attributes</USER>
          <USER>bunch of attributes</USER>
    </USERS>

I want to display the first user in a different way from the others.


Use (USERS/USER)[position() > 1]

            

16.

How to count the number of Choice nodes whose text is true.

David Carlisle

<xsl:value-of select="count(Choice[normalize-space(.)='true'])"/>

17.

Combining two tests using the and operator

David Carlisle

How to combine

<xsl:if test ="@imagehref">
and
<xsl:if test =".[@imagehref !='']">


<xsl:if test ="@imagehref and not(@imagehref = '')" >

Michael Kay adds: If you want to test that the current node has an imagehref attribute and that its value is not the empty string, two simple ways to do it are:

<xsl:if test="string(@imagehref)">
<xsl:if test="@imagehref!=''">

You don't actually need to use the and operator.

18.

normlise-space function

David Carlisle

   "normalize-space(list[listitem='Related topics'])"

would select the list nodes with listitemm child equal to (exactly) 'Related topics' Then take the string value of that list, and normalise it's white space. As it's used in a boolean context, this swill be true if its non empty.

Probably what you want to do is select the list elements with listitem elements with normalised value 'Related topics'

which is
   "list[normalize-space(listitem)='Related topics']"

            

19.

How to implement a for loop in XSLT

Michael Kay


> In XSL, how can I get something like (not from XML)
> 
> for i=1 to 4
> <td>i</td>
> next

<xsl:call-template name="repeat-td">
<xsl:with-param name="n" select="4"/>
</xsl:call-template>

<xsl:template name="repeat-td">
<xsl:param name="n"/>
<xsl:if test="$n != 0">
   <td>i</td>
   <xsl:call-template name="repeat-td">
      <xsl:with-param name="n" select="$n - 1"/>
   </xsl:call-template>
</xsl:if>
</xsl:template>


            

20.

Logical and function in XSL Transformations

Mike Kay


I want to know whether operations like 'AND' can be performed in XSL 
transformations. Example:

<xsl:template match="order & paid">
<DONE/>
</xsl:template>

<xsl:template match="*[order and paid]"> will match any element that has at
least one order child and at least one paid child, if that's what you mean. 


            

21.

How to use the not function

Juliane Harbarth

>How do I say <xsl:apply-templates select="[all but element3]" />
<xsl:apply-templates select="*[name() != 'element3']" />

or Mike Kay adds:

<xsl:apply-templates
select="*[not(self::element3)]"/> 

This selects all the child elements that don't satisfy the boolean condition self::element3, i.e. all those for which self::element3 is an empty node-set, i.e. all those which are not element3 elements.

Generally speaking, it's best to avoid testing the result of the name() function, because two XML documents that are equivalent except for their choice of namespace prefixes will give different answers. Using "not(self::element3)" is safer.

22.

Character Translation

David Carlisle


Q expansion. 
> Could I use translate to translate new lines code to \n?

No translate can only translate characters to characters, however you can use a named function to replace the substring &#10; by the substring \n, eg this one:

<!-- replace all occurences of the character(s) `from'
     by the string `to' in the string `string'.-->
<xsl:template name="string-replace" >
  <xsl:param name="string"/>
  <xsl:param name="from"/>
  <xsl:param name="to"/>
  <xsl:choose>
    <xsl:when test="contains($string,$from)">
      <xsl:value-of select="substring-before($string,$from)"/>
      <xsl:value-of select="$to"/>
      <xsl:call-template name="string-replace">
      <xsl:with-param name="string" 
          select="substring-after($string,$from)"/>
      <xsl:with-param name="from" select="$from"/>
      <xsl:with-param name="to" select="$to"/>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="$string"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

23.

How to get the position of an element with descendant y with value z

Kay Michael


> I would want to get the position of the element 
> that has a descendant 'y' whose value is 'z' and 
> put it in a variable, in the example the value of
> the variable should be 2.


<?xml version="1.0"?>
<elementList>
    <element>
        <x>
            <y>a</y>
        </x>
    </element>
    <element>
        <x>
            <y>z</y>
        </x>
    </element>
</elementList>

Try something like:

<xsl:variable name="pos">
   <xsl:for-each select="//element[descendant::y[.='z']][1]">
       <xsl:number/>
   <xsl:for-each>
</xsl:variable>

or (perhaps more efficient)

<xsl:variable name="pos">
   <xsl:for-each select="//element">
       <xsl:if test="*[descendant::y[.='z']]">
           <xsl:value-of select="position()"/>
       </xsl:if>
   <xsl:for-each>
</xsl:variable>
    

This won't work if there is more than one element that satisfies the condition.

24.

Sum function

Steve Muench


Q expansion

I'm trying to sum report totals, but I only want to sum
certain attributes.  For example

My.xml

<report>
<tranxs>
	<tranx cccf="N" amt="200">
</tranx>
</report>

My.xsl

<xsl:for-each select="report">
...
<xsl:value-of select="sum(tranxs/tranx[@cccf = 'N']/@amt)"/>

I know the above line is wrong, but I don't know how to fix
it.  I want it to sum depending on the cccf attribute value.
      

Given:

<report>
  <tranxs>
    <tranx cccf="N" amt="1"/>
    <tranx cccf="Y" amt="2"/>
    <tranx cccf="N" amt="3"/>
    <tranx cccf="Y" amt="4"/>
  </tranxs>
 </report>

and:

<foo xsl:version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:for-each select="report">
    <xsl:value-of 
       select="sum(tranxs/tranx[@cccf = 'N']/@amt)"/>
  </xsl:for-each>
</foo>

I get:

<foo>4</foo>

which is the sum of 1 and 3.

25.

position function

Mike Brown, David Carlisle, Ken Holman.

Problem: position is returning a different number depndant on where its called from. I'm using it in the root template in a for-each loop and in a template. The 'number' I'm getting back is different in each case.

If you trace the order of execution of templates, starting at the root node, and taking into account the built-in templates, you will probably find that you told the XSL processor to process the elements as part of a list of nodes in which those element nodes occupy the 2nd, 6th, 10th, etc positions in the list, rather than the 1 to 7th position expected.

David C adds

> I'm using position() function in two places.

Or more particularly, in two current node lists, position() gives the position of the current node in the current node list so you get two different answers.

<xsl:for-each select="faq/body/section" >

so here you have constructed a list of all sections that are children of body elements which are children of faq elements of the current node. So position() returns the position in that list.

 <xsl:template match="section"  >
  <xsl:variable name="secno"><xsl:value-of
     select="position()"/></xsl:variable>

Here you didn't show what the node list was.

for instance if you go <xsl:apply-templates select="section[3]"/> then you'll construct a node list of length one consisting of the third section child of the current element, so the above template will show a value of position() as 1.

If you go <xsl:apply-templates /> in a template matching on the parent of your section element then the above template will fire and this time position() will be the position in the list selected by default, which includes all elements text, comments, etc.

So you don't need to give a template and a for-each example, the same template will give different values for position, depending on the list to which it is applied.

Ken Holman adds

Perhaps you are in <body> and you are asking that all child nodes be pushed through your stylesheet with <xsl:apply-templates/> (with no select= attribute) ... you are going to get *all* child nodes of <body> pushed, not just <section> nodes.

   <body>
    <title/>
    <section/>
    <section/>
    <note/>
    <section/>
   </body>

The sections above would be counted as 4, 6, and 10 due to the intervening element nodes and the intervening text nodes. It is often the text nodes that I forget about. I've illustrated this with a diagram on page 58 of the 7th edition.

The SHOWTREE stylesheet in the free resources section of our web site exposes a report of all nodes in the instance ... run your input through that stylesheet to see all of the intervening nodes.

26.

Using concat

>The XPath spec uses concat(expression1,expression2,expression3*) as the
>example...
>The SAXON documentation shows concat(expression1{,expression2}*)...
>I can't get either to work.

Christopher R. Maden

Both of those syntactic expressions are somewhat misleading. The XPath one means that concat() takes multiple expressions, separated by commas. The asterisk indicates that there should be two, and that the third one is not only optional but also repeatable (i.e., that there may be more than three arguments).

The SAXON documentation correctly observes that concat() can take one expression. The curly braces encapsulate both the comma and the second expression, and indicate that the encapsulation may be repeated. [Mike Kay notes: (July 2000, Saxon at 5.3.2)

The Saxon code actually allows concat() to have zero or more arguments. This is a non-conformance (sigh), the spec says an error should be reported if there are less than two.]

Both are saying that concat() takes multiple arguments separated by commas.

The main problem you're having is with the nature of an expression. The variable part ($sect1num) is correct, since a variable reference is a valid expression. But the .htm for instance is wrong. A literal string expression must be surrounded by quotes: '.htm'. It's not clear whether sect1 is an XPath or a string. If it were a string, you'd want

concat('sect1', $sect1num, '.htm')

If sect1 is a path intended to get the value of the sect1 child of the current node, then you would do

concat(sect1, $sect1num, '.htm')