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.
Related
Can any guide me to split the given xml element values into multiple child elements based on a token. Here is my sample input xml and desired output. I have a limitation to use xsl 1.0. Thank you.
Input XML:
<?xml version='1.0' encoding='UTF-8'?>
<SQLResults>
<SQLResult>
<ACTION1>Action1</ACTION1>
<ACTION2>Action2</ACTION2>
<Encrypt>Program=GPG;Code=23FCS;</Encrypt>
<SENDER>Program=WebPost;Protocol=WS;Path=/home/Inbound</SENDER>
</SQLResult>
</SQLResults>
Output XML:
<?xml version='1.0' encoding='UTF-8'?>
<SQLResults>
<SQLResult>
<ACTION1>Action1</ACTION1>
<ACTION2>Action2</ACTION2>
<Encrypt>
<Program>GPG</Program>
<Code>23FCS</Code>
</Encrypt>
<SENDER>
<Program>Action4</Program>
<Protocol>WS</Protocol>
<Path>/home/Inbound</Path>
</SENDER>
</SQLResult>
</SQLResults>
In XSLT 2 it would be easy, just with the following template:
<xsl:template match="Encrypt|SENDER">
<xsl:copy>
<xsl:analyze-string select="." regex="(\w+)=([\w/]+);?">
<xsl:matching-substring>
<element name="{regex-group(1)}">
<xsl:value-of select="regex-group(2)"/>
</element>
</xsl:matching-substring>
</xsl:analyze-string>
</xsl:copy>
</xsl:template>
Because you want to do it in XSLT 1, you have to express it another way.
Instead of analyze-string you have to:
Tokenize the content into non-empty tokens contained between ; chars.
You have to add tokenize template.
Each such token divide into 2 substrings, before and after = char.
Create an element with the name equal to the first substring.
Write the content of this element - the second substring.
XSLT 1 has also such limitation that the result of the tokenize template
is a result tree fragment (RTF) not the node set and thus it cannot be
used in XPath expressions.
To circumvent this limitation, you must use exsl:node-set function.
So the whole script looks like below:
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:transform version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common">
<xsl:output method="xml" encoding="UTF-8" indent="yes" />
<xsl:strip-space elements="*"/>
<xsl:template match="Encrypt|SENDER">
<xsl:copy>
<xsl:variable name="tokens">
<xsl:call-template name="tokenize">
<xsl:with-param name="txt" select="."/>
<xsl:with-param name="delim" select="';'"/>
</xsl:call-template>
</xsl:variable>
<xsl:for-each select="exsl:node-set($tokens)/token">
<xsl:variable name="t1" select="substring-before(., '=')"/>
<xsl:variable name="t2" select="substring-after(., '=')"/>
<xsl:element name="{$t1}">
<xsl:value-of select="$t2" />
</xsl:element>
</xsl:for-each>
</xsl:copy>
</xsl:template>
<xsl:template name="tokenize">
<xsl:param name="txt" />
<xsl:param name="delim" select="' '" />
<xsl:choose>
<xsl:when test="$delim and contains($txt, $delim)">
<token>
<xsl:value-of select="substring-before($txt, $delim)" />
</token>
<xsl:call-template name="tokenize">
<xsl:with-param name="txt" select="substring-after($txt, $delim)" />
<xsl:with-param name="delim" select="$delim" />
</xsl:call-template>
</xsl:when>
<xsl:when test="$txt">
<token><xsl:value-of select="$txt" /></token>
</xsl:when>
</xsl:choose>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy><xsl:apply-templates select="#*|node()"/></xsl:copy>
</xsl:template>
</xsl:transform>
I would like to ask if there is a function that can be use to to remove a duplicate value inside a string separated by | simplest possible way. I have below example of the string
1111-1|1111-1|1111-3|1111-4|1111-5|1111-3
the output that I'm expecting is:
1111-1|1111-3|1111-4|1111-5
Thanks in advance.
All presented XSLT 1.0 solutions so far produce the wrong result:
1111-1|1111-4|1111-5|1111-3
whereas the wanted, correct result is:
1111-1|1111-3|1111-4|1111-5
Now, the following transformation (no extensions, pure XSLT 1.0):
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="text()" name="distinctSubstrings">
<xsl:param name="pText" select="."/>
<xsl:param name="poutDelim"/>
<xsl:param name="pFoundDistinctSubs" select="'|'"/>
<xsl:param name="pCountDistinct" select="0"/>
<xsl:if test="$pText">
<xsl:variable name="vnextSub" select="substring-before(concat($pText, '|'), '|')"/>
<xsl:variable name="vIsNewDistinct" select=
"not(contains(concat($pFoundDistinctSubs, '|'), concat('|', $vnextSub, '|')))"/>
<xsl:variable name="vnextDistinct" select=
"substring(concat($poutDelim,$vnextSub), 1 div $vIsNewDistinct)"/>
<xsl:value-of select="$vnextDistinct"/>
<xsl:variable name="vNewFoundDistinctSubs"
select="concat($pFoundDistinctSubs, $vnextDistinct)"/>
<xsl:variable name="vnextOutDelim"
select="substring('|', 2 - ($pCountDistinct > 0))"/>
<xsl:call-template name="distinctSubstrings">
<xsl:with-param name="pText" select="substring-after($pText, '|')"/>
<xsl:with-param name="pFoundDistinctSubs" select="$vNewFoundDistinctSubs"/>
<xsl:with-param name="pCountDistinct" select="$pCountDistinct + $vIsNewDistinct"/>
<xsl:with-param name="poutDelim" select="$vnextOutDelim"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
when applied on this XML document (with string value the provided string in the question):
<t>1111-1|1111-1|1111-3|1111-4|1111-5|1111-3</t>
produces the wanted, correct result:
1111-1|1111-3|1111-4|1111-5
Explanation:
All found distinct substrings are concatenated in the parameter $pFoundDistinctSubs -- whenever we get the next substring from the delimited input, we compare it to the distinct substrings passed in this parameter. This ensures that the first in order distinct substring will be output -- not the last as in the other two solutions.
We use conditionless value determination, based on the fact that XSLT 1.0 implicitly converts a Boolean false() to 0 and true() to 1 whenever it is used in a context that requires a numeric value. In particular, substring($x, 1 div true()) is equivalent to substring($x, 1 div 1) that is: substring($x, 1) and this is the entire string $x. On the other side, substring($x, 1 div false()) is equivalent to substring($x, 1 div 0) -- that is: substring($x, Infinity) and this is the empty string.
To know why avoiding conditionals is important: watch this Pluralsight course:
Tactical Design Patterns in .NET: Control Flow, by Zoran Horvat
To do this in pure XSLT 1.0, with no extension functions, you will need to use a recursive named template:
<xsl:template name="distinct-values-from-list">
<xsl:param name="list"/>
<xsl:param name="delimiter" select="'|'"/>
<xsl:choose>
<xsl:when test="contains($list, $delimiter)">
<xsl:variable name="token" select="substring-before($list, $delimiter)" />
<xsl:variable name="next-list" select="substring-after($list, $delimiter)" />
<!-- output token if it is unique -->
<xsl:if test="not(contains(concat($delimiter, $next-list, $delimiter), concat($delimiter, $token, $delimiter)))">
<xsl:value-of select="concat($token, $delimiter)"/>
</xsl:if>
<!-- recursive call -->
<xsl:call-template name="distinct-values-from-list">
<xsl:with-param name="list" select="$next-list"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$list"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
Full demo: http://xsltransform.net/ncdD7mM
Added:
The above method outputs the last occurrence of each value in the list, because that's the simplest way to remove the duplicates.
The side effect of this is that the original order of the values is not preserved. Or - more correctly - it is the reverse order that is being preserved.
I would not think preserving the original forward order is of any importance here. But in case you do need it, it could be done this way (which I believe is much easier to follow than the suggested alternative):
<xsl:template name="distinct-values-from-list">
<xsl:param name="list"/>
<xsl:param name="delimiter" select="'|'"/>
<xsl:param name="result"/>
<xsl:choose>
<xsl:when test="$list">
<xsl:variable name="token" select="substring-before(concat($list, $delimiter), $delimiter)" />
<!-- recursive call -->
<xsl:call-template name="distinct-values-from-list">
<xsl:with-param name="list" select="substring-after($list, $delimiter)"/>
<xsl:with-param name="result">
<xsl:value-of select="$result"/>
<!-- add token if this is its first occurrence -->
<xsl:if test="not(contains(concat($delimiter, $result, $delimiter), concat($delimiter, $token, $delimiter)))">
<xsl:value-of select="concat($delimiter, $token)"/>
</xsl:if>
</xsl:with-param>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="substring($result, 2)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
Assuming that you can use XSLT 2.0, and assuming that the input looks like
<?xml version="1.0" encoding="UTF-8"?>
<root>1111-1|1111-1|1111-3|1111-4|1111-5|1111-3</root>
you could use the distinct-values and tokenize functions:
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output method="xml" encoding="UTF-8" indent="yes" />
<xsl:template match="/root">
<result>
<xsl:value-of separator="|" select="distinct-values(tokenize(.,'\|'))"/>
</result>
</xsl:template>
</xsl:transform>
And the result will be
<?xml version="1.0" encoding="UTF-8"?>
<result>1111-1|1111-3|1111-4|1111-5</result>
I have adapted a stylesheet below from (XSLT 1.0 How to get distinct values)
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output omit-xml-declaration="yes"/>
<xsl:template match="/">
<output>
<xsl:call-template name="distinctvalues">
<xsl:with-param name="values" select="root"/>
</xsl:call-template>
</output>
</xsl:template>
<xsl:template name="distinctvalues">
<xsl:param name="values"/>
<xsl:variable name="firstvalue" select="substring-before($values, '|')"/>
<xsl:variable name="restofvalue" select="substring-after($values, '|')"/>
<xsl:if test="not(contains($values, '|'))">
<xsl:value-of select="$values"/>
</xsl:if>
<xsl:if test="contains($restofvalue, $firstvalue) = false">
<xsl:value-of select="$firstvalue"/>
<xsl:text>|</xsl:text>
</xsl:if>
<xsl:if test="$restofvalue != ''">
<xsl:call-template name="distinctvalues">
<xsl:with-param name="values" select="$restofvalue" />
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
with a sample input of:
<root>1111-1|1111-1|1111-3|1111-4|1111-5|1111-3</root>
and the output is
<output>1111-1|1111-4|1111-5|1111-3</output>
**** EDIT ****
per Michael's comment below, here is the revised stylesheet which uses a saxon extension:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:saxon="http://icl.com/saxon"
exclude-result-prefixes="saxon"
version="1.1">
<xsl:output omit-xml-declaration="yes"/>
<xsl:variable name="aaa">
<xsl:call-template name="tokenizeString">
<xsl:with-param name="list" select="root"/>
<xsl:with-param name="delimiter" select="'|'"/>
</xsl:call-template>
</xsl:variable>
<xsl:template match="/">
<xsl:for-each select="saxon:node-set($aaa)/token[not(preceding::token/. = .)]">
<xsl:if test="position() > 1">
<xsl:text>|</xsl:text>
</xsl:if>
<xsl:value-of select="."/>
</xsl:for-each>
</xsl:template>
<xsl:template name="tokenizeString">
<!--passed template parameter -->
<xsl:param name="list"/>
<xsl:param name="delimiter"/>
<xsl:choose>
<xsl:when test="contains($list, $delimiter)">
<token>
<!-- get everything in front of the first delimiter -->
<xsl:value-of select="substring-before($list,$delimiter)"/>
</token>
<xsl:call-template name="tokenizeString">
<!-- store anything left in another variable -->
<xsl:with-param name="list" select="substring-after($list,$delimiter)"/>
<xsl:with-param name="delimiter" select="$delimiter"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:choose>
<xsl:when test="$list = ''">
<xsl:text/>
</xsl:when>
<xsl:otherwise>
<token>
<xsl:value-of select="$list"/>
</token>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
given an input of:
<root>cat|cat|catalog|catalog|red|red|wired|wired</root>
it outputs
cat|catalog|red|wired
and with this input:
<root>1111-1|1111-1|1111-3|1111-4|1111-5|1111-3</root>
the output is
1111-1|1111-3|1111-4|1111-5
What would be an efficient way to reorder a group of nodes selected using xsl:choose (XSLT 1.0).
Below is the sample source XML:
<Universe>
<CObj>
<Galaxies>
<Galaxy>
<Profile>
<Name>MilkyWay</Name>
<Age>12.5</Age>
</Profile>
<PlanetarySystem>
<Name>Solar</Name>
<Location></Location>
<Planet>
<Name>Earth</Name>
<Satellite>Y</Satellite>
...
...
...
</Planet>
...
...
...
</PlanetarySystem>
<PlanetarySystem>
...
...
...
</PlanetarySystem>
</Galaxy>
<Galaxy>
...
...
...
</Galaxy>
</Galaxies>
</CObj>
</Universe>
XSL snippet:
<xsl:template name="get-galaxy-types">
<xsl:variable name="galaxy_age1" select ="1" />
<xsl:variable name="galaxy_age2" select ="5" />
<xsl:variable name="galaxy_age3" select ="10" />
<xsl:for-each select="Galaxies/Galaxy/Profile/Age">
<xsl:choose>
<xsl:when test=".=$galaxy_age2">
<GalaxyType2>
<xsl:value-of select="../Profile/Name"/>
</GalaxyType2>
</xsl:when>
<xsl:when test=".=$galaxy_age3">
<GalaxyType3>
<xsl:value-of select="../Profile/Name"/>
</GalaxyType3>
</xsl:when>
<xsl:when test=".=$galaxy_age1">
<GalaxyType1>
<xsl:value-of select="../Profile/Name"/>
</GalaxyType1>
</xsl:when>
</xsl:choose>
</xsl:for-each>
Above XSL template is called from main template like:
<xsl:template match="Universe">
<GalaxyTypes>
<xsl:call-template name="get-galaxy-types"/>
</GalaxyTypes>
</xsl:template>
Output XML:
Note that the order of <GalaxyType> cannot be changed.
<Universe>
...
...
...
<GalaxyTypes>
<GalaxyType2>xxxxxx</GalaxyType2>
<GalaxyType3>xxxxxx</GalaxyType3>
<GalaxyType1>xxxxxx</GalaxyType1>
</GalaxyTypes>
...
...
...
</Universe>
Since xsl:choose returns the XML nodes as and when it finds a match I am unable to find a straight forward way to control the order in which I want GalaxyType to appear in the output XML.
How can I have a generic template to perform reordering for any elements that might get added in the future that may fall in to similar requirement. I am fine with having a remapping template within this XSL but I am not really sure how to accomplish this in a really elegant and efficient way.
I am going to guess you want to put the galaxies matching galaxy-age1 first, then the ones matching galaxy-age2 next, and finally the ones for galaxy-age3. I am also assuming the ages specified may not be in ascending order (that is to say, galaxy-age3 could be less that galaxy-age1.
To start with, it might be more natural to do your xsl:for-each over the Galaxy elements
<xsl:for-each select="Galaxies/Galaxy">
Then, to do your customisable sort, you could first define a variable like so...
<xsl:variable name="sortAges"
select="concat('-', $galaxy_age1, '-', $galaxy_age2, '-', $galaxy_age3, '-')" />
Note the order the parameters appear in the concat statement corresponds to the order they need to be output.
Then, your xsl:for-each could look this this...
<xsl:for-each select="Galaxies/Galaxy">
<xsl:sort select="string-length(substring-before($sortAges, concat('-', Profile/Age, '-')))" />
But this is not very elegant. It might be better to simply have a template that matches Galaxy and use three separate xsl:apply-templates to select the Galaxy; one for each age.
Try this XSLT too
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes" />
<xsl:param name="galaxy_age1" select="1" />
<xsl:param name="galaxy_age2" select="5" />
<xsl:param name="galaxy_age3" select="10" />
<xsl:template match="Universe">
<GalaxyTypes>
<xsl:apply-templates select="Galaxies/Galaxy[Profile/Age = $galaxy_age1]">
<xsl:with-param name="num" select="1" />
</xsl:apply-templates>
<xsl:apply-templates select="Galaxies/Galaxy[Profile/Age = $galaxy_age2]">
<xsl:with-param name="num" select="2" />
</xsl:apply-templates>
<xsl:apply-templates select="Galaxies/Galaxy[Profile/Age = $galaxy_age3]">
<xsl:with-param name="num" select="3" />
</xsl:apply-templates>
</GalaxyTypes>
</xsl:template>
<xsl:template match="Galaxy">
<xsl:param name="num" />
<xsl:element name="Galaxy{$num}">
<xsl:value-of select="Profile/Name"/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
EDIT: To make this more efficient, consider using a key to look up the Galaxy elements by their name. Try this XSLT too
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes" />
<xsl:param name="galaxy_age1" select="1" />
<xsl:param name="galaxy_age2" select="10" />
<xsl:param name="galaxy_age3" select="5" />
<xsl:key name="galaxy" match="Galaxy" use="Profile/Age" />
<xsl:template match="Universe">
<GalaxyTypes>
<xsl:apply-templates select="key('galaxy', $galaxy_age1)">
<xsl:with-param name="num" select="1" />
</xsl:apply-templates>
<xsl:apply-templates select="key('galaxy', $galaxy_age2)">
<xsl:with-param name="num" select="2" />
</xsl:apply-templates>
<xsl:apply-templates select="key('galaxy', $galaxy_age3)">
<xsl:with-param name="num" select="3" />
</xsl:apply-templates>
</GalaxyTypes>
</xsl:template>
<xsl:template match="Galaxy">
<xsl:param name="num" />
<xsl:element name="Galaxy{$num}">
<xsl:value-of select="Profile/Name"/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
It's very difficult to navigate between the scattered snippets of your code. Still, it seems to me you should change your strategy to something like:
<xsl:template match="?">
...
<GalaxyTypes>
<xsl:apply-templates select="??/???/Galaxy">
<xsl:sort select="Profile/Age" data-type="number" order="ascending"/>
</xsl:apply-templates=>
</GalaxyTypes>
...
</xsl:template>
<xsl:template match="Galaxy">
<xsl:choose>
<xsl:when test="Profile/Age=$galaxy_age1">
<GalaxyType1>
<xsl:value-of select="Profile/Name"/>
</GalaxyType1>
</xsl:when>
<xsl:when test="Profile/Age=$galaxy_age2">
<GalaxyType2>
<xsl:value-of select="Profile/Name"/>
</GalaxyType2>
</xsl:when>
<xsl:when test="Profile/Age=$galaxy_age3">
<GalaxyType3>
<xsl:value-of select="Profile/Name"/>
</GalaxyType3>
</xsl:when>
</xsl:choose>
</xsl:template>
--
Note that your output would be much better formatted if all galaxies were a uniform <Galaxy> element, with a type attribute to tell them apart.
Input:
<text>
Please see the registered mark® .
Please see the copy right ©.
Please see the Trade mark™.
</text>
Output:
<text>
Please see the registered mark<registeredTrademark></registeredTrademark>.
Please see the copy right <copyright></copyright>.
Please see the Trade mark <trademark></trademark>.
</text>
I need to replace all special symbols with the elements as shown above
Can any one help.
Thanks
As this is XSLT 1.0, you are going to have to use a recursive named template to check each character in turn.
Firstly, it may be more flexible to create a sort of 'look-up' in your XSLT where you can specify a list of symbols and the required element name to replace them with
<lookup:codes>
<code symbol="®">registeredTrademark</code>
<code symbol="©">copyright</code>
<code symbol="™">trademark</code>
</lookup:codes>
(The 'lookup' namespace could actually be named anything, just as long as it is declard in the XSLT).
Then, to access this, you could define a variable to access this look-up
<xsl:variable name="lookup" select="document('')/*/lookup:codes"/>
And, to look-up an actually code based on a symbol would do something like this (where $text) is a variable that contains the text you are checking.
<xsl:variable name="char" select="substring($text, 1, 1)"/>
<xsl:variable name="code" select="$lookup/code[#symbol = $char]"/>
All the named template would do, is check the first character of the text, replacing it with an element if it exists in the lookup, and then recursively call the template with the remaining part of the text.
Here is the full XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:lookup="lookup" exclude-result-prefixes="lookup">
<xsl:output method="xml" indent="no"/>
<lookup:codes>
<code symbol="®">registeredTrademark</code>
<code symbol="©">copyright</code>
<code symbol="™">trademark</code>
</lookup:codes>
<xsl:variable name="lookup" select="document('')/*/lookup:codes"/>
<xsl:template match="text[text()]">
<text>
<xsl:call-template name="text"/>
</text>
</xsl:template>
<xsl:template name="text">
<xsl:param name="text" select="text()"/>
<xsl:variable name="char" select="substring($text, 1, 1)"/>
<xsl:variable name="code" select="$lookup/code[#symbol = $char]"/>
<xsl:choose>
<xsl:when test="$code"><xsl:element name="{$code}" /></xsl:when>
<xsl:otherwise>
<xsl:value-of select="$char"/>
</xsl:otherwise>
</xsl:choose>
<xsl:if test="string-length($text) > 1">
<xsl:call-template name="text">
<xsl:with-param name="text" select="substring($text, 2, string-length($text) - 1)"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
When applied to your sample XML, the following is output
<text>
Please see the registered mark<registeredTrademark /> .
Please see the copy right <copyright />.
Please see the Trade mark<trademark />.
</text>
This transformation is more efficient by avoiding char-by-char recursion and using "biggest-possible-step" recursion:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="my:my">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<my:reps>
<r char="®">registeredTrademark</r>
<r char="©">copyright</r>
<r char="™">trademark</r>
</my:reps>
<xsl:variable name="vReps" select="document('')/*/my:reps/*"/>
<xsl:template match="text()" name="multReplace">
<xsl:param name="pText" select="."/>
<xsl:param name="pReps" select="$vReps"/>
<xsl:if test="$pText">
<xsl:variable name="vTarget" select="$pReps[1]/#char"/>
<xsl:choose>
<xsl:when test="not($vTarget)">
<xsl:value-of select="$pText"/>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="vReplacement" select="$pReps[1]"/>
<xsl:call-template name="multReplace">
<xsl:with-param name="pText" select=
"substring-before(concat($pText, $vTarget), $vTarget)"/>
<xsl:with-param name="pReps" select="$pReps[position() >1]"/>
</xsl:call-template>
<xsl:if test="contains($pText, $vTarget)">
<xsl:element name="{$vReplacement}"/>
<xsl:call-template name="multReplace">
<xsl:with-param name="pText" select="substring-after($pText, $vTarget)"/>
<xsl:with-param name="pReps" select="$pReps"/>
</xsl:call-template>
</xsl:if>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
When applied to the provided XML document:
<text>
Please see the registered mark® .
Please see the copy right ©.
Please see the Trade mark™.
</text>
the correctly-replaced text is produced:
Please see the registered mark<registeredTrademark/> .
Please see the copy right <copyright/>.
Please see the Trade mark<trademark/>.
I saw a similar question on creating a Map.
That answer has this code:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt">
<xsl:template match="/">
<xsl:variable name="map">
<map>
<entry key="key-1">value1</entry>
<entry key="key-2">value2</entry>
<entry key="key-3">value3</entry>
</map>
</xsl:variable>
<output>
<xsl:value-of select="msxsl:node-set($map)/map/entry[#key='key-1']"/>
</output>
</xsl:template>
I would like to replace the output command to use a value in my XML to see if it is a key in the map and then replace it with the value.
Is the best way to do a for-each select on the map and compare with contains?
Here is a snippet of the XML:
<document>
<content name="PART_DESC_SHORT" type="text" vse-streams="2" u="22" action="cluster" weight="1">
SCREW - ADJUST
</content>
</document>
The content node value may have a string containing an abbreviation that I want to replace with the full value.
Thanks,
Paul
No need to use a for-each - this XSLT:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt">
<xsl:output indent="yes" method="xml" />
<xsl:variable name="abbreviations">
<abbreviation key="Brkt Pivot R">Bracket Pivot R</abbreviation>
<abbreviation key="Foo">Expanded Foo</abbreviation>
<abbreviation key="Bar">Expanded Bar</abbreviation>
</xsl:variable>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="text()">
<xsl:variable name="text" select="."/>
<xsl:variable name="abbreviation" select="msxsl:node-set($abbreviations)/*[#key=$text]"/>
<xsl:choose>
<xsl:when test="$abbreviation">
<xsl:value-of select="$abbreviation"/>
</xsl:when>
<xsl:otherwise>
<xsl:copy/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
will convert any XML in an exact copy expanding all the text matching an abbreviation defined in the abbreviations variable at the top.
If you want to expand the abbreviations only within specific elements you can modify the second template match="..." rule.
On the other hand if you want to expand ANY occurance of all the abbreviations within the text you need loops - that means recursion in XSLT:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt">
<xsl:output indent="yes" method="xml" />
<xsl:variable name="abbreviations">
<abbreviation key="Brkt">Bracket</abbreviation>
<abbreviation key="As">Assembly</abbreviation>
<abbreviation key="Foo">Expanded Foo</abbreviation>
<abbreviation key="Bar">Expanded Bar</abbreviation>
</xsl:variable>
<!-- Replaces all occurrences of a string with another within a text -->
<xsl:template name="replace">
<xsl:param name="text"/>
<xsl:param name="from"/>
<xsl:param name="to"/>
<xsl:choose>
<xsl:when test="contains($text,$from)">
<xsl:value-of select="concat(substring-before($text,$from),$to)"/>
<xsl:call-template name="replace">
<xsl:with-param name="text" select="substring-after($text,$from)"/>
<xsl:with-param name="from" select="$from"/>
<xsl:with-param name="to" select="$to"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$text"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- Replace all occurences of a list of abbreviation with their expanded version -->
<xsl:template name="replaceAbbreviations">
<xsl:param name="text"/>
<xsl:param name="abbreviations"/>
<xsl:choose>
<xsl:when test="count($abbreviations)>0">
<xsl:call-template name="replaceAbbreviations">
<xsl:with-param name="text">
<xsl:call-template name="replace">
<xsl:with-param name="text" select="$text"/>
<xsl:with-param name="from" select="$abbreviations[1]/#key"/>
<xsl:with-param name="to" select="$abbreviations[1]"/>
</xsl:call-template>
</xsl:with-param>
<xsl:with-param name="abbreviations" select="$abbreviations[position()>1]"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$text"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="text()">
<xsl:call-template name="replaceAbbreviations">
<xsl:with-param name="text" select="."/>
<xsl:with-param name="abbreviations" select="msxsl:node-set($abbreviations)/*"/>
</xsl:call-template>
</xsl:template>
</xsl:stylesheet>
Applying this second XSLT to
<document>
<content name="PART_DESC_SHORT" type="text" vse-streams="2" u="22" action="cluster" weight="1">
Brkt Pivot R
</content>
</document>
produces
<document>
<content name="PART_DESC_SHORT" type="text" vse-streams="2" u="22" action="cluster" weight="1">
Bracket Pivot R
</content>
</document>
Note that:
this solution assumes that no abbreviation ovelap (e.g. two separate abbreviations Brk and Brkt)
it uses XSLT 1.0 - a better solution is probably possible with XSLT 2.0
this kind of heavy string processing is likely quite inefficient in XSLT, it is probably better to write an extension function in some other language and call it from the XSLT.