Re: [xsl] How to do... regular 1 to n loops, 0 instead of null, maximum element, call template succinctly, etc.

Subject: Re: [xsl] How to do... regular 1 to n loops, 0 instead of null, maximum element, call template succinctly, etc.
From: Jeni Tennison <jeni@xxxxxxxxxxxxxxxx>
Date: Tue, 23 Mar 2004 13:42:57 +0000
Hi Paul,

> 1) How to do regular programming loops, like for i = 1 to 10 For
> example I needed to do this recently to do a 16x16 grid to display a
> class C network. I can only see how to loop on XML nodes, so I had
> to create a nodeset like:
> <rows><row/><row/>....<row/></rows>

Use recursion rather than iteration. Have a template that looks
something like:

<xsl:template name="repeat">
  <xsl:param name="count" select="0" />
  <xsl:if test="$count">
    ... do something ...
    <xsl:call-template name="repeat">
      <xsl:with-param name="count" select="$count - 1" />
    </xsl:call-template>
  </xsl:if>
</xsl:template>

To repeat something 16 times, call it with $count set to 16.

In XSLT 2.0, you can use iteration like so:

  <xsl:for-each select="1 to 16">
    ... do something ...
  </xsl:for-each>

Here, the expression "1 to 16" creates a sequence of integers from 1
to 16, and the <xsl:for-each> iterates over them just as it does over
a node-set in XSLT 1.0.

> 2) When a node doesn't exist I want to get zero instead of null A
> good solution seems to be to use sum(...), but this only works if
> you pass it a nodeset. Sometimes I'm inside a template that may be
> passed a nodeset or maybe a number. The simplest way I figured then
> was number(concat('0', ...)) but this seems less than ideal.

I think that your "number(concat('0', ...))" is the best one-liner
given you're getting either a number or a node, or an empty node-set.
A better design for your template would be to have two parameters, one
for a node and one for a number, and pass in a value for $node when
you have a node and for $number when you have a number. The $number
parameter is the one you should use in the body of the template; it
can default to the number specified by the $node parameter when that's
provided:

  <xsl:param name="node" select="/.." />
  <xsl:param name="number">
    <xsl:choose>
      <xsl:when test="$node">
        <xsl:value-of select="$node" />
      </xsl:when>
      <xsl:otherwise>0</xsl:otherwise>
    </xsl:choose>
  </xsl:param>

This would also be better in XSLT 2.0, where it's good idea to specify
a type for your parameters, but in which you can't specify the type
"node or number".

> 3) How to get maximum element of a list? (or minimum)
> Best solution found was something like the following, which seems quite
> long and complicated:
> <xsl:variable name="max">
> <xsl:for-each select="nodes">
> <xsl:sort order="descending"/>
> <xsl:if test="position() = 1"><xsl:value-of select="text()"/></xsl:if>
> </xsl:for-each>
> </xsl:variable>

That's one way. You can also do it by recursion, which is more
long-winded, but more efficient if you have a lot of nodes:

<xsl:template name="max">
  <xsl:param name="nodes" />
  <xsl:param name="max" select="0" />
  <xsl:choose>
    <xsl:when test="$nodes">
      <xsl:call-template name="max">
        <xsl:with-param name="nodes"
                        select="$nodes[position() > 1]" />
        <xsl:with-param name="max">
          <xsl:choose>
            <xsl:when test="$nodes[1] > $max">
              <xsl:value-of select="$nodes[1]" />
            </xsl:when>
            <xsl:otherwise>
              <xsl:value-of select="$max" />
            </xsl:otherwise>
          </xsl:choose>
        </xsl:with-param>
      </xsl:call-template>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="$max" />
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

(There are other recursive tactics here: you could use a
Divide-and-Conquer (DVC) algorithm instead.)

XSLT 2.0 has the max() function. Many XSLT 1.0 processors support the
math:max() function from EXSLT.

> 4) How to succinctly call templates with parameters?
> Is there a neat and short way to call a template? The following seems
> awfully long-winded:
> <xsl:call-template name="show-trend">
> <xsl:with-parameter name="curval">10</xsl:with-parameter>
> <xsl:with-parameter name="prevval">1</xsl:with-parameter>
> </xsl:call-template>
> I'd love to be able to do:
> <xsl:call-template name="show-trend" curval="10" prevval="1"/>
> But that doesn't work :-( Any workarounds?

If this is a real burden, write the stylesheet with the shorter syntax
you prefer and then run it through the following stylesheet:

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

<xsl:namespace-alias stylesheet-prefix="#default"
                     result-prefix="xsl" />

<xsl:template match="node()|@*">
  <xsl:copy>
    <xsl:apply-templates select="@*|node()" />
  </xsl:copy>
</xsl:template>

<xsl:template match="xsl:call-template">
  <xsl:copy>
    <xsl:copy-of select="@name" />
    <xsl:for-each select="@*[name() != 'name']">
      <with-param name="{name()}" select="{.}" />
    </xsl:for-each>
    <xsl:apply-templates />
  </xsl:copy>
</xsl:template>

</xsl:stylesheet>

This is basically an identity transformation, except that the
attributes of <xsl:call-template> (aside from the 'name' attribute)
get transformed into <xsl:with-param> elements.

In XSLT 2.0, you can define functions rather than templates, which can
then be called with the usual function-calling syntax such as:

  <xsl:copy-of select="my:show-trend(10, 1)" />

Many XSLT 1.0 processors also support the ability to define functions
using <func:function> from EXSLT.

> 5) How to sort so nulls come at top not bottom?
> Say I have <fred size="10"/><fred size="11"/><fred/>
> If you use <xsl:sort select="@size"/> then the fred with no size
> comes at the end. How can I make it come at the beginning?

Use two sorts, with the first sort sorting on whether or not the size
attribute is present, and the second on the value of the size
attribute:

  <xsl:sort select="boolean(@size)" />
  <xsl:sort select="@size" data-type="number" />

The first sort will put those without a size ("boolean(@size)" =
'false') after those with a size ("boolean(@size)" = 'true').

Cheers,

Jeni

---
Jeni Tennison
http://www.jenitennison.com/

Current Thread