Binjan
Binjan

Reputation: 11

XPath statement to find count same preceding sibling

I have the following XML structure

<containers>
    <container type="1"></container>
    <container type="2"></container>
    <container type="2"></container>
    <container type="1"></container>
    <container type="2"></container>
    <container type="2"></container>
    <container type="2"></container> 
</containers>

I want to find the count of immediate preceding-sibling of the same type. How can check in XSLT e.g.

  1. for 1st container count of preceding-sibling of same type = 0
  2. for 2st container count of preceding-sibling of same type = 0
  3. for 3rd container count of preceding-sibling of same type = 1
  4. for 4th container count of preceding-sibling of same type = 0
  5. for 5th container count of preceding-sibling of same type = 0
  6. for 6th container count of preceding-sibling of same type = 1
  7. for 7th container count of preceding-sibling of same type = 2

my XSLT condition

    <xsl:choose>
  <xsl:when test="count(preceding-sibling::*[type = 2]) mod 2 = 0">
    <xsl:attribute name="class">classX</xsl:attribute>
  </xsl:when>
  <xsl:otherwise>   
  </xsl:otherwise>
</xsl:choose> 

Upvotes: 0

Views: 735

Answers (2)

zx485
zx485

Reputation: 29052

In XSLT-2.0 this is easy:

<xsl:template match="containers">
  <xsl:copy>
    <xsl:for-each-group select="container" group-adjacent="@type">
      <xsl:for-each select="current-group()">
        <container>
            <xsl:copy-of select="@*" />
            <xsl:attribute name="class"><xsl:value-of select="position()-1" /></xsl:attribute>
        </container>
      </xsl:for-each>
    </xsl:for-each-group>
  </xsl:copy>
</xsl:template>

Its output is:

<?xml version="1.0" encoding="UTF-8"?>
<containers>
   <container type="1" class="0"/>
   <container type="2" class="0"/>
   <container type="2" class="1"/>
   <container type="1" class="0"/>
   <container type="2" class="0"/>
   <container type="2" class="1"/>
   <container type="2" class="2"/>
</containers>

An XSLT-1.0 solution is this (Inspired by this SO answer):

<xsl:template match="containers">
  <xsl:copy>
    <xsl:for-each select="container">        
      <container>
        <xsl:copy-of select="@*" />
        <xsl:attribute name="class"><xsl:value-of select="count(preceding-sibling::container) - count(preceding-sibling::container[@type!=current()/@type][1]/preceding-sibling::container | preceding-sibling::container[@type!=current()/@type][1])" /></xsl:attribute>
      </container>
    </xsl:for-each>
  </xsl:copy>
</xsl:template>

It is more complicated and slower - and far less elegant. But it does its job. The output is the same.

Upvotes: 1

Martin Honnen
Martin Honnen

Reputation: 167716

In XSLT 3, you could implement it using an accumulator:

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

  <xsl:mode on-no-match="shallow-copy" streamable="yes" use-accumulators="type-count"/>

  <xsl:accumulator name="type-count" as="map(*)" initial-value="map{}" streamable="yes">
      <xsl:accumulator-rule match="containers" select="map {}"/>
      <xsl:accumulator-rule
        match="containers/container"
        select="map { 
                  'type' : string(@type), 
                  'count' :
                     if (not(@type = $value?type))
                     then 0
                     else $value?count + 1
                }"/>
  </xsl:accumulator>

  <xsl:template match="container">
      <xsl:copy>
          <xsl:apply-templates select="@*"/>
          <xsl:attribute name="sibling-count" select="accumulator-before('type-count')?count"/>
      </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/gWEaSuW

Or, for your example, to check and create an attribute:

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

  <xsl:mode on-no-match="shallow-copy" streamable="yes" use-accumulators="type-count"/>

  <xsl:accumulator name="type-count" as="map(*)" initial-value="map{}" streamable="yes">
      <xsl:accumulator-rule match="containers" select="map {}"/>
      <xsl:accumulator-rule
        match="containers/container"
        select="map { 
                  'type' : string(@type), 
                  'count' :
                     if (not(@type = $value?type))
                     then 0
                     else $value?count + 1
                }"/>
  </xsl:accumulator>

  <xsl:template match="container">
      <xsl:copy>
          <xsl:apply-templates select="@*"/>
          <xsl:if test="accumulator-before('type-count')?count mod 2 = 0">
              <xsl:attribute name="class">classX</xsl:attribute>
          </xsl:if>
      </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/gWEaSuW/1

Upvotes: 0

Related Questions