user3419276
user3419276

Reputation: 33

Grouping until new node is found in XSLT 1.0

I need to group data until a new data element comes and then logic repeats.

My Input XML:

<File>
        <Detail>
            <Data>1</Data>
            <Value>4</Value>
        </Detail>
        <Detail>
            <Data>1</Data>
            <Value>7</Value>
        </Detail>
        <Detail>
            <Data>2</Data>
            <Value>4</Value>
        </Detail>
        <Detail>
            <Data>1</Data>
            <Value>5</Value>
        </Detail>
             <Detail>
            <Data>1</Data>
            <Value>1</Value>
        </Detail>
<File>

The resultant ouput XML should look like:

<File>
        <Detail>
            <Data>1</Data>
            <Value>11</Value>
        </Detail>
        <Detail>
            <Data>2</Data>
            <Value>4</Value>
        </Detail>
        <Detail>
            <Data>1</Data>
            <Value>6</Value>
        </Detail>

<File>

The Values are added accordingly. As you can see the the data tags with value 1 is grouped separately as they are separated by Data tag 2.

Please help.

Upvotes: 1

Views: 302

Answers (2)

Ian Roberts
Ian Roberts

Reputation: 122364

A common XSLT 1.0 approach to what is essentially a while loop is to use tail recursion. You start by applying a template to the first Detail element in each group, and each element then applies templates to the next one, etc.

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">

  <xsl:strip-space elements="*" />
  <xsl:output indent="yes" />

  <xsl:template match="/*">
    <!-- apply templates to the _first_ Detail element in each group -->
    <xsl:copy>
      <xsl:apply-templates select="Detail[
           not(Data = preceding-sibling::Detail[1]/Data)]" />
    </xsl:copy>
  </xsl:template>

  <!-- Detail element that is not the _last_ one in a contiguous group -->
  <xsl:template match="Detail[Data = following-sibling::Detail[1]/Data]">
    <xsl:param name="runningTotal" select="0" />
    <xsl:apply-templates select="following-sibling::Detail[1]">
      <xsl:with-param name="runningTotal" select="$runningTotal + Value" />
    </xsl:apply-templates>
  </xsl:template>

  <!-- Detail element that _is_ the last one in a contiguous group -->
  <xsl:template match="Detail">
    <xsl:param name="runningTotal" select="0" />
    <Detail>
      <xsl:copy-of select="Data" />
      <Value><xsl:value-of select="$runningTotal + Value" /></Value>
    </Detail>
  </xsl:template>

</xsl:stylesheet>

The idea here is that the first of the two Detail templates will fire for elements that have the same Data value as their immediately following sibling, and will accumulate a running total Value passed down the chain. When we reach the last element in a group the second template will output the grand total Value. (In the case of singleton groups like the second one in your example, the first element in the group is also the last one so we go directly to the second template with its default runningTotal value of zero).

The XSLT 2.0 version is far simpler, as for-each-group has native support for grouping contiguous runs:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">

  <xsl:strip-space elements="*" />
  <xsl:output indent="yes" />

  <xsl:template match="/*">
    <xsl:copy>
      <xsl:for-each-group select="Detail" group-adjacent="Data">
        <Detail>
          <xsl:copy-of select="Data" />
          <Value><xsl:value-of select="sum(current-group()/Value)" /></Value>
        </Detail>
      </xsl:for-each-group>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

Upvotes: 1

michael.hor257k
michael.hor257k

Reputation: 116992

Here's one way you could look at it:

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="/">
    <File>
        <xsl:for-each select="File/Detail[not(Data=preceding-sibling::Detail[1]/Data)]">
            <Detail>
                <xsl:copy-of select="Data"/>
                <xsl:variable name="start" select="count(preceding-sibling::Detail)" />
                <xsl:variable name="end" select="count(following-sibling::Detail[not(Data=current()/Data)][1]/preceding-sibling::Detail)" />
                <xsl:variable name="n">
                    <xsl:choose>
                        <xsl:when test="$end">
                            <xsl:value-of select="$end - $start - 1"/>
                        </xsl:when>
                        <xsl:otherwise>
                            <xsl:value-of select="count(following-sibling::Detail)"/>
                        </xsl:otherwise>
                    </xsl:choose>
                </xsl:variable>
                <Value>
                    <xsl:value-of select="sum(Value | following-sibling::Detail[position() &lt;= $n]/Value)"/>
                </Value>
            </Detail>
        </xsl:for-each>
    </File>
</xsl:template>

</xsl:stylesheet>

Upvotes: 0

Related Questions