Search code examples
xsltdynamictagsgrouping

Grouping 3 elements into a node


I have the following xml where a the xml element name have a number in it..

<Root>
      <Row>
        <Coverage>Partial</Coverage>
        <Admission>Self</Admission>
        <Sequence1>1</Sequence1>
        <Qualifier1>221</Qualifier1>
        <Date1>2017-06-01</Date1>
        <Sequence2>2</Sequence2>
        <Qualifier2>222</Qualifier2>
        <Date2>2022-05-06</Date2>       
      </Row>
      <Row>
       <Coverage>Partial</Coverage>
       <Admission>Self</Admission>
       <Sequence1>1</Sequence1>
       <Qualifier1>321</Qualifier1>
       <Date1>2017-06-01</Date1>
       <Sequence2>2</Sequence2>
       <Qualifier2>322</Qualifier2>
       <Date2>2022-05-06</Date2>        
      </Row>
      <Row>
        <Coverage>Full</Coverage>
        <Admission>Self</Admission>
        <Sequence1>1</Sequence1>
        <Qualifier1>421</Qualifier1>
        <Date1>2017-06-01</Date1>
        <Sequence2>2</Sequence2>
        <Qualifier2>422</Qualifier2>
        <Date2>2022-05-06</Date2>       
      </Row>
    </Root>

I would like to group Sequence, Qualifier and Date into a group node called Benefit like below. Also, Based on the first element "Coverage" value, should be merged. XML output should as below.

<Root>
    <Row>
    <Coverage>Partial</Coverage>
    <Admission>Self</Admission>
    <Benefits>
        <Benefit>
            <Sequence>1</Sequence>
            <Qualifier>221</Qualifier>
            <Date>2017-06-01</Date>
        </Benefit>
        <Benefit>
            <Sequence>2</Sequence>
            <Qualifier>222<Qualifier>
            <Date>2022-05-06</Date>
        <Benefit>
        <Benefit>
            <Sequence>3</Sequence>
            <Qualifier>321</Qualifier>
            <Date>2017-06-01</Date>
        </Benefit>
        <Benefit>
            <Sequence>4</Sequence>
            <Qualifier>322<Qualifier>
            <Date>2022-05-06</Date>
        <Benefit>
      </Benefits>
    </Row>
    <Row>
    <Coverage>Full</Coverage>
    <Admission>Self</Admission>
    <Benefits>
        <Benefit>
            <Sequence>1</Sequence>
            <Qualifier>421</Qualifier>
            <Date>2017-06-01</Date>
        </Benefit>
        <Benefit>
            <Sequence>2</Sequence>
            <Qualifier>422<Qualifier>
            <Date>2022-05-06</Date>
        <Benefit>
    </Benefits>
   </Row>
</Root>

Any help is greatly appreciated.


Solution

  • Here's a simple method, based on the assumption that the 3 elements to be grouped will always come in a contiguous block that starts with a SequenceX:

    XSLT 1.0

    <xsl:stylesheet version="1.0" 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
    
    <xsl:template match="/Row">
        <xsl:copy>
            <xsl:copy-of select="Coverage | Admission"/>
            <Benefits>
                <xsl:for-each select="*[starts-with(name(), 'Sequence')]">
                    <Benefit>
                        <xsl:for-each select=". | following-sibling::*[position() &lt; 3]">
                            <xsl:element name="{translate(name(), '0123456789', '')}">
                                <xsl:value-of select="." />
                            </xsl:element>
                        </xsl:for-each>                     
                    </Benefit>
                </xsl:for-each>
            </Benefits>
        </xsl:copy>
    </xsl:template>
    
    </xsl:stylesheet>
    

    If the above assumption is not true, you can perform actual grouping based on the number included in the element names:

    XSLT 2.0

    <xsl:stylesheet version="2.0" 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
    
    <xsl:template match="/Row">
        <xsl:copy>
            <xsl:copy-of select="Coverage | Admission"/>
            <Benefits>
                <xsl:for-each-group select="*[matches(name(), '^Sequence|^Qualifier|^Date')]" group-by="replace(name(), '\D', '')">
                    <Benefit>
                        <xsl:for-each select="current-group()">
                            <xsl:element name="{replace(name(), '\d', '')}">
                                <xsl:value-of select="." />
                            </xsl:element>
                        </xsl:for-each>
                    </Benefit>
                </xsl:for-each-group>
            </Benefits>
        </xsl:copy>
    </xsl:template>
    
    </xsl:stylesheet>