Search code examples
xmlxsltxslt-1.0xslt-grouping

Sort groups by arregate element property in xslt


I'm transforming some xml using xslt (version 1.0, using MSXSL).

Say my xml data looks like this:

<table>
 <record><name>A</name><value>a</value><size>10</size></record>
 <record><name>A</name><value>b</value><size>35</size></record>
 <record><name>A</name><value>c</value><size>60</size></record>
 <record><name>B</name><value>x</value><size>15</size></record>
 <record><name>B</name><value>y</value><size>90</size></record>
 <record><name>B</name><value>z</value><size>20</size></record>
 ...
</table>

My goal is:

  1. to group the records by <name>
  2. per group, determine the maximum <size>, say maxsize
  3. sort the groups by their maxsize (descending)
  4. per group, list the records (in original order)

So the result could be:

<table>
 <group>B<maxsize>90</maxsize>
  <record><value>x</value><size>15</size>
  <record><value>y</value><size>90</size>
  <record><value>z</value><size>20</size>
 </group>
 <group>A<maxsize>60</maxsize>
  <record><value>a</value><size>10</size>
  <record><value>a</value><size>35</size>
  <record><value>a</value><size>60</size>
 </group>
</table>

Now steps 1, 2 and 4, I can do that. But... how can I order the groups by their maximum size?

I tried building a new node set in a variable, containing the groups. I can build such a set but I can only access it as a string.

Should be possible, right?


Solution

  • Here you go:

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
        <xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>
      <xsl:key name="kRecord" match="record" use="name"/>
    
        <xsl:template match="@* | node()">
            <xsl:copy>
                <xsl:apply-templates select="@* | node()"/>
            </xsl:copy>
        </xsl:template>
    
      <xsl:template match="/*">
        <xsl:copy>
          <xsl:apply-templates
            select="record[generate-id() = 
                           generate-id(key('kRecord', name)[1])]" 
            mode="group">
            <xsl:sort select="key('kRecord', name)/size
                                  [not(. &lt; key('kRecord', ../name)/size)]" 
                      data-type="number"
                      order="descending" />
          </xsl:apply-templates>
        </xsl:copy>
      </xsl:template>
    
      <xsl:template match="record" mode="group">
        <xsl:variable name="members" select="key('kRecord', name)" />
        <group>
          <xsl:value-of select="name" />
          <maxsize>
            <xsl:value-of select="$members/size[not(. &lt; $members/size)]"/>
          </maxsize>
          <xsl:apply-templates select="$members" />
        </group>
      </xsl:template>
    
      <xsl:template match="record/name" />
    </xsl:stylesheet>
    

    When run on your sample input, the result is:

    <table>
      <group>
        B<maxsize>90</maxsize><record>
          <value>x</value>
          <size>15</size>
        </record><record>
          <value>y</value>
          <size>90</size>
        </record><record>
          <value>z</value>
          <size>20</size>
        </record>
      </group>
      <group>
        A<maxsize>60</maxsize><record>
          <value>a</value>
          <size>10</size>
        </record><record>
          <value>b</value>
          <size>35</size>
        </record><record>
          <value>c</value>
          <size>60</size>
        </record>
      </group>
    </table>
    

    Incidentally, it is possible to access a constructed node set in a variable if you use the node-set() function which is available in most XSLT processors. I like to avoid the node-set() function when I can because it is non-standard and does not have complete support (and its namespace isn't even consistent across processors that do support it), but here is how you could do it:

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                    xmlns:exslt="http://exslt.org/common" 
                    exclude-result-prefixes="exslt">
      <xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>
      <xsl:key name="kRecord" match="record" use="name"/>
    
      <xsl:template match="@* | node()">
        <xsl:copy>
          <xsl:apply-templates select="@* | node()"/>
        </xsl:copy>
      </xsl:template>
    
      <xsl:template match="/table">
        <xsl:copy>
          <xsl:variable name="groups">
            <xsl:apply-templates
              select="record[generate-id() = 
                             generate-id(key('kRecord', name)[1])]"
              mode="group" />
          </xsl:variable>
          <xsl:apply-templates select="exslt:node-set($groups)/*">
            <xsl:sort select="maxsize" data-type="number" order="descending" />
          </xsl:apply-templates>
        </xsl:copy>
      </xsl:template>
    
      <xsl:template match="record" mode="group">
        <xsl:variable name="members" select="key('kRecord', name)" />
        <group>
          <xsl:value-of select="name" />
          <maxsize>
            <xsl:apply-templates select="$members/size" mode="max">
              <xsl:sort select="." data-type="number" order="descending" />
            </xsl:apply-templates>
          </maxsize>
          <xsl:apply-templates select="$members" />
        </group>
      </xsl:template>
    
      <xsl:template match="record/name" />
    
      <xsl:template match="*" mode="max">
        <xsl:if test="position() = 1">
          <xsl:value-of select="." />
        </xsl:if>
      </xsl:template>
    </xsl:stylesheet>