Search code examples
xslt-2.0xslt-3.0

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

  • 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>