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.

7
  • 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? Commented Mar 15, 2014 at 7:32
  • 1
    3. Each data element other than year has a parent data element. Commented Mar 15, 2014 at 10:09
  • possible duplicate of Creating a nested tree structure from a path in XSLT Commented Mar 15, 2014 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. Commented Mar 15, 2014 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. Commented Mar 15, 2014 at 15:19

3 Answers 3

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>
Sign up to request clarification or add additional context in comments.

1 Comment

This looks quite straight forward and actually think I understand what's going on. Will try it out shortly.
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.

6 Comments

looks like it won't work for more than 1 year, @michael.hor257k
@LingamurthyCS Bad data: you must have a "parent" element for <data>2013/2/2</data> in the form of <data>2013/2</data>
Could be.. I thought the requirement is to even convert this kind of data :)
@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
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 :)
|
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>

5 Comments

This is how it works: Tokenize the date by '/', group on 1st token that gives you year, then tokenize again on month, and then on day.
Is tokenize in XSLT/XPath 1.0?
No, it's not present in 1.0.
You can write your own template to do it, though!
Apparently for-each-group isn't in XSLT 1.0 either. Seems 1.0 doesn't really support grouping at all. Trying to figure out of an alternative called Muenchian Grouping now...

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.