[XSL-LIST Mailing List Archive Home] [By Thread] [By Date] [Recent Entries] [Reply To This Message]

Functional trim() (Was: Re: text::trim())

Subject: Functional trim() (Was: Re: text::trim())
From: Dimitre Novatchev <dnovatchev@xxxxxxxxx>
Date: Thu, 27 Dec 2001 12:58:46 -0800 (PST)
xsl trim
>   Does anyone know if there is a function such as trim() that can be
> applied on a text node, i.e., remove all leading and trailing white-space
> ('\t', '\n', '\r', ' ', '\f'), given the text node can contain non
> white-space characters.

In Haskell one solution is the following:

> trim      :: [Char] -> [Char]
> trim      = applyTwice (reverse . trim1) 
>     where  trim1 = dropWhile (`elem` delim) 
>            delim    = [' ', '\t', '\n', '\r']
>            applyTwice f = f . f 

This means that we have two functions:

  - a simple function "trim1" that "eats out" the starting group of predefined
whitespace characters from a string. We could call this function "trimLeft". 
It uses the function "dropWhile", which drops from a list all starting elements
satisfying a predicate argument.

  - the function "reverse", the XSLT implementation of which can be found at:
http://aspn.activestate.com/ASPN/Mail/Message/XSL-List/926231

What is described above is quite simple -- we trim-left the string, then reverse it,
then trim-left the reversed string (at this point all the trimming is done), then
finally reverse the reversed and trimmed string.

For brevity the function composition operator "." is used:

(f . g) x = f(g(x))

We can implement exactly the same algorithm in XSLT:

trim.xsl:
--------
<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
xmlns:myTrimDropController="f:myTrimDropController" 
xmlns:myTrim1="f:myTrim1" 
xmlns:myReverse="f:myReverse" 
exclude-result-prefixes="xsl myTrimDropController myTrim1 myReverse"
>
  <xsl:import href="str-dropWhile.xsl"/>
  <xsl:import href="compose-flist.xsl"/>
  <xsl:import href="reverse.xsl"/>
  
  <myTrimDropController:myTrimDropController/>
  
  <xsl:template name="trim">
    <xsl:param name="pStr"/>
    
    <xsl:variable name="vrtfParam">
      <myReverse:myReverse/>
      <myTrim1:myTrim1/>
      <myReverse:myReverse/>
      <myTrim1:myTrim1/>
    </xsl:variable>

    <xsl:call-template name="compose-flist">
        <xsl:with-param name="pFunList" select="msxsl:node-set($vrtfParam)/*"/>
        <xsl:with-param name="pArg1" select="$pStr"/>
    </xsl:call-template>
    
  </xsl:template>
  
  <xsl:template name="trim1" match="myTrim1:*">
    <xsl:param name="pArg1"/>
    
  <xsl:variable name="vTab" select="'&#9;'"/>
  <xsl:variable name="vNL" select="'&#10;'"/>
  <xsl:variable name="vCR" select="'&#13;'"/>
  <xsl:variable name="vWhitespace" 
                select="concat(' ', $vTab, $vNL, $vCR)"/>

    <xsl:variable name="vFunController" 
                  select="document('')/*/myTrimDropController:*[1]"/>
           
    
    <xsl:call-template name="str-dropWhile">
      <xsl:with-param name="pStr" select="$pArg1"/>
      <xsl:with-param name="pController" select="$vFunController"/>
      <xsl:with-param name="pContollerParam" select="$vWhitespace"/>
    </xsl:call-template>
  </xsl:template>
  
  <xsl:template match="myTrimDropController:*">
    <xsl:param name="pChar"/>
    <xsl:param name="pParams"/>
    
    <xsl:if test="contains($pParams, $pChar)">1</xsl:if>
  </xsl:template>
  
  <xsl:template name="myReverse" match="myReverse:*">
    <xsl:param name="pArg1"/>
    
    <xsl:call-template name="strReverse">
      <xsl:with-param name="pStr" select="$pArg1"/>
    </xsl:call-template>
</xsl:template>
</xsl:stylesheet>

The "trim" template is short and simple:

  <xsl:template name="trim">
    <xsl:param name="pStr"/>
    
    <xsl:variable name="vrtfParam">
      <myReverse:myReverse/>
      <myTrim1:myTrim1/>
      <myReverse:myReverse/>
      <myTrim1:myTrim1/>
    </xsl:variable>

    <xsl:call-template name="compose-flist">
        <xsl:with-param name="pFunList" select="msxsl:node-set($vrtfParam)/*"/>
        <xsl:with-param name="pArg1" select="$pStr"/>
    </xsl:call-template>
    
  </xsl:template>

It just calls the "compose-flist" template -- a generic template, which is passed a
list of template references "pFunList" and an initial argument "pArg1". It performs
the functional composition of the functions referred to by the template references
in reverse order. In this specific case it will instantiate the "trim1" template
passing to it the string contained in "pArg1", then will pass the result to the
"reverse" template, then will pass the result to the "trim1" template, then finally
will pass the result to the "reverse" template.

If we have the following source xml document:

<someText>
   
   This is    some text   
   
</someText>

and apply the following transformation to it:

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <xsl:import href="trim.xsl"/>
  
  <xsl:output method="text"/>
  <xsl:template match="/">
    '<xsl:call-template name="trim">
        <xsl:with-param name="pStr" select="string(/*)"/>
    </xsl:call-template>'
  </xsl:template>
</xsl:stylesheet>

the result is:


    'This is    some text'
  
This is obviously the correct result -- the string is trimmed both from left and
from right, while white space within the string is left intact as it should be.


The "trim1" template is also very simple:

  <xsl:template name="trim1" match="myTrim1:*">
    <xsl:param name="pArg1"/>
    
  <xsl:variable name="vTab" select="'&#9;'"/>
  <xsl:variable name="vNL" select="'&#10;'"/>
  <xsl:variable name="vCR" select="'&#13;'"/>
  <xsl:variable name="vWhitespace" 
                select="concat(' ', $vTab, $vNL, $vCR)"/>

    <xsl:variable name="vFunController" 
                  select="document('')/*/myTrimDropController:*[1]"/>
           
    
    <xsl:call-template name="str-dropWhile">
      <xsl:with-param name="pStr" select="$pArg1"/>
      <xsl:with-param name="pController" select="$vFunController"/>
      <xsl:with-param name="pContollerParam" select="$vWhitespace"/>
    </xsl:call-template>
  </xsl:template>

It has a single parameter -- the string to be left-trimmed. It just calls the
standard and generic "str-dropWhile" template to do the processing, passing to it in
"pArg1" the string, from which some starting characters must be dropped, a function
"pController" (template reference to a template) that will signal if any individual
character passed to it is to be dropped, and parameter  "pContollerParam" to be
passed to this controller.

The code of the "drop controller" is straightforward:

  <xsl:template match="myTrimDropController:*">
    <xsl:param name="pChar"/>
    <xsl:param name="pParams"/>
    
    <xsl:if test="contains($pParams, $pChar)">1</xsl:if>
  </xsl:template>

It just tests if the current "pChar" character belongs to the group of characters
"pParams" and returns 1 if this is so, otherwise it doesn't return anything. In the
last case this will signal to "str-dropWhile" that all the necessary starting
characters have been dropped and the remainder of the string will be returned.


Finally, let me present the code of the two templates used by "trim" and "trim1" --
"compose-flist" and "str-dropWhile".

As already described, "compose-flist" performs the functional composition of a list
of functions:

(f1 . f2 .   ... fn)(x) = f1(f2(...(fn(x))...))

This can be defined in Haskell as follows:

 multCompose :: [a -> a] -> a -> a
 multCompose xs y  = foldr ($) y xs

where "$" is the function application operator, xs is a list of functions the
composition of which must be produced, and y is the the argument on which the
functional composition will be applied.

We could use an XSLT implementation of foldr in implementng compose-flist, however
this time I prefer to produce directly the code to make it more understandable:

compose-flist.xsl:
-----------------
<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt">
  
  <xsl:template name="compose-flist">
    <xsl:param name="pFunList" select="/.."/>
    <xsl:param name="pArg1"/>
    
    <xsl:choose>
      <xsl:when test="not($pFunList)">
        <xsl:copy-of select="$pArg1"/>
      </xsl:when>
      <xsl:otherwise>
	<xsl:variable name="vrtfFunRest">
	  <xsl:call-template name="compose-flist">
            <xsl:with-param name="pFunList" select="$pFunList[position() > 1]"/>
	    <xsl:with-param name="pArg1" select="$pArg1"/>
	  </xsl:call-template>
	</xsl:variable>
	    
	<xsl:apply-templates select="$pFunList[1]">
	  <xsl:with-param name="pArg1" select="msxsl:node-set($vrtfFunRest)/node()"/>
	</xsl:apply-templates>
      </xsl:otherwise>
    </xsl:choose>

  </xsl:template>
</xsl:stylesheet>

It is worth to note that functional composition is one of the most powerful tools
with which we can create new functions dynamically on the fly.

And finally here's the code of "str-dropWhile":

str-dropWhile.xsl:
-----------------
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" 
exclude-result-prefixes="xsl msxsl"
>
  <xsl:template name="str-dropWhile">
    <xsl:param name="pStr" select="''"/>
    <xsl:param name="pController" select="/.."/>
    <xsl:param name="pContollerParam" select="/.."/>

    <xsl:if test="not($pController)">
      <xsl:message terminate="yes">
         [str-dropWhile]Error: pController not specified.
      </xsl:message>
    </xsl:if>   
    
      <xsl:if test="$pStr">
	<xsl:variable name="vDrop">
	  <xsl:apply-templates select="$pController">
	    <xsl:with-param name="pChar" select="substring($pStr, 1, 1)"/>
	    <xsl:with-param name="pParams" select="$pContollerParam"/>
	  </xsl:apply-templates>
	</xsl:variable>
	    
	<xsl:choose>
	  <xsl:when test="string($vDrop)">
	    <xsl:call-template name="str-dropWhile">
	      <xsl:with-param name="pStr" select="substring($pStr, 2)" />
	      <xsl:with-param name="pController" select="$pController"/>
	      <xsl:with-param name="pContollerParam" select="$pContollerParam"/>
	    </xsl:call-template>
	  </xsl:when>
	  <xsl:otherwise>
	    <xsl:copy-of select="$pStr"/>
	  </xsl:otherwise>
	</xsl:choose>
      
      </xsl:if>
  </xsl:template>

</xsl:stylesheet>

On every character this function calls the provided controller "pController" passing
to it the provided parameters "pContollerParam" and only continues processing if the
controller has output something.

In conclusion, this solution is yet another example how using the functional
programming style and the FP standard library functions in XSLT can help to produce
solutions to a variety of problems by simply combining and reusing existing
functions.

Cheers,
Dimitre Novatchev.


__________________________________________________
Do You Yahoo!?
Send your FREE holiday greetings online!
http://greetings.yahoo.com

 XSL-List info and archive:  http://www.mulberrytech.com/xsl/xsl-list


Current Thread

PURCHASE STYLUS STUDIO ONLINE TODAY!

Purchasing Stylus Studio from our online shop is Easy, Secure and Value Priced!

Buy Stylus Studio Now

Download The World's Best XML IDE!

Accelerate XML development with our award-winning XML IDE - Download a free trial today!

Don't miss another message! Subscribe to this list today.
Email
First Name
Last Name
Company
Subscribe in XML format
RSS 2.0
Atom 0.3
Site Map | Privacy Policy | Terms of Use | Trademarks
Free Stylus Studio XML Training:
W3C Member
Stylus Studio® and DataDirect XQuery ™are products from DataDirect Technologies, is a registered trademark of Progress Software Corporation, in the U.S. and other countries. © 2004-2013 All Rights Reserved.