Sort the XML nodes by date in descending order using XSLT 2.0

881 views Asked by At

I have a set of nodes that display compensation of workers along with effective date from two different systems (different nodes). I need to sort these values in ascending / descending order irrespective of whether they are from previous system or current system.

I tried to use the sort function in the for-each, but I am getting an error that not more than 1 node can be passed while sorting.

<?xml version="1.0" encoding="UTF-8"?>
<Workers xmlns:ch="test/Compensation_History">
    <ch:Worker_Entry>
        <ch:Employee_ID>12345</ch:Employee_ID>
        <ch:Compensation_History_Previous_System>
            <ch:Amount>100000</ch:Amount>
            <ch:Effective_Date>2018-01-01-07:00</ch:Effective_Date>
        </ch:Compensation_History_Previous_System>
        <ch:Compensation_History_Previous_System>
            <ch:Amount>95000</ch:Amount>
            <ch:Effective_Date>2017-01-01-07:00</ch:Effective_Date>
        </ch:Compensation_History_Previous_System>
        <ch:Compensation_History_Previous_System>
            <ch:Amount>90000</ch:Amount>
            <ch:Effective_Date>2016-01-01-07:00</ch:Effective_Date>
        </ch:Compensation_History_Previous_System>
        <ch:Compensation_History_Current_System>
            <ch:Amount>105000</ch:Amount>
            <ch:Effective_Date>2019-01-01-07:00</ch:Effective_Date>
        </ch:Compensation_History_Current_System>
        <ch:Compensation_History_Current_System>
            <ch:Amount>110000</ch:Amount>
            <ch:Effective_Date>2020-01-01-07:00</ch:Effective_Date>
        </ch:Compensation_History_Current_System>
    </ch:Worker_Entry>
</Workers>

Expected Output:

<?xml version="1.0" encoding="UTF-8"?>
<Workers xmlns:ch="test/Compensation_History">
    <ch:Worker_Entry>
        <ch:Employee_ID>12345</ch:Employee_ID>
        <ch:Compensation_History_Current_System>
            <ch:Amount>110000</ch:Amount>
            <ch:Effective_Date>2020-01-01-07:00</ch:Effective_Date>
        </ch:Compensation_History_Current_System>
        <ch:Compensation_History_Current_System>
            <ch:Amount>105000</ch:Amount>
            <ch:Effective_Date>2019-01-01-07:00</ch:Effective_Date>
        </ch:Compensation_History_Current_System>
        <ch:Compensation_History_Previous_System>
            <ch:Amount>100000</ch:Amount>
            <ch:Effective_Date>2018-01-01-07:00</ch:Effective_Date>
        </ch:Compensation_History_Previous_System>
        <ch:Compensation_History_Previous_System>
            <ch:Amount>95000</ch:Amount>
            <ch:Effective_Date>2017-01-01-07:00</ch:Effective_Date>
        </ch:Compensation_History_Previous_System>
        <ch:Compensation_History_Previous_System>
            <ch:Amount>90000</ch:Amount>
            <ch:Effective_Date>2016-01-01-07:00</ch:Effective_Date>
        </ch:Compensation_History_Previous_System>
    </ch:Worker_Entry>
</Workers>

Below is the code that I tried:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:ch="test/Compensation_History" exclude-result-prefixes="xs"
    version="2.0">

    <xsl:template match="/">
        <Workers>
            <xsl:for-each-group select="ch:Workers" group-by="ch:Employee_ID">
                <xsl:sort select="//ch:Effective_Date" order="descending" />
                <xsl:copy-of select="." />
            </xsl:for-each-group>
        </Workers>
    </xsl:template>
</xsl:stylesheet>
2

There are 2 answers

0
Dubh On

If you really need an XSLT 2 solution then the following should work:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  xmlns:ch="test/Compensation_History" exclude-result-prefixes="xs"
  version="2.0">
  
  <xsl:template match="Workers">
    <xsl:copy>
      <xsl:for-each select="ch:Worker_Entry">
        <xsl:copy>
          <xsl:copy-of select="ch:Employee_ID"/>
          <xsl:for-each select="ch:Compensation_History_Previous_System | ch:Compensation_History_Current_System">
            <xsl:sort select="xs:date(ch:Effective_Date)" order="descending"/>
            <xsl:copy-of select="."/>
          </xsl:for-each>
        </xsl:copy>
      </xsl:for-each>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>
0
Martin Honnen On

In XSLT 3 using the sort function you could use

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  version="3.0"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  xpath-default-namespace="test/Compensation_History"
  exclude-result-prefixes="#all"
  expand-text="yes">

  <xsl:mode on-no-match="shallow-copy"/>
  
  <xsl:output indent="yes"/>

  <xsl:template match="Worker_Entry">
    <xsl:copy>
      <xsl:apply-templates 
        select="@*, 
                Employee_ID, 
                (* except Employee_ID) 
                => sort((), function($c) { $c/Effective_Date => xs:date() } ) 
                => reverse()"/>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

fn:sort as a higher-order function needs Saxon 10 all editions or Saxon 9.8 and later PE or EE or Saxon-JS 2.

In the code you have added you are trying to group and sort although the <xsl:for-each-group select="ch:Workers" group-by="ch:Employee_ID"> doesn't make any sense as the root element is not in the ch namespace and grouping a single element only makes sense if you provide a group-by expression resulting in several grouping keys for the element; your expression ch:Employee_ID does not make sense in that context. As for the attempt to use xsl:sort, use a relative expression there so e.g. <xsl:for-each-group select="//ch:Worker_Entry" group-by="ch:Employee_ID"><xsl:sort select="ch:Effective_Date" order="descending"/> could make sense if you want to group and sort the groups.