XSLT Mapping to copy all nodes but modify one node

Viewed 163

I am relatively new to XSLT and have a requirement. Below is my input XML

<Response>
    <STF_N>123</STF_N>
    <STF_1NM_T>Moon Go</STF_1NM_T>
    <STF_SURNM_T>Kimm</STF_SURNM_T>
    <STF_FULLNM_T>Kimm Moon Go</STF_FULLNM_T>
    <STF_HIRE_D>1990-06-25T00:00:00.000</STF_HIRE_D>
    <APPT_ID>A</APPT_ID>
</Response>
<Response>
    <STF_N>123</STF_N>
    <STF_1NM_T>Moon Go</STF_1NM_T>
    <STF_SURNM_T>Kimm</STF_SURNM_T>
    <STF_FULLNM_T>Kimm Moon Go</STF_FULLNM_T>
    <STF_HIRE_D>2020-06-25T00:00:00.000</STF_HIRE_D>
    <APPT_ID>B</APPT_ID>
</Response>

Required output -

<Response>
    <STF_N>123</STF_N>
    <STF_1NM_T>Moon Go</STF_1NM_T>
    <STF_SURNM_T>Kimm</STF_SURNM_T>
    <STF_FULLNM_T>Kimm Moon Go</STF_FULLNM_T>
    <STF_HIRE_D>1990-06-25T00:00:00.000</STF_HIRE_D>
    <APPT_ID>A</APPT_ID>
</Response>
<Response>
    <STF_N>123</STF_N>
    <STF_1NM_T>Moon Go</STF_1NM_T>
    <STF_SURNM_T>Kimm</STF_SURNM_T>
    <STF_FULLNM_T>Kimm Moon Go</STF_FULLNM_T>
    <STF_HIRE_D>1990-06-25T00:00:00.000</STF_HIRE_D>
    <APPT_ID>B</APPT_ID>
</Response>

The STF_HIRE_D should be the same (first value) for all records with the same STF_N.

I used the below XSLT but it is not working as expected -

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

<xsl:template match="Response/STF_HIRE_D">
              <xsl:for-each-group select="Response" group-by="STF_N">
              <STF_HIRE_D>
                  <xsl:value-of select="current-group()[1]/STF_HIRE_D"/>
              </STF_HIRE_D> 
              </xsl:for-each-group>

Any help is much appreciated.

Thank you

2 Answers

It is also possible to use a key and simply pull in the necessary value based on node identity:

<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:key name="stf_n" match="Response" use="STF_N"/>
  
  <xsl:mode on-no-match="shallow-copy"/>
  
  <xsl:template match="Response[not(. is key('stf_n', STF_N)[1])]/STF_HIRE_D">
    <xsl:copy>{key('stf_n', ../STF_N)[1]/STF_HIRE_D}</xsl:copy>
  </xsl:template>

</xsl:stylesheet>

If you work with an XSLT 2 instead of an XSLT 3 processor you can declare <xsl:mode on-no-match="shallow-copy"/> but would keep your template

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

instead. The key works as well in XSLT 2 as in XSLT 3.

Instead of a key in XSLT 3 you could also use an accumulator to store the value pair (STF_N, STF_HIRE_D) in a map:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  version="3.0"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  xmlns:map="http://www.w3.org/2005/xpath-functions/map"
  exclude-result-prefixes="#all"
  expand-text="yes">

  <xsl:accumulator name="hire-date" as="map(xs:string, xs:string)" initial-value="map{}">
    <xsl:accumulator-rule match="Response/STF_HIRE_D"
      select="if (map:contains($value, ../STF_N/string()))
              then $value
              else map:put($value, ../STF_N/string(), string())"/>
  </xsl:accumulator>
  
  <xsl:mode on-no-match="shallow-copy" use-accumulators="hire-date"/>
  
  <xsl:template match="Response/STF_HIRE_D">
    <xsl:copy>{accumulator-before('hire-date')(../STF_N/string())}</xsl:copy>
  </xsl:template>

</xsl:stylesheet>

Finally, using streaming you could use two accumulators, one to keep track of the STF_N value inside of a Response element, the other to store the STF_N -> STF_HIRE_D:

<?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"
  xmlns:map="http://www.w3.org/2005/xpath-functions/map"
  exclude-result-prefixes="#all"
  expand-text="yes">
  
  <xsl:accumulator name="stf_n" as="xs:string?" initial-value="()" streamable="yes">
    <xsl:accumulator-rule match="Response/STF_N/text()" select="string()"/>
    <xsl:accumulator-rule phase="end" match="Response" select="()"/>
  </xsl:accumulator>

  <xsl:accumulator name="hire-date" as="map(xs:string, xs:string)" initial-value="map{}" streamable="yes">
    <xsl:accumulator-rule match="Response/STF_HIRE_D/text()"
      select="if (map:contains($value, accumulator-before('stf_n')))
              then $value
              else map:put($value, accumulator-before('stf_n'), string())"/>
  </xsl:accumulator>
  
  <xsl:mode streamable="yes" on-no-match="shallow-copy" use-accumulators="stf_n hire-date"/>
  
  <xsl:template match="Response/STF_HIRE_D/text()">
    <xsl:copy>{accumulator-before('hire-date')(accumulator-before('stf_n'))}</xsl:copy>
  </xsl:template>

</xsl:stylesheet>

That way and with Saxon EE which supports streaming in XSLT 3, you could process huge documents without parsing the whole XML document in an XDM tree.

For this you could use a tunnel-parameter.

Using this source xml with one root-element:

<?xml version='1.0' encoding='UTF-8'?>
<Responses>
  <Response>
    <STF_N>123</STF_N>
    <STF_1NM_T>Moon Go</STF_1NM_T>
    <STF_SURNM_T>Kimm</STF_SURNM_T>
    <STF_FULLNM_T>Kimm Moon Go</STF_FULLNM_T>
    <STF_HIRE_D>1990-06-25T00:00:00.000</STF_HIRE_D>
    <APPT_ID>A</APPT_ID>
  </Response>
  <Response>
    <STF_N>123</STF_N>
    <STF_1NM_T>Moon Go</STF_1NM_T>
    <STF_SURNM_T>Kimm</STF_SURNM_T>
    <STF_FULLNM_T>Kimm Moon Go</STF_FULLNM_T>
    <STF_HIRE_D>2020-06-25T00:00:00.000</STF_HIRE_D>
    <APPT_ID>B</APPT_ID>
  </Response>  
</Responses>

Using this xslt:

<?xml version='1.0' encoding='UTF-8'?>
<xsl:stylesheet 
  version="2.0" 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
  exclude-result-prefixes="#all">

  <xsl:output indent="yes"/>
  <xsl:template match="@*|node()">
    <xsl:copy>
      <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
  </xsl:template>
  
  <xsl:template match="Responses">
    <xsl:copy>
      <xsl:for-each-group select="Response" group-by="STF_N">
      <xsl:apply-templates select="current-group()">
        <xsl:with-param name="hireDate" select="current-group()[1]/STF_HIRE_D" tunnel="yes"/>
      </xsl:apply-templates>
    </xsl:for-each-group>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="STF_HIRE_D">
    <xsl:param name="hireDate" tunnel="yes"/>
    <xsl:copy>
      <xsl:value-of select="$hireDate"/>
    </xsl:copy>
  </xsl:template>
  
</xsl:stylesheet>

Will give the expected result (also with one root-element)

<?xml version="1.0" encoding="UTF-8"?>
<Responses>
   <Response>
      <STF_N>123</STF_N>
      <STF_1NM_T>Moon Go</STF_1NM_T>
      <STF_SURNM_T>Kimm</STF_SURNM_T>
      <STF_FULLNM_T>Kimm Moon Go</STF_FULLNM_T>
      <STF_HIRE_D>1990-06-25T00:00:00.000</STF_HIRE_D>
      <APPT_ID>A</APPT_ID>
   </Response>
   <Response>
      <STF_N>123</STF_N>
      <STF_1NM_T>Moon Go</STF_1NM_T>
      <STF_SURNM_T>Kimm</STF_SURNM_T>
      <STF_FULLNM_T>Kimm Moon Go</STF_FULLNM_T>
      <STF_HIRE_D>1990-06-25T00:00:00.000</STF_HIRE_D>
      <APPT_ID>B</APPT_ID>
   </Response>
</Responses>
Related