Laxmikanth Samudrala
Laxmikanth Samudrala

Reputation: 2243

XSLT grouping based on XML element values

My Input XML File looks like

<test-message>
      <segment id="MSH"/>
      <segment id="SFT"/>
      <segment id="PID"/>
      <segment id="NTE"/>
      <segment id="NK1"/>
      <segment id="PV1"/>
      <segment id="ORC"/>
      <segment id="OBR"/>
      <segment id="NTE"/>
      <segment id="OBX"/>
      <segment id="NTE"/>
      <segment id="SPM"/>
   </test-message>

in My above input XML file element segment with id="ORC" is optional

I want to group My input XML file based on element segment with id="ORC" or element segment with id="OBR"

for above Input XML file I want to have below result when element segment with id="ORC" is present

<message-group>
    <test-message>
          <segment id="MSH"/>
          <segment id="SFT"/>
          <segment id="PID"/>
          <segment id="NTE"/>
          <segment id="NK1"/>
          <segment id="PV1"/>
</test-message>
<test-message>
          <segment id="ORC"/>
          <segment id="OBR"/>
          <segment id="NTE"/>
          <segment id="OBX"/>
          <segment id="NTE"/>
          <segment id="SPM"/>
       </test-message>
</message-group>

for above Input XML file I want to have below result when element segment with id="ORC" is not present

<message-group>
    <test-message>
          <segment id="MSH"/>
          <segment id="SFT"/>
          <segment id="PID"/>
          <segment id="NTE"/>
          <segment id="NK1"/>
          <segment id="PV1"/>
</test-message>
<test-message>
          <segment id="OBR"/>
          <segment id="NTE"/>
          <segment id="OBX"/>
          <segment id="NTE"/>
          <segment id="SPM"/>
       </test-message>
</message-group>

Can I have the XSLT (2.0) template or function to handle the above scenario

Note : I am making use of XSLT 2.0 and saxon parsers

Upvotes: 1

Views: 604

Answers (3)

Michael Kay
Michael Kay

Reputation: 163468

If you're always splitting the sequence into exactly two groups, then I think I would do this:

<xsl:variable name="split" select="segment[@id=('ORC', 'OBR')][1]"/>
<test-message>
  <xsl:copy-of select="$split/preceding-sibling::*"/>  
</test-message>
<test-message>
  <xsl:copy-of select="$split, $split/following-sibling::*"/>  
</test-message>

Upvotes: 0

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243529

This transformation:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:template match="/*">
  <message-group>
   <xsl:for-each-group select="*" group-starting-with=
   "segment[@id='ORC'][not(preceding-sibling::segment[1][@id='OBR'])]
  | segment[@id='OBR'][not(preceding-sibling::segment[1][@id='ORC'])]

   ">

      <test-message><xsl:sequence select="current-group()"/></test-message>
   </xsl:for-each-group>
  </message-group>
 </xsl:template>
</xsl:stylesheet>

when applied on the provided XML document:

<test-message>
      <segment id="MSH"/>
      <segment id="SFT"/>
      <segment id="PID"/>
      <segment id="NTE"/>
      <segment id="NK1"/>
      <segment id="PV1"/>
      <segment id="ORC"/>
      <segment id="OBR"/>
      <segment id="NTE"/>
      <segment id="OBX"/>
      <segment id="NTE"/>
      <segment id="SPM"/>
</test-message>

produces the wanted, correct result:

<message-group>
   <test-message>
      <segment id="MSH"/>
      <segment id="SFT"/>
      <segment id="PID"/>
      <segment id="NTE"/>
      <segment id="NK1"/>
      <segment id="PV1"/>
   </test-message>
   <test-message>
      <segment id="ORC"/>
      <segment id="OBR"/>
      <segment id="NTE"/>
      <segment id="OBX"/>
      <segment id="NTE"/>
      <segment id="SPM"/>
   </test-message>
</message-group>

When the same transformation (above) is applied on this XML document ('ORC' is not present):

<test-message>
      <segment id="MSH"/>
      <segment id="SFT"/>
      <segment id="PID"/>
      <segment id="NTE"/>
      <segment id="NK1"/>
      <segment id="PV1"/>
      <segment id="OBR"/>
      <segment id="NTE"/>
      <segment id="OBX"/>
      <segment id="NTE"/>
      <segment id="SPM"/>
</test-message>

again the wanted, correct result is produced:

<message-group>
   <test-message>
      <segment id="MSH"/>
      <segment id="SFT"/>
      <segment id="PID"/>
      <segment id="NTE"/>
      <segment id="NK1"/>
      <segment id="PV1"/>
   </test-message>
   <test-message>
      <segment id="OBR"/>
      <segment id="NTE"/>
      <segment id="OBX"/>
      <segment id="NTE"/>
      <segment id="SPM"/>
   </test-message>
</message-group>

Wwhen the same transformation is applied on this XML document ('OBR' is not present):

<test-message>
      <segment id="MSH"/>
      <segment id="SFT"/>
      <segment id="PID"/>
      <segment id="NTE"/>
      <segment id="NK1"/>
      <segment id="PV1"/>
      <segment id="ORC"/>
      <segment id="NTE"/>
      <segment id="OBX"/>
      <segment id="NTE"/>
      <segment id="SPM"/>
</test-message>

again the wanted, correct result is produced:

<message-group>
   <test-message>
      <segment id="MSH"/>
      <segment id="SFT"/>
      <segment id="PID"/>
      <segment id="NTE"/>
      <segment id="NK1"/>
      <segment id="PV1"/>
   </test-message>
   <test-message>
      <segment id="ORC"/>
      <segment id="NTE"/>
      <segment id="OBX"/>
      <segment id="NTE"/>
      <segment id="SPM"/>
   </test-message>
</message-group>

Finally, when both 'ORC' and 'OBR' are present, but 'OBR' precedes 'ORC':

<test-message>
      <segment id="MSH"/>
      <segment id="SFT"/>
      <segment id="PID"/>
      <segment id="NTE"/>
      <segment id="NK1"/>
      <segment id="PV1"/>
      <segment id="OBR"/>
      <segment id="ORC"/>
      <segment id="NTE"/>
      <segment id="OBX"/>
      <segment id="NTE"/>
      <segment id="SPM"/>
</test-message>

Again the correct, wanted result is produced:

<message-group>
   <test-message>
      <segment id="MSH"/>
      <segment id="SFT"/>
      <segment id="PID"/>
      <segment id="NTE"/>
      <segment id="NK1"/>
      <segment id="PV1"/>
   </test-message>
   <test-message>
      <segment id="OBR"/>
      <segment id="ORC"/>
      <segment id="NTE"/>
      <segment id="OBX"/>
      <segment id="NTE"/>
      <segment id="SPM"/>
   </test-message>
</message-group>

Upvotes: 1

C. M. Sperberg-McQueen
C. M. Sperberg-McQueen

Reputation: 25034

There are two fairly simple ways that come quickly to mind.

(1) In your template for test-message, include two test-message output elements, each containing an apply-templates instruction. Give apply-templates a parameter to distinguish the first and second calls.

Say, something like (not tested):

<xsl:template match="test-message">
  <test-message>
    <xsl:apply-templates>
      <xsl:with-param name="flag" select="1"/>
    </xsl:apply-templates>
  </test-message>
  <test-message>
    <xsl:apply-templates>
      <xsl:with-param name="flag" select="2"/>
    </xsl:apply-templates>
  </test-message>
</xsl:template>

In the template for segment, write out a copy of the element if (a) $flag = 1 and neither this segment nor any preceding sibling segment has an id of OBR or ORC, or else if (b) $flag = 2 and this segment or some preceding sibling segment has such an id. Something like

<xsl:template match="segment">
  <xsl:param name="flag"/>
  <xsl:if test="(
                  $flag = 1 
                  and not(@id = ('ORC', 'OBR'))
                  and not(preceding-sibling::segment
                         [@id=('ORC','OBR')])
                ) or (
                  $flag = 2 
                  and ((@id = ('ORC', 'OBR'))
                       or preceding-sibling::segment
                         [@id=('ORC','OBR')]
                )">
    <xsl:copy-of select="."/>
 </xsl:if> 

(2) Make the test-message template as above, but add select="./segment[1]" to the two calls to xsl:apply-templates.

Then make the template for segment do its work and then recur on its immediately following sibling. To keep the logic simple, we distinguish several cases: first, $flag=1 and we haven't seen OBR or ORC yet: copy the current element and keep going.

<xsl:template match="segment">
  <xsl:param name="flag"/>
  <xsl:choose>
    <xsl:when test="$flag=1 and not(@id=('OBR', 'ORC'))">
      <xsl:copy-of select="."/>
      <xsl:if test="not(@id=('OBR','ORC'))">
         <xsl:apply-templates select="following-sibling::*[1]">
            <xsl:with-param name="flag" select="$flag"/>
         </xsl:apply-templates>
      </xsl:if>
    </xsl:when>

Second, $flag = 1 and we are now encountering OBR or ORC. Don't copy the current element and don't keep going; the first test-message element is now completed.

    <xsl:when test="$flag=1 and @id=('OBR', 'ORC')">
      <!--* do nothing, stop recursion *-->
    </xsl:when>

Third, $flag = 2 and we haven't yet encountered OBR or ORC. Keep going.

    <xsl:when test="$flag=2 and not(@id=('OBR', 'ORC'))">
      <!--* don't copy yet, keep looking for OBR/ORC *-->
      <xsl:apply-templates select="following-sibling::*[1]">
        <xsl:with-param name="flag" select="$flag"/>
      </xsl:apply-templates>
    </xsl:when>

Fourth, $flag = 2 and we are now encountering OBR or ORC. Copy the current element and keep going; switch the flag to a third value which means we are in the second test-message element and we have seen OBR or ORC:

    <xsl:when test="$flag=2 and @id=('OBR', 'ORC')">
      <xsl:copy-of select="."/>
      <xsl:apply-templates select="following-sibling::*[1]">
        <xsl:with-param name="flag" select="3"/>
      </xsl:apply-templates>
    </xsl:when>

Finally, if $flag = 3 then we just copy the current element and keep going.

    <xsl:when test="$flag=3">
      <xsl:copy-of select="."/>
      <xsl:apply-templates select="following-sibling::*[1]">
        <xsl:with-param name="flag" select="3"/>
      </xsl:apply-templates>
    </xsl:when>
  </xsl:choose>
</xsl:template>

Upvotes: 0

Related Questions