'Unable to Copy unmatched Nodes while Grouping

I am unable to find out what I should be doing to get desired output as below.

Can anyone help me please? Thank you!

Source XML

<?xml version="1.0" encoding="UTF-8"?>
<Workers>
    <Worker>
        <empID>12345</empID>
        <Location>NY</Location>
        <Remote/>
        <Amount>1450</Amount>
    </Worker>
    <Worker>
        <empID>23456</empID>
        <Local/>
        <Location>NY</Location>
        <City>NYC</City>
        <CityAllowance>100</CityAllowance>
        <Remote>Y</Remote>
        <Amount>1450</Amount>
    </Worker>
    <Worker>
        <empID>23456</empID>
        <Local>Y</Local>
        <Location>NY</Location>
        <City>Syracuse</City>
        <CityAllowance>150</CityAllowance>
        <Remote>Y</Remote>
        <Amount>1450</Amount>
    </Worker>
    <Worker>
        <empID>23456</empID>
        <Local>Y</Local>
        <Location>NY</Location>
        <City>Ithaca</City>
        <CityAllowance>250</CityAllowance>
        <Remote>Y</Remote>
        <Amount>1450</Amount>
    </Worker>
    <Worker>
        <empID>88001</empID>
        <Local/>
        <Location>CA</Location>
        <City>San Franscisco</City>
        <CityAllowance>200</CityAllowance>
        <Remote>N</Remote>
        <Amount>1450</Amount>        
    </Worker>
    <Worker>
        <empID>88001</empID>
        <Local>Y</Local>
        <Location>CA</Location>
        <City>San Jose</City>
        <CityAllowance>190</CityAllowance>
        <Remote>N</Remote>
        <Amount>9450</Amount>        
    </Worker>
    <Worker>
        <empID>88001</empID>
        <Local>Y</Local>
        <Location>CA</Location>
        <City>Oakland</City>
        <CityAllowance>600</CityAllowance>
        <Remote>N</Remote>
        <Amount>4500</Amount>        
    </Worker>
</Workers>

My XSLT is

<?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" exclude-result-prefixes="xs" version="2.0">
    <xsl:output method="xml" indent="yes"/>

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

    <xsl:template match="Workers">
        <Workers>
            <xsl:for-each-group select="Worker[Remote = 'Y' or Remote = 'N']" group-by="empID">
                <Worker>                    
                    <empID>
                        <xsl:value-of select="current-grouping-key()"/>
                    </empID>                    
                </Worker>
            </xsl:for-each-group>
        </Workers>
    </xsl:template>

</xsl:stylesheet>

Expected Output

<?xml version="1.0" encoding="UTF-8"?>
<Workers>
    <Worker>
        <empID>12345</empID>
        <Location>NY</Location>
        <Remote/>
        <Amount>1450</Amount>
    </Worker>
    <Worker>
        <empID>23456</empID>
    </Worker>
    <Worker>
        <empID>88001</empID>
    </Worker>
</Workers>

Current Output

<?xml version="1.0" encoding="UTF-8"?>
<Workers>
   <Worker>
      <empID>23456</empID>
   </Worker>
   <Worker>
      <empID>88001</empID>
   </Worker>
</Workers>
  • I wanted to copy all nodes exactly as they appear in Source XML if value of <Local> is either Y or N. However, this is not getting copied down with my current XSLT

  • For-each-group statement <xsl:for-each-group select="Worker[Remote = 'Y' or Remote = 'N']" group-by="empID"> was written to handle records that have <Local> value is either set to Y or N

My application supports both XSLT 3.0 and XSLT 2.0



Solution 1:[1]

One way is to use a key and add empty templates for the elements you don't want to output:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                version="3.0"
                xmlns:xs="http://www.w3.org/2001/XMLSchema"
                exclude-result-prefixes="#all"
                expand-text="yes">
  
    <xsl:strip-space elements="*"/>
    <xsl:output method="xml" indent="yes"/>
  
    <xsl:key name="group" match="Worker[Remote = 'Y' or Remote = 'N']" use="empID"/>

    <xsl:template match="Worker[Remote = 'Y' or Remote = 'N'][not(. is key('group', empID)[1])]"/>
    
    <xsl:template match="Worker[Remote = 'Y' or Remote = 'N'][. is key('group', empID)[1]]/*[not(self::empID)]"/>

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

</xsl:stylesheet>

If you want to group some elements but process all with for-each-group one way would be to use a variable:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                version="3.0"
                xmlns:xs="http://www.w3.org/2001/XMLSchema"
                exclude-result-prefixes="#all"
                expand-text="yes">
  
    <xsl:strip-space elements="*"/>
    <xsl:output method="xml" indent="yes"/>
    
    <xsl:template match="Workers">
      <xsl:copy>
        <xsl:variable name="groups" as="element(Worker)*">
          <xsl:for-each-group select="Worker" composite="yes" group-by="Remote = 'Y' or Remote = 'N', empID">
            <xsl:choose>
              <xsl:when test="current-grouping-key()[1]">
                <xsl:sequence select="."/>
              </xsl:when>
              <xsl:otherwise>
                <xsl:sequence select="current-group()"/>
              </xsl:otherwise>
            </xsl:choose>
          </xsl:for-each-group>
        </xsl:variable>
        <xsl:apply-templates select="$groups/."/>
      </xsl:copy>
    </xsl:template>
    
    <xsl:template match="Worker[Remote = 'Y' or Remote = 'N']/*[not(self::empID)]"/>
  
    <xsl:mode on-no-match="shallow-copy"/>

</xsl:stylesheet>

Perhaps it is better to move the grouping code to a function, so that the template body for Workers remains compact:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                version="3.0"
                xmlns:xs="http://www.w3.org/2001/XMLSchema"
                exclude-result-prefixes="#all"
                xmlns:mf="http://example.com/mf"
                expand-text="yes">
  
    <xsl:function name="mf:group" as="element(Worker)*">
      <xsl:param name="workers" as="element(Worker)*"/>
      <xsl:for-each-group select="$workers" composite="yes" group-by="Remote = 'Y' or Remote = 'N', empID">
        <xsl:choose>
          <xsl:when test="current-grouping-key()[1]">
            <xsl:sequence select="."/>
          </xsl:when>
          <xsl:otherwise>
            <xsl:sequence select="current-group()"/>
          </xsl:otherwise>
        </xsl:choose>
      </xsl:for-each-group>
    </xsl:function>
  
    <xsl:strip-space elements="*"/>
    <xsl:output method="xml" indent="yes"/>
    
    <xsl:template match="Workers">
      <xsl:copy>
        <xsl:apply-templates select="mf:group(Worker)/."/>
      </xsl:copy>
    </xsl:template>
    
    <xsl:template match="Worker[Remote = 'Y' or Remote = 'N']/*[not(self::empID)]"/>
  
    <xsl:mode on-no-match="shallow-copy"/>

</xsl:stylesheet>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1