dmendezg
dmendezg

Reputation: 1928

Create hierarchy from list based on attribute

I have been trying to put this list on a hierarchy based on the attribute count but I can't get right. At first was trying to do it all at one, the in phases but I keep running into the same problems when using preceding-sibling and following-sibling.

This is what I have

<body>
    <element count="2"/>
    <element count="2"/> 
    <element count="2"/> 
    <element count="2"/>
    <element count="4"/>
    <element count="4"/>
    <element count="6"/>
    <element count="4"/>
    <element count="2"/>
    <element count="4"/>
    <element count="2"/>
    <element count="4"/>
    <element count="6"/>
    <element count="6"/>
    <element count="4"/>
    <element count="2"/> 
 </body>

This is what I want

<body>
    <element count="2"/>
    <element count="2"/> 
    <element count="2"/> 
    <element count="2">
        <element count="4"/>
        <element count="4">
            <element count="6"/>
        </element>
        <element count="4"/>
    </element>
    <element count="2">
        <element count="4"/>
    </element>
    <element count="2">
        <element count="4">
            <element count="6"/>
            <element count="6"/>
        </element>
        <element count="4"/>
    </element> 
    <element count="2"/> 
 </body>

Any help will be appreciated, thanks!

Upvotes: 1

Views: 229

Answers (2)

Ian Roberts
Ian Roberts

Reputation: 122394

Here is a possible XSLT 1.0 solution. The trick here is to use a key to link each element to its nearest preceding sibling that has a strictly smaller count value.

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:strip-space elements="*" />
  <xsl:output method="xml" indent="yes" />

  <xsl:key name="childrenByParent" match="element"
    use="generate-id(preceding-sibling::element[@count &lt; current()/@count][1])" />

  <xsl:template match="/body">
    <body>
      <xsl:apply-templates select="key('childrenByParent', '')" />
    </body>
  </xsl:template>

  <xsl:template match="element">
    <element>
      <xsl:copy-of select="@*" />
      <xsl:apply-templates select="key('childrenByParent', generate-id())" />
    </element>
  </xsl:template>
</xsl:stylesheet>

The starting point key('childrenByParent', '') works because we need to start from the elements that don't have a "parent", i.e. those for which preceding-sibling::element[@count &lt; current()/@count][1] selects an empty node set, and the generate-id of an empty node set is defined to be the empty string.

You ask in a comment what would need to change if the elements could be named anything as long as they have a count attribute. In that case you'd just need to make a small change to the key definition to make it (a) match any element with a count and (b) look for the parent with any name rather than just element:

  <xsl:key name="childrenByParent" match="*[@count]"
    use="generate-id(preceding-sibling::*[@count &lt; current()/@count][1])" />

and to the second template to give it a more general match pattern, and to use xsl:copy instead of hard-coding the element name to create:

  <xsl:template match="*">
    <xsl:copy>
      <xsl:copy-of select="@*" />
      <xsl:apply-templates select="key('childrenByParent', generate-id())" />
    </xsl:copy>
  </xsl:template>

Upvotes: 0

Martin Honnen
Martin Honnen

Reputation: 167696

If you know the count attributes have values 2,4,6,.. respectively you know the initial count and the difference between counts then it is easy:

<xsl:stylesheet 
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  xmlns:mf="http://example.com/mf"
  exclude-result-prefixes="xs mf">

<xsl:output indent="yes"/>

<xsl:function name="mf:group" as="element(element)*">
  <xsl:param name="elements" as="element(element)*"/>
  <xsl:param name="level" as="xs:integer"/>
  <xsl:param name="step" as="xs:integer"/>
  <xsl:for-each-group select="$elements" group-starting-with="element[@count = $level]">
    <xsl:copy>
      <xsl:copy-of select="@*"/>
      <xsl:sequence select="mf:group(current-group() except ., $level + $step, $step)"/>
    </xsl:copy>  
  </xsl:for-each-group>
</xsl:function>

<xsl:template match="body">
  <xsl:copy>
    <xsl:sequence select="mf:group(element, 2, 2)"/>
  </xsl:copy>
</xsl:template>

</xsl:stylesheet>

Upvotes: 1

Related Questions