Select questions

1. Matching based on a range of values
2. Conditional selection of an attribute value
3. Matching an element with one of n attribute values
4. Unique attribute values in XSLT2
5. Boolean tests in 2.0
6. Tabular alternating colours
7. Select a range of nodes
8. Add default attributes.
9. Listing terms selectively
10. Unique attribute values
11. Distinct values
12. Using . as a predicate
13. Using except

1.

Matching based on a range of values

Michael Kay



> "Select all nodes where current nodes @timebeg falls between the 
> @timebeg and @timeend of all event1 elements".

You can do this in XPath 2 as

//node[every $n in //node[@id=1] satisfies 
        (@timebeg ge $n/@timebeg and @timebeg le $n/@timeend)]

You can't do general joins in XPath 1 (it's not relationally complete). The nearest you can get is

<xsl:for-each select="//node">
  <xsl:variable name="n" select="."/>
  <xsl:copy-of select="$n[not(//node[@id=1][@timebeg &lt;= $n/@timebeg or 
                                            @timeget &gt;= $n/@timeend)]"/>
</xsl:for-each>

2.

Conditional selection of an attribute value

Michael Kay



> choose a link for the title, based on the following conditions:
>   1. if the value of the link node has 'http://' string
>   2. if there's no 'http://' string get the value of the link node 
> that contains 'ftp://' string

> output should be: <a href="selected link">title</a>
> <?xml version="1.0" encoding="ISO-8859-1" standalone="yes"?> <record>  
> <data>
>   <link>http://www.link1.com</link>
>   <link>3csbv</link>
>   <link>ftp://link2.com</link>
>   <link>http://www.link3.com</link>
>   <title>title</title>
>  </data>
>  <data>
>   <link>45csgh</link>
>   <link>invalid link</link>
>   <link>ftp://link1.com</link>
>   <title>title</title>
>  </data>
> </record>
>

<xsl:template match="data">
<a href="{(link[matches(.,'^http://')],link[matches(.,'^ftp://')])[1]}">
<xsl:value-of select="title"/>
</a>
</xsl:template>

Ednote. Note the use of braces inside the AVT; and the use of the matches function in the predicate.

3.

Matching an element with one of n attribute values

David Carlisle


> I want to do something like <xsl:template match="volume/book[chapter/@title in ('monkeys','buffalo')]">
> So in this example I would want to match any book that had a chapter titled 'monkeys' or 'buffalo'.
>
>I realize I can do this, but this will get very tedious...
>
>< xsl:template match="volume/book[chapter/@title = 'monkeys'] | 
                          volume/book[chapter/@title = 'buffalo']"/>

Try

<xsl:template match="volume/book[chapter/@title=('monkeys','buffalo')]">

4.

Unique attribute values in XSLT2

You just need

<xsl:variable name="x" 
 select="distinct-values(/A/B/C/@atc)"/>

5.

Boolean tests in 2.0

Andrew Welsh



 Can I use a boolean variable in an xsl:if test

beware though that that will get you burned again when you start using XSLT2 scented water.

xsl:value-of returns a text node with string value the string value of the expression. This is subtly or not so subtly different from a string. It doesn't make so much difference in XSLT1 as the only way to carry strings around is to put them in text nodes, but in xpath2 you can have sequences of strings and sequences of text nodes (and sequences that contain both strings and text nodes) the rules for the two cases (and in particular whether spaces are automatically inserted between adjacent items) are different on the two cases.

Yes, but I think the 2.0 way makes more sense.

Just to make sure we are talking about the same thing (and to help cement my knowledge), consider:

<root>
  <node>foo</node>
  <node>bar</node>
</root>

In 1.0:

<xsl:template match="root">
  <xsl:value-of select="node"/>
</xsl:template>

Returns:

'foo'

Because in XSLT 1.0 'first item semantics' apply when a value-of is performed on a sequence.

In 2.0 the same template would return:

'foo bar'

That is, all items in the sequence with a single space as a seperator. In order to remove/control the space, we can use the @separator on value-of:

<xsl:value-of select="node" separator=""/>

Which would produce:

'foobar'

For me, that's much more intuitive than just picking the first one. Another plus for 2.0 :)

Of course, if there is another sequence related area to get burned on please post an example - it's good to know the gotchas up front.

6.

Tabular alternating colours

Michael Kay


> Hi, I'm trying implementing a XSL which distinguishes odd and 
> even row 
> of a table by using different colors. 

Another solution to add to the list you've been given: In XSLT 2.0 you can do

<td bgcolor="{if (position() mod 2) then 'black' else 'white'}">
  ...
</td>

7.

Select a range of nodes

Michael Kay


 Is it possible to use Xpath to select a range of nodes.
>
> <chapter>
> <title>X</title>
> <para></para>,
> <para></para>,
> <title>Y</title>
> </chapter>
>
> I would like an Xpath statement that would select //title[1]
> THROUGH //title[2] and include all nodes between.  Is this possible?
>

I'm assuming that <para> represents <para>....</para>, i.e. a complete element.

If you know that the nodes are siblings, and you are positioned on their parent, then you can do

(title[1] , *[. >> title[1] and . << title[2]] , title[2])

If they aren't siblings and you are positioned on the root, then you can do

for $T1 in (//title)[1], $T2 in (//title)[2]
return ($T1, //*[. >> $T1 and . << $T2], $T2)

8.

Add default attributes.

George Cristian Bina




> I'm trying to add default attribute values to elements, but I get
> errors. Is there a simple way to do it using xpath? What i'm trying is:
>
> <input type="text" value="{un|'Please enter your username'}" />
>
> Is there a simple way of doing this, or do I have to throw an xsl:choose
> inside an xsl:attribute?

In XSLT 2.0 you can write

value="{(un,'Please enter your username')[1]}"

which selects the first from the set, which is what you want.

9.

Listing terms selectively

Andrew Welch


> <areaserved>
>         <district name="South Hams">
>                 <town name="Dartmouth">
>                         <settlement name="Kingswear"/>
>                 </town>
>                 <town name="Totnes"/>
>         </district>
>         <district name="Torbay"/>
> </areaserved>
>
> I am trying to write some XSLT to transform this to a list as follows:
>
> Kingswear, Totnes, Torbay

In 2.0 you can use:

string-join(//*[not(*)]/@name, ', ')

10.

Unique attribute values

David Carlisle


I have an XML like this:

      <target:row Category=3D'A-B-C-D'/>
      <target:row Category=3D'A-B'/>
      <target:row Category=3D'A-C-D'/>
      <target:row Category=3D'C'/>
      <target:row Category=3D'B-C-D'/>
      <target:row Category=3D'A'/>

with a combination of values separated by '-'. I need to populate 
a listbox with unique values:

A
B
C
D

David C offers

<x xmlns:target="t">

      <target:row Category='A-B-C-D'/>
      <target:row Category='A-B'/>
      <target:row Category='A-C-D'/>
      <target:row Category='C'/>
      <target:row Category='B-C-D'/>
      <target:row Category='A-t'/>
</x>

<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"; xmlns:target="t" >

<xsl:output indent="yes"/>

<xsl:template match="x">
 <xsl:for-each select="distinct-values(target:row/tokenize(@Category,'-'))">
  <option><xsl:value-of select="."/></option>
 </xsl:for-each>
</xsl:template>
</xsl:stylesheet>

$ saxon8 spl.xml spl.xsl
<?xml version="1.0" encoding="UTF-8"?>
<option xmlns:target="t">A</option>
<option xmlns:target="t">B</option>
<option xmlns:target="t">C</option>
<option xmlns:target="t">D</option>
<option xmlns:target="t">t</option>

Florent Georges offers

or (I prefer this one):

   distinct-values(target:row/tokenize(@Category, '-'))

11.

Distinct values

Wendell Piez


>I'm trying to get a list of distinct items from an XML. I've done this
>many times using a predicate containing a preceding axis, but this one
>has got me stumped:

<page>
        <front_back>F</front_back>
        <page_no>1</page_no>
        <colours>
                <colour>Red</colour>
                <rgb>00FFFF</rgb>
                <colour>Green</colour>
                <rgb>00FF00</rgb>
                <colour>Blue</colour>
                <rgb>FFFF00</rgb>
        </colours>
</page> 

One can frequently use raw XPath to achieve the same end as using a key. It's just that for certain applications in certain processors, this can give very poor performance.

In order to get all 'colour' values, as you know, you can traverse "//colour".

In order to exclude Red and Blue, use a predicate:

//colour[not(.='Red' or 'Blue')]

You have also stipulated that "rgb value is not FFFFFF". But 'colour' sometimes has several 'rgb' element siblings. Your source code sample suggests it's the 'rgb' value directly following each 'colour' that is a concern, so:

//colour[not(.='Red' or 'Blue')][not(following-sibling::rgb[1]='FFFFFF']

which returns all colour elements whose value is not 'Red' or 'Blue' and whose directly succeeding 'rgb' element's value is not 'FFFFFF'.

To pull the distinct values from this set without a key, the easiest thing is to perform a test within your for-each (or your matched template), as in

<xsl:for-each select="$colours">
  <xsl:variable name="pos" select="position()"/>
  <xsl:if test="not(. = $colours[position &lt; $pos])">
    ...

... which is very direct and useful for many purposes.

Not for all, however. (For example, if positions are going to be important within the for-each.) Then you have to filter $colours itself to keep only distinct values, which means brute-force testing using the predicates again:

$colours[not(. = preceding::colour[not(.='Red' or 'Blue')]
                  [not(following-sibling::rgb[1]='FFFFFF'])]

... which is so outlandish (and potentially so poor in performance) that one wonders why you'd "prefer to avoid using keys if possible", since a key is so straightforward:

<xsl:key name="c" use="."
  match="colour[not(.='Red' or 'Blue')]
               [not(following-sibling::rgb[1]='FFFFFF']"/>

and then select="$colours[generate-id()=generate-id(key('c',.)[1])]"/>

12.

Using . as a predicate

Mike Kay, David Carlisle


I have 3 optional param values that are either true/false.  I need a
solution which will return 4 different numbers when:

1) all param values are true
2) two param values are true
3) one param value is true
4) none are true

I was about to write one big choose and then thought maybe there is a
more efficient way of doing it.  So any ideas on this one?  For sake
of discussion, here is a mock up of the template and param values I
have:

<xsl:template name="test">
  <xsl:param name="opt.one" select="true()" />
  <xsl:param name="opt.two" select="true()" />
  <xsl:param name="opt.three" select="true()" />

  <!--
    Return:

    When All, return 3.00
    When Two, return 2.25
    When One, return 1.70
    When Zero, return 1.00
   -->

 
select="(1.0, 1.7, 2.25, 3.0)[count(($opt.one, $opt.two, $opt.three)[.])+1]"

The predicate [.] selects items in the sequence that are true.

It will select all items in the sequence than have an effective boolean value of true (other than numbers, because [] is overloaded for numbers). In the case for the OP the items in the sequence are booleans, so it will select the true() values and reject the false() ones.

You can read it as

($opt.one, $opt.two, $opt.three)[. = true()]

if you prefer. But writing X=true() when X is a single boolean is always the same as writing X.

DC adds

The values in questions are boolean so 'true' means true() as opposed to false()

($opt.one, $opt.two, $opt.three)[.]

is a sequence of three things, filtered by [ ] exppression just like

(1,2,3)[.=2]

The result is the sequence of all items for which the predicate is true. In thr second example that would result in (2)

In the first example the predicate is just the item .. so it is true if . is true and false if it is false, so if $opt.one and $opt.three are true() and $opt.two is false() then

($opt.one, $opt.two, $opt.three)[.]

is

($opt.one,$opt.three)

and count($opt.one,$opt.three) is 2, telling you that 2 items are true.

13.

Using except

Various



I'm trying to remove the h1 from my source with the "except" and it
doesn't work.
ex:<xsl:copy-of  select="* except
(//xhtml:div[@class='border']//xhtml:h1)" />

When I do the same logic to class=border section it work?? 
<xsl:copy-of  select="* except(//xhtml:div[@class='border'])" />

What I'm I missing, with the except for the removal of that section
h1???



source:

<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
</head>
<body>
<!--googleoff:all-->
<div class="page">
<div class="center">
<div class="border">
<h1>
<a name="cont" id="cont">
 Aviation Security </a>
 </h1>
<p>The Government of Canada has committed more than programs include the
following:</p>
</div>
</div>
</div>
</body>
</html>


Mike Kay answers

<xsl:copy-of select="* except X"/>

means "make an exact copy of all the children, except any child selected by the expression X (which should not be copied at all).

I suspect you are misunderstanding it to mean "copy all the children, modifying them as you do so to leave out any nodes selected by X".

Clearly that doesn't work: the select expression selects all the children, then unselects some of them, then xsl:copy-of copies all those that remain.

Wendell Piez adds

What you are missing is an understanding of XPath syntax and how it is relating to your tree architecture.

It becomes clearer if you expand your XPaths into long syntax. You are currently using abbreviated syntax.

So --

First the XPath that is working for you:

* except(//xhtml:div[@class='border'])

is short for

child::* except 
  (/descendant-or-self::node()/child::xhtml:div[attribute::class='border'])

This is saying: "copy all children, excluding from this set any members of the set of xhtml:div[@class='border'] elements anywhere in the document".

Now the XPath that isn't working:

* except (//xhtml:div[@class='border']//xhtml:h1)

This is short for

child::* except 
   (/descendant-or-self::node()/child::xhtml:div[attribute::class='border']
                 /descendant-or-self::node()/child::xhtml:h1)

This is saying: "copy all children, excluding from this set any members of the set of xhtml:h1 descendants of xhtml:div[@class='border'] elements anywhere in the document"

Your stylesheet is doing this. Since no xhtml:h1 elements are children of the context node where the expression is evaluated (whichever xhtml:div[@class='center'] has matched your template), all the children are copied. (There are some among the descendants of the children you have asked to copy, but the 'except' operation doesn't work that way.)

In order to accomplish your filtering, you can't simply use a copy-of instruction, since xsl:copy-of simply copies subtrees from your source document without change. That is, you can exclude branches from being copied by using XPath as in your first case, but having decided to copy a branch using xsl:copy-of, you can't also modify it.

In order to do this, you need to use a somewhat more sophisticated approach. Your problem is actually a good example of what we call a "modified identity transform", since the result is just a modified version of the source. It could be done in a stylesheet with two templates like this:


<xsl:template match="*">
  <xsl:copy>
    <xsl:copy-of select="@*"/>
    <xsl:apply-templates/>
  </xsl:copy>
</xsl:template>

<xsl:template match="xhtml:h1"/>

How does this work? You need to read up on templates and the xsl:apply-template instruction.

The idea of the "context node" and its relation to the evaluation of XPath is also critical.

Note that if you apply templates to attributes and child nodes in the 'remove' mode, you also have to match them in 'remove' mode to have them copied (or removed).

I left the 'remove' mode off the templates I then wrote to illustrate.