XML.com: XML From the Inside Out
oreilly.comSafari Bookshelf.Conferences.

advertisement

Comparing XSLT and XQuery
by J. David Eisenberg | Pages: 1, 2, 3, 4

Creating a List Item for the Index Page

Let us now turn our attention to the XSLT that provides the list items with four pig names per entry. The numbers in the circles refer to the notes that follow the listing.

<xsl:template match="animal" mode="indexList">
<xsl:variable name="start" 
                  select="(position()-1)*$perPage + 1"/> note 1

    <xsl:variable name="end"> note 2
        <xsl:choose>
          <xsl:when test="$start + $perPage &gt;
           count(/pig-rescue/animal)">
          <xsl:value-of select="count(/pig-rescue/animal)"/>
            </xsl:when>
            <xsl:otherwise>
              <xsl:value-of select="$start + $perPage - 1"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:variable>

    <xsl:variable name="filename">animals<xsl:value-of
       select="position()"/>.html</xsl:variable> note 3

      <li>
      <xsl:for-each
      select="/pig-rescue/animal[position() &gt;= $start and
      position() &lt;= $end]">
            
            <xsl:variable name="url"><xsl:value-of note 4
            select="$filename"/>#a<xsl:value-of
            select="$start+position()-1"/></xsl:variable>
            
            <a href="{$url}">
                <xsl:value-of select="name"/>
            </a>
            <xsl:call-template name="seriesSeparator"> note 5
              <xsl:with-param name="start" select="$start"/>
                <xsl:with-param name="end" select="$end"/>
            </xsl:call-template>
        </xsl:for-each>
    </li>
    
    <xsl:call-template name="makeSubfile"> note 6
        <xsl:with-param name="start" select="$start"/>
        <xsl:with-param name="end" select="$end"/>
        <xsl:with-param name="filename" select="$filename"/>
    </xsl:call-template>
</xsl:template>
  1. We can’t just use position() for this; though the calling template selected every fourth item, the template sees the resulting nodes in that list as being numbered one, two, three, etc.
  2. The last animal to be processed is the starting animal plus the number per page or the total number of animals, whichever is less.
  3. The pigs’ names have to link to the page where their full descriptions will be. For the first four pigs, this is animals1.html, for the next four it is animals2.html, etc.
  4. Construct the destination URL for each pig.
  5. Listing the names separated by commas in a grammatically correct manner is tricky business, so we hand that off to a named template.
  6. Finally, as long as we have figured out which pigs to process, we pass that information to a template that will construct the file we named in step 4 above.

The XQuery equivalent for this is the local:make-name-list function. The logic is the same, so the notes will concentrate on the XQuery-specific aspects.

declare function local:
make-name-list( $animalList as element()* ) as item()+ note 1
{
  for $pig at $pos in 
    $animalList[position() mod $perPage = 1] note 2
    let
        $n := count($animalList), note 3
        $filename := fn:concat("animals", $pos, ".html"),
        $start := ($pos - 1) * $perPage + 1,
        $end := if ($start + $perPage > $n)
            then 
                $n
            else
                $start + $perPage - 1
    return
    (
        <li> note 4
        {
 for $animal at $pos in $animalList[position() >= $start and
                position() <= $end] 
            return (
                <a href="{$filename}#a{$start + $pos - 1}">
                    {$animal/name/text()} note 5
                </a>,
                local:series-separator( $start, $pos, $end )
            )
        }
        </li>,
local:make-subfile( $animalList, $start, $end, $filename) note 6
    )
};
  1. In an XQuery function, you should always specify the types of function parameters and return values. In this case, we need to specify that the $animalList parameter will consist of element()*, which means zero or more elements. The function returns item()+, which means one or more items.

    If you do not specify a type for the parameter or return value, XQuery assigns item()*, meaning zero or more items, where an item() is equivalent to XML Schema’s xs:anyType. This is normally not what you want.

  2. Here is a for clause, stepping through every fourth animal in the list. The at $pos modifier has the same effect as <xsl:value-of select="position()">. In XQuery, you can use the position() function only inside a predicate of an XPath expression.

  3. You may do several different assignments within a let clause by separating them with commas. Notice the assignment to $end, which uses an if expression. Since this is an expression and not a statement, you must always have both a then and else so that it always yields a value.

  4. The return’s first value is the list item. The <li> puts us into direct constructor mode, so we need braces to re-enter XQuery mode to create the contents.

  5. This line is the reason we needed to declare $animalList as element()*. You cannot use an anonymous item as a path step; you must have a node or an element.

    Also, we can’t just say $animal/name. Unlike <xsl:value-of select="animal/name"/>, which yields a text value, $animal/name puts a copy of the <name> element, tags and all, into the return value. If we want just the text, we have to explicitly add the extra text() step to the XPath expression.

  6. Making the page with the pigs’ description is a task that we hand off to another local function. Its output will be the second item in this function’s return value (note the comma on the preceding line), and that value will eventually make its way into the output, so the function will have to return the null string as its value.

Notice that the return expression switches between direct element constructor mode and XQuery expression evaluation mode several times. In XSLT, the difference between commands to the XSLT processor and elements destined for output is fairly easy to distinguish due to the leading xsl: prefix. When you first start writing XQuery, it can be difficult to see—but always important to remember—which mode you are in.

Putting the correct separator after a pig’s name boils down to one of four cases:

  • last pig in the series: no comma
  • next to last pig in a group of two: “ and ”
  • next to last pig in a group of three or more: “ , and ”
  • other pig in a series: a comma followed by a blank

In XSLT, this is a simple <xsl:choose>, and we won’t show it here. In XQuery, it is a simple nested if. The types in the following declaration are based on XML Schema’s predefined types, which means you also get all the quirks and non-extensibility of the XML Schema type list. The function doesn’t need a FLWOR expression; the result of the nested if is the function’s return value.

declare function local:
series-separator( $start as xs:integer, $pos as xs:integer, 
$end as xs:integer) as xs:string   
{
    if (($start + $pos < $end) and ($end - $start > 1))
    then
        ", "
   else if (($start + $pos = $end) and ($end - $start >= 2))
    then
        ", and "
    else if (($start + $pos = $end) and ($end - $start = 1))
    then
        " and "
    else
        ""
};

Pages: 1, 2, 3, 4

Next Pagearrow