Jens
Jens

Reputation: 9150

How do I flatten groups of adjacent same nodes?

Suppose I have the following sequence of elements:

<outer>
  <e>…</e>  <!-- Adjacent <e> should be grouped if they aren’t yet. -->
  <e>…</e>
  <group>
    <e>…</e>
    <e>…</e>
  </group>
  <e>…</e>
  <e>…</e>
</outer>

And I’d like to consolidate those elements <e> that have not yet been grouped, i.e. the output would be

<outer>
  <group-foo>  <!-- Grouped elements. -->
    <e>…</e>
    <e>…</e>
  </group-foo>
  <group-bar>
    <e>…</e>
    <e>…</e>
  </group-bar>
  <group-foo>
    <e>…</e>
    <e>…</e>
  </group-foo>
</outer>

I just can’t quite figure out how to select a group of adjacent elements (a node set); closest idea was to select //e[name(parent::*) = 'outer'] or some such but that assumes a certain parent element and it returns a single node set, whereas I’d need two.

Upvotes: 1

Views: 111

Answers (2)

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243599

No recursion. Using keys (Muenchian grouping):

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>
 <xsl:key name="kE-ByPrecedingNonE" match="e[not(name(..) = 'group')]"
   use="generate-id(preceding-sibling::*[not(self::e)][1])"/>

  <xsl:template match="node()|@*" name="identity">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="e[not(name(..) = 'group')]"/>

  <xsl:template match=
   "e[generate-id()
    = generate-id(key('kE-ByPrecedingNonE',
                      generate-id(preceding-sibling::*[not(self::e)][1])
                      )[1]
                  )]">
      <group-generated>
        <xsl:apply-templates select=
        "key('kE-ByPrecedingNonE',
             generate-id(preceding-sibling::*[not(self::e)][1])
             )" mode="inGroup"/>
      </group-generated>
    </xsl:template>

    <xsl:template match="node()" mode="inGroup">
      <xsl:call-template name="identity"/>
    </xsl:template>
</xsl:stylesheet>

When this transformation is applied on the provided source-xml document:

<outer>
  <e>…</e>
  <e>…</e>
  <group>
    <e>…</e>
    <e>…</e>
  </group>
  <e>…</e>
  <e>…</e>
</outer>

the wanted correct result is produced:

<outer>
   <group-generated>
      <e>…</e>
      <e>…</e>
   </group-generated>
   <group>
      <e>…</e>
      <e>…</e>
   </group>
   <group-generated>
      <e>…</e>
      <e>…</e>
   </group-generated>
</outer>

Explanation:

  1. The identity rule (template) copies to the output every node as is. We also give it a name so that we can not only use it when applying templates, but also call it directly -- as we do in step 4. below.

  2. A template matching any <e> element that is not already inside a <group>. This has no body -- we want to only handle such elements in our next templates -- not when the identity template selects it for execution.

  3. A template that matches any <e> element that is the first in a "non-covered" group. This uses the idea behind the Muenchian grouping method -- if you are unfamiliar with it, learn it well -- you won't be sorry. This template generates a wrapper element (called "group-generated") for the whole group and inside this wrapper applies templates to all <e>s in the group in mode "inGroup" -- where these are simply copied.

  4. The template in mode "inGroup" simply delegates to the identity rule to copy the selected node. Thus any matched node is copied as-is. Here, if we decide, we can perform any other required "inGroup" processing.

Upvotes: 0

michael.hor257k
michael.hor257k

Reputation: 117165

One way to solve this is to use a so-called sibling recursion:

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="/outer">
    <xsl:copy>
        <xsl:apply-templates select="e[not(preceding-sibling::*[1][self::e])] | group"/>
    </xsl:copy>
</xsl:template>

<xsl:template match="e">
    <group-foo>
        <xsl:copy-of select="."/>
        <!-- immediate sibling in the same group -->
        <xsl:apply-templates select="following-sibling::*[1][self::e]" mode="collect" />
    </group-foo>
</xsl:template>

<xsl:template match="e" mode="collect">
    <xsl:copy-of select="."/>
    <!-- immediate sibling in the same group -->
    <xsl:apply-templates select="following-sibling::*[1][self::e]" mode="collect" />
</xsl:template> 

<xsl:template match="group">
    <group-bar>
        <xsl:copy-of select="*"/>
    </group-bar>
</xsl:template> 

</xsl:stylesheet>

Upvotes: 1

Related Questions