purum
purum

Reputation: 323

xsl for ordering subnodes of xml

Here is sample xml

<Data version="2.0">
   <Group>
        <Item>3</Item>
        <Item>1</Item>
        <Item>2</Item>
   </Group>
   <Group>
        <Item>7</Item>
        <Item>5</Item>
   </Group>
</Data>

And for ordering nodes in Group by Item value I tried to use the following xsl:

  <xsl:template match="/Data">

    <xsl:apply-templates select="Group">
      <xsl:sort select="Item" />
    </xsl:apply-templates>

  </xsl:template>

But get only values, even without sorting:

    3
    1
    2

    7
    5

So the questions are: 1. why sorting not work 2. How to keep all nodes and keep structure of xml?

Upvotes: 2

Views: 130

Answers (2)

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243449

@Jim's answer is basically correct.

However, applied to a slightly more realistic XML document, such as this:

<Data version="2.0">
   <Group>
        <Item>3</Item>
        <Item>1</Item>
        <Item>10</Item>
        <Item>2</Item>
   </Group>
   <Group>
        <Item>7</Item>
        <Item>5</Item>
   </Group>
</Data>

the result produced is clearly not what you want (10 comes before 2 and 3):

<?xml version="1.0" encoding="utf-8"?>
<Data version="2.0">

   <Group>
      <Item>1</Item>
      <Item>10</Item>
      <Item>2</Item>
      <Item>3</Item>
   </Group>

   <Group>
      <Item>5</Item>
      <Item>7</Item>
   </Group>

</Data>

Here is a correct solution (that is also slightly shorter):

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

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

 <xsl:template match="Group">
  <Group>
   <xsl:apply-templates select="*">
    <xsl:sort data-type="number"/>
   </xsl:apply-templates>
  </Group>
 </xsl:template>
</xsl:stylesheet>

when this transformation is applied on the same XML document (above), the wanted, correct result is produced:

<Data version="2.0">
   <Group>
      <Item>1</Item>
      <Item>2</Item>
      <Item>3</Item>
      <Item>10</Item>
   </Group>
   <Group>
      <Item>5</Item>
      <Item>7</Item>
   </Group>
</Data>

Explanation: Use of the data-type attribute of <xsl:sort> to specify that the sort keys value should be treated as number, not as (the default) string.

Upvotes: 2

Jim Garrison
Jim Garrison

Reputation: 86774

This is what you want:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    version="1.0">
    <xsl:output method="xml" indent="yes"/>
    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="Group">
        <xsl:copy>
            <xsl:apply-templates select="@*"/>
            <xsl:apply-templates select="Item">
                <xsl:sort select="text()" />
            </xsl:apply-templates>
        </xsl:copy>
    </xsl:template>    
</xsl:stylesheet>

The first template is the "Identity Template" (Google it) which copies input to output unchanged. Then for the Group node we copy it to the output (<xsl:copy>), copy its attributes, then copy the nested Item nodes after sorting them. They get copied because the inner <xsl:apply-templates select="Item"> ends up using the Identity template since there's no more-specific template for Item nodes.

Note that if Group nodes can contain other stuff besides Item nodes, you'll have to make sure it gets copied as well. The template above would discard them.

Upvotes: 4

Related Questions