Search code examples
xsltxslt-1.0xslt-grouping

Sort data in the xml alphabetical order


Input XML :

<?xml version="1.0" encoding="utf-8" ?>
<infoset>
  <info>
    <title>Bill</title>
    <group>
      <code>state</code>
    </group>
  </info>
  <info>
    <title>Auto</title>
    <group>
      <code>state</code>
    </group>
  </info>
  <info>
    <title>Auto2</title>
  </info>
  <info>
    <title>Auto3</title>
  </info>
  <info>
    <title>Auto5</title>
  </info>
  <info>
    <title>Certificate</title>
    <group>
      <code>Auto4</code>
    </group>
  </info>
  </infoset>

Expected output :

A

Auto2
Auto3
Auto4
   Certificate
Auto5

S
state
   Auto
   Bill

I need to arrange the title and code in alphabetical order.If the info has group the tile should come under the group. I am using visual studio2010 , xslt1.0 Processor and xml editor.


Solution

  • Sorting in XSLT is straight-forward. What you are actually really needing to know is how to 'group' items. As Michael Kay commented, this is much easier in XSLT 2.0 than in XSLT 1.0. In XSLT 1.0 you tend to use the Muenchian Grouping method, which appears confusing when you first see it, but is generally the most efficient way of doing it.

    From the looks of your output, you are doing two lots of grouping. Firstly, by the first letter, then by either group/code (if it exists), or title.

    Muenchian Grouping works by defining a key, to enable quick look up of all items in a 'group'. For the first letter of eithe group/code or title, you would define it like so

    <xsl:key name="letter" match="info" use="substring(concat(group/code, title), 1, 1)"/>
    

    (Note: This is case sensitive, so you may need to use the 'translate' function if you can have a mix of lower and upper case start letters).

    If group/code exists, it will use the first letter of that, otherwise it will pick up the first letter of the title.

    For the group/code or title itself, the key would be as follows

    <xsl:key name="info" match="info" use="title[not(../group)]|group/code"/>
    

    So, it only uses "title" elements where there is no "group" element present.

    To get the distinct first letters for your first grouping, you select all the info elements and check whether they are the first element in the key for their given letter. This is done like so

    <xsl:apply-templates 
         select="info
                 [generate-id() 
                  = generate-id(key('letter', substring(concat(group/code, title), 1, 1))[1])]" 
         mode="letter">
      <xsl:sort select="substring(concat(group/code, title), 1, 1)" />
    </xsl:apply-templates>
    

    The 'mode' is used here because the final XSLT will have to templates matching info.

    Within the matching template, to group by either code/group or title you can then do this

    <xsl:apply-templates 
         select="key('letter', $letter)
                [generate-id() = generate-id(key('info', title[not(../group)]|group/code)[1])]">
      <xsl:sort select="title[not(../group)]|group/code" />
    </xsl:apply-templates>
    

    And finally, to output all the elements within the final group, you would just use the key again

    <xsl:apply-templates select="key('info', $value)[group/code=$value]/title">
    

    Here is the full XSLT.

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
      <xsl:output method="text" indent="yes"/>
    
      <xsl:key name="letter" match="info" use="substring(concat(group/code, title), 1, 1)"/>
      <xsl:key name="info" match="info" use="title[not(../group)]|group/code"/>
    
      <xsl:template match="/*">
        <xsl:apply-templates select="info[generate-id() = generate-id(key('letter', substring(concat(group/code, title), 1, 1))[1])]" mode="letter">
          <xsl:sort select="substring(concat(group/code, title), 1, 1)" />
        </xsl:apply-templates>
      </xsl:template>
    
      <xsl:template match="info" mode="letter">
        <xsl:variable name="letter" select="substring(concat(group/code, title), 1, 1)" />
        <xsl:value-of select="concat($letter, '&#10;')" />
        <xsl:apply-templates select="key('letter', $letter)[generate-id() = generate-id(key('info', title[not(../group)]|group/code)[1])]">
          <xsl:sort select="title[not(../group)]|group/code" />
        </xsl:apply-templates>
      </xsl:template>
    
      <xsl:template match="info">
        <xsl:variable name="value" select="title[not(../group)]|group/code" />
        <xsl:value-of select="concat($value, '&#10;')" />
        <xsl:apply-templates select="key('info', $value)[group/code=$value]/title">
          <xsl:sort select="." />
        </xsl:apply-templates>
      </xsl:template>
    
      <xsl:template match="title">
        <xsl:value-of select="concat('   ', ., '&#10;')" />
      </xsl:template>
    </xsl:stylesheet>
    

    When applied to your XML, the following is output

    A
    Auto2
    Auto3
    Auto4
       Certificate
    Auto5
    s
    state
       Auto
       Bill