Question

I've seen some variations of this quesiton asked here, but I'm not sure how to apply them to my situation, so I'm hoping maybe someone can help me here.

I have a flat XML file in a format similar to this one:

<item id="1"/>
<item id="1.1"/>
<item id="1.1.1"/>
<item id="1.1.2"/>
<item id="1.1.2.1"/>
<item id="1.2"/>
<item id="1.3"/>

I'm looking to set up the tags hierarchically based on the id attribute, like so:

<item id="1">
  <item id="1.1">
    <item id="1.1.1"/>
    <item id="1.1.2">
      <item id="1.1.2.1"/>
    </item>
  </item>
  <item id="1.2"/>
  <item id="1.3"/>
</item>

Some of the id values have two digit numbers (e.g., "1.2.3.15.1") which makes comparing them even more challenging.

Help?

Was it helpful?

Solution

Selecting the correct nodes can be tricky but as you have no hierarchy this works for your sample input (if you add a root element to it)

  <!-- start somewhere -->
  <xsl:template match="/root">
    <root>
      <!-- select all with no . in the id -->
      <xsl:apply-templates  select="//item[string-length(translate(@id,'1234567890',''))=0]" />
    </root>
  </xsl:template>

  <xsl:template match="item">
    <xsl:variable name="id" select="@id"/>
    <!-- how many . have we ? That is how deep we are and add 1 -->
    <xsl:variable name="deep" select="string-length(translate(@id,'1234567890',''))+1" />
    <xsl:copy>
      <!-- copy attribs over -->
      <xsl:apply-templates select="@*"/> 
      <!-- select all nodes that start with our curent id,  
           select nodes that are just one level below us 
           and don't select our selfs-->
      <xsl:apply-templates select="//item[starts-with(@id, $id) and string-length(translate(@id,'1234567890',''))=$deep and not(@id=$id)]"/>
    </xsl:copy>
  </xsl:template>

  <!-- copy attribs -->
  <xsl:template match="@*">
    <xsl:copy />
  </xsl:template>

OTHER TIPS

I. Here is a simple XSLT 2.0 solution (and a similar XSLT 1.0 solution follows after this one):

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:my="my:my">
    <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:template match="/*">
     <xsl:sequence select="my:grouping(*, 1)"/>
 </xsl:template>

 <xsl:function name="my:grouping" as="element()*">
  <xsl:param name="pNodes" as="element()*"/>
  <xsl:param name="pLevel" as="xs:integer"/>

  <xsl:if test="$pNodes">
      <xsl:for-each-group select="$pNodes" group-by="tokenize(@id, '\.')[$pLevel]">
       <xsl:copy>
         <xsl:copy-of select="@*"/>
         <xsl:sequence select="
          my:grouping(current-group()[tokenize(@id, '\.')[$pLevel+1]], $pLevel+1)"/>
       </xsl:copy>
  </xsl:for-each-group>
  </xsl:if>
 </xsl:function>
</xsl:stylesheet>

When this transformation is applied on this XML document (the provided XML fragment, wrapped within a single top element to make it a well-formed XML document):

<t>
    <item id="1"/>
    <item id="1.1"/>
    <item id="1.1.1"/>
    <item id="1.1.2"/>
    <item id="1.1.2.1"/>
    <item id="1.2"/>
    <item id="1.3"/>
</t>

the wanted, correct result is produced:

<item id="1">
   <item id="1.1">
      <item id="1.1.1"/>
      <item id="1.1.2">
         <item id="1.1.2.1"/>
      </item>
   </item>
   <item id="1.2"/>
   <item id="1.3"/>
</item>

II. Here is a similar XSLT 1.0 solution:

<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="kFollowing" match="item"
   use="generate-id(preceding-sibling::*
                     [string-length(current()/@id) > string-length(@id)
                    and
                      starts-with(current()/@id, concat(@id, '.'))]
                       [1])"/>

 <xsl:template match="/*">
     <xsl:call-template name="grouping">
      <xsl:with-param name="pNodes" select="*"/>
      <xsl:with-param name="pLevel" select="1"/>
     </xsl:call-template>
 </xsl:template>

 <xsl:template name="grouping">
  <xsl:param name="pNodes"/>
  <xsl:param name="pLevel" select="1"/>

  <xsl:for-each select=
    "$pNodes[$pLevel > string-length(@id) - string-length(translate(@id, '.', ''))]">
   <xsl:copy>
     <xsl:copy-of select="@*"/>

     <xsl:call-template name="grouping">
       <xsl:with-param name="pNodes" select="key('kFollowing', generate-id())"/>
         <xsl:with-param name="pLevel" select="$pLevel+1"/>
     </xsl:call-template>
   </xsl:copy>
  </xsl:for-each>
 </xsl:template>
</xsl:stylesheet>

When this XSLT 1.0 transformation is applied on the same document (above), the same wanted, correct result is produced:

<item id="1">
   <item id="1.1">
      <item id="1.1.1"/>
      <item id="1.1.2">
         <item id="1.1.2.1"/>
      </item>
   </item>
   <item id="1.2"/>
   <item id="1.3"/>
</item>
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top