0

I'm using XSLT 1.0 Input xml document:

<Table>
   <Numbers>10,100,1000</Numbers>
   <Values>
      <Value>1</Value>
      <Value>2</Value>
      <Value>3</Value>
   </Values>
</Table>

Expected output:

<Table>
   <Results>
      <Values>
         <Value>1</Value>
         <Number>10</Number>
      </Values>
      <Values>
         <Value>2</Value>
         <Number>100</Number>
      </Values>
      <Values>
         <Value>3</Value>
         <Number>1000</Number>
      </Values>
   </Results>
</Table>

I've implemented common solution for tokenize function in XSLT 1.0, described here: http://www.heber.it/?p=1088

Current implementation:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format">
    <xsl:template match="/">
        <table name="Results">
            <xsl:variable name="Numbers">
                <xsl:value-of select="Table/Numbers"/>
            </xsl:variable>
            <Results>
            <xsl:for-each select="Table/Values/Value">
            <Values>
            <xsl:variable name="Value" select="."/>
            <xsl:variable name="n" select="count(preceding-sibling::*)+1."/>
            <Value>
                <xsl:value-of select="$Value"/>
            </Value>                                                                    
                <xsl:call-template name="tokenizeString">
                    <xsl:with-param name="list" select="$Numbers"/>
                    <xsl:with-param name="delimiter" select="','"/>                                                                         
                </xsl:call-template>                        
            </Values>
            </xsl:for-each>
            </Results>
        </table>
    </xsl:template>
    <xsl:template name="tokenizeString">
        <!--passed template parameter -->
        <xsl:param name="list"/>
        <xsl:param name="delimiter"/>
        <xsl:choose>
            <xsl:when test="contains($list, $delimiter)">                               
                    <!-- get everything in front of the first delimiter -->
                <Number>    
                <xsl:value-of select="substring-before($list,$delimiter)"/>
                </Number>   
                <xsl:call-template name="tokenizeString">
                    <!-- store anything left in another variable -->
                    <xsl:with-param name="list" select="substring-after($list,$delimiter)"/>
                    <xsl:with-param name="delimiter" select="$delimiter"/>
                </xsl:call-template>
            </xsl:when>            
            <xsl:otherwise>
            <Number>    
                <xsl:value-of select="$list"/>
            </Number>   
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
</xsl:stylesheet>

Current output:

<table xmlns:fo="http://www.w3.org/1999/XSL/Format" name="Results">
    <Results>
        <Values>
            <Value>1</Value>
            <Number>10</Number>
            <Number>100</Number>
            <Number>1000</Number>
        </Values>
        <Values>
            <Value>2</Value>
            <Number>10</Number>
            <Number>100</Number>
            <Number>1000</Number>
        </Values>
        <Values>
            <Value>3</Value>
            <Number>10</Number>
            <Number>100</Number>
            <Number>1000</Number>
        </Values>
    </Results>
</table>

But the output is not correct. I can't get, how can I return results, after applying the tokenize template, in correct order, but not all of those for each Value node.

It is clear how to implement the same using XSLT 2.0, as there I can do following:

<xsl:variable name"Foo" select="tokenize($string,$delimter)[$n]">

Update: solution to use node-set works fine to me to represent tokenize function, using XSLT1. But I got another case, which is additional to initial requirements. The case is to have only one Number corresponding to a set of Values:

<Table>
   <Foo>    
   <Numbers>10,100,1000</Numbers>
   <Values>
      <Value>1</Value>
      <Value>2</Value>
      <Value>3</Value>
   </Values>
   </Foo>
   <Foo>
   <Numbers>10</Numbers>
   <Values>
      <Value>4</Value>
      <Value>5</Value>
      <Value>6</Value>
   </Values>
   </Foo>
</Table>

Expected output:

<Table>
   <Results>
      <Values>
         <Value>1</Value>
         <Number>10</Number>
      </Values>
      <Values>
         <Value>2</Value>
         <Number>100</Number>
      </Values>
      <Values>
         <Value>3</Value>
         <Number>1000</Number>
      </Values>
   </Results>
   <Results>
      <Values>
         <Value>4</Value>
         <Number>10</Number>
      </Values>
      <Values>
         <Value>5</Value>
         <Number>10</Number>
      </Values>
      <Values>
         <Value>6</Value>
         <Number>10</Number>
      </Values>
   </Results>
</Table>

My vision is to add following condition to michael-hor257k:

<Number>
<xsl:choose>
    <xsl:when test="$numbers-set[$i] != ''">
    <xsl:value-of select="$numbers-set[$i]"/>
</xsl:when>
<xsl:otherwise>
    <xsl:value-of select="$numbers-set[1]"/>
</xsl:otherwise>
</xsl:choose>
</Number> 

Does that looks sensible or I can avoid hardcode and place such case in template?

4 Answers4

1

Try it this way:

XSLT 1.0

<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"/>

<xsl:template match="/Table">
    <xsl:variable name="Numbers">
        <xsl:call-template name="tokenizeString">
            <xsl:with-param name="list" select="Numbers"/>
            <xsl:with-param name="delimiter" select="','"/>                                                                         
        </xsl:call-template>                        
    </xsl:variable>

    <xsl:variable name="numbers-set" select="exsl:node-set($Numbers)/Number" />

    <table name="Results">
        <Results>
            <xsl:for-each select="Values/Value">
                <xsl:variable name="i" select="position()" />
                <Values>
                    <Value>
                        <xsl:value-of select="."/>
                    </Value>       
                    <Number>
                        <xsl:value-of select="$numbers-set[$i]"/>
                    </Number>       
                </Values>
            </xsl:for-each> 
        </Results>
    </table>
</xsl:template>

<xsl:template name="tokenizeString">
    <xsl:param name="list"/>
    <xsl:param name="delimiter"/>
    <xsl:choose>
        <xsl:when test="contains($list, $delimiter)">                               
            <Number>    
                <xsl:value-of select="substring-before($list,$delimiter)"/>
            </Number>   
            <xsl:call-template name="tokenizeString">
                <xsl:with-param name="list" select="substring-after($list,$delimiter)"/>
                <xsl:with-param name="delimiter" select="$delimiter"/>
            </xsl:call-template>
        </xsl:when>            
        <xsl:otherwise>
            <Number>    
                <xsl:value-of select="$list"/>
            </Number>   
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>

</xsl:stylesheet>

Or a bit shorter:

<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"/>

<xsl:template match="/Table">
    <xsl:variable name="numbers">
        <xsl:call-template name="tokenize">
            <xsl:with-param name="text" select="Numbers"/>
        </xsl:call-template>
    </xsl:variable>

    <xsl:variable name="numbers-set" select="exsl:node-set($numbers)/Number" />

    <xsl:copy>
        <Results>
            <xsl:for-each select="Values/Value">
                <xsl:variable name="i" select="position()" />
                <Values>
                    <xsl:copy-of select="."/>
                    <xsl:copy-of select="$numbers-set[$i]"/>
                </Values>
            </xsl:for-each> 
        </Results>
    </xsl:copy>
</xsl:template>

<xsl:template name="tokenize">
    <xsl:param name="text"/>
    <xsl:param name="delimiter" select="','"/>
        <xsl:variable name="token" select="substring-before(concat($text, $delimiter), $delimiter)" />
        <xsl:if test="$token">
            <Number>
                <xsl:value-of select="$token"/>
            </Number>
        </xsl:if>
        <xsl:if test="contains($text, $delimiter)">
            <!-- recursive call -->
            <xsl:call-template name="tokenize">
                <xsl:with-param name="text" select="substring-after($text, $delimiter)"/>
            </xsl:call-template>
        </xsl:if>
</xsl:template>

</xsl:stylesheet>
michael.hor257k
  • 113,275
  • 6
  • 33
  • 51
  • I got your solution. Quick question: does .net support exlt or we just need to put correct namespace and that should work? I got an error in xml spy: Error in XPath expression Unknown function - Name and number of arguments do not match any function signature in the static context - 'http://exslt.org/common:node-set' – user3305630 Dec 23 '15 at 14:04
  • @user3305630, Microsoft's .NET `XslCompiledTransform` does support `exsl:node-set`, `XslTransform` however not. As for XML Spy, it has its own XSLT implementation which should support XSLT 2.0 where you don't need an extension function of that form. – Martin Honnen Dec 23 '15 at 14:08
  • I'm only performing testing in xml spy, so I can't use XSLT 2.0 functionality. – user3305630 Dec 23 '15 at 14:12
  • @user3305630, if you want to test XSLT 1.0 then you should at least test with your chosen XSLT 1.0 processor, in particular, when you use extension functions. – Martin Honnen Dec 23 '15 at 14:16
  • Yes, so i'm doing that and getting error, described above. Does above code works for you? – user3305630 Dec 23 '15 at 14:32
  • @user3305630 Yes, it does. You can see it working here (use Saxon 6.5.5 or Xalan as the engine to test XSLT 1.0): http://xsltransform.net/ej9EGcw – michael.hor257k Dec 23 '15 at 14:36
  • I can't understand the difference then... Question1: as you suggest to use esxlt, then why can't we use existing str:tokenize() function from there? Question2: is there any way to implement this task using only XSLT 1.0? – user3305630 Dec 23 '15 at 14:41
  • **1.** Not all processors support all EXSLT functions. The `str:tokenize()` function is only supported by libxslt and Xalan (and perhaps by some of the more esoteric processors). **2** Yes, there is , but it is considerably more work. I don't know of a XSLT 1.0 processor that does not support a node-set() function. If you're using Microsoft, try using the function in their own namespace - see: https://msdn.microsoft.com/en-us/library/hz88kef0%28v=vs.110%29.aspx – michael.hor257k Dec 23 '15 at 14:47
  • So 2 things make code works for me: 1. using msxsl, instead of exsl; 2. Change settings of XML SPY to Microsoft XML Parser. Now need to test that via .NET – user3305630 Dec 23 '15 at 15:13
  • @user3305630 What was the engine before you switched to Microsoft? – michael.hor257k Dec 23 '15 at 15:15
  • It called: Built-in XSLT engine (Important: the buil-in XSLT engine is always used for XSLT debugging). – user3305630 Dec 23 '15 at 15:40
  • @user3305630 That doesn't mean anything. See here how to identify the XSLT processor: http://stackoverflow.com/questions/25244370/how-can-i-check-which-xslt-processor-is-being-used-in-solr/25245033#25245033 – michael.hor257k Dec 23 '15 at 16:16
  • Altova GmbH version 1 – user3305630 Dec 23 '15 at 16:22
  • @user3305630 Well, that's Altova's proprietary processor. I suggest you test this with the processor that will be used in actual production. – michael.hor257k Dec 23 '15 at 16:27
  • Yes, now that is clear to me. Thank you. I will need to read more node-set, but in terms of performance, does above solutions works fine and let's say like tokenize in XSLT 2.0? – user3305630 Dec 23 '15 at 16:39
  • @user3305630 I don't know how to measure it or what to compare it with. All I can say is that using `node-set()` is pretty much standard in XSLT 1.0. – michael.hor257k Dec 23 '15 at 17:05
1

FWIW, here's a purely XSLT 1.0 solution that does not require a node-set() extension function:

XSLT 1.0

<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="/Table">
    <xsl:copy>
        <Results>
            <xsl:call-template name="process">
                <xsl:with-param name="values" select="Values/Value"/>
                <xsl:with-param name="numbers" select="Numbers"/>
            </xsl:call-template>
        </Results>
    </xsl:copy>
</xsl:template>

<xsl:template name="process">
    <xsl:param name="values" select="dummy-node"/>
    <xsl:param name="numbers"/>
    <xsl:param name="i" select="1"/>
    <!-- output -->
    <Values>
        <xsl:copy-of select="$values[1]"/>
        <Number>
            <xsl:value-of select="substring-before(concat($numbers, ','), ',')"/>
        </Number>
    </Values>
    <!-- recursive call --> 
    <xsl:if test="count($values) > 1">
        <xsl:call-template name="process">
            <xsl:with-param name="values" select="$values[position() > 1]"/>
            <xsl:with-param name="numbers" select="substring-after($numbers, ',')"/>
            <xsl:with-param name="i" select="$i + 1"/>
        </xsl:call-template>
    </xsl:if>
</xsl:template>

</xsl:stylesheet>
michael.hor257k
  • 113,275
  • 6
  • 33
  • 51
0

In general, the main problem is that a template in XSLT 1.0 returns a result tree fragment and to apply XPath on it you first need to convert it into a node set, this is usually done calling exsl:node-set($result-tree-fragment) or a similar extension function offered by your XSLT 1.0 processor.

So assuming you have e.g.

<xsl:variable name="tokens-rtf">
  <xsl:call-template name="tokenize">...</xsl:call-template>
</xsl:variable>

where the template returns e.g.

  <Values>
     <Value>1</Value>
     <Number>10</Number>
  </Values>
  <Values>
     <Value>2</Value>
     <Number>100</Number>
  </Values>
  <Values>
     <Value>3</Value>
     <Number>1000</Number>
  </Values>

you can do

<xsl:variable name="tokens" select="exsl:node-set($tokens-rtf)/Values" xmlns:exsl="http://exslt.org/common"/>

and now you have a node set you can apply any XPath to, like a positional predicate as you want to do e.g. <xsl:variable name"Foo" select="$tokens[$n]"/>.

Martin Honnen
  • 160,499
  • 6
  • 90
  • 110
0

This solution is the only one so far that doesn't use extension functions and doesn't assume that the numbers will be retrieved in any predefined order:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output omit-xml-declaration="yes" indent="yes"/>
  <xsl:strip-space elements="*"/>

  <xsl:template match="Value">
     <Values>
       <xsl:copy-of select="."/>
       <Number>
         <xsl:apply-templates select="/*/Numbers" mode="call">
           <xsl:with-param name="pPos" select="."/>
         </xsl:apply-templates>
       </Number>
     </Values>
  </xsl:template>

  <xsl:template match="Numbers" name="getNth" mode="call">
    <xsl:param name="pNums" select="concat(., ',')"/>
    <xsl:param name="pPos"/>
    <xsl:param name="pResult"/>

    <xsl:if test="not($pPos &gt; 0)">
      <xsl:value-of select="$pResult"/>
    </xsl:if>

    <xsl:if test="$pNums and $pPos > 0">
            <xsl:call-template name="getNth">
             <xsl:with-param name="pNums" select="substring-after($pNums, ',')"/>
             <xsl:with-param name="pResult" select="substring-before($pNums, ',')"/>
             <xsl:with-param name="pPos" select="$pPos -1"/>
            </xsl:call-template>
    </xsl:if>
  </xsl:template>

  <xsl:template match="/*">
    <xsl:copy>
        <Results><xsl:apply-templates/></Results>
    </xsl:copy>
  </xsl:template>
  <xsl:template match="text()"/>
</xsl:stylesheet>

When this transformation is applied on the provided XML document:

<Table>
   <Numbers>10,100,1000</Numbers>
   <Values>
      <Value>1</Value>
      <Value>2</Value>
      <Value>3</Value>
   </Values>
</Table>

The wanted, correct result is produced:

<Table>
    <Results>
        <Values>
            <Value>1</Value>
            <Number>10</Number>
        </Values>
        <Values>
            <Value>2</Value>
            <Number>100</Number>
        </Values>
        <Values>
            <Value>3</Value>
            <Number>1000</Number>
        </Values>
    </Results>
</Table>

When the same transformation is applied on this XML document (notice the changed order of values):

<Table>
   <Numbers>10,100,1000</Numbers>
   <Values>
      <Value>3</Value>
      <Value>1</Value>
      <Value>2</Value>
   </Values>
</Table>

The result for this XML document is produced again correctly:

<Table>
    <Results>
        <Values>
            <Value>3</Value>
            <Number>1000</Number>
        </Values>
        <Values>
            <Value>1</Value>
            <Number>10</Number>
        </Values>
        <Values>
            <Value>2</Value>
            <Number>100</Number>
        </Values>
    </Results>
</Table>
Dimitre Novatchev
  • 240,661
  • 26
  • 293
  • 431