XSLT to sort by value of multiple attributes

I have a huge configuration file in XML format. The system does not care about the order of tags, but we humans do! (First of all, to compare versions.) I already got XSLT, below which it works well, but I found that this is not enough.

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="*"> <xsl:copy> <xsl:copy-of select="@*"/> <xsl:apply-templates> <xsl:sort select="(@name, name())[1]"/> </xsl:apply-templates> </xsl:copy> </xsl:template> </xsl:stylesheet> 

I want to sort all tags recursively by the value of their name attribute (this works!), But since the attribute is not always present, it must also sort by additional attributes, any of which may or may not be present in any particular element.

I basically have zero understanding of XSLT, so I'm experimenting. I hacked above, but this does not work as desired. The result of this seems to be identical to the one above.

 <xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="*"> <xsl:copy> <xsl:copy-of select="@*"/> <xsl:apply-templates> <xsl:sort select="@name"/> <xsl:sort select="@row" data-type="number"/> <xsl:sort select="@col" data-type="number"/> <xsl:sort select="@sequence" data-type="number"/> <xsl:sort select="@tabindex" data-type="number"/> </xsl:apply-templates> </xsl:copy> </xsl:template> </xsl:stylesheet> 

My data looks like this, and the problem is that the cell elements are not sorted at all (inside their grid group), because they do not have a name attribute. This is why I would like to extend the sorting logic to use the name attribute, if present, otherwise the sorting should be done using additional attributes like tabindex . Within any group, it can be considered that the same attributes are present.

 <sections> <section name="SomeList"> <caption> <![CDATA[Candidates]]> </caption> ... <parameters> <parameter name="pageSize"> <![CDATA[50]]> </parameter> </parameters> ... <grid> <cell row="0" col="7" tabindex="9" colspan="10"> <field name="Entered" /> </cell> </grid> </section> </sections> 

Update:
With Vincent very good help, I created a sort that works well enough for our purposes. Here he is.

+7
source share
3 answers

This is an answer that assumes that you do not have mixed content in your data. It takes into account only the first two steps (@name and @col), you can adapt for further steps. Perhaps it can be rewritten using a recursive named template that takes your sort parameter as an input parameter. Could you provide a sample XML if my XSLT does not work for you.

XSLT 2.0 Example:

 <?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0"> <xsl:template match="*"> <xsl:copy> <xsl:copy-of select="@*"/> <xsl:for-each-group select="*" group-by="if (exists(@name)) then @name else ''"> <xsl:sort select="current-grouping-key()" data-type="text"/> <xsl:for-each-group select="current-group()" group-by="if (exists(@row)) then @row else -1"> <xsl:sort select="current-grouping-key()" data-type="number"/> <xsl:apply-templates select="current-group()"/> </xsl:for-each-group> </xsl:for-each-group> </xsl:copy> </xsl:template> </xsl:stylesheet> 

Please note that the code iterates in a group with the same values, therefore, if the attribute is absent in the elements, the elements are grouped together.

I take the following XML as input:

 <?xml version="1.0" encoding="UTF-8"?> <items> <item row="5" col="9"></item> <item name="d" row="20" col="12" tabindex="" sequence=""></item> <item row="1" col="5" ></item> <item name="d" row="5" col="6" ></item> <item name="a" row="7" col="8" ></item> <item name="s" row="1" col="5" ></item> <item name="c" row="5" col="9"></item> <item row="2" col="5" ></item> <item row="20" col="9"></item> <item row="0" col="9"></item> <item name="s" row="2" col="10" tabindex="" sequence=""></item> <item name="z" row="8" col="15" tabindex="" sequence=""></item> </items> 

I have the following result:

 <?xml version="1.0" encoding="UTF-8"?> <items> <item row="0" col="9"/> <item row="1" col="5"/> <item row="2" col="5"/> <item row="5" col="9"/> <item row="20" col="9"/> <item name="a" row="7" col="8"/> <item name="c" row="5" col="9"/> <item name="d" row="5" col="6"/> <item name="d" row="20" col="12" tabindex="" sequence=""/> <item name="s" row="1" col="5"/> <item name="s" row="2" col="10" tabindex="" sequence=""/> <item name="z" row="8" col="15" tabindex="" sequence=""/> </items> 
+4
source

Consider this XSLT for strong elements with required attributes defined:

  <?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0"> <xsl:output indent="yes"/> <xsl:template match="*"> <xsl:copy> <xsl:copy-of select="@*"/> <xsl:apply-templates> <xsl:sort select="(@name, name())[1]"/> </xsl:apply-templates> </xsl:copy> </xsl:template> <xsl:template match="grid"> <xsl:copy> <xsl:copy-of select="@*"/> <xsl:for-each-group select="*" group-by="if (exists(@row)) then @row else -1"> <xsl:sort select="current-grouping-key()" data-type="number"/> <xsl:for-each-group select="current-group()" group-by="if (exists(@col)) then @col else -1"> <xsl:sort select="current-grouping-key()" data-type="number"/> <xsl:apply-templates select="current-group()"/> </xsl:for-each-group> </xsl:for-each-group> </xsl:copy> </xsl:template> </xsl:stylesheet> 

My example should cover sections and options sorting using the first pattern *. As well as sorting the grid by row and column. You can expand for any orther elements that have different sorting attributes by duplicating a template.

If you have multiple elements for the same attributes, use match="elt1|elt2|elt3" .

+1
source

Here is a general, simple, and short-lived (60 well-formatted string) solution .

Sorting is performed according to all required attributes, and this does not require manual duplication of templates :

 <xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:my="my:my" exclude-result-prefixes="my"> <xsl:output omit-xml-declaration="yes" indent="yes"/> <xsl:param name="pSortTypes" as="element()*"> <attr name="name" type="alpha" maxLength="15"/> <attr name="row" type="numeric" maxLength="6"/> <attr name="col" type="numeric" maxLength="4"/> <attr name="tabindex" type="numeric" maxLength="2"/> <attr name="sequence" type="numeric" maxLength="3"/> </xsl:param> <xsl:template match="*"> <xsl:copy> <xsl:copy-of select="@*"/> <xsl:apply-templates select="*"> <xsl:sort select="my:OrderedAttributeTuple(.)"/> </xsl:apply-templates> </xsl:copy> </xsl:template> <xsl:function name="my:OrderedAttributeTuple" as="xs:string"> <xsl:param name="pElem" as="element()"/> <xsl:variable name="vResult" as="xs:string*"> <xsl:apply-templates select="$pSortTypes"> <xsl:with-param name="pElem" select="$pElem"/> </xsl:apply-templates> </xsl:variable> <xsl:sequence select="string-join($vResult, '')"/> </xsl:function> <xsl:template match="attr"> <xsl:param name="pElem" as="element()"/> <xsl:variable name="vVal" select= "string($pElem/@*[name() eq current()/@name])"/> <xsl:variable name="vPad" as="xs:string*" select= "for $cnt in xs:integer(@maxLength) - string-length($vVal), $i in 1 to $cnt return '.' "/> <xsl:variable name="vPadding" select="string-join($vPad, '')"/> <xsl:variable name="vTuple"> <xsl:sequence select= "if(@type eq 'alpha') then concat($vVal, $vPadding) else concat($vPadding, $vVal) "/> </xsl:variable> <xsl:sequence select="string($vTuple)"/> </xsl:template> </xsl:stylesheet> 

When this conversion is applied to this XML document :

 <items> <item row="5" col="9"/> <item name="d" row="20" col="12" tabindex="" sequence=""/> <item row="1" col="5" /> <item name="d" row="5" col="6" /> <item name="a" row="7" col="8" /> <item name="s" row="1" col="5" tabindex="3" sequence="4"/> <item name="s" row="3" col="3" tabindex="3" sequence="4"/> <item name="c" row="5" col="9"/> <item row="2" col="5" /> <item row="20" col="9"/> <item row="0" col="9"/> <item name="s" row="3" col="3" tabindex="1" sequence="2"/> <item name="s" row="2" col="10" tabindex="1" sequence="2"/> <item name="z" row="8" col="15" tabindex="" sequence=""/> </items> 

The desired, correctly sorted result is called :

 <items> <item row="0" col="9"/> <item row="1" col="5"/> <item row="2" col="5"/> <item row="5" col="9"/> <item row="20" col="9"/> <item name="a" row="7" col="8"/> <item name="c" row="5" col="9"/> <item name="d" row="5" col="6"/> <item name="d" row="20" col="12" tabindex="" sequence=""/> <item name="s" row="1" col="5" tabindex="3" sequence="4"/> <item name="s" row="2" col="10" tabindex="1" sequence="2"/> <item name="s" row="3" col="3" tabindex="1" sequence="2"/> <item name="s" row="3" col="3" tabindex="3" sequence="4"/> <item name="z" row="8" col="15" tabindex="" sequence=""/> </items> 

Please note :

  • Sorting is performed on all attributes specified in the external parameter ( $pSortTypes ). Compare this to the currently accepted answer, which only sorts on @name and @row and requires hard-coded ordering and sorting of the data type.

  • The exact sort order of the attributes can be specified . This is their order, as in $pSortTypes .

  • The sort data type for each attribute is specified in the type attribute in $pSortTypes (currently only "alpha" and "numeric" )

  • The maximum length of the string representation of the attribute value is specified as the maxLength attribute in $pSortTypes . This is used for proper filling / alignment, and also improves sorting efficiency.

  • This demonstrates how to solve even the most complex sorting problems with a custom xsl:function (in this case my:OrderedAttributeTuple() ), which generates a single sorting key.

+1
source

All Articles