XSLT concat string, remove last comma - xslt

I need to build up a string using XSLT and separate each string with a comma but not include a comma after the last string. In my example below I will have a trailing comma if I have Distribution node and not a Note node for instance. I don't know of anyway to build up a string as a variable and then truncate the last character in XSLT. Also this is using the Microsoft XSLT engine.
My String =
<xsl:if test="Locality != ''">
<xsl:value-of select="Locality"/>,
</xsl:if>
<xsl:if test="CollectorAndNumber != ''">
<xsl:value-of select="CollectorAndNumber"/>,
</xsl:if>
<xsl:if test="Institution != ''">
<xsl:value-of select="Institution"/>,
</xsl:if>
<xsl:if test="Distribution != ''">
<xsl:value-of select="Distribution"/>,
</xsl:if>
<xsl:if test="Note != ''">
<xsl:value-of select="Note"/>
</xsl:if>
[Man there's gotta be a better way to enter into this question text box :( ]

This is very easy to accomplish with XSLT (No need to capture the results in a variable, or to use special named templates):
I. XSLT 1.0:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/*/*">
<xsl:for-each select=
"Locality/text() | CollectorAndNumber/text()
| Institution/text() | Distribution/text()
| Note/text()
"
>
<xsl:value-of select="."/>
<xsl:if test="not(position() = last())">,</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
when this transformation is applied on the following XML document:
<root>
<record>
<Locality>Locality</Locality>
<CollectorAndNumber>CollectorAndNumber</CollectorAndNumber>
<Institution>Institution</Institution>
<Distribution>Distribution</Distribution>
<Note></Note>
<OtherStuff>Unimportant</OtherStuff>
</record>
</root>
the wanted result is produced:
Locality,CollectorAndNumber,Institution,Distribution
If the wanted elements should be produced not in document order (something not required in the question, but raised by Tomalak), it is still quite easy and elegant to achieve this:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:param name="porderedNames"
select="' CollectorAndNumber Locality Distribution Institution Note '"/>
<xsl:template match="/*/*">
<xsl:for-each select=
"*[contains($porderedNames, concat(' ',name(), ' '))]">
<xsl:sort data-type="number"
select="string-length(
substring-before($porderedNames,
concat(' ',name(), ' ')
)
)"/>
<xsl:value-of select="."/>
<xsl:if test="not(position() = last())">,</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Here the names of the wanted elements and their wanted order are provided in the string parameter $porderedNames, which contains a space-separated list of all wanted names.
When the above transformation is applied on the same XML document, the wanted result is produced:
CollectorAndNumber,Locality,Distribution,Institution
II. XSLT 2.0:
In XSLT this task is even simpler (again, no special function is necessary):
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/*/*">
<xsl:value-of separator="," select=
"(Locality, CollectorAndNumber,
Institution, Distribution,
Note)[text()]" />
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the same XML document, the same correct result is produced:
Locality,CollectorAndNumber,Institution,Distribution
Do note that the wanted elements will be produced in any desired order, because we are using the XPath 2.0 sequence type (vs the union in the XSLT 1.0 solution), which by definition contains items in any desired (specified) order.

I would prefer a short call-template to join the node values together. This also works if a node in the middle of your concatenated list, e.g. Institution, is missing:
<xsl:template name="join">
<xsl:param name="list" />
<xsl:param name="separator"/>
<xsl:for-each select="$list">
<xsl:value-of select="." />
<xsl:if test="position() != last()">
<xsl:value-of select="$separator" />
</xsl:if>
</xsl:for-each>
</xsl:template>
Here is a short example how to use it:
Sample input document:
<?xml version="1.0" encoding="utf-8"?>
<items>
<item>
<Locality>locality1</Locality>
<CollectorAndNumber>collectorAndNumber1</CollectorAndNumber>
<Distribution>distribution1</Distribution>
<Note>note1</Note>
</item>
<item>
<Locality>locality2</Locality>
<CollectorAndNumber>collectorAndNumber2</CollectorAndNumber>
<Institution>institution2</Institution>
<Distribution>distribution2</Distribution>
<Note>note2</Note>
</item>
<item>
<Locality>locality3</Locality>
<CollectorAndNumber>collectorAndNumber3</CollectorAndNumber>
<Institution>institution3</Institution>
<Distribution>distribution3</Distribution>
</item>
</items>
XSL transformation:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<summary>
<xsl:apply-templates />
</summary>
</xsl:template>
<xsl:template match="item">
<item>
<xsl:call-template name="join">
<xsl:with-param name="list" select="Locality | CollectorAndNumber | Institution | Distribution | Note" />
<xsl:with-param name="separator" select="','" />
</xsl:call-template>
</item>
</xsl:template>
<xsl:template name="join">
<xsl:param name="list" />
<xsl:param name="separator"/>
<xsl:for-each select="$list">
<xsl:value-of select="." />
<xsl:if test="position() != last()">
<xsl:value-of select="$separator" />
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Generated output document:
<?xml version="1.0" encoding="utf-8"?>
<summary>
<item>locality1,collectorAndNumber1,distribution1,note1</item>
<item>locality2,collectorAndNumber2,institution2,distribution2,note2</item>
<item>locality3,collectorAndNumber3,institution3,distribution3</item>
</summary>
NB: If you were using XSLT/XPath 2.0 then there would be fn:string-join
fn:string-join**($operand1 as string*, $operand2 as string*) as string
which could be used as follows:
fn:string-join({Locality, CollectorAndNumber, Distribution, Note}, ",")

Supposing you have something like the following input XML:
<root>
<record>
<Locality>Locality</Locality>
<CollectorAndNumber>CollectorAndNumber</CollectorAndNumber>
<Institution>Institution</Institution>
<Distribution>Distribution</Distribution>
<Note>Note</Note>
<OtherStuff>Unimportant</OtherStuff>
</record>
</root>
Then this template would do it:
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:output method="text" />
<xsl:template match="record">
<xsl:variable name="values">
<xsl:apply-templates mode="concat" select="Locality" />
<xsl:apply-templates mode="concat" select="CollectorAndNumber" />
<xsl:apply-templates mode="concat" select="Institution" />
<xsl:apply-templates mode="concat" select="Distribution" />
<xsl:apply-templates mode="concat" select="Note" />
</xsl:variable>
<xsl:value-of select="substring($values, 1, string-length($values) - 1)" />
<xsl:value-of select="'
'" /><!-- LF -->
</xsl:template>
<xsl:template match="Locality | CollectorAndNumber | Institution | Distribution | Note" mode="concat">
<xsl:value-of select="." />
<xsl:text>,</xsl:text>
</xsl:template>
</xsl:stylesheet>
Output on my system:
Locality,CollectorAndNumber,Institution,Distribution,Note

I think it might be useful to mention,
position() doesn't work right when I use a complicated select
that filters some nodes,
in that case I came up which this trick:
you can define a string variable that hold value of nodes, separated
by a specific character, then by using str:tokenize()
you can create a complete node list which position works fine with it.
something like this:
<!-- Since position() doesn't work as expected(returning node position of current
node list), I got round it by a string variable and tokenizing it in which
absolute position is equal to relative(context) position. -->
<xsl:variable name="measObjLdns" >
<xsl:for-each select="h:measValue[#measObjLdn=$currentMeasObjLdn]/h:measResults" >
<xsl:value-of select="concat(.,'---')"/> <!-- is an optional separator. -->
</xsl:for-each>
</xsl:variable>
<xsl:for-each select="str:tokenize($measObjLdns,'---')" ><!-- Since position() doesn't
work as expected(returning node position of current node list),
I got round it by a string variable and tokenizing it in which
absolute position is equal to relative(context) position. -->
<xsl:value-of select="."></xsl:value-of>
<xsl:if test="position() != last()">
<xsl:text>,</xsl:text>
</xsl:if>
</xsl:for-each>
<xsl:if test="position() != last()">
<xsl:text>,</xsl:text>
</xsl:if>

Do you not have a value that is always going to be there? If you do then you can turn it around and put commas infront of everything apart from the first item (which would be your value that's always there).

This would be a bit messy but might do the trick if there's only a few elements like in your example:
<xsl:if test="Locality != ''">
<xsl:value-of select="Locality"/>
<xsl:if test="CollectorAndNumber != '' or Institution != '' or Distribution != '' or Note != ''">
<xsl:value-of select="','"/>
</xsl:if>
</xsl:if>
<xsl:if test="CollectorAndNumber != ''">
<xsl:value-of select="CollectorAndNumber"/>
<xsl:if test="Institution != '' or Distribution != '' or Note != ''">
<xsl:value-of select="','"/>
</xsl:if>
</xsl:if>
<xsl:if test="Institution != ''">
<xsl:value-of select="Institution"/>
<xsl:if test="Distribution != '' or Note != ''">
<xsl:value-of select="','"/>
</xsl:if>
</xsl:if>
<xsl:if test="Distribution != ''">
<xsl:value-of select="Distribution"/>
<xsl:if test="Note != ''">
<xsl:value-of select="','"/>
</xsl:if>
</xsl:if>
<xsl:if test="Note != ''">
<xsl:value-of select="Note"/>
</xsl:if>

Related

Creating a JSON from xml with date and time logic templates

I'm trying to create a JSON from a XML which has messages and each message has its date/time.
Below is the XML
<message>
<messageText heading="Temporary Maintenance Message 1">test message1</messageText>
<displayScheduleContainer>
<startDate>22/05/2019</startDate>
<startTimeHrs>12</startTimeHrs>
<startTimeMins>45</startTimeMins>
<noEndDate>true</noEndDate>
</displayScheduleContainer>
</message>
<message>
<messageText heading="Temporary Maintenance Message 1">test message2</messageText>
<displayScheduleContainer>
<startDate>22/06/2019</startDate>
<startTimeHrs>12</startTimeHrs>
<startTimeMins>45</startTimeMins>
<noEndDate>true</noEndDate>
</displayScheduleContainer>
</message>
The logic inside XSLT reads the date and time to activate the message
<xsl:for-each select="xalan:nodeset($messageData)/activeMessage/message">
<xsl:variable name="variableN">
<xsl:call-template name="jsonMsg" />
</xsl:variable>
<xsl:choose>
<xsl:when test="$variableN = 'true'">
<xsl:copy-of select="messageText/text()" />
<xsl:if test="position() < last()">,</xsl:if>
</xsl:when>
</xsl:choose>
</xsl:for-each>
<xsl:template name="jsonMsg">
<xsl:choose>
<xsl:when test="displayScheduleContainer/noEndDate = 'true'">
<xsl:variable name="messageInDateTime">
<xsl:call-template name="noEndDateTemplate">
<xsl:with-param name="startDateTime"
select="concat(displayScheduleContainer/startDate, ' ', displayScheduleContainer/startTimeHrs, ':', displayScheduleContainer/startTimeMins)" />
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="$messageInDateTime" />
</xsl:when>
</xsl:choose>
</xsl:template>
<xsl:template name="noEndDateTemplate">
<xsl:param name="startDateTime" />
<xsl:variable name="sdf"
select="java:text.SimpleDateFormat.new('dd/MM/yyyy hh:mm')" />
<xsl:variable name="currentDateTime" select="java:util.Date.new()" />
<xsl:choose>
<xsl:when
test="java:compareTo(java:parse($sdf, $startDateTime), $currentDateTime) < 0">
<xsl:text>true</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text>false</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
The problem i'm facing here is if the last value is false, I end up getting the comma at the end. As i'm checking for the last position and adding the comma. Due to this the whole JSON is broken. In this case it adds the comma because i'm displaying the text only if it is true.
"message": ["test message1", ]
I'm using XSLT 1.0
Instead of xsl:choose, append a predicate to your select expression. Here's a simplified example:
XML
<messages>
<message>
<messageText>test message1</messageText>
<displayScheduleContainer>
<noEndDate>true</noEndDate>
</displayScheduleContainer>
</message>
<message>
<messageText>test message2</messageText>
<displayScheduleContainer>
<noEndDate>true</noEndDate>
</displayScheduleContainer>
</message>
<message>
<messageText>test message3</messageText>
<displayScheduleContainer>
<noEndDate>false</noEndDate>
</displayScheduleContainer>
</message>
</messages>
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:template match="/messages">
<xsl:for-each select="message[displayScheduleContainer/noEndDate = 'true']">
<xsl:value-of select="messageText" />
<xsl:if test="position() < last()">,</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Result
"test message1,test message2"
Added:
If the test is too complex to fit in a predicate, do the transformation in two passes. Here, again, a simplified example:
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="text" encoding="UTF-8"/>
<xsl:template match="/messages">
<!-- first pass -->
<xsl:variable name="eligible-messages">
<xsl:for-each select="message">
<xsl:if test="displayScheduleContainer/noEndDate = 'true'">
<xsl:copy-of select="."/>
</xsl:if>
</xsl:for-each>
</xsl:variable>
<!-- output -->
<xsl:for-each select="exsl:node-set($eligible-messages)/message">
<xsl:value-of select="messageText" />
<xsl:if test="position() < last()">,</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Replace the test in:
<xsl:if test="displayScheduleContainer/noEndDate = 'true'">
with the test/s you want to perform.

Looping Element to Single Element in XSLT

I need to convert the following content
<QTLS_ITEM>
<ID>123</ID>
<ID1>1345</ID1>
<SERAIL_NUMBER>1026977­04257</SERAIL_NUMBER>
<PROD_NAME>upgrade</PROD_NAME>
</QTLS_ITEM>
<QTLS_ITEM>
<ID>123</ID>
<ID1>1345</ID1>
<SERAIL_NUMBER>1026977­04257</SERAIL_NUMBER>
<PROD_NAME>Plug­in</PROD_NAME>
</QTLS_ITEM>
<QTLS_ITEM>
<ID>123</ID>
<ID1>1345</ID1>
<SERAIL_NUMBER>1026977­04257</SERAIL_NUMBER>
<PROD_NAME>License</PROD_NAME>
</QTLS_ITEM>
This is a looping element type.<QTLS_ITEM> is a repeating element. For each element I have to concatenate those two fields and get the value as below.
I want to transform it into a single Element like
<Item_description>
1026977­04257 upgrade
1026977­04257 Plug­in
1026977­04257 License
<Item_Description>
Which means I need to concatenate both the SERAIL_NUMBER and PROD_NAME.
Can anyone help on this?
<xsl:template name="string-join">
<xsl:param name="nodes"/>
<xsl:param name="delimiter"/>
<xsl:for-each select="$nodes">
<xsl:value-of select="."/>
<xsl:if test="$nodes[position()!=last()-1]">
<xsl:value-of select="$delimiter"/>
</xsl:if>
</xsl:for-each>
</xsl:template>
I am using this to get the Serial numbers separated by comma. But I want to concatenate and get the result in the above format.
The answer for my case is
<xsl:template name="join">
<xsl:param name="list"/>
<xsl:param name="separator"/>
<xsl:for-each select="db:SERAIL_NUMBER | db:PROD_NAME">($list value)
<xsl:value-of select="."/>
<xsl:if test="position() != last()">
<xsl:value-of select="$separator"/>
</xsl:if>
</xsl:for-each>
</xsl:template>
The input that you show us is not well-formed XML, because it does not have a single root element. Given a well-formed XML input such as:
XML
<root>
<QTLS_ITEM>
<SERAIL_NUMBER>102697704257</SERAIL_NUMBER>
<PROD_NAME>upgrade</PROD_NAME>
</QTLS_ITEM>
<QTLS_ITEM>
<SERAIL_NUMBER>102697704257</SERAIL_NUMBER>
<PROD_NAME>Plugin</PROD_NAME>
</QTLS_ITEM>
<QTLS_ITEM>
<SERAIL_NUMBER>102697704257</SERAIL_NUMBER>
<PROD_NAME>License</PROD_NAME>
</QTLS_ITEM>
</root>
you can do simply:
XSLT 2.0
<xsl:stylesheet version="2.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="/root">
<Item_description>
<xsl:for-each select="QTLS_ITEM">
<xsl:value-of select="SERAIL_NUMBER, PROD_NAME" />
<xsl:text>
</xsl:text>
</xsl:for-each>
</Item_description>
</xsl:template>
</xsl:stylesheet>
to get:
Result
<?xml version="1.0" encoding="UTF-8"?>
<Item_description>102697704257 upgrade
102697704257 Plugin
102697704257 License
</Item_description>
This has worked out in my case
<xsl:template name="join">
<xsl:param name="list"/>
<xsl:param name="separator"/>
<xsl:for-each select="db:SERAIL_NUMBER|db:PROD_NAME">($list value)
<xsl:value-of select="."/>
<xsl:if test="position() != last()">
<xsl:value-of select="$separator"/>
</xsl:if>
</xsl:for-each>
</xsl:template>

How to apply a function to a sequence of nodes in XSLT

I need to write an XSLT function that transforms a sequence of nodes into a sequence of strings. What I need to do is to apply a function to all the nodes in the sequence and return a sequence as long as the original one.
This is the input document
<article id="4">
<author ref="#Guy1"/>
<author ref="#Guy2"/>
</article>
This is how the calling site:
<xsl:template match="article">
<xsl:text>Author for </xsl:text>
<xsl:value-of select="#id"/>
<xsl:variable name="names" select="func:author-names(.)"/>
<xsl:value-of select="string-join($names, ' and ')"/>
<xsl:value-of select="count($names)"/>
</xsl:function>
And this is the code of the function:
<xsl:function name="func:authors-names">
<xsl:param name="article"/>
<!-- HELP: this is where I call `func:format-name` on
each `$article/author` element -->
</xsl:function>
What should I use inside func:author-names? I tried using xsl:for-each but the result is a single node, not a sequence.
<xsl:sequence select="$article/author/func:format-name(.)"/> is one way, the other is <xsl:sequence select="for $a in $article/author return func:format-name($a)"/>.
I am not sure you would need the function of course, doing
<xsl:value-of select="author/func:format-name(.)" separator=" and "/>
in the template of article should do.
If only a sequence of #ref values should be generated there is no need for a function or xsl version 2.0.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html" />
<xsl:template match="article">
<xsl:apply-templates select="author" />
</xsl:template>
<xsl:template match="author">
<xsl:value-of select="#ref"/>
<xsl:if test="position() !=last()" >
<xsl:text>,</xsl:text>
</xsl:if>
</xsl:template>
</xsl:styleshee
This will generate:
#Guy1,#Guy2
Update:
Do have the string join by and and have a count of items. Try this:
<xsl:template match="article">
<xsl:text>Author for </xsl:text>
<xsl:value-of select="#id"/>
<xsl:apply-templates select="author" />
<xsl:value-of select="count(authr[#ref])"/>
</xsl:template>
<xsl:template match="author">
<xsl:value-of select="#ref"/>
<xsl:if test="position() !=last()" >
<xsl:text> and </xsl:text>
</xsl:if>
</xsl:template>
With this output:
Author for 4#Guy1 and #Guy20

How to Index / Count nodes containing a subnode, in the format 1, 2, 3, 4

I'm trying to find a way to index nodes that contain a specific type of sub-node, and have them indexed starting at one and incrementing by one per each sub-node found. I tried using count() but this displays the position of the node in my code, not the index of the sub-node found. In other languages I would use a variable but this isn't an option in XSLT.
I have read some examples of a recursive count using templates, but I'm not familiar enough with XSLT to integrate it into my existing code.
XML
<?xml version="1.0" encoding="UTF-8"?>
<entry>
<node>
<subnodeA>test_subnodeA1</subnodeA>
<subnodeB>test_subnodeB1</subnodeB>
</node>
<node>
<subnodeA>test_subnodeA2</subnodeA>
<subnodeB>test_subnodeB2</subnodeB>
</node>
<node>
<subnodeA>test_subnodeA3</subnodeA>
</node>
<node>
<subnodeA>test_subnodeA4</subnodeA>
<subnodeB>test_subnodeB3</subnodeB>
</node>
</entry>
XSLT
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/">
<!-- Initial usage -->
<xsl:for-each select="node">
<xsl:value-of select="subnodeA"/>
<xsl:if test="subnodeB != ''">
<xsl:value-of select="position()"/>
</xsl:if>
</xsl:for-each>
<!-- Second usage -->
<xsl:for-each select="node">
<xsl:if test="subnodeB != ''">
<xsl:value-of select="position()"/>.
<xsl:value-of select="subnodeB"/>
</xsl:if>
</xsl:for-each>
</xsl:template>
I want to display something like
test_subnodeA1 1
test_subnodeA2 2
test_subnodeA3
test_subnodeA4 3
1 test_subnodeB1
2 test_subnodeB2
3 test_subnodeB3
But using my method I can only get
test_subnodeA1 1
test_subnodeA2 2
test_subnodeA3
test_subnodeA4 4
1 test_subnodeB1
2 test_subnodeB2
4 test_subnodeB3
Some entries have a few nodes with no subnodeB and then a node with one, so my lists start at 3, 4 or 5.
In the first case, you can get the position by counting preceding siblings
<xsl:value-of select="count(preceding-sibling::node[subnodeA and subnodeB]) + 1" />
In the second case, you just want node elements with a subnodeB
<xsl:for-each select="node[subnodeA and subnodeB]">
Here is the example XSLT. Note that I have switched from using xsl:for-each to using xsl:apply-templates
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/entry">
<xsl:apply-templates select="node" mode="initial" />
<xsl:apply-templates select="node[subnodeB]" mode="second" />
</xsl:template>
<xsl:template match="node" mode="initial">
<xsl:value-of select="subnodeA" />
<xsl:if test="subnodeB != ''">
<xsl:text> - </xsl:text><xsl:value-of select="count(preceding-sibling::node[subnodeA and subnodeB]) + 1" />
</xsl:if>
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template match="node" mode="second">
<xsl:value-of select="position()" /> - <xsl:value-of select="subnodeB" />
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
This generates the following text output
test_subnodeA1 - 1
test_subnodeA2 - 2
test_subnodeA3
test_subnodeA4 - 3
1 - test_subnodeB1
2 - test_subnodeB2
3 - test_subnodeB3
We can use count also applying templates directly on the interested node:
Given subnodeA as current node:
count(preceding::subnodeB[not(../subnodeA)]) count all preceding nodes without subnodeA (current shift)
count(preceding::subnodeA[../subnodeB]) count all preceding nodes with subnodeB (relative position - 1)
Let's see how it can work with repetition (subnodeA only, as subnodeB is trivial):
<xsl:template match="entry">
<xsl:for-each select="node/subnodeA">
<xsl:value-of select="."/>
<xsl:if test="../subnodeB">
<xsl:value-of select="
count(preceding::subnodeB[not(../subnodeA)])
+ count(preceding::subnodeA[../subnodeB])
+ 1 "/>
</xsl:if>
<xsl:if test="position()!=last()">
<xsl:value-of select="'
'"/>
</xsl:if>
</xsl:for-each>
</xsl:template>
and with apply templates:
<xsl:template match="entry">
<xsl:apply-templates select="node/subnodeA"/>
</xsl:template>
<xsl:template match="node/subnodeA">
<xsl:value-of select="."/>
<xsl:if test="../subnodeB">
<xsl:value-of select="
count(preceding::subnodeB[not(../subnodeA)])
+ count(preceding::subnodeA[../subnodeB])
+ 1 "/>
</xsl:if>
<xsl:if test="position()!=last()">
<xsl:value-of select="'
'"/>
</xsl:if>
</xsl:template>
This is one of the simplest possible solutions -- no xsl:for-each, no count(), no special axes and no explicit conditional logic at all:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/">
<xsl:apply-templates select="*/node/subnodeA"/>
<xsl:apply-templates select="*/node/subnodeB"/>
</xsl:template>
<xsl:template match="*[subnodeB]/subnodeA">
<xsl:value-of select="concat(., ' ')"/>
<xsl:number level="any" count="*[subnodeB]/subnodeA"/>
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template match="subnodeB">
<xsl:value-of select="concat(position(), ' ', ., '
' )"/>
</xsl:template>
<xsl:template match="text()">
<xsl:value-of select="concat(., '
')"/>
</xsl:template>
</xsl:stylesheet>
when applied on the provided XML document:
<entry>
<node>
<subnodeA>test_subnodeA1</subnodeA>
<subnodeB>test_subnodeB1</subnodeB>
</node>
<node>
<subnodeA>test_subnodeA2</subnodeA>
<subnodeB>test_subnodeB2</subnodeB>
</node>
<node>
<subnodeA>test_subnodeA3</subnodeA>
</node>
<node>
<subnodeA>test_subnodeA4</subnodeA>
<subnodeB>test_subnodeB3</subnodeB>
</node>
</entry>
the wanted, correct result is produced:
test_subnodeA1 1
test_subnodeA2 2
test_subnodeA3
test_subnodeA4 3
1 test_subnodeB1
2 test_subnodeB2
3 test_subnodeB3

Counting distinct items and parsing comma-delimited values using XSLT

Suppose I have XML like this:
<child_metadata>
<metadata>
<attributes>
<metadata_valuelist value="[SampleItem3]"/>
</attributes>
</metadata>
<metadata>
<attributes>
<metadata_valuelist value="[SampleItem1]"/>
</attributes>
</metadata>
<metadata>
<attributes>
<metadata_valuelist value="[SampleItem1, SampleItem2]"/>
</attributes>
</metadata>
</child_metadata>
What I want to do is count the number of distinct values that are in the metadata_valuelists. There are the following distinct values: SampleItem1, SampleItem2, and SampleItem3. So, I want to get a value of 3. (Although SampleItem1 occurs twice, I only count it once.)
How can I do this in XSLT?
I realize there are two problems here: First, separating the comma-delimited values in the lists, and, second, counting the number of unique values. However, I'm not certain that I could combine solutions to the two problems, which is why I'm asking it as one question.
Another way without extension:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:variable name="all-value" select="/*/*/*/*/#value"/>
<xsl:template match="/">
<xsl:variable name="count">
<xsl:apply-templates select="$all-value"/>
</xsl:variable>
<xsl:value-of select="string-length($count)"/>
</xsl:template>
<xsl:template match="#value" name="value">
<xsl:param name="meta" select="translate(.,'[] ','')"/>
<xsl:choose>
<xsl:when test="contains($meta,',')">
<xsl:call-template name="value">
<xsl:with-param name="meta" select="substring-before($meta,',')"/>
</xsl:call-template>
<xsl:call-template name="value">
<xsl:with-param name="meta" select="substring-after($meta,',')"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:if test="count(.|$all-value[contains(translate(.,'[] ','
'),
concat('
',$meta,'
'))][1])=1">
<xsl:value-of select="1"/>
</xsl:if>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Note: maybe can be optimize with xsl:key instead of xsl:variable
Edit: Match tricky metadata.
This (note: just a single) transformation:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
>
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:key name="kValue" match="value" use="."/>
<xsl:template match="/">
<xsl:variable name="vRTFPass1">
<values>
<xsl:apply-templates/>
</values>
</xsl:variable>
<xsl:variable name="vPass1"
select="msxsl:node-set($vRTFPass1)"/>
<xsl:for-each select="$vPass1">
<xsl:value-of select=
"count(*/value[generate-id()
=
generate-id(key('kValue', .)[1])
]
)
"/>
</xsl:for-each>
</xsl:template>
<xsl:template match="metadata_valuelist">
<xsl:call-template name="tokenize">
<xsl:with-param name="pText" select="translate(#value, '[],', '')"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="tokenize">
<xsl:param name="pText" />
<xsl:choose>
<xsl:when test="not(contains($pText, ' '))">
<value><xsl:value-of select="$pText"/></value>
</xsl:when>
<xsl:otherwise>
<value>
<xsl:value-of select="substring-before($pText, ' ')"/>
</value>
<xsl:call-template name="tokenize">
<xsl:with-param name="pText" select=
"substring-after($pText, ' ')"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
when applied on the provided XML document:
<child_metadata>
<metadata>
<attributes>
<metadata_valuelist value="[SampleItem3]"/>
</attributes>
</metadata>
<metadata>
<attributes>
<metadata_valuelist value="[SampleItem1]"/>
</attributes>
</metadata>
<metadata>
<attributes>
<metadata_valuelist value="[SampleItem1, SampleItem2]"/>
</attributes>
</metadata>
</child_metadata>
produces the wanted, correct result:
3
Do note: Because this is an XSLT 1.0 solution, it is necessary to convert the results of the first pass from the infamous RTF type to a regular tree. This is done using your XSLT 1.0 processor's xxx:node-set() function -- in my case I used msxsl:node-set().
You probably want to think about doing this in two stages; first, do a transform that breaks down these value attributes, then it's fairly trivial to count them.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="#value">
<xsl:call-template name="breakdown">
<xsl:with-param name="itemlist" select="substring-before(substring-after(.,'['),']')" />
</xsl:call-template>
</xsl:template>
<xsl:template name="breakdown">
<xsl:param name="itemlist" />
<xsl:choose>
<xsl:when test="contains($itemlist,',')">
<xsl:element name="value">
<xsl:value-of select="normalize-space(substring-before($itemlist,','))" />
</xsl:element>
<xsl:call-template name="breakdown">
<xsl:with-param name="itemlist" select="substring-after($itemlist,',')" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:element name="value">
<xsl:value-of select="normalize-space($itemlist)" />
</xsl:element>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Aside from the 'catch all' template at the bottom, this picks up any value attributes in the format you gave, and breaks them down into separate elements (as sub-elements of the 'metadata_valuelist' element) like this:
...
<metadata_valuelist>
<value>SampleItem1</value>
<value>SampleItem2</value>
</metadata_valuelist>
...
The 'substring-before/substring-after select you see near the top strips off the '[' and ']' before passing it to the 'breakdown' template. This template will check if there's a comma in it's 'itemlist' parameter, and if there is it spits out the text before it as the content of a 'value' element, before recursively calling itself with the rest of the list. If there was no comma in the parameter, it just outputs the entire content of the parameter as a 'value' element.
Then just run this:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" />
<xsl:key name="itemvalue" match="value" use="text()" />
<xsl:template match="/">
<xsl:value-of select="count(//value[generate-id(.) = generate-id(key('itemvalue',.)[1])])" />
</xsl:template>
</xsl:stylesheet>
on the XML you get from the first transform, and it'll just spit out a single value as text output that tells you how many distinct values you have.
EDIT: I should probably point out, this solution makes a few assumptions about your input:
There are no attributes named 'value' anywhere else in the document; if there are, you can modify the #value match to pick out these ones specifically.
There are no elements named 'value' anywhere else in the document; as the first transform creates them, the second will not be able to distinguish between the two. If there are, you can replace the two <xsl:element name="value"> lines with an element name that's not already used.
The content of the #value attribute always begins with '[' and ends with ']', and there are no ']' characters within the list; if there are, the 'substring-before' function will drop everything after the first ']', rather than just the ']' at the end.
There are no commas in the names of the items you want to count, e.g. [SampleItem1, "Sample2,3"]. If there are, '"Sample2' and '3"' would be treated as separate items.