tags:

views:

1079

answers:

3

I have a XML like this:

<node id="1">
    <data alias="Show">ShowName1</data>
    <data alias="Dates">21/04/2009,23/04/2009,27/04/2009,</data>
</node>

<node id="2">
    <data alias="Show">ShowName2</data>
    <data alias="Dates">22/04/2009,25/04/2009,29/04/2009,</data>
</node>

It has X number of nodes, each with a name of a show, and a string of comma separated show dates. I can tokenize the show dates, but I want to make a sorted list with all show dates for all shows, sorted by dates. Like this:

<shows>
    <show>
        <name>ShowName1</name>
        <date>21/04/2009</date>
    </show>
    <show>
        <name>ShowName2</name>
        <date>22/04/2009</date>
    </show>
    <show>
        <name>ShowName1</name>
        <date>23/04/2009</date>
    </show>
    <show>
        <name>ShowName2</name>
        <date>25/04/2009</date>
    </show>
    <show>
        <name>ShowName1</name>
        <date>27/04/2009</date>
    </show>
    <show>
        <name>ShowName2</name>
        <date>29/04/2009</date>
    </show>
</shows>

Is this possible at all?

+1  A: 

To do this using only one transform process would require your newly created show elements to be seen as a node-set so the templates can use the internal xsl:sort mechanics. The XSL Transformations (XSLT) Version 1.0 spec does not have a method to turn self created elements into a node set. However, the MSXML parser (3.0 or greater) does have an extension to provide that functionality. Also, there's an exslt extension that I think some newer versions of Firefox support that will allow this dynamic swap of a variable to a node-set. But anyways, here's a stylesheet that works properly using the MSXML parser.

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
                xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
  <xsl:output method="xml"/>

  <xsl:template match="/">
    <xsl:variable name="shows">
      <xsl:apply-templates select="//data[@alias='Dates']"/>
    </xsl:variable>
    <shows>
      <xsl:apply-templates select="msxsl:node-set($shows)//show">
        <xsl:sort select="substring(date, 7, 4)"/><!-- year -->
        <xsl:sort select="substring(date, 4, 2)"/><!-- month -->
        <xsl:sort select="substring(date, 1, 2)"/><!-- day -->
      </xsl:apply-templates>
    </shows>
  </xsl:template>

  <xsl:template match="show">
    <xsl:copy-of select="."/>
  </xsl:template>

  <xsl:template match="data[@alias='Dates']">
    <xsl:call-template name="eachDate">
      <xsl:with-param name="node" select=".."/>
      <xsl:with-param name="dates" select="."/>
    </xsl:call-template>
  </xsl:template>

  <xsl:template name="eachDate">
    <xsl:param name="node" select="."/>
    <xsl:param name="dates" select="''"/>
    <xsl:if test="string-length($dates)">
      <show>
        <name><xsl:value-of select="$node/data[@alias='Show']/text()"/></name>
        <date><xsl:value-of select="substring-before($dates, ',')"/></date>
      </show>
      <xsl:call-template name="eachDate">
        <xsl:with-param name="node" select="$node"/>
        <xsl:with-param name="dates" select="substring-after($dates, ',')"/>
      </xsl:call-template>
    </xsl:if>
  </xsl:template>

</xsl:stylesheet>
Mister Lucky
This gets the date sorting wrong. You need to sort by individual dates, *and* they are not sort-friendly "DDMMYYYY", so they must be tokenized as well, and turned to "YYYYMMDD" before sorting.
Tomalak
good catch, it worked plenty well for the supplied small amount of data.all you need to do is sort by substring(data, 7, 4), etc. updated my xsl:sort to show what I mean. no need to tokenize, or disrupt the text value at all.
Mister Lucky
Thanks a lot. You guys are the best! Just what I needed, and problem solved (and my apologies to Dimitre for not providing a well-formed XML, won't happen again :) )
Arild
+1  A: 

Since the input XML is in a rather bad shape, we must jump through some hoops to get what you want.

As Mister Lucky already outlined in his answer, we must transform the document into a more useful temporary form first. The more useful temporary form would then be turned back into a node-set by an extension function, and processed again to produce the desired result.

In my answer I'll use a modified identity transform to achieve the following temporary form:

<root>
  <node id="1">
    <data alias="Show">ShowName1</data>
    <data alias="Dates">
      <date sort="20090421">21/04/2009</date>
      <date sort="20090423">23/04/2009</date>
      <date sort="20090427">27/04/2009</date>
    </data>
  </node>
  <node id="2">
    <data alias="Show">ShowName2</data>
    <data alias="Dates">
      <date sort="20090422">22/04/2009</date>
      <date sort="20090425">25/04/2009</date>
      <date sort="20090429">29/04/2009</date>
    </data>
  </node>
</root>

With this input, sorting is straightforward, using the @sort attribute of the <date> elements.

<xsl:stylesheet 
  version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:msxsl="urn:schemas-microsoft-com:xslt"
  exclude-result-prefixes="msxsl "
>
  <xsl:output method="xml" indent="yes" omit-xml-declaration="yes" />

  <xsl:template match="/">
    <!-- prepare our temporary form (a "result tree fragment") -->
    <xsl:variable name="rtf">
      <xsl:apply-templates mode="rtf" />
    </xsl:variable>

    <!-- transform the result tree fragment back to a node-set -->
    <xsl:variable name="doc" select="msxsl:node-set($rtf)" />

    <!-- transform the temporary node-set, sorted by date -->
    <shows>
      <xsl:apply-templates select="$doc//date">
        <xsl:sort select="@sort" />
      </xsl:apply-templates>
    </shows>
  </xsl:template>

  <xsl:template match="date">
    <show show_id="{ancestor::node/@id}">
      <name><xsl:value-of select="../../data[@alias='Show'][1]/text()" /></name>
      <date><xsl:value-of select="." /></date>
    </show>
  </xsl:template>

  <!-- all following templates are for producing the temporary form only -->

  <xsl:template match="@*|node()" mode="rtf">
    <xsl:copy>
      <xsl:apply-templates select="@*|node()" mode="rtf" />
    </xsl:copy>
  </xsl:template>

  <xsl:template match="data[@alias='Dates']" mode="rtf">
    <xsl:copy>
      <!-- copy all attributes -->
      <xsl:apply-templates select="@*" mode="rtf" />
      <!-- this produces the <date> elements -->
      <xsl:call-template name="tokenize-datelist" />
    </xsl:copy>
  </xsl:template>

  <xsl:template name="tokenize-datelist">
    <xsl:param name="input" select="." />
    <xsl:param name="delim" select="','" />

    <xsl:variable name="temp" select="concat($input, $delim)" />
    <xsl:variable name="head" select="substring-before($temp, $delim)" />
    <xsl:variable name="tail" select="substring-after($input, $delim)" />

    <xsl:if test="$head != ''">
      <date>
        <!-- this produces the @sort attribute -->
        <xsl:call-template name="tokenize-date">
          <xsl:with-param name="input" select="$head" />
        </xsl:call-template>
        <xsl:value-of select="$head" />
      </date>
      <xsl:if test="$tail != ''" >
        <xsl:call-template name="tokenize-datelist">
          <xsl:with-param name="input" select="$tail" />
          <xsl:with-param name="delim" select="$delim" />
        </xsl:call-template>
      </xsl:if>
    </xsl:if>
  </xsl:template>

  <xsl:template name="tokenize-date">
    <xsl:param name="input" select="''" />
    <xsl:param name="delim" select="'/'" />

    <xsl:variable name="dd" select="substring-before($input, $delim)" />
    <xsl:variable name="my" select="substring-after($input, $delim)" />
    <xsl:variable name="mm" select="substring-before($my, $delim)" />
    <xsl:variable name="yy" select="substring-after($my, $delim)" />

    <xsl:attribute name="sort">
      <xsl:value-of select="concat($yy, $mm, $dd)" />
    </xsl:attribute>
  </xsl:template>

</xsl:stylesheet>

When this is run through msxsl.exe, an XSLT 1.0 processor, the following output is produced:

<shows>
  <show show_id="1">
    <name>ShowName1</name>
    <date>21/04/2009</date>
  </show>
  <show show_id="2">
    <name>ShowName2</name>
    <date>22/04/2009</date>
  </show>
  <show show_id="1">
    <name>ShowName1</name>
    <date>23/04/2009</date>
  </show>
  <show show_id="2">
    <name>ShowName2</name>
    <date>25/04/2009</date>
  </show>
  <show show_id="1">
    <name>ShowName1</name>
    <date>27/04/2009</date>
  </show>
  <show show_id="2">
    <name>ShowName2</name>
    <date>29/04/2009</date>
  </show>
</shows>
Tomalak
+1  A: 

I. AN XSLT 1.0 solution using FXSL 1.x

This transformation:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:ext="http://exslt.org/common"
 exclude-result-prefixes="ext"
>
   <xsl:import href="strSplit-to-Words.xsl"/>

   <xsl:output indent="yes" omit-xml-declaration="yes"/>

    <xsl:template match="/*">
      <xsl:variable name="vDates">
        <xsl:for-each select="node">
            <nodeData name="{data[@alias = 'Show']}">
              <xsl:call-template name="str-split-to-words">
               <xsl:with-param name="pStr" select="data[@alias = 'Dates']"/>
               <xsl:with-param name="pDelimiters"
                               select="','"/>
             </xsl:call-template>
            </nodeData>
        </xsl:for-each>
        </xsl:variable>

      <xsl:apply-templates select="ext:node-set($vDates)/*/*[text()]">
        <xsl:sort data-type="number" select="substring(.,7)"/>
        <xsl:sort data-type="number" select="substring(.,4,2)"/>
        <xsl:sort data-type="number" select="substring(.,1,2)"/>
      </xsl:apply-templates>
    </xsl:template>

    <xsl:template match="word">
      <show>
        <name>
          <xsl:value-of select="../@name"/>
        </name>
          <date>
            <xsl:value-of select="."/>
          </date>
      </show>
    </xsl:template>
</xsl:stylesheet>

when applied on the provided "XML document", corrected to be well-formed (When will you, people, learn to provide a well-formed XML document? Is it that difficult?):

<t>
    <node id="1">
     <data alias="Show">ShowName1</data>
     <data alias="Dates">21/04/2009,23/04/2009,27/04/2009,</data>
    </node>
    <node id="2">
     <data alias="Show">ShowName2</data>
     <data alias="Dates">22/04/2009,25/04/2009,29/04/2009,</data>
    </node>
</t>

produces the wanted result:

<show>
   <name>ShowName1</name>
   <date>21/04/2009</date>
</show>
<show>
   <name>ShowName2</name>
   <date>22/04/2009</date>
</show>
<show>
   <name>ShowName1</name>
   <date>23/04/2009</date>
</show>
<show>
   <name>ShowName2</name>
   <date>25/04/2009</date>
</show>
<show>
   <name>ShowName1</name>
   <date>27/04/2009</date>
</show>
<show>
   <name>ShowName2</name>
   <date>29/04/2009</date>
</show>

II. One possible XSLT 2.0 solution:

<xsl:stylesheet version="2.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    exclude-result-prefixes="xs">
    <xsl:output omit-xml-declaration="yes" indent="yes"/>

    <xsl:template match="/*">

     <xsl:variable name="vAllData" as="xs:string+">
       <xsl:for-each select="node">
         <xsl:variable name="vName" select="data[@alias='Show']"/>

        <xsl:for-each select=
          "tokenize(data[@alias='Dates'], ',')[.]">

          <xsl:value-of select="concat($vName, '+',.)"/>
         </xsl:for-each>
       </xsl:for-each>
     </xsl:variable>

      <xsl:for-each select="$vAllData">
        <xsl:sort data-type="number" select=
         "substring(substring-after(.,'+'),7)"/>
        <xsl:sort data-type="number" select=
         "substring(substring-after(.,'+'),4,2)"/>
        <xsl:sort data-type="number" select=
         "substring(substring-after(.,'+'),1,2)"/>

        <show>
          <name>
            <xsl:value-of select="substring-before(.,'+')"/>
          </name>
          <date>
            <xsl:value-of select="substring-after(.,'+')"/>
          </date>
        </show>
       </xsl:for-each>
    </xsl:template>
</xsl:stylesheet>

When the above XSLT 2.0 transformation is applied on the same document, the same correct result is produced.

Dimitre Novatchev