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">
    <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

Xetius
  • 44,755
  • 24
  • 88
  • 123

6 Answers6

10

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
  • 332,285
  • 67
  • 532
  • 628
3

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">
  <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
  • 187,081
  • 35
  • 232
  • 306
  • This is one way to do it, though not overly efficient. +1 still :) Can you fix the horizontal scrolling? – Tomalak Jun 03 '09 at 10:09
  • No not efficient but dead simple however I prefer your solution. Adjusted the content a little to reduce indentation but still scrolls. – AnthonyWJones Jun 03 '09 at 11:47
  • 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 Jun 03 '09 at 11:53
  • 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 Jun 03 '09 at 11:57
  • Nice catch, I'd forgotten about current(). – AnthonyWJones Jun 03 '09 at 13:27
1

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
  • 56,243
  • 7
  • 59
  • 69
1

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">
    <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
  • 23,230
  • 17
  • 71
  • 111
  • This only works if is exactly one 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 Jun 03 '09 at 10:15
  • Thanks for your suggestion Tomalak, I have edited the xslt to support multiple competition elements. – Rashmi Pandit Jun 03 '09 at 10:29
0

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
  • 2,081
  • 3
  • 14
  • 10
  • It does not understand next-sibling. instead I used following-sibling::*[1] – Xetius Jun 03 '09 at 09:15
  • next-sibling is not an axis I recognize? – AnthonyWJones Jun 03 '09 at 09:31
  • 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 Jun 04 '09 at 07:05
0

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">
    <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
  • 44,755
  • 24
  • 88
  • 123