I have a delimited string (delimited by spaces in my example below) that I need to tokenize, sort, and then join back together and I need to do all this using XSLT 1.0. How would I do that? I know I need to use xsl:sort
somehow, but everything I’ve tried so far has given me some sort of error.
For example, if I run the code at the bottom of this posting, I get this:
strawberry blueberry orange raspberry
lime lemon
What would I do if I wanted to get this instead?:
blueberry lemon lime orange raspberry
strawberry
Note that I’m using XSLT 1.0.
Here is the code, which is based on code by Jeni Tennison.
<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="tokenize1.xsl"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:call-template name="tokenize">
<xsl:with-param name="string" select="'strawberry blueberry orange raspberry lime lemon'" />
</xsl:call-template>
</xsl:template>
<xsl:template name="tokenize">
<xsl:param name="string" />
<xsl:param name="delimiter" select="' '" />
<xsl:choose>
<xsl:when test="$delimiter and contains($string, $delimiter)">
<token>
<xsl:value-of select="substring-before($string, $delimiter)" />
</token>
<xsl:text> </xsl:text>
<xsl:call-template name="tokenize">
<xsl:with-param name="string"
select="substring-after($string, $delimiter)" />
<xsl:with-param name="delimiter" select="$delimiter" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<token><xsl:value-of select="$string" /></token>
<xsl:text> </xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Here's an inefficient pure version 1 solution:
<!-- Sort the tokens -->
<xsl:template name="sortTokens">
<xsl:param name="tokens" select="''"/> <!-- The list of tokens -->
<xsl:param name="separator" select="' '"/> <!-- What character separates the tokens? -->
<xsl:param name="pivot" select="''"/> <!-- A pivot word used to divide the list -->
<xsl:param name="lessThan" select="''"/> <!-- Accumulator for tokens less than the pivot (with leading separator) -->
<xsl:param name="moreThan" select="''"/> <!-- Accumulator for tokens more than the pivot (with leading separator) -->
<xsl:param name="leadWith" select="''"/> <!-- If set, output this before sorting -->
<xsl:param name="trailWith" select="''"/> <!-- If set, output this after sorting -->
<!-- The first token -->
<xsl:variable name="firstToken" select="substring-before(concat($tokens,$separator),$separator)"/>
<!-- Is the first token more or less than the pivot? -->
<xsl:variable name="pivotVsFirstToken">
<xsl:call-template name="compareStrings">
<xsl:with-param name="a" select="$pivot"/>
<xsl:with-param name="b" select="$firstToken"/>
</xsl:call-template>
</xsl:variable>
<xsl:choose>
<!-- No input, no output -->
<xsl:when test="$tokens = '' and $pivot = ''"></xsl:when>
<!-- At the outset, the first token becomes the pivot -->
<xsl:when test="$pivot = ''">
<xsl:value-of select="$leadWith"/>
<xsl:call-template name="sortTokens">
<xsl:with-param name="separator" select="$separator"/>
<xsl:with-param name="tokens" select="substring-after($tokens,$separator)"/>
<xsl:with-param name="pivot" select="$firstToken"/>
</xsl:call-template>
<xsl:value-of select="$trailWith"/>
</xsl:when>
<!-- When all tokens are in a bucket, output the pivot between sorted buckets -->
<xsl:when test="$tokens = ''">
<xsl:call-template name="sortTokens">
<xsl:with-param name="separator" select="$separator"/>
<xsl:with-param name="tokens" select="substring-after($lessThan,$separator)"/>
<xsl:with-param name="trailWith" select="$separator"/>
</xsl:call-template>
<xsl:value-of select="$pivot"/>
<xsl:call-template name="sortTokens">
<xsl:with-param name="separator" select="$separator"/>
<xsl:with-param name="tokens" select="substring-after($moreThan,$separator)"/>
<xsl:with-param name="leadWith" select="$separator"/>
</xsl:call-template>
</xsl:when>
<!-- If the first token is less than the pivot, put it in the lessThan bucket -->
<xsl:when test="number($pivotVsFirstToken) = 1">
<xsl:call-template name="sortTokens">
<xsl:with-param name="separator" select="$separator"/>
<xsl:with-param name="tokens" select="substring-after($tokens,$separator)"/>
<xsl:with-param name="pivot" select="$pivot"/>
<xsl:with-param name="lessThan" select="concat($separator,$firstToken,$lessThan)"/>
<xsl:with-param name="moreThan" select="$moreThan"/>
</xsl:call-template>
</xsl:when>
<!-- If the first token is more than the pivot, put it in the moreThan bucket -->
<xsl:otherwise>
<xsl:call-template name="sortTokens">
<xsl:with-param name="separator" select="$separator"/>
<xsl:with-param name="tokens" select="substring-after($tokens,$separator)"/>
<xsl:with-param name="pivot" select="$pivot"/>
<xsl:with-param name="lessThan" select="$lessThan"/>
<xsl:with-param name="moreThan" select="concat($separator,$firstToken,$moreThan)"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- Quote an apostrophe -->
<xsl:variable name="apos" select=""'""/>
<!-- The comparison order of the characters -->
<xsl:variable name="characterOrder" select="concat(' !"#$%&',$apos,'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~')"/>
<!-- Return -1 if string a is less, 1 if string b is less, or 0 if they are equal -->
<xsl:template name="compareStrings">
<xsl:param name="a" select="''"/>
<xsl:param name="b" select="''"/>
<xsl:choose>
<xsl:when test="$a = '' and $b = ''">0</xsl:when>
<xsl:when test="$a = ''">-1</xsl:when>
<xsl:when test="$b = ''">1</xsl:when>
<xsl:when test="substring($a,1,1) = substring($b,1,1)">
<xsl:call-template name="compareStrings">
<xsl:with-param name="a" select="substring($a,2)"/>
<xsl:with-param name="b" select="substring($b,2)"/>
</xsl:call-template>
</xsl:when>
<xsl:when test="contains(substring-after($characterOrder,substring($a,1,1)),substring($b,1,1))">-1</xsl:when>
<xsl:otherwise>1</xsl:otherwise>
</xsl:choose>
</xsl:template>
If your processor supports EXSLT, you'd better use str:tokenize
For sorting, why not use xsl:sort?
<xsl:template match="/">
<xsl:variable name="tokens">
<xsl:call-template name="tokenize">
<xsl:with-param name="string" select="'strawberry blueberry orange raspberry lime lemon'" />
</xsl:call-template>
</xsl:variable>
<xsl:for-each select="$tokens">
<xsl:sort select="text()" />
<xsl:value-of select="." />
<xsl:if test="not(last())">
<xsl:text> </xsl:text>
</xsl:if>
</xsl:for-each>
</xsl:template>
Note that you might need exsl:node-set do to the iteration.