xmlxslt

How to use XSLT to count consecutive following elements satisfying a certain condition


See the XML code of a table below. My task is to add an attribute (MOREROWS) to every ENTRY element which has one or more consecutive following rows with the text "##rowspan##" in the ENTRY element with the same COLNAME value.

The MOREROWS attribute should contain the number of following consecutive rows satisfying the desired condition.

I've been experimenting with for-each-group, for-each and count, but haven't managed to find a solution yet. My main problem is to ignore the gaps. After all I want to count only the consecutive following rows satisfying the condition, and not all the following rows satisfying the condition.

Ideally I would want to use count as that seems like the logical choice here since I don't need to apply templates but rather just want a number.

Any help will be appreciated.

Current table:

<TABLE>
  <ROW>
    <ENTRY COLNUM="1" COLNAME="col1">
      <CONTENT>42</CONTENT>
    </ENTRY>
    <ENTRY COLNUM="2" COLNAME="col2">
      <CONTENT>155</CONTENT>
    </ENTRY>
    <ENTRY COLNUM="3" COLNAME="col3">
      <CONTENT/>
    </ENTRY>
    <ENTRY COLNUM="4" COLNAME="col4">
      <CONTENT>148</CONTENT>
    </ENTRY>
  </ROW>
  <ROW>
    <ENTRY COLNUM="1" COLNAME="col1">
      <CONTENT>147</CONTENT>
    </ENTRY>
    <ENTRY COLNUM="2" COLNAME="col2">
      <CONTENT>##rowspan##</CONTENT>
    </ENTRY>
    <ENTRY COLNUM="3" COLNAME="col3">
      <CONTENT/>
    </ENTRY>
    <ENTRY COLNUM="4" COLNAME="col4">
      <CONTENT>##rowspan##</CONTENT>
    </ENTRY>
  </ROW>
  <ROW>
    <ENTRY COLNUM="1" COLNAME="col1">
      <CONTENT>147</CONTENT>
    </ENTRY>
    <ENTRY COLNUM="2" COLNAME="col2">
      <CONTENT>##rowspan##</CONTENT>
    </ENTRY>
    <ENTRY COLNUM="3" COLNAME="col3">
      <CONTENT/>
    </ENTRY>
    <ENTRY COLNUM="4" COLNAME="col4">
      <CONTENT>300</CONTENT>
    </ENTRY>
  </ROW>
  <ROW>
    <ENTRY COLNUM="1" COLNAME="col1">
      <CONTENT>1814</CONTENT>
    </ENTRY>
    <ENTRY COLNUM="2" COLNAME="col2">
      <CONTENT/>
    </ENTRY>
    <ENTRY COLNUM="3" COLNAME="col3">
      <CONTENT/>
    </ENTRY>
    <ENTRY COLNUM="4" COLNAME="col4">
      <CONTENT>1905</CONTENT>
    </ENTRY>
  </ROW>
  <ROW>
    <ENTRY COLNUM="1" COLNAME="col1">
      <CONTENT>147</CONTENT>
    </ENTRY>
    <ENTRY COLNUM="2" COLNAME="col2">
      <CONTENT>##rowspan##</CONTENT>
    </ENTRY>
    <ENTRY COLNUM="3" COLNAME="col3">
      <CONTENT/>
    </ENTRY>
    <ENTRY COLNUM="4" COLNAME="col4">
      <CONTENT>##rowspan##</CONTENT>
    </ENTRY>
  </ROW>
</TABLE>

Desired result:

<TABLE>
  <ROW>
    <ENTRY COLNUM="1" COLNAME="col1">
      <CONTENT>42</CONTENT>
    </ENTRY>
    <ENTRY COLNUM="2" COLNAME="col2"** MOREROWS="2"**>
      <CONTENT>155</CONTENT>
    </ENTRY>
    <ENTRY COLNUM="3" COLNAME="col3">
      <CONTENT/>
    </ENTRY>
    <ENTRY COLNUM="4" COLNAME="col4"** MOREROWS="1"**>
      <CONTENT>148</CONTENT>
    </ENTRY>
  </ROW>
  <ROW>
    <ENTRY COLNUM="1" COLNAME="col1">
      <CONTENT>147</CONTENT>
    </ENTRY>
    <ENTRY COLNUM="2" COLNAME="col2">
      <CONTENT>##rowspan##</CONTENT>
    </ENTRY>
    <ENTRY COLNUM="3" COLNAME="col3">
      <CONTENT/>
    </ENTRY>
    <ENTRY COLNUM="4" COLNAME="col4">
      <CONTENT>##rowspan##</CONTENT>
    </ENTRY>
  </ROW>
  <ROW>
    <ENTRY COLNUM="1" COLNAME="col1">
      <CONTENT>147</CONTENT>
    </ENTRY>
    <ENTRY COLNUM="2" COLNAME="col2">
      <CONTENT>##rowspan##</CONTENT>
    </ENTRY>
    <ENTRY COLNUM="3" COLNAME="col3">
      <CONTENT/>
    </ENTRY>
    <ENTRY COLNUM="4" COLNAME="col4">
      <CONTENT>300</CONTENT>
    </ENTRY>
  </ROW>
  <ROW>
    <ENTRY COLNUM="1" COLNAME="col1">
      <CONTENT>1814</CONTENT>
    </ENTRY>
    <ENTRY COLNUM="2" COLNAME="col2"** MOREROWS="1"**>
      <CONTENT/>
    </ENTRY>
    <ENTRY COLNUM="3" COLNAME="col3">
      <CONTENT/>
    </ENTRY>
    <ENTRY COLNUM="4" COLNAME="col4"** MOREROWS="1"**>
      <CONTENT>1905</CONTENT>
    </ENTRY>
  </ROW>
  <ROW>
    <ENTRY COLNUM="1" COLNAME="col1">
      <CONTENT>147</CONTENT>
    </ENTRY>
    <ENTRY COLNUM="2" COLNAME="col2">
      <CONTENT>##rowspan##</CONTENT>
    </ENTRY>
    <ENTRY COLNUM="3" COLNAME="col3">
      <CONTENT/>
    </ENTRY>
    <ENTRY COLNUM="4" COLNAME="col4">
      <CONTENT>##rowspan##</CONTENT>
    </ENTRY>
  </ROW>
</TABLE>

Solution

  • I think this can be solved with xsl:for-each-group group-adjacent e.g. (in XSLT 3)

      <xsl:template match="ROW/ENTRY[not(CONTENT = '##rowspan##')]">
        <xsl:copy>
          <xsl:apply-templates select="@*"/>
          <xsl:for-each-group select="../following-sibling::ROW/ENTRY[@COLNAME = current()/@COLNAME]" group-adjacent="CONTENT = '##rowspan##'">
            <xsl:if test="position() = 1 and current-grouping-key()">
              <xsl:attribute name="MOREROWS" select="count(current-group())"/>
            </xsl:if>
          </xsl:for-each-group>
        </xsl:copy>
      </xsl:template>
      
      <xsl:mode on-no-match="shallow-copy"/>
    

    Example online fiddle.

    It might be a bit more efficient to group once to store the adjacent ENTRYs in a map:

      <xsl:variable name="groups" as="map(xs:string, xs:integer)*">
        <xsl:map>
          <xsl:for-each select="TABLE/ROW[1]/ENTRY">
            <xsl:variable name="pos" select="position()"/>
            <xsl:for-each-group select="../following-sibling::ROW/ENTRY[$pos]" group-adjacent="CONTENT = '##rowspan##'">
              <xsl:if test="current-grouping-key()">
                <xsl:map-entry key="current-group()[1]/../preceding-sibling::ROW[1]//ENTRY[$pos]/generate-id()" select="count(current-group())"/>
              </xsl:if>
            </xsl:for-each-group>
          </xsl:for-each>      
        </xsl:map>
      </xsl:variable>
      
      <xsl:template match="ROW/ENTRY[not(CONTENT = '##rowspan##') and map:contains($groups, generate-id(current()))]">
        <xsl:copy>
          <xsl:apply-templates select="@*"/>
          <xsl:attribute name="MOREROWS" select="$groups(generate-id(current()))"/>
        </xsl:copy>
      </xsl:template>
      
      <xsl:mode on-no-match="shallow-copy"/>
    

    To use the map functions you need the stylesheet to declare xmlns:map="http://www.w3.org/2005/xpath-functions/map".