0

I have a list of paths that I need to make into a tree structure. Additionally I need to add some specific info related to each level.

Example input

<root>
  <data>2013</data>
  <data>2013/1</data>
  <data>2013/1/0</data>
  <data>2013/1/1</data>
  <data>2013/1/2</data>
  <data>2013/2</data>
  <data>2013/2/0</data>
  <data>2013/2/1</data>
  <data>2013/2/2</data>
  <data>2013/2/3</data>
</root>

I need to make this look like something similar to for example this:

<root>
  <year value="2013">
    <info />
    <month value="1">
      <info />
      <day value="0">
        <info />
      </day>
      <day value="1">
        <info />
      </day>
      ...
    </month>
    ...
  </year>
  ...
</root>

Where the info elements would be info I get about each path from somewhere else.

Thinking I probably need grouping or something, but never used it before and generally just stuck here. Don't know how to attack this. Any help would be much appreciated.

Svish
  • 152,914
  • 173
  • 462
  • 620
  • Is this a *representative* example? Specifically: **1**. records are sorted by hierarchy; **2**. the hierarchy is no more than three levels deep. Are those things one can rely on? – michael.hor257k Mar 15 '14 at 07:32
  • 1
    **3.** Each data element other than year has a parent data element. – michael.hor257k Mar 15 '14 at 10:09
  • possible duplicate of [Creating a nested tree structure from a path in XSLT](http://stackoverflow.com/questions/872067/creating-a-nested-tree-structure-from-a-path-in-xslt) – Tomalak Mar 15 '14 at 10:13
  • @michael.hor257k Yes, they are a result from scanning the file system for xml files which they are organized by year/month/day. So the goal here is to pull some info out of each file and make an tree overview sort of. – Svish Mar 15 '14 at 15:18
  • @Tomalak Yes, I have looked at that question, but haven't been able to figure out how to use it for my case. I managed to make the tree, but not how to differentiate in a good way for each level to push in the various info. – Svish Mar 15 '14 at 15:19
  • I could probably show you if you were more specific. – Tomalak Mar 15 '14 at 15:37
  • @Tomalak Your answer here seems pretty much dead on, so I will try to test that out first. I know how to get the info in there if I just know where I am in the tree and at what level, which seems pretty easy to see in your example. – Svish Mar 15 '14 at 15:42

3 Answers3

2

Using an XSL key this can be done relatively easily. (This answer is based on the one by michael.hor257k.)

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output indent="yes"/>

  <xsl:key name="kLevel" match="data" use="
    string-length(.) - string-length(translate(., '/', ''))
  " />

  <xsl:template match="/*">
    <xsl:copy>
      <xsl:apply-templates mode="year" select="key('kLevel', 0)" />
    </xsl:copy>
  </xsl:template>

  <xsl:template match="data" mode="year">
    <year value="{.}">
      <xsl:apply-templates mode="month" select="key('kLevel', 1)[starts-with(., concat(current(), '/'))]" />
    </year>
  </xsl:template>

  <xsl:template match="data" mode="month">
    <month value="{substring-after(., '/')}">
      <xsl:apply-templates mode="day" select="key('kLevel', 2)[starts-with(., concat(current(), '/'))]" />
    </month>
  </xsl:template>

  <xsl:template match="data" mode="day">
    <day value="{substring-after(substring-after(., '/'), '/')}">
      <info />
    </day>
  </xsl:template>
</xsl:stylesheet>

which gives

<root>
  <year value="2013">
    <month value="1">
      <day value="0">
        <info />
      </day>
      <day value="1">
        <info />
      </day>
      <day value="2">
        <info />
      </day>
    </month>
    <month value="2">
      <day value="0">
        <info />
      </day>
      <day value="1">
        <info />
      </day>
      <day value="2">
        <info />
      </day>
      <day value="3">
        <info />
      </day>
    </month>
  </year>
</root>
Community
  • 1
  • 1
Tomalak
  • 332,285
  • 67
  • 532
  • 628
  • This looks quite straight forward and actually think I understand what's going on. Will try it out shortly. – Svish Mar 15 '14 at 15:21
1

I am assuming the hierarchy is exactly the given three levels deep. It would be hard for it to be otherwise, if each level requires an element with its own name. For this reason also, it is necessary to have a separate template for each level, even though the code is largely similar. Otherwise we would need some sort of a lookup directory to find out what comes after "month", for example.

(edit)
It is also assumed that each data element - other than a year - has a "parent" data element; i.e. no intermediate elements have to be created during the transformation.

XSLT 1.0

<?xml version="1.0" encoding="utf-8"?>
<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="/root">
    <xsl:copy>
        <xsl:apply-templates 
            select="data[not(contains(., '/'))]" 
            mode="year"/>
    </xsl:copy>
</xsl:template>

<xsl:template match="data" mode="year">
    <year value="{.}">
    <xsl:variable name="dir" select="concat(., '/')" />
        <xsl:apply-templates 
            select="/root/data
                [starts-with(., $dir)]
                [not (contains(substring-after(., $dir), '/'))]"
            mode="month"/>
    </year>
</xsl:template>

<xsl:template match="data" mode="month">
    <month value="{substring-after(., '/')}">
    <xsl:variable name="dir" select="concat(., '/')" />
        <xsl:apply-templates 
            select="/root/data
                [starts-with(., $dir)]
                [not (contains(substring-after(., $dir), '/'))]"
            mode="day"/>
    </month>
</xsl:template>

<xsl:template match="data" mode="day">
    <day value="{substring-after(substring-after(., '/'), '/')}">
    </day>
</xsl:template>

</xsl:stylesheet> 

When applied to your input, the result is:

<?xml version="1.0" encoding="UTF-8"?>
<root>
   <year value="2013">
      <month value="1">
         <day value="0"/>
         <day value="1"/>
         <day value="2"/>
      </month>
      <month value="2">
         <day value="0"/>
         <day value="1"/>
         <day value="2"/>
         <day value="3"/>
      </month>
   </year>
</root>

Where the info elements would be info I get about each path from somewhere else.

I left this part out because it's not at all clear to me how this would work. I hope you won't be disappointed when you get to it.

michael.hor257k
  • 113,275
  • 6
  • 33
  • 51
  • looks like it won't work for more than 1 year, @michael.hor257k – Lingamurthy CS Mar 15 '14 at 09:27
  • @LingamurthyCS Bad data: you must have a "parent" element for `2013/2/2` in the form of `2013/2` – michael.hor257k Mar 15 '14 at 10:07
  • Could be.. I thought the requirement is to even convert this kind of data :) – Lingamurthy CS Mar 15 '14 at 10:09
  • @LingamurthyCS I don't see that the requirement is to **create** missing intermediate elements. I have added a clarification regarding this to my answer above – michael.hor257k Mar 15 '14 at 10:14
  • There won't be any missing intermediate elements since this is a result of a filesystem scan. So if something was missing it would be an error in my PHP scanning code :) – Svish Mar 15 '14 at 15:23
  • @michael.hor257k The paths are pointing towards xml files (just with the .xml chopped off), so I will be getting the extra info through the `document` info. Basically what I'm aiming for is to pull out some info from each file to create a tree structured overview of all the files which will then be used to create a menu. – Svish Mar 15 '14 at 15:24
0

here is the stylesheet using XSLT2.0:

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

<xsl:template match="/">
    <root>
        <xsl:for-each-group select="root/data" group-by="tokenize(.,'/')[1]">
            <year value="{current-grouping-key()}">
                <info/>
                <xsl:for-each-group select="current-group()" group-by="tokenize(.,'/')[2]">
                    <month value="{current-grouping-key()}">
                        <info/>
                        <xsl:for-each-group select="current-group()" group-by="tokenize(.,'/')[3]">
                            <day value="{current-grouping-key()}">
                                <info/>
                            </day>
                        </xsl:for-each-group>
                    </month>
                </xsl:for-each-group>
            </year>
        </xsl:for-each-group>
    </root>
</xsl:template>
</xsl:stylesheet>
Lingamurthy CS
  • 5,412
  • 2
  • 13
  • 21