DasDave
DasDave

Reputation: 811

XSLT Number nodes without sorting

I have the following xml:

<policy>
    <games>
         <game startTime="11:00"/>
         <game startTime="11:20"/>
         <game startTime="11:40"/>
    </games>
    <games>
         <game startTime="11:10"/>
         <game startTime="11:30"/>
         <game startTime="11:50"/>
    </games>
</Policy>

I am trying to write an xslt that will add a new attribute to each game node and add the value in time order e.g.

<policy>
    <games>
         <game startTime="11:00" id="1"/>
         <game startTime="11:20" id="3"/>
         <game startTime="11:40" id="5"/>
    </games>
    <games>
         <game startTime="11:10" id="2"/>
         <game startTime="11:30" id="4"/>
         <game startTime="11:50" id="6"/>
    </games>
</policy>

I need the game nodes to stay in their current order so I'm not sure an xsl:sort would work.

At the moment I have this which obviously just numbers them in their current order and won't take account of the time attribute:

<xsl:template match="game">
    <xsl:copy>
      <xsl:attribute name="id">
        <xsl:value-of select="count(preceding::game) + 1"/>
      </xsl:attribute>
      <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
</xsl:template>

Upvotes: 5

Views: 94

Answers (2)

Marcus Rickert
Marcus Rickert

Reputation: 4238

If you replace your match template by

<xsl:template match="game">
  <xsl:variable name="current_time" select="number(substring-before(@startTime,':'))*60 + number(substring-after(@startTime,':'))"/>
  <xsl:copy>
    <xsl:attribute name="id">
      <xsl:value-of select="count(../../games/game[number(substring-before(@startTime,':'))*60 + number(substring-after(@startTime,':')) &lt; $current_time]) + 1"/>
    </xsl:attribute>
    <xsl:apply-templates select="@*|node()"/>
  </xsl:copy>
</xsl:template>

you will also get the desired result. This approach does not use sorting but for each entry counts all the entries below the current one. This was a really interesting task because I learnt the amazing fact today that you cannot compare strings in XSLT 1.0! Although the overall structure of your original template is maintained (as compared to the solution of @Rubens) it requires the time string to be converted into a number. Of course, this is inconvenient. However, you will probably have to add some extra string functionality to the other solution to make it robust with respect to times before 10:00 o'clock, too.

By the way: if time stamps occur multiple times the numbering corresponds to ranking with gaps (as opposed to a dense ranking without gaps).

Upvotes: 1

Rubens Farias
Rubens Farias

Reputation: 57976

I hope there is a better way than this:

<xsl:template match="game">
    <xsl:copy>
        <xsl:variable name="time" select="@startTime" />
        <xsl:for-each select="//game">
            <xsl:sort select="@startTime" />
            <xsl:if test="current()/@startTime = $time">
                <xsl:attribute name="id">
                    <xsl:value-of select="position()"/>
                </xsl:attribute>
            </xsl:if>
        </xsl:for-each>
        <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
</xsl:template>

Upvotes: 2

Related Questions