Minja
Minja

Reputation: 786

Sorting parent elements by the attribute values of a child element

I'm trying to figure out a (relatively) efficient way of sorting elements in XSLT so that I don't have to go back into my Java code and mess with the DOM.

If I have a file like this:

<?xml-stylesheet type="text/xsl" href="test.xsl"?>
<Root>
  <Player name="Jane Doe">
    <Location distance="90"/>
    <Location distance="45"/>
  </Player>
  <Player name="John Doe">
    <Location distance="50"/>
    <Location distance="20"/>
  </Player>
</Root>

My goal is to sort the players by the distance of the closest location. In other words, since John Doe has a location within 20 miles, he's closer and his node needs to be sorted above Jane Doe whose closest locations is 45 miles.

Is such a thing even possible? If not, no biggie. I just wanted to throw this out there before I begin DOM manipulation.

Upvotes: 1

Views: 233

Answers (2)

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243449

Here is a short XSLT 1.0 transformation:

<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="/*">
    <xsl:copy>
      <xsl:apply-templates>
        <xsl:sort select="*/@distance[not(. > ../../*/@distance)]" data-type="number"/>
      </xsl:apply-templates>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

When this transformation is applied on the provided XML document:

<Root>
    <Player name="Jane Doe">
        <Location distance="90"/>
        <Location distance="45"/>
    </Player>
    <Player name="John Doe">
        <Location distance="50"/>
        <Location distance="20"/>
    </Player>
</Root>

the wanted correctly-sorted result is produced:

<Root>
   <Player name="John Doe">
      <Location distance="50"/>
      <Location distance="20"/>
   </Player>
   <Player name="Jane Doe">
      <Location distance="90"/>
      <Location distance="45"/>
   </Player>
</Root>

Upvotes: 2

Martin Honnen
Martin Honnen

Reputation: 167516

Using XSLT 2.0 (and an XSLT 2.0 processor like Saxon 9) you can use

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    exclude-result-prefixes="xs"
    version="2.0">

    <xsl:output indent="yes"/>

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

    <xsl:template match="Root">
        <xsl:copy>
            <xsl:apply-templates select="Player">
                <xsl:sort select="min(Location/@distance)"/>
            </xsl:apply-templates>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>

Here is an XSLT 1.0 solution using exsl:node-set to process a result tree fragment created in a variable where the minimum distance has been identified using the usual XSLT 1.0 approach of sorting and taking the first item in sort order:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
    xmlns:exsl="http://exslt.org/common"
    exclude-result-prefixes="exsl">

    <xsl:output indent="yes"/>

    <xsl:variable name="players-rtf">
        <xsl:for-each select="Root/Player">
            <xsl:copy>
                <xsl:attribute name="min-dist">
                    <xsl:for-each select="Location/@distance">
                        <xsl:sort select="." data-type="number"/>
                        <xsl:if test="position() = 1">
                            <xsl:value-of select="."/>
                        </xsl:if>
                    </xsl:for-each>
                </xsl:attribute>
                <xsl:copy-of select="@* | *"/>
            </xsl:copy>
        </xsl:for-each>
    </xsl:variable>

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

    <xsl:template match="Root">
        <xsl:copy>
            <xsl:apply-templates select="exsl:node-set($players-rtf)/Player">
                <xsl:sort select="@min-dist" data-type="number"/>
            </xsl:apply-templates>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="Player/@min-dist"/>

</xsl:stylesheet>

Upvotes: 0

Related Questions