Jeni Tennison
> I'm wondering if anyone has created a template (or has ideas on how
> to efficiently create such) that generates a list of items that is
> missing from a given source document. Assume that a node-set is
> passed that contains an assorted cluster of indices, the template
> should start at one, and continue to the highest provided element,
> outputting all missing elements.
Hrm... It'd be a lot easier if your nodes were sorted to start with --
are you happy to use a node-set() extension function? If so, sort
them: <xsl:variable name="sorted-childnode-rtf">
<xsl:for-each select="obj/childnode">
<xsl:sort select="." data-type="number" />
<xsl:copy-of select="." />
</xsl:for-each>
</xsl:variable>
<xsl:variable name="sorted-childnodes"
select="exsl:node-set($sorted-childnodes)" />
Then have a recursive template that takes a running count and a bunch
of nodes as parameters: <xsl:template name="fill-in-gaps">
<xsl:param name="count" select="1" />
<xsl:param name="nodes" select="/.." />
...
</xsl:template>
If there aren't any nodes left, you're done. If there are, compare the
first node with the count. If it's the same, then add one to the count
and move on to the next node:
<xsl:template name="fill-in-gaps">
<xsl:param name="count" select="1" />
<xsl:param name="format" select="'1'" />
<xsl:param name="nodes" select="/.." />
<xsl:if test="$nodes">
<xsl:variable name="number">
<xsl:number value="$count" format="{$format}" />
</xsl:variable>
<xsl:choose>
<xsl:when test="$number = $nodes[1]">
...
</xsl:when>
<xsl:otherwise>
...
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:template>
Otherwise there's a gap, so output the thing to fill it, add one to
the count but keep the set of nodes the same for the next call:
<xsl:template name="fill-in-gaps">
<xsl:param name="count" select="1" />
<xsl:param name="nodes" select="/.." />
<xsl:if test="$nodes">
<xsl:choose>
<xsl:when test="$nodes[1] = $count">
<xsl:call-template name="fill-in-gaps">
<xsl:with-param name="count" select="count + 1" />
<xsl:with-param name="nodes"
select="$nodes[position() > 1]" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<obj>
<childnode><xsl:value-of select="$count" /></childnode>
</obj>
<xsl:call-template name="fill-in-gaps">
<xsl:with-param name="count" select="count + 1" />
<xsl:with-param name="nodes" select="$nodes" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:template>
If you don't want to use an extension function, then you should apply
templates to the nodes in ascending order:
<xsl:apply-templates select="obj/childnode">
<xsl:sort select="." data-type="number" />
</xsl:apply-templates>
Then have a recursive template that matches the node and extracts from
it a value (one less than its value) and a bunch of nodes (those
childnode elements that have a value less than it does):
<xsl:template match="childnode" name="fill-in-gaps">
<xsl:param name="value" select=". - 1" />
<xsl:param name="nodes"
select="../../obj/childnode[. < current()]" />
...
</xsl:template>
In the recursive template work down through the values until there's a
node in $nodes that has the same value as $value:
<xsl:template match="childnode" name="fill-in-gaps">
<xsl:param name="value" select=". - 1" />
<xsl:param name="nodes"
select="../../obj/childnode[. < current()]" />
<xsl:if test="not($value = $nodes)">
<obj><childnode><xsl:value-of select="$value" />
</childnode></obj>
<xsl:call-template name="fill-in-gaps">
<xsl:with-param name="value" select="$value - 1" />
<xsl:with-param name="nodes" select="$nodes" />
</xsl:call-template>
</xsl:if>
</xsl:template>
Easiest XSLT 2.0 way, I think, would be to create a sequence of
numbers from 1 to the maximum value of the childnodes, then filter it
to contain only those values that aren't the same as the values of one
of the nodes:
<xsl:variable name="nodes" select="obj/childnode" />
<xsl:for-each select="(1 to max($nodes))[not($nodes = .)]">
<obj><childnode><xsl:value-of select="." /></childnode></obj>
</xsl:for-each>
It's not very efficient, though, especially for lots of nodes. |