Camel case conversion for special case also in XSLT - xslt

Below XSLT converts First character of each word to upper case.But Fails for special case (example:for input-> the 'lion ,king output is-> The 'Lion ,King). Need solution for the special case also.
<xsl:template name="Split">
<xsl:param name="pText"/>
<xsl:if test="string-length($pText)">
<xsl:variable name="vConvertedWord" select="concat(translate(substring(substring-before(concat($pText,$vSeparator),$vSeparator),1,1), $smallcase,$uppercase),substring(substring-before(concat($pText,$vSeparator),$vSeparator),2))"/>
<xsl:value-of select="$vConvertedWord"/>
<xsl:value-of select="$vSeparator"/>
<xsl:call-template name="Split">
<xsl:with-param name="pText" select="substring-after($pText,$vSeparator)"/>
</xsl:call-template>
</xsl:template>

I think what you are asking is that you may have multiple separators, not just a space, but a comma too? And also maybe you want some 'small' words not to be capitalised?
Just for my own personal challenge, the solution I came up with is to iterate over the text one character at a time, building up a 'word' until you reach a 'separator'. You can then capitalise the word you have built up, unless it happens to be a 'small' word. You then continue iterating with a new empty word to be built up.
Try this XSLT
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:variable name="smallcase">abcdefghijklmnopqrstuvwxyz</xsl:variable>
<xsl:variable name="uppercase">ABCDEFGHIJKLMNOPQRSTUVWXYZ</xsl:variable>
<xsl:variable name="vSeparator"> ,.</xsl:variable>
<xsl:variable name="vProp"> a an the and </xsl:variable>
<xsl:template match="text()">
<xsl:call-template name="Split">
<xsl:with-param name="pText" select="." />
</xsl:call-template>
</xsl:template>
<xsl:template name="Split">
<xsl:param name="pText"/>
<xsl:param name="letterPos" select="1" />
<xsl:param name="currentWord" select="''" />
<xsl:variable name="nextChar" select="translate(substring($pText, $letterPos, 1), $uppercase ,$smallcase)" />
<xsl:variable name="separatorFound" select="($nextChar = '' or contains($vSeparator, $nextChar))" />
<xsl:if test="$separatorFound">
<xsl:choose>
<xsl:when test="contains($vProp, concat(' ', $currentWord, ' ')) and $letterPos > string-length($currentWord) + 1">
<xsl:value-of select="$currentWord" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="translate(substring($currentWord, 1, 1), $smallcase ,$uppercase)" />
<xsl:value-of select="substring($currentWord, 2, string-length($currentWord) - 1)" />
</xsl:otherwise>
</xsl:choose>
<xsl:value-of select="$nextChar" />
</xsl:if>
<xsl:variable name="newWord">
<xsl:if test="not($separatorFound)"><xsl:value-of select="concat($currentWord, $nextChar)"/></xsl:if>
</xsl:variable>
<xsl:if test="$letterPos <= string-length($pText)">
<xsl:call-template name="Split">
<xsl:with-param name="pText" select="$pText" />
<xsl:with-param name="letterPos" select="$letterPos + 1" />
<xsl:with-param name="currentWord" select="$newWord" />
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
When applied to this XML
<text>The Lion king, the tiger queen and the hippo prince</text>
The following is output
The Lion King, the Tiger Queen and the Hippo Prince
EDIT - Alternate solution
If you want to capitalise letters that occur after , or ., then try this XSLT:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:variable name="smallcase">abcdefghijklmnopqrstuvwxyz</xsl:variable>
<xsl:variable name="uppercase">ABCDEFGHIJKLMNOPQRSTUVWXYZ</xsl:variable>
<xsl:variable name="vSeparator">,.</xsl:variable>
<xsl:variable name="vProp"> a an the and </xsl:variable>
<xsl:template match="text()">
<xsl:call-template name="Split">
<xsl:with-param name="pText" select="." />
</xsl:call-template>
</xsl:template>
<xsl:template name="Split">
<xsl:param name="pText"/>
<xsl:param name="letterPos" select="1" />
<xsl:param name="currentWord" select="''" />
<xsl:param name="startOfBlock" select="1" />
<xsl:variable name="nextChar" select="translate(substring($pText, $letterPos, 1), $uppercase ,$smallcase)" />
<xsl:variable name="separatorFound" select="($nextChar = '' or $nextChar = ' ' or contains($vSeparator, $nextChar))" />
<xsl:if test="$separatorFound">
<xsl:choose>
<xsl:when test="contains($vProp, concat(' ', $currentWord, ' ')) and $startOfBlock = 0">
<xsl:value-of select="$currentWord" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="translate(substring($currentWord, 1, 1), $smallcase ,$uppercase)" />
<xsl:value-of select="substring($currentWord, 2, string-length($currentWord) - 1)" />
</xsl:otherwise>
</xsl:choose>
<xsl:value-of select="$nextChar" />
</xsl:if>
<xsl:variable name="newWord">
<xsl:if test="not($separatorFound)"><xsl:value-of select="concat($currentWord, $nextChar)"/></xsl:if>
</xsl:variable>
<xsl:variable name="newStartOfBlock">
<xsl:choose>
<xsl:when test="contains($vSeparator, $nextChar)"><xsl:value-of select="1" /></xsl:when>
<xsl:when test="$nextChar = ' ' and $currentWord != ''"><xsl:value-of select="0" /></xsl:when>
<xsl:otherwise><xsl:value-of select="$startOfBlock" /></xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:if test="$letterPos <= string-length($pText)">
<xsl:call-template name="Split">
<xsl:with-param name="pText" select="$pText" />
<xsl:with-param name="letterPos" select="$letterPos + 1" />
<xsl:with-param name="currentWord" select="$newWord" />
<xsl:with-param name="startOfBlock" select="$newStartOfBlock" />
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
When applied to this XML
<text>The Lion king, the tiger queen and the hippo prince.... And cats, bats and rats.</text>
The following is output:
The Lion King, The Tiger Queen and the Hippo Prince.... And Cats, Bats and Rats.

Related

xslt case change transformation is removing the <br/ > tag from xml

I am doing a upper case to first character content in the xml data using the delimiters (space and hyphen) and am able to get the output correctly however this template is removing the break line tag in the table td area of xml. The output should be an xml.
eg:<text>
<table>
<tbody>
<tr>
<td>test<br />testing<br />tested</td>
</tr>
I see the code transforming as continous line without break tag in the output as below:I need to see the same br tag in xml output as in input xml however by capitalizing first letter of the word.
<text>
<table>
<tbody>
<tr>
<td>Testtestingtested</td>
</tr>
I am expecting to preserve and display the same break line tag to be in output xml even after the xslt tranformation so that the output will look correct instead of continous line
I am using the below xslt transformation:
<?xml version="1.0" encoding="utf-8"?>
<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"
xmlns:n1="urn:hl7-org:v3" xmlns:n2="urn:hl7-org:sdtc">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match='n1:ClinicalDocument/n1:component/n1:structuredBody/n1:component/n1:section/n1:text/n1:table/n1:tbody/n1:tr'>
<xsl:copy>
<xsl:apply-templates select='n1:td'/>
</xsl:copy>
</xsl:template>
<xsl:template match="n1:td">
<xsl:copy>
<xsl:if test="./#ID">
<xsl:attribute name="ID" xml:space="default">
<xsl:value-of select="./#ID"/>
</xsl:attribute>
</xsl:if>
<xsl:call-template name="capitalize">
<xsl:with-param name="text" select="string(.)"/>
</xsl:call-template>
</xsl:copy>
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template name="capitalize">
<xsl:param name="text" />
<xsl:param name="delimiter" select = "' '"/>
​
<xsl:variable name="upper-case" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<xsl:variable name="lower-case" select="'abcdefghijklmnopqrstuvwxyz'"/>
<xsl:variable name="word" select="substring-before(concat($text, $delimiter), $delimiter)" />
<xsl:choose>
<xsl:when test="$delimiter=' '">
<!-- tokenize word by 2nd delimiter -->
<xsl:call-template name="capitalize">
<xsl:with-param name="text" select="$word"/>
<xsl:with-param name="delimiter" select="'-'"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<!-- capitalize word -->
<xsl:value-of select="translate(substring($word, 1, 1), $lower-case, $upper-case) " />
<xsl:value-of select="translate(substring($word, 2), $upper-case, $lower-case)" />
</xsl:otherwise>
</xsl:choose>
<xsl:if test="contains($text, $delimiter)">
<xsl:value-of select="$delimiter"/>
<!-- recursive call -->
<xsl:call-template name="capitalize">
<xsl:with-param name="text" select="substring-after($text, $delimiter)" />
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Can anyone check and answer?
UPDATE: Both XSLT 1.0 and 2.0 solutions
All solutions preserve your line breaks and also work if you have inline markup of the text content.
First I make the lc and uc parameters global for more minified templates:
<xsl:param name="lc" select="'abcdefghijklmnopqrstuvwxyzàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿžšœ'"/>
<xsl:param name="uc" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞŸŽŠŒ'"/>
XSLT 1.0 or 2.0: You need to rewrite your first template to this regardless of the following XSLT solutions:
<xsl:template match="n1:td">
<xsl:copy>
<xsl:if test="./#ID">
<xsl:attribute name="ID" xml:space="default">
<xsl:value-of select="./#ID"/>
</xsl:attribute>
</xsl:if>
<xsl:apply-templates select="node()" />
</xsl:copy>
<xsl:text>
</xsl:text>
</xsl:template>
XSLT 1.0
With XSLT 1.0 you need to combine a match template with a name template.
Match template: n1:td//text()
With this match template it becomes precisely what you request, make initial letter uppercase, and make letters following space and - uppercase:
<xsl:template match="n1:td//text()" >
<xsl:param name="text" select="." />
<xsl:param name="currentTextBlock" select="ancestor::n1:td[1]" />
<xsl:param name="isFirstTextNode">
<xsl:choose>
<xsl:when test="preceding::text()[ancestor::n1:td[generate-id(.) = generate-id($currentTextBlock)]]">false</xsl:when>
<xsl:otherwise>true</xsl:otherwise>
</xsl:choose>
</xsl:param>
<xsl:choose>
<xsl:when test="$isFirstTextNode = 'true'">
<xsl:value-of select="translate(substring($text, 1, 1), $lc, $uc)" />
<xsl:call-template name="capitalize">
<xsl:with-param name="text" select="$text" />
<xsl:with-param name="remainingText" select="substring($text,2)" />
<xsl:with-param name="index" select="2" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="capitalize">
<xsl:with-param name="text" select="$text" />
<xsl:with-param name="remainingText" select="$text" />
<xsl:with-param name="index" select="1" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
If you also want initial letter for each text node to be uppercase without adding space or hyphen, you can use this:
<xsl:template match="n1:td//text()" >
<xsl:param name="text" select="." />
<xsl:value-of select="translate(substring($text, 1, 1), $lc, $uc)" />
<xsl:call-template name="capitalize">
<xsl:with-param name="text" select="$text" />
<xsl:with-param name="remainingText" select="substring($text,2)" />
<xsl:with-param name="index" select="2" />
</xsl:call-template>
</xsl:template>
Name template: capitalize
This name template is can be used "as is" with both of the above match templates.
<xsl:template name="capitalize">
<xsl:param name="text" select="''" />
<xsl:param name="remainingText" select="''" />
<xsl:param name="index" select="1" />
<xsl:if test="$remainingText != ''">
<xsl:variable name="currentChar" select="substring($remainingText, 1, 1)" />
<xsl:choose>
<xsl:when test="$index = 1">
<xsl:value-of select="translate($currentChar, $uc, $lc)" />
</xsl:when>
<xsl:otherwise>
<xsl:variable name="previousChar" select="substring($text, $index - 1, 1)" />
<xsl:choose>
<xsl:when test="$previousChar = ' ' or $previousChar = '-'">
<xsl:value-of select="translate($currentChar, $lc, $uc)" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="translate($currentChar, $uc, $lc)" />
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
<xsl:call-template name="capitalize">
<xsl:with-param name="text" select="$text" />
<xsl:with-param name="remainingText" select="substring($remainingText, 2)" />
<xsl:with-param name="index" select="$index + 1" />
</xsl:call-template>
</xsl:if>
</xsl:template>
XSLT 2.0 (same as before):
This solution does precisely what you request, make initial letter uppercase, and make letters following space and - uppercase:
<xsl:template match="n1:td//text()" >
<xsl:param name="text" select="." />
<xsl:param name="currentTextBlock" select="ancestor::n1:td[1]" />
<xsl:param name="isFirstTextNode">
<xsl:choose>
<xsl:when test="preceding::text()[ancestor::n1:td[generate-id(.) = generate-id($currentTextBlock)]]">false</xsl:when>
<xsl:otherwise>true</xsl:otherwise>
</xsl:choose>
</xsl:param>
<xsl:for-each select="tokenize(replace(replace($text,'(.)','$1\\n'),'\\n$',''),'\\n')">
<xsl:variable name="pos" select="position()" />
<xsl:variable name="char" select="." />
<xsl:choose>
<xsl:when test="$isFirstTextNode = 'true' and $pos = 1">
<xsl:value-of select="translate($char, $lc, $uc) " />
</xsl:when>
<xsl:when test="substring($text, $pos - 1, 1) = ' ' or substring($text, $pos - 1, 1) = '-'">
<xsl:value-of select="translate($char, $lc, $uc) " />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="translate($char, $uc, $lc)" />
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:template>
If you also want initial letter for each text node to be uppercase without adding space or hyphen, you can use this:
<xsl:template match="n1:td//text()" >
<xsl:param name="text" select="." />
<xsl:param name="currentTextBlock" select="ancestor::n1:td[1]" />
<xsl:for-each select="tokenize(replace(replace($text,'(.)','$1\\n'),'\\n$',''),'\\n')">
<xsl:variable name="pos" select="position()" />
<xsl:variable name="char" select="." />
<xsl:choose>
<xsl:when test="$pos = 1">
<xsl:value-of select="translate($char, $lc, $uc) " />
</xsl:when>
<xsl:when test="substring($text, $pos - 1, 1) = ' ' or substring($text, $pos - 1, 1) = '-'">
<xsl:value-of select="translate($char, $lc, $uc) " />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="translate($char, $uc, $lc)" />
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:template>
The problem is your call on string(.), which takes the string value of the element: this strips out all markup.
You should be doing a recursive descent using xsl:apply-templates at each level to apply template rules to child nodes, all the way down to the leaf text nodes, and then your template rule for text nodes should be doing the case conversion.
Except that your capitalize logic seems to be trying to do something smart (I'm not sure what) which means it might need to look at something larger than a single text node. You might need a different rule for the first text node and for subsequent text nodes: it's hard to be sure without seeing a spec.
Another approach to this kind of problem is to do multiple passes: the first pass replaces the <br/> elements with some textual marker, e.g. "§br§", in the next pass you process the text as a string, and then finally you convert the markers back to element nodes.
With XSLT 3.0 you could do the first pass using fn:serialize() and the final pass using fn:parse-xml-fragment(); the "textual marker" would then be the actual lexical markup "<br/>" as a string of five characters. (But take care not to capitalise it!)

Converting first string letter to capital and rest of the letters in lower case, with two delimiters

I started learning XSLT, kind of got stuck while writing xslt functions for converting from lower-case to upper-case and upper-case to lower-case in xslt
I have tried a lot by writing different xslt functions but I think some where I'm doing mistake in my code
<xsl:template name="ConvertXmlStyleToCamelCase">
<xsl:param name="occupation"/>
<xsl:template match="node()"/>
<xsl:variable name="uppercase" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'" />
<xsl:variable name="lowercase" select="'abcdefghijklmnopqrstuvwxyz'" />
<xsl:param name="delimiter" select='/'/>
<xsl:param name="delimiter2" select= "' '"/>
<xsl:if test="not($occupation = '')" >
<xsl:choose>
<xsl:when test="contains($occupation, $delimiter)">
<xsl:variable name="word" select="substring-before(concat($occupation, $delimiter), $delimiter)"></xsl:variable>
<xsl:if test="$word">
<xsl:value-of select="translate(substring($word, 1, 1), $lowercase, $uppercase)"/>
<xsl:value-of select="translate(substring($word,2), $uppercase, $lowercase)"/>
</xsl:if>
</xsl:when>
<xsl:when test="contains( $occupation, $delimiter)">
<xsl:value-of select="$delimiter"/>
<!-- Recursive call to template to translate the text after delimeter -->
<xsl:call-template name="ConvertXmlStyleToCamelCase">
<xsl:with-param name="occupation" select="substring-after($occupation, $delimiter)"/>
</xsl:call-template>
</xsl:when>
<xsl:when test="contains($occupation, $delimiter2)">
<xsl:variable name="word2" select="substring-before(concat($occupation, $delimiter2), $delimiter2)"></xsl:variable>
<xsl:if test="$word2">
<xsl:value-of select="translate(substring($word2, 1, 1), $lowercase, $uppercase)"></xsl:value-of>
<xsl:value-of select="translate(substring($word2, 2), $uppercase, $lowercase)"/>
</xsl:if>
</xsl:when>
<xsl:when test="contains($occupation, $delimiter2)">
<xsl:value-of select="$delimiter2"/>
<!-- Recursive call to template to translate the text after delimeter2 -->
<xsl:call-template name="ConvertXmlStyleToCamelCase">
<xsl:with-param name="occupation" select="substring-after($occupation, $delimiter2)"/>
</xsl:call-template>
</xsl:when>
</xsl:choose>
</xsl:if>
<xsl:if test="not($occupation = $delimiter and $delimiter2)">
<xsl:value-of select="substring(occupation, 1, 1)"/>
<xsl:value-of select="translate(substring(occupation, 2), $uppercase, $lowercase)"/>
</xsl:if>
</xsl:template>
input will be any one value from the below
1.SELF/EMPLOYED
2.SKILL TRADE
3.GOVERNMENT
Expected output as below
Self/Employed
Skill Trade
Government
But the actual outcome is
SelfSelf employed
Skill/trade
Government
As I mentioned in a comment, your code is not reproducible. From the results you report it is clear that your 2nd delimiter is not applied. AFAICT, it is because you check first for the existence of the 1st delimiter - and if you find it, you do not bother to test if the 2nd delimiter exists before the 1st one.
Consider the following example (adapted from Converting first letter of a string to capital in xslt):
XML
<input>
<item>Self/Employed</item>
<item>Skill Trade</item>
<item>Government</item>
<item>a combi/na/tion of various de/limi/ters</item>
</input>
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:strip-space elements="*"/>
<xsl:template match="/input">
<output>
<xsl:for-each select="item">
<caps>
<xsl:call-template name="capitalize">
<xsl:with-param name="text" select="."/>
</xsl:call-template>
</caps>
</xsl:for-each>
</output>
</xsl:template>
<xsl:template name="capitalize">
<xsl:param name="text"/>
<xsl:param name="delimiter" select="' '"/>
<xsl:variable name="upper-case" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<xsl:variable name="lower-case" select="'abcdefghijklmnopqrstuvwxyz'"/>
<xsl:variable name="word" select="substring-before(concat($text, $delimiter), $delimiter)" />
<xsl:choose>
<xsl:when test="$delimiter=' '">
<!-- tokenize word by 2nd delimiter -->
<xsl:call-template name="capitalize">
<xsl:with-param name="text" select="$word"/>
<xsl:with-param name="delimiter" select="'/'"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<!-- capitalize word -->
<xsl:value-of select="translate(substring($word, 1, 1), $lower-case, $upper-case)"/>
<xsl:value-of select="translate(substring($word, 2), $upper-case, $lower-case)"/>
</xsl:otherwise>
</xsl:choose>
<xsl:if test="contains($text, $delimiter)">
<xsl:value-of select="$delimiter"/>
<!-- recursive call -->
<xsl:call-template name="capitalize">
<xsl:with-param name="text" select="substring-after($text, $delimiter)"/>
<xsl:with-param name="delimiter" select="$delimiter"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Result
<?xml version="1.0" encoding="UTF-8"?>
<output>
<caps>Self/Employed</caps>
<caps>Skill Trade</caps>
<caps>Government</caps>
<caps>A Combi/Na/Tion Of Various De/Limi/Ters</caps>
</output>
Could you have more than two delimiters in the future? If so, try this XSLT, which can be readily extended to have more (single character) delimiters. Just change the delimiters parameters in the ConvertXmlStyleToCamelCase template.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="item">
<item>
<xsl:call-template name="ConvertXmlStyleToCamelCase" />
</item>
</xsl:template>
<xsl:template name="ConvertXmlStyleToCamelCase">
<xsl:param name="text" select="."/>
<xsl:param name="delimiters" select="' /'"/>
<xsl:variable name="upper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<xsl:variable name="lower" select="'abcdefghijklmnopqrstuvwxyz'"/>
<xsl:variable name="nextDelimiter" select="substring(translate($text, translate($text, $delimiters, ''), ''), 1, 1)" />
<xsl:variable name="string" select="substring-before(concat($text, ' '), substring(concat($nextDelimiter, ' '), 1, 1))" />
<xsl:message terminate="no">Next delimiter is <xsl:value-of select="$nextDelimiter" /></xsl:message>
<xsl:value-of select="translate(substring($string, 1, 1), $lower, $upper)"/>
<xsl:value-of select="translate(substring($string, 2), $upper, $lower)"/>
<xsl:if test="$nextDelimiter">
<xsl:value-of select="$nextDelimiter" />
<xsl:call-template name="ConvertXmlStyleToCamelCase">
<xsl:with-param name="text" select="substring-after($text, $nextDelimiter)"/>
<xsl:with-param name="delimiters" select="$delimiters"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
See it in action at http://xsltfiddle.liberty-development.net/gWvjQeR where I have used a third delimiters as an example.
With reference to the double-translate, the purpose of this is to find the next delimiter in the string. To do this (in XSLT 1.0) you need to remove all the characters that are not delimiters. Doing translate($text, $delimiters, '') removes all delimiters, and so returns all characters that are not delimiters. If you then apply this result to the original string, you are left with just the delimiters present. The first character will then be the next delimiter.

Why is the XSLT count function not working as expected for me?

I'm using XSLT to convert XML to certain file format. I'm excluding reversal transactions matching two fields (OrigTxnId and TxnId). The problem is the count I'm doing in the header for the number of transactions still include the Transactions which have been removed.
Herewith input XML exsample:
<XML>
<Record><GroupId>10028</GroupId><Id>1</Id><User>CHRISVI</User><TxnId>264-10028-1-516739-2</TxnId><Date>30-Sep-2014</Date><Time>12:21:24</Time><Account>12440531</Account><Amount>217090</Amount><AllowableMOP>0</AllowableMOP><BankBranchCode>280071</BankBranchCode><ChequeAccNo>62247628681</ChequeAccNo><ChequeNo>000040</ChequeNo></Record>
<Record><GroupId>10028</GroupId><Id>1</Id><User>CHRISVI</User><TxnId>264-10028-1-516743-2</TxnId><Date>30-Sep-2014</Date><Time>12:21:52</Time><Account>10895388</Account><Amount>150000</Amount><AllowableMOP>1</AllowableMOP></Record>
<Record><GroupId>10028</GroupId><Id>1</Id><User>CHRISVI</User><TxnId>264-10028-1-516748-1</TxnId><Date>30-Sep-2014</Date><Time>12:22:26</Time><OrigTxnId>264-10028-1-516743-2</OrigTxnId><Account>10895388</Account><Amount>150000</Amount><AllowableMOP>1</AllowableMOP></Record>
<Record><GroupId>10028</GroupId><Id>1</Id><User>CHRISVI</User><TxnId>264-10028-1-516756-1</TxnId><Date>30-Sep-2014</Date><Time>12:23:01</Time><Account>10895388</Account><Amount>10000</Amount><AllowableMOP>1</AllowableMOP></Record>
<Record><GroupId>10028</GroupId><Id>1</Id><User>CHRISVI</User><TxnId>264-10028-1-516760-2</TxnId><Date>30-Sep-2014</Date><Time>12:23:24</Time><Account>10605762</Account><Amount>15000</Amount><AllowableMOP>1</AllowableMOP></Record>
</XML>
The XSLT code to convert XML:
]]>
<xsl:key name="original" match="/XML/Record" use="TxnId" />
<xsl:key name="copy" match="/XML/Record" use="OrigTxnId" />
<xsl:template match="/">
<?Header Starts?>
<xsl:value-of select="user:IncrementBatchNo('Batchcow','C:\WebRiposte\Agents\Configuration\Configurations.xml')"/>
<?RECORDTYPE?>
<xsl:text>H</xsl:text>
<?FILETYPE?>
<xsl:text>PNP</xsl:text>
<?COMPANYCODE?>
<xsl:text>WHK</xsl:text>
<?COMPANYNAME?>
<xsl:text> Windhoek Municipality</xsl:text>
<?ACTIONDATETIME?>
<xsl:value-of select="user:getdatetime()"/>
<?PAYMENTBATCHNO?>
<xsl:value-of select="format-number(user:GetBatchNo('Batchcow','C:\WebRiposte\Agents\Configuration\Configurations.xml'),'000000')"/>
<?RECORDSIZE?>
<xsl:text>000256</xsl:text>
<?NUMRECORDS?>
<xsl:value-of select="format-number(count(//SessionId),'000000')"/>
<?TESTLIVE?>
<xsl:text>L</xsl:text>
<?FILLER?>
<xsl:call-template name="pad-some-space">
<xsl:with-param name="currentlength" select="1"/>
<xsl:with-param name="newlength" select="177"/>
</xsl:call-template>
<?Line Feed?>
<xsl:text>
</xsl:text>
<?Header Ends ?>
<?Body Starts?>
<xsl:for-each select="XML/Record[not(key('original', OrigTxnId) or key('copy', TxnId))]">
<?Record Type - 1 - Fixed value “D”(etail)?>
<xsl:text>D</xsl:text>
<?PAYMENTBATCHNO?>
<xsl:value-of select="format-number(user:GetBatchNo('Batchcow','C:\WebRiposte\Agents\Configuration\Configurations.xml'),'000000')"/>
<?SeqNo - 6 - Right justified, zero padded?>
<xsl:value-of select="format-number(count(preceding-sibling::Record)+1, '000000')"/>
<?CompanyCode - 3 - Leave Blank, Space padded?>
<xsl:text>WHK</xsl:text>
<?CustAccountNo - 20 - Right justified, zero padded?>
<xsl:value-of select="format-number(Account, '00000000000000000000')"/>
<?Invoice No and Ref no?>
<xsl:text>0000000000000000000000000</xsl:text>
<?Create Group id Variable?>
<xsl:variable name="GroupId" select="GroupId"/>
<?NamPostBranch - 50 - ?>
<xsl:call-template name="reformat-string-length">
<xsl:with-param name="value" select="user:GetPostOfficeName(string($GroupId),'C:\WebRiposte\Agents\Configuration\Configurations.xml')"/>
<xsl:with-param name="str-len" select="50"/>
</xsl:call-template>
<?NamPostReceiptNo - 16 - Group-Node-Sequence No?>
<xsl:call-template name="reformat-string-length">
<xsl:with-param name="value" select="substring(SessionId,5,16)"/>
<xsl:with-param name="str-len" select="16"/>
<xsl:with-param name="alignment" select=" 'right' "/>
</xsl:call-template>
<?MOPCheck?>
<xsl:choose>
<xsl:when test="ChequeNo > 0">
<xsl:text>1</xsl:text>
<?BankBranchCode - 6 - space padded?>
<xsl:call-template name="reformat-string-length">
<xsl:with-param name="value" select="BankBranchCode"/>
<xsl:with-param name="str-len" select="6"/>
</xsl:call-template>
<?ChequeAccountNo -15- Left justified, space padded?>
<xsl:call-template name="reformat-string-length">
<xsl:with-param name="value" select="ChequeAccNo"/>
<xsl:with-param name="str-len" select="15"/>
</xsl:call-template>
<?ChequeNo - 6 - Right justified, zero padded?>
<xsl:value-of select="format-number(ChequeNo, '000000')"/>
</xsl:when>
<xsl:otherwise>
<xsl:text>2</xsl:text>
<?BankBranchCode - 6 - space padded?>
<xsl:text>      </xsl:text>
<?ChequeAccountNo -15- Left justified, space padded?>
<xsl:text>               </xsl:text>
<?ChequeNo - 6 - Right justified, zero padded?>
<xsl:value-of select="format-number(0, '000000')"/>
</xsl:otherwise>
</xsl:choose>
<?PayAmountCents - 9 - Right justified, zero padded?>
<xsl:value-of select="format-number(Amount, '000000000')"/>
<?PaymentDateTime (YYYYMMDDHHMMSS)?>
<xsl:value-of select="substring(Date,8,4)"/>
<xsl:call-template name="format-month-3letter-to-number">
<xsl:with-param name="month-3letter" select="substring(Date,4,3)"/>
</xsl:call-template>
<xsl:value-of select="substring(Date,1,2)"/>
<xsl:value-of select="substring(Time,1,2)"/>
<xsl:value-of select="substring(Time,4,2)"/>
<xsl:value-of select="substring(Time,7,2)"/>
<?Entry Mode?>
<xsl:text>M</xsl:text>
<?AmountSign - 1- ?>
<xsl:text>D</xsl:text>
<?Filler?>
<xsl:call-template name="pad-some-space">
<xsl:with-param name="currentlength" select="1"/>
<xsl:with-param name="newlength" select="77"/>
</xsl:call-template>
<?Line?>
<xsl:text>
</xsl:text>
</xsl:for-each>
<?Body Ends?>
<?Trailer Starts?>
<?RECORDTYPE?>
<xsl:text>T</xsl:text>
<?COMPANYCODE?>
<xsl:text>WHK</xsl:text>
<?PAYMENTBATCHNO?>
<xsl:value-of select="format-number(user:GetBatchNo('Batchcow','C:\WebRiposte\Agents\Configuration\Configurations.xml'),'000000')"/>
<?TOTALAMOUNTCENTS?>
<xsl:value-of select="format-number(sum(//Amount), '00000000000')"/>
<?AMOUNTSIGN?>
<xsl:text>D</xsl:text>
<?FILLER?>
<xsl:call-template name="pad-some-space">
<xsl:with-param name="currentlength" select="1"/>
<xsl:with-param name="newlength" select="235"/>
</xsl:call-template>
<?Trailer Ends?>
</xsl:template>
<?Support functions ------------------- ?>
<?Date time format?>
<xsl:template name="format-date-time">
<xsl:param name="currdatetime"/>
<xsl:value-of select="concat(substring($currdatetime,1,4),substring($currdatetime,6,2),substring($currdatetime,9,2),substring($currdatetime,12,2),substring($currdatetime,15,2),substring($currdatetime,18,2))"/>
</xsl:template>
<?Convert month from text to number?>
<xsl:template name="format-month-3letter-to-number">
<xsl:param name="month-3letter"/>
<xsl:variable name="MonthName" select="'Jan01Feb02Mar03Apr04May05Jun06Jul07Aug08Sep09Oct10Nov11Dec12'"/>
<xsl:value-of select="substring(concat(substring-after($MonthName,$month-3letter),'00'),1,2)"/>
</xsl:template>
<?Pad space?>
<xsl:template name="pad-some-space">
<xsl:param name="currentlength"/>
<xsl:param name="newlength"/>
<xsl:if test="number($currentlength) < number($newlength)">
<xsl:text> </xsl:text>
<xsl:call-template name="pad-some-space">
<xsl:with-param name="currentlength" select="number($currentlength)+1"/>
<xsl:with-param name="newlength" select="$newlength"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
<?Evaluate-string-length?>
<xsl:template name="reformat-string-length">
<xsl:param name="value"/>
<xsl:param name="str-len"/>
<xsl:choose>
<xsl:when test="string-length($value) > number($str-len)">
<xsl:value-of select="substring($value,1,$str-len)"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$value"/>
<xsl:call-template name="pad-some-space">
<xsl:with-param name="currentlength" select="string-length($value)"/>
<xsl:with-param name="newlength" select="number($str-len)"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
Simplifying problems is always good. Although it involved some guess work, I simplified your problem to this
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:key name="original" match="/XML/Record" use="TxnId"/>
<xsl:key name="copy" match="/XML/Record" use="OrigTxnId"/>
<xsl:template match="/">
<xsl:value-of select="format-number(count(//GroupId),'000000')"/>
<xsl:text>
</xsl:text>
<xsl:for-each select="XML/Record[not(key('original', OrigTxnId) or key('copy', TxnId))]">
<xsl:value-of select="format-number(Account, '00000000000000000000')"/>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
(Note, as mentioned in my comments, I have replaced <xsl:value-of select="format-number(count(//Session),'000000')"/> with <xsl:value-of select="format-number(count(//GroupId),'000000')"/>)
This outputs the following
000005
00000000000012440531
00000000000010895388
00000000000010605762
So, the first count says 5 items, but there are only 3 rows.
Now, in your code, you have this xsl:for-each
<xsl:for-each
select="XML/Record[not(key('original', OrigTxnId) or key('copy', TxnId))]">
So, all you need to do is add the condition in you xsl:for-each to your count statement.
<xsl:value-of
select="format-number(count(XML/Record[not(key('original', OrigTxnId) or key('copy', TxnId))]/GroupId),'000000')"/>
It might be better to make use of a variable to remove the repetitive coding.
As an example, try this XSLT as a basis
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:key name="original" match="/XML/Record" use="TxnId"/>
<xsl:key name="copy" match="/XML/Record" use="OrigTxnId"/>
<xsl:template match="/">
<xsl:variable name="records" select="XML/Record[not(key('original', OrigTxnId) or key('copy', TxnId))]" />
<xsl:value-of select="format-number(count($records/GroupId),'000000')"/>
<xsl:text>
</xsl:text>
<xsl:for-each select="$records">
<xsl:value-of select="format-number(Account, '00000000000000000000')"/>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
This outputs the following:
000003
00000000000012440531
00000000000010895388
00000000000010605762

Rolling sum of template result in XSL1.0

I've searched far and wide for an example of this, and have so far had no luck in applying a sum template to make my XSTL work.
This is the XML (number of lines varies on each planfeature)
<PlanFeatures>
<PlanFeature name="Line0001">
<CoordGeom>
<Line>
<Start pntRef="7540">5605 8950 1020</Start>
<End pntRef="7541">5605 8951 1019</End>
</Line>
<Line>
<Start pntRef="7541">5605 8951 1019</Start>
<End pntRef="7542">5605 8947 1019</End>
</Line>
<Line>
<Start pntRef="7542">5605 8947 1019</Start>
<End pntRef="7543">5605 8940 1011</End>
</Line>
<Line>
<Start pntRef="7543">5605 8940 1011</Start>
<End pntRef="7544">5605 8931 1020</End>
</Line>
</CoordGeom>
</PlanFeature>
</PlanFeatures>
This is where I'm at with the XSL, which uses a recursive call template to calculate the distance of each line segment.
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0" xmlns:landxml="http://www.landxml.org/schema/LandXML-1.2" xmlns:hexagon="http://xml.hexagon.com/schema/HeXML-1.5" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" version="1.0" encoding="UTF-16" indent="no" omit-xml-declaration="yes"/>
<xsl:variable name="XML" select="/"/>
<xsl:variable name="fileExt" select="'txt'"/>
<xsl:variable name="fileDesc" select="'line distance report'"/>
<xsl:template match="/">
<xsl:for-each select="$XML">
<xsl:for-each select="landxml:LandXML/landxml:PlanFeatures/landxml:PlanFeature">
<xsl:value-of select="#name"/><xsl:text>::</xsl:text>
<xsl:for-each select="landxml:CoordGeom/landxml:Line">
<xsl:value-of select="landxml:Start/#pntRef"/><xsl:text>-</xsl:text>
<xsl:variable name="lista" select="landxml:Start"/>
<xsl:variable name="x1" select="substring-before($lista,' ')"/>
<xsl:variable name="yt1" select="substring-after($lista,' ')"/>
<xsl:variable name="y1" select="substring-before($yt1,' ')"/>
<xsl:variable name="z1" select="substring-after($yt1,' ')"/>
<xsl:variable name="listb" select="landxml:End"/>
<xsl:value-of select="landxml:End/#pntRef"/><xsl:text>: </xsl:text>
<xsl:variable name="x2" select="substring-before($listb,' ')"/>
<xsl:variable name="yt2" select="substring-after($listb,' ')"/>
<xsl:variable name="y2" select="substring-before($yt2,' ')"/>
<xsl:variable name="z2" select="substring-after($yt2,' ')"/>
<xsl:variable name="seg" select= "((($x2 - $x1)*($x2 - $x1))+(($y2 - $y1)*($y2 - $y1))+(($z2 - $z1)*($z2 - $z1)))"/>
<xsl:call-template name="root">
<xsl:with-param name="X" select="$seg"/>
</xsl:call-template>
<xsl:text>, </xsl:text>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:for-each>
</xsl:template>
<xsl:template name="root">
<xsl:param name="X"/>
<xsl:param name="xn" select="0"/>
<xsl:param name="xn_1" select="($X+1) div 2"/>
<xsl:choose>
<xsl:when test="string(number($X)) = 'NaN'">
<xsl:value-of select=" ' ' "/>
</xsl:when>
<xsl:when test="($xn_1 - $xn) * ($xn_1 - $xn) < 0.00000001">
<xsl:value-of select='format-number($xn_1, "#.000")'/>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="root">
<xsl:with-param name="X" select="$X"/>
<xsl:with-param name="xn" select="$xn_1"/>
<xsl:with-param name="xn_1" select="($xn_1 + ($X div $xn_1)) div 2"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
I need to sum the value of X (distance) from the root call template, to create a value which represents the sum of each line segment. I think I need to use a match template, but so far it hand even got close to working.
Currently exporting LINEID::StartPt-EndPt: dist, StartPt-EndPt: dist, etc. I need the sum of the 'dist' to be shown at the end of each line as well. As below
Line0001::7540-7541: 1.414, 7541-7542: 2.000, 7542-7543: 12.042, 7543-7544: 12.720
but I would like
Line0001::7540-7541: 1.414, 7541-7542: 2.000, 7542-7543: 12.042, 7543-7544: 12.728 -- 28.184
Any help would be appreciated... the examples on this site have helped me so much already, but I just can't seem to get through this roadblock.
Cheers,
Chris
You can accomplish this with some relatively simple recursion and parameter passing. Try replacing your first template with these four templates:
<xsl:template match="/">
<xsl:for-each select="$XML">
<xsl:apply-templates
select="landxml:LandXML/landxml:PlanFeatures/landxml:PlanFeature" />
</xsl:for-each>
</xsl:template>
<xsl:template match ="landxml:PlanFeature">
<xsl:value-of select="concat(#name, '::')" />
<xsl:apply-templates select="landxml:CoordGeom/landxml:Line[1]" />
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template match="landxml:Line">
<xsl:param name="total" select="0" />
<xsl:value-of
select="concat(landxml:Start/#pntRef, '-', landxml:End/#pntRef, ': ')"/>
<xsl:variable name="len">
<xsl:call-template name="root">
<xsl:with-param name="X">
<xsl:call-template name="seg" />
</xsl:with-param>
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="$len"/>
<xsl:variable name="next" select="following-sibling::landxml:Line[1]" />
<xsl:variable name="newTot" select="$total + $len" />
<xsl:choose>
<xsl:when test="$next">
<xsl:text>, </xsl:text>
<xsl:apply-templates select="$next">
<xsl:with-param name="total" select="$newTot" />
</xsl:apply-templates>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="concat(' -- ', format-number($newTot, '#.000'))" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="seg">
<xsl:variable name="lista" select="landxml:Start"/>
<xsl:variable name="x1" select="substring-before($lista,' ')"/>
<xsl:variable name="yt1" select="substring-after($lista,' ')"/>
<xsl:variable name="y1" select="substring-before($yt1,' ')"/>
<xsl:variable name="z1" select="substring-after($yt1,' ')"/>
<xsl:variable name="listb" select="landxml:End"/>
<xsl:variable name="x2" select="substring-before($listb,' ')"/>
<xsl:variable name="yt2" select="substring-after($listb,' ')"/>
<xsl:variable name="y2" select="substring-before($yt2,' ')"/>
<xsl:variable name="z2" select="substring-after($yt2,' ')"/>
<xsl:value-of select= "($x2 - $x1)*($x2 - $x1)+
($y2 - $y1)*($y2 - $y1)+
($z2 - $z1)*($z2 - $z1)"/>
</xsl:template>
When run on your sample input (after adjusting it to match the paths in your XSLT), the result is:
Line0001::7540-7541: 1.414, 7541-7542: 4.000, 7542-7543: 10.630, 7543-7544: 12.728 -- 28.772

How to limit string word count in XSLT 1.0?

How can I limit a string's word count in XSLT 1.0?
How about something like:
<xsl:template match="data"> <!-- your data element or whatever -->
<xsl:call-template name="firstWords">
<xsl:with-param name="value" select="."/>
<xsl:with-param name="count" select="4"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="firstWords">
<xsl:param name="value"/>
<xsl:param name="count"/>
<xsl:if test="number($count) >= 1">
<xsl:value-of select="concat(substring-before($value,' '),' ')"/>
</xsl:if>
<xsl:if test="number($count) > 1">
<xsl:variable name="remaining" select="substring-after($value,' ')"/>
<xsl:if test="string-length($remaining) > 0">
<xsl:call-template name="firstWords">
<xsl:with-param name="value" select="$remaining"/>
<xsl:with-param name="count" select="number($count)-1"/>
</xsl:call-template>
</xsl:if>
</xsl:if>
</xsl:template>
This is an XSLT 1.0 solution:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ext="http://exslt.org/common"
>
<xsl:import href="strSplit-to-Words.xsl"/>
<xsl:output indent="yes" omit-xml-declaration="yes"/>
<xsl:template match="/">
<xsl:variable name="vwordNodes">
<xsl:call-template name="str-split-to-words">
<xsl:with-param name="pStr" select="/"/>
<xsl:with-param name="pDelimiters"
select="',
()-'"/>
</xsl:call-template>
</xsl:variable>
<xsl:call-template name="strTakeWords">
<xsl:with-param name="pN" select="10"/>
<xsl:with-param name="pText" select="/*"/>
<xsl:with-param name="pWords"
select="ext:node-set($vwordNodes)/*"/>
</xsl:call-template>
</xsl:template>
<xsl:template match="word" priority="10">
<xsl:value-of select="concat(position(), ' ', ., '
')"/>
</xsl:template>
<xsl:template name="strTakeWords">
<xsl:param name="pN" select="10"/>
<xsl:param name="pText"/>
<xsl:param name="pWords"/>
<xsl:param name="pResult"/>
<xsl:choose>
<xsl:when test="not($pN > 0)">
<xsl:value-of select="$pResult"/>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="vWord" select="$pWords[1]"/>
<xsl:variable name="vprecDelims" select=
"substring-before($pText,$pWords[1])"/>
<xsl:variable name="vnewText" select=
"concat($vprecDelims, $vWord)"/>
<xsl:call-template name="strTakeWords">
<xsl:with-param name="pN" select="$pN -1"/>
<xsl:with-param name="pText" select=
"substring-after($pText, $vnewText)"/>
<xsl:with-param name="pWords" select=
"$pWords[position() > 1]"/>
<xsl:with-param name="pResult" select=
"concat($pResult, $vnewText)"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
when this transformation is applied on the following XML document:
<t>
(CNN) -- Behind closed doors in recent days,
senior White House aides have been saying that
measuring President Obama's first 100 days
is the journalistic equivalent of a Hallmark holiday.
</t>
the wanted result is returned:
(CNN) -- Behind closed doors in recent days,
senior White House
Do note:
The str-split-to-words template from FXSL is used for tokenization.
This template accepts a parameter pDelimiters which is a string consisting of all characters that should be treated as delimiters. Thus, in contrast with other solutions, it is possible to specify every delimiter (and not just a "space") -- in this case 8 of them.
The named template strTakeWords calls itself recursively to accumulate the text before and including every word from the wordlist produced by the tokenization, until the specified number of words has been processed.