6
votes

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

6
Not only that, your xslt will not group same timelines if they are not one after another.SO User
Check out my soln ... will work even if timelines are spread out in your xml instead of being sequential.SO User
@Rashmi: where do you drive the requirement to group same timelines together from? I don't see any suggestion that timeline values are not unique.AnthonyWJones
Past exp of handling xmls ... though this may or may not be the case here.SO User

6 Answers

10
votes

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>
3
votes

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.

1
votes

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>
1
votes

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>
0
votes

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>
0
votes

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>