tags:

views:

472

answers:

6

I am trying to group sibling data in an XML file.

Given :

<?xml version="1.0" encoding="UTF-8"?>
<data>
    <competition>
     <timeline>10:00</timeline>
     <fixture>team a v team b</fixture>
     <fixture>team c v team d</fixture>
     <timeline>12:00</timeline>
     <fixture>team e v team f</fixture>
     <timeline>16:00</timeline>
     <fixture>team g v team h</fixture>
     <fixture>team i v team j</fixture>
     <fixture>team k v team l</fixture>
    </competition>
</data>

I am trying to produce :

<?xml version="1.0" encoding="UTF-8"?>
<data>
    <competition>
     <timeline time="10:00">
      <fixture>team a v team b</fixture>
      <fixture>team c v team d</fixture>
     </timeline>
     <timeline time="12:00">
      <fixture>team e v team f</fixture>
     </timeline>
     <timeline time="16:00">
      <fixture>team g v team h</fixture>
      <fixture>team i v team j</fixture>
      <fixture>team k v team l</fixture>
     </timeline>
    </competition>
</data>

I am using the following XSLT:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

    <xsl:template match="competition" >

     <xsl:apply-templates select="timeline" />

    </xsl:template>

    <xsl:template match="timeline">
     <timeline>
      <xsl:attribute name="time" >
       <xsl:value-of select="." />
      </xsl:attribute>

      <xsl:apply-templates select="following-sibling::*" mode="copy"/>

     </timeline>
    </xsl:template>

    <xsl:template match="fixture" mode="copy">
     <fixture>
      <xsl:value-of select="." />
     </fixture>
    </xsl:template>

    <xsl:template match="timeline" mode="copy">
     <xsl:apply-templates select="following-sibling::*" mode="null" />
    </xsl:template>

    <xsl:template match="*" mode="null">
    </xsl:template>
</xsl:stylesheet>

My problem is that it is not stopping processing fixture nodes when it gets to the next timeline

A: 

Try something like that :

<xsl:template match="timeline">
    <timeline>
            <xsl:attribute name="time" >
                    <xsl:value-of select="." />
            </xsl:attribute>

            <xsl:apply-templates select="following-sibling::*[name()=fixture][1]" />

    </timeline>
</xsl:template>

<xsl:template match="fixture">
    <fixture>
            <xsl:value-of select="." />
    </fixture>
    <xsl:apply-templates select="following-sibling::*[name()=fixture][1]" />
</xsl:template>
g andrieu
It does not understand next-sibling. instead I used following-sibling::*[1]
Xetius
next-sibling is not an axis I recognize?
AnthonyWJones
I edited to remove the next-sibling stuff.
g andrieu
And it still does not work. Have you tested it?
Tomalak
Obviously not. I never doubted it may not work. That's why I said "something like". I provided this answer as a tip, while nobody had answered already, without double-checking anything. Then I got negative feedback although my answer, without being perfect, was still helpful. So I corrected a little. Sorry, no time to do more.
g andrieu
A: 

With help from g andrieu I had to make it only get the next item and not the list following:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

    <xsl:template match="competition" >

     <xsl:apply-templates select="timeline" />

    </xsl:template>

    <xsl:template match="timeline">
     <timeline>
      <xsl:attribute name="time" >
       <xsl:value-of select="." />
      </xsl:attribute>

      <xsl:apply-templates select="following-sibling::*[1]" mode="copy"/>

     </timeline>
    </xsl:template>

    <xsl:template match="fixture" mode="copy">
     <fixture>
      <xsl:value-of select="." />
     </fixture>
     <xsl:apply-templates select="following-sibling::*[1]" mode="copy"/>
    </xsl:template>

    <xsl:template match="timeline" mode="copy" />

</xsl:stylesheet>
Xetius
That looks like it works but it is a bit hard to follow.
AnthonyWJones
A: 

G Andrieu's solution doesn't work, as there is no such axes as 'next-sibling' unfortunately.

And alternative solution would be the following:

<xsl:template match="timeline">
<timeline>
  <xsl:attribute name="time" >
    <xsl:value-of select="." />
  </xsl:attribute>

  <xsl:apply-templates select="following-sibling::*[local-name()='fixture' and position()=1]" />

</timeline>
</xsl:template>

<xsl:template match="fixture">
  <fixture>
      <xsl:value-of select="." />
  </fixture>
  <xsl:apply-templates select="following-sibling::*[local-name()='fixture' and position()=1]" />
</xsl:template>
samjudson
+1  A: 

The following xslt will work even if same timelines are scattered in multiple places. For e.g. in the foll xml there are 2 entries for timeline 10:00

<?xml version="1.0" encoding="UTF-8"?>
<data>
    <competition>
     <timeline>10:00</timeline>
     <fixture>team a v team b</fixture>
     <fixture>team c v team d</fixture>
     <timeline>12:00</timeline>
     <fixture>team e v team f</fixture>
     <timeline>16:00</timeline>
     <fixture>team g v team h</fixture>
     <fixture>team i v team j</fixture>
     <fixture>team k v team l</fixture>
     <timeline>10:00</timeline>
     <fixture>team a v team b new</fixture>
     <fixture>team c v team d new</fixture>
    </competition>
</data>

Xslt:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
    <xsl:key name="TimelineDistint" match="timeline" use="."/>

    <xsl:template match="data">
     <xsl:apply-templates select="competition"/>
    </xsl:template>

    <xsl:template match="competition">
     <data>
      <competition>
       <xsl:for-each select="timeline[generate-id() = generate-id(key('TimelineDistint', .)[1])]">
        <timeline>
         <xsl:variable name="varTimeline" select="."/>
         <xsl:attribute name="time"><xsl:value-of select="normalize-space(.)"/></xsl:attribute>
         <xsl:for-each select="../fixture[preceding::timeline[1] = $varTimeline]">
          <fixture>
           <xsl:value-of select="normalize-space(.)"/>
          </fixture>
         </xsl:for-each>
        </timeline>
       </xsl:for-each>
      </competition>
     </data>
    </xsl:template>
</xsl:stylesheet>

Output:

<?xml version="1.0" encoding="UTF-8"?>
<data>
    <competition>
     <timeline time="10:00">
      <fixture>team a v team b</fixture>
      <fixture>team c v team d</fixture>
      <fixture>team a v team b new</fixture>
      <fixture>team c v team d new</fixture>
     </timeline>
     <timeline time="12:00">
      <fixture>team e v team f</fixture>
     </timeline>
     <timeline time="16:00">
      <fixture>team g v team h</fixture>
      <fixture>team i v team j</fixture>
      <fixture>team k v team l</fixture>
     </timeline>
    </competition>
</data>
Rashmi Pandit
Excellent answer, thanks
Xetius
This only works if is exactly one <competition> per document, which is most likely a wrong assumption. There is no need to use the "//" shorthand all over the place, the solution would even benefit if you removed them completely.
Tomalak
Thanks for your suggestion Tomalak, I have edited the xslt to support multiple competition elements.
Rashmi Pandit
+2  A: 

Here is my attempt. One assumption I have made which simplifies things is that timeline elements with a specific text value are already unique.

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
  <xsl:output method="xml" encoding="UTF-8" />

  <xsl:template match="/data">
    <data>
      <xsl:apply-templates select="competition" />
    </data>
  </xsl:template>

  <xsl:template match="competition">
    <xsl:for-each select="timeline">
      <timeline time="{text()}">
        <xsl:copy-of
          select="./following-sibling::fixture[count(preceding-sibling::timeline[1] | current()) = 1]" />
      </timeline>
    </xsl:for-each>
  </xsl:template>

</xsl:stylesheet>

The above is edited to use current() instead of a variable as per Tomalak's suggestion.

AnthonyWJones
This is one way to do it, though not overly efficient. +1 still :) Can you fix the horizontal scrolling?
Tomalak
No not efficient but dead simple however I prefer your solution. Adjusted the content a little to reduce indentation but still scrolls.
AnthonyWJones
It is allowed in XML to put as many line breaks and white space into an attribute as you like. This allows for a nice and clean format of the XPath in your select expression, just break it up a bit.
Tomalak
And the XPath expression could be simplified to "following-sibling::fixture[count(preceding-sibling::timeline[1] | current()) = 1]", enabling you to throw out the variable. :)
Tomalak
Nice catch, I'd forgotten about current().
AnthonyWJones
+7  A: 

This is easy to do when the following is true (which I assume it is):

  • all <timeline>s within a <competition> are unique
  • only the <fixture>s right after a given <timeline> belong to it
  • there is no <fixture> without a <timeline> element before it

This XSLT 1.0 solution:

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

  <xsl:key name="kFixture" 
           match="fixture" 
           use="generate-id(preceding-sibling::timeline[1])" 
  />

  <xsl:template match="data">
    <xsl:copy>
      <xsl:apply-templates select="competition" />
    </xsl:copy>
  </xsl:template>

  <xsl:template match="competition">
    <xsl:copy>
      <xsl:apply-templates select="timeline" />
    </xsl:copy>
  </xsl:template>

  <xsl:template match="timeline">
    <xsl:copy>
      <xsl:attribute name="time">
        <xsl:value-of select="." />
      </xsl:attribute>
      <xsl:copy-of select="key('kFixture', generate-id())" />
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

produces:

<data>
  <competition>
    <timeline time="10:00">
      <fixture>team a v team b</fixture>
      <fixture>team c v team d</fixture>
    </timeline>
    <timeline time="12:00">
      <fixture>team e v team f</fixture>
    </timeline>
    <timeline time="16:00">
      <fixture>team g v team h</fixture>
      <fixture>team i v team j</fixture>
      <fixture>team k v team l</fixture>
    </timeline>
    </competition>
</data>

Note the use of an <xsl:key> to match all <fixture>s that belong to ("are preceded by") a given <timeline>.

A slightly shorter but less obvious solution would be a modified identity transform:

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

  <xsl:key name="kFixture" 
           match="fixture" 
           use="generate-id(preceding-sibling::timeline[1])" 
  />

  <xsl:template match="* | @*">
    <xsl:copy>
      <xsl:apply-templates select="*[not(self::fixture)] | @*" />
    </xsl:copy>
  </xsl:template>

  <xsl:template match="timeline">
    <xsl:copy>
      <xsl:attribute name="time">
        <xsl:value-of select="." />
      </xsl:attribute>
      <xsl:copy-of select="key('kFixture', generate-id())" />
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>
Tomalak
+1. Nice one. :)
AnthonyWJones