Based on the following XML, what is the best way to achieve an alphanumeric sort in XSL?
Edit: to clarify, the XML below is just a simple sample the real XML would contain much more variant values.
<colors>
<item>
<label>Yellow 100</label>
</item>
<item>
<label>Blue 12</label>
</item>
<item>
<label>Orange 3</label>
</item>
<item>
<label>Yellow 10</label>
</item>
<item>
<label>Orange 26</label>
</item>
<item>
<label>Blue 117</label>
</item>
</colors>
E.g. I want a final outcome in this order:
Blue 12, Blue 117, Orange 3, Orange 26, Yellow 10, Yellow 100
This is "effectively" what I would want.
<xsl:apply-templates select="colors/item">
<xsl:sort select="label" data-type="text" order="ascending"/><!--1st sort-->
<xsl:sort select="label" data-type="number" order="ascending"/><!--2nd sort-->
</xsl:apply-templates>
<xsl:template match="item">
<xsl:value-of select="label"/>
<xsl:if test="position() != last()">,</xsl:if>
</xsl:template>
Splitting the label text into text and number part using substring-before and substring-after will do in your example (however, this is not a general approach, but you get the idea):
<xsl:template match="/">
<xsl:apply-templates select="colors/item">
<xsl:sort select="substring-before(label, ' ')" data-type="text" order="ascending"/>
<!--1st sort-->
<xsl:sort select="substring-after(label, ' ')" data-type="number" order="ascending"/>
<!--2nd sort-->
</xsl:apply-templates>
</xsl:template>
<xsl:template match="item">
<xsl:value-of select="label"/>
<xsl:if test="position() != last()">, </xsl:if>
</xsl:template>
This gives the following output:
Blue 12, Blue 117, Orange 3, Orange 26, Yellow 10, Yellow 100
Update
A more generic way to solve your sorting problem would be to have the select attribute of the xls:sort element contain a string which is sortable according to the sort rules that you expect. E.g. in this string all numbers could be padded with leading 0's so that lexicgraphically sorting them as data-type="text" will result in the correct alphanumeric order.
If you are using the XSLT engine of .NET you could use a simple extension function in C# to pad numbers with leading 0's:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:myExt="urn:myExtension"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
exclude-result-prefixes="msxsl myExt">
<xsl:output method="xml" indent="yes" />
<msxsl:script language="C#" implements-prefix="myExt">
<![CDATA[
private static string PadMatch(Match match)
{
// pad numbers with zeros to a maximum length of the largest int value
int maxLength = int.MaxValue.ToString().Length;
return match.Value.PadLeft(maxLength, '0');
}
public string padNumbers(string text)
{
return System.Text.RegularExpressions.Regex.Replace(text, "[0-9]+", new System.Text.RegularExpressions.MatchEvaluator(PadMatch));
}
]]>
</msxsl:script>
<xsl:template match="/">
<sorted>
<xsl:apply-templates select="colors/item">
<xsl:sort select="myExt:padNumbers(label)" data-type="text" order="ascending"/>
</xsl:apply-templates>
</sorted>
</xsl:template>
<xsl:template match="item">
<xsl:value-of select="label"/>
<xsl:if test="position() != last()">, </xsl:if>
</xsl:template>
</xsl:stylesheet>
Related
I'm new at XSL and still not sure of some of my terminology. I'm currently facing something I can't seem to crack. I'm attempting to
Search through all data nodes (leaf elements?) of input XML and replace text
Search through all attribute values in input XML and replace text
Copy over other nodes to output
Copy over processor instructions and comments to output
Match and process specific nodes in the input
The issues I'm facing are:
A. Not sure of the terminology (see the comments in files below) and the tack I'm taking in attacking this
B. The template (5) above, whenever it matches a node, seems to be preventing the other templates (1 and 2) from processing it
If it makes a difference, I'm running this on Windows, using Microsoft's processor and am using XSLT 1.0. I've included simplified versions of the input (Input.xml), XSLT (Transform.xslt) and the output (Output.xml) I'm getting.
I did try using "mode" to run the targeted template (5) from the generic search and replace templates (1 and 2) but, in that case, template 1 and 2 run but the targeted template (5) itself doesn't run.
I'd appreciate any comments and suggestions.
Input.xml
<?xml version="1.0" encoding="UTF-8"?>
<List>
<Item Name="Item1" Text="abcd"/>
<Item Name="Itembc" Text="qrst"/>
<Item Name="Special" Text="Hello, Worldbc"/>
<Item Name="Special" Text=""/>
</List>
Transform.xslt
<?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" xmlns:myjs="urn:custom-javascript" exclude-result-prefixes="msxsl myjs">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<msxsl:script language="JavaScript" implements-prefix="myjs">
<![CDATA[
function EscapeRegExp(str)
{
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
}
function StringReplace(strWhere, strWhat, strBy, strFlags)
{
return strWhere.replace ( new RegExp(EscapeRegExp(strWhat), strFlags), strBy);
}
]]>
</msxsl:script>
<!-- ********************************************************************************************************************** -->
<!-- -->
<!-- Because of the following 4 templates, the identity transform is not needed in this XSLT -->
<!-- -->
<!-- ********************************************************************************************************************** -->
<!-- 1 of 4: Copy all nodes from source XML to the final XML, searching and replacing -->
<!-- Modify (1 of 4) and (2 of 4) to support additional replacement -->
<!-- Search and Replace in attributes -->
<xsl:template match="#*">
<xsl:attribute name="{name()}" namespace="{namespace-uri()}">
<xsl:variable name="TempAttrValue" select="."/>
<xsl:value-of select="myjs:StringReplace(string($TempAttrValue), 'bc', '2', 'g')"/>
</xsl:attribute>
<!-- xsl:apply-templates mode="TargetedTemplate"/ -->
</xsl:template>
<!-- 2 of 4: Copy all nodes from source XML to the final XML, searching and replacing -->
<!-- Modify (1 of 4) and (2 of 4) to support additional replacement -->
<!-- Search and Replace in data nodes -->
<xsl:template match="text()">
<xsl:variable name="TempTextValue" select="."/>
<xsl:value-of select="myjs:StringReplace(string($TempTextValue), 'bc', '3', 'g')"/>
<!-- xsl:apply-templates mode="TargetedTemplate"/ -->
</xsl:template>
<!-- 3 of 4: Copy all nodes from source XML to the final XML, searching and replacing -->
<!-- Process element nodes but not attributes -->
<xsl:template match="*">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<!-- 4 of 4: Copy all nodes from source XML to the final XML, searching and replacing -->
<!-- Leave the comment nodes and processing instruction nodes alone -->
<xsl:template match="comment() | processing-instruction()">
<xsl:copy/>
</xsl:template>
<!-- 5 : Process specific nodes -->
<!-- Assumes an item in the input called Special and fills it with (No Data) if it is empty -->
<!-- Seems to be interfering with 1-4 above. Changing the [#Name='Special'] to [#Name='SpecialA'] will let 1-4 above to work -->
<xsl:template match="Item[#Name='Special']/#Text">
<xsl:attribute name="Text">
<xsl:variable name="TempSpecialText" select="."/>
<xsl:choose>
<xsl:when test="($TempSpecialText = '')">(No Data)</xsl:when>
<xsl:otherwise><xsl:value-of select="$TempSpecialText"/></xsl:otherwise>
</xsl:choose>
</xsl:attribute>
<xsl:apply-templates/>
</xsl:template>
</xsl:stylesheet>
Output.xml
<?xml version="1.0" encoding="UTF-8"?>
<List>
<Item Name="Item1" Text="a2d">
</Item>
<Item Name="Item2" Text="qrst">
</Item>
<Item Name="Special" Text="Hello, Worldbc">
</Item>
<Item Name="Special" Text="(No Data)">
</Item>
</List>
Output - Desired.xml
<?xml version="1.0" encoding="UTF-8"?>
<List>
<Item Name="Item1" Text="a2d">
</Item>
<Item Name="Item2" Text="qrst">
</Item>
<Item Name="Special" Text="Hello, World2">
</Item>
<Item Name="Special" Text="(No Data)">
</Item>
</List>
I think the main problem is with your final template
<xsl:template match="Item[#Name='Special']/#Text">
<xsl:attribute name="Text">
<xsl:variable name="TempSpecialText" select="."/>
<xsl:choose>
<xsl:when test="($TempSpecialText = '')">(No Data)</xsl:when>
<xsl:otherwise><xsl:value-of select="$TempSpecialText"/></xsl:otherwise>
</xsl:choose>
</xsl:attribute>
<xsl:apply-templates/>
</xsl:template>
This will match your <Item Name="Special" Text="Hello, Worldbc"> element ahead of the template that just matches #*. However, the Text attribute is not empty, and your xsl:otherwise simply outputs the value again, and doesn't do the replace you want. The <xsl:apply-templates/> here is unnecessary because attributes do not have any child nodes to select.
What you can do is this...
<xsl:template match="Item[#Name='Special']/#Text">
<xsl:attribute name="Text">
<xsl:variable name="TempSpecialText" select="."/>
<xsl:choose>
<xsl:when test="($TempSpecialText = '')">(No Data)</xsl:when>
<xsl:otherwise>
<xsl:value-of select="myjs:StringReplace(string($TempSpecialText), 'bc', '2', 'g')"/>
</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
<xsl:apply-templates/>
</xsl:template>
But this is duplication of code. A better solution would be to change the template only match empty attributes, like so:
<xsl:template match="Item[#Name='Special']/#Text[. = '']">
<xsl:attribute name="Text">(No Data)</xsl:attribute>
</xsl:template>
That way it will only match the <Item Name="Special" Text=""/> element, where as the <Item Name="Special" Text="Hello, Worldbc"/> will be matched by the generic #* template.
Be wary of using Microsoft specific extension functions, as this obviously limits you to running on Microsoft platforms. If you limited to XSLT 1.0, this means using a recursive template. (See Find and replace entity in xslt as an example). Alternatively, if you can switch to XSLT 2.0, the replace function comes as standard.
Would this work for you?
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
xmlns:myjs="urn:custom-javascript"
exclude-result-prefixes="msxsl myjs">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<msxsl:script language="JavaScript" implements-prefix="myjs">
<![CDATA[
function EscapeRegExp(str)
{
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
}
function StringReplace(strWhere, strWhat, strBy, strFlags)
{
return strWhere.replace ( new RegExp(EscapeRegExp(strWhat), strFlags), strBy);
}
]]>
</msxsl:script>
<!-- identity transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="#*[contains(., 'bc')]">
<xsl:attribute name="{name()}" namespace="{namespace-uri()}">
<xsl:value-of select="myjs:StringReplace(string(.), 'bc', '2', 'g')"/>
</xsl:attribute>
</xsl:template>
<xsl:template match="#*[not(string())]">
<xsl:attribute name="{name()}" namespace="{namespace-uri()}">
<xsl:text>(No Data)</xsl:text>
</xsl:attribute>
</xsl:template>
</xsl:stylesheet>
I have following xml
<Report>
<Items>
<Item>
<Id>1</Id>
<TotalSent>251</TotalSent>
<Opened>48</Opened>
<LastSend>01/07/2013 16:38:18</LastSend>
<Bounced>1</Bounced>
<Unopened>202</Unopened>
</Item>
</Items>
</Report>
i want to transform it to another xml using xslt , my desired o/p is like below
<chart subcaption ="Last sent on Monday 01 July 2013 at 16:38">
<set label="Opened" value="48"/>
<set label="Bounced" value="1"/>
</chart>
I am not able to get date as i want for subcaption attribute.
I tried below xslt code but it is not working
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:ms="urn:schemas-microsoft-com:xslt">
<xsl:output method="xml" indent="yes" omit-xml-declaration="no"/>
<xsl:template match="/">
<chart>
<xsl:variable name='lastSend' select='Report/Items/Item/LastSend' />
<xsl:attribute name="subcaption">
<xsl:value-of select="ms:format-date($lastSend, ' Last sent on MMM dd, yyyy at')"/>
<xsl:value-of select="ms:format-time($lastSend, ' hh:mm')"/>
</xsl:attribute>
<xsl:for-each select="Report/Items/Item">
<set>
<xsl:attribute name="label">Opened</xsl:attribute>
<xsl:attribute name="value">
<xsl:value-of select="Opened" />
</xsl:attribute>
</set>
<set>
<xsl:attribute name="label">Bounced</xsl:attribute>
<xsl:attribute name="value">
<xsl:value-of select="Bounced" />
</xsl:attribute>
</set>
</xsl:for-each>
</chart>
</xsl:template>
</xsl:stylesheet>
when i am passing hard coded value in ms:format-date() & ms:format-time() functions, like 01/07/2013 16:38:18 it was working fine , but when i am passing variable value $lastSend it is not working.
Note: I can use any version of xsl.
If you want to use XSLT 2.0 then you need to convert your custom date respectively dateTime format into an xs:dateTime and then you can use the format-dateTime function that XSLT 2.0 provides (see http://www.w3.org/TR/xslt20/#format-date):
<xsl:template match="LastSend">
<!-- 01/07/2013 16:38:18 -->
<xsl:variable name="dt" as="xs:dateTime" select="xs:dateTime(concat(substring(., 7, 4), '-', substring(., 4, 2), '-', substring(., 1, 2), 'T', substring(., 12)))"/>
<xsl:attribute name="subcaption" select="format-dateTime($dt, 'Last sent on [F] [D01] [MNn] [Y0001] at [H01]:[m01]')"/>
</xsl:template>
Take the above second argument "picture string" as an example on how to format a dateTime, you might need to adjust it for your needs, based on the picture string arguments documented in the XSLT 2.0 specification.
I need the maximum of three three kinds of values.
I've got a structure similar to this.
note:the first two answers are based on a previous example xml (in set 2 of night the max-above-average was 8). This was confusing, so I changed it to 7.
<data>
<record>
<max>60</max>
</record>
<day>
<set>
<average>49</average>
<max-above-average>3</max-above-average>
</set>
<set>
<average>45</average>
<max-above-average>9</max-above-average>
</set>
</day>
<night>
<set>
<average>50</average>
<max-above-average>5</max-above-average>
</set>
<set>
<average>52</average>
<max-above-average>7</max-above-average>
</set>
</night>
</data>
Now I need the maximum of the record, day and night. This would be maximum: 60, the value of the record in this example: 60 = 60, > 49 + 3, 45 +9, 50 + 5, 52+7. Day and night maximums need to be calculated. Because of this
max(//record/max | //day/set/(average + max-above-average)) | //night/set/(average +max-above-average))
does not work. The |-sign only works for nodes.
It gives following error:
Required item type of second operand of '|' is node(); supplied value has item type xs:double
I'm using xpath 2.0 and xslt 2.0.
Here are the wanted two XPath 2.0 expressions (for producing the "max" and the "min" value, respectively):
max(/*/(day|night)/*/(average+max-above-average))
and
min(/*/(day|night)/*/(average -min-above-average))
XSLT 2.0 - based verification:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="/">
max: <xsl:text/>
<xsl:sequence select=
"max(/*/(day|night)/*/(average+max-above-average))"/>
min: <xsl:text/>
<xsl:sequence select=
"min(/*/(day|night)/*/(average -min-above-average))"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML document:
<data>
<record>
<min>40</min>
<max>60</max>
</record>
<day>
<set>
<average>49</average>
<max-above-average>3</max-above-average>
<min-above-average>15</min-above-average>
</set>
<set>
<average>45</average>
<max-above-average>9</max-above-average>
<min-above-average>2</min-above-average>
</set>
</day>
<night>
<set>
<average>50</average>
<max-above-average>5</max-above-average>
<min-above-average>6</min-above-average>
</set>
<set>
<average>52</average>
<max-above-average>8</max-above-average>
<min-above-average>11</min-above-average>
</set>
</night>
</data>
the two XPath expressions are evaluated and the results of these evaluations are copied to the output:
max: 60
min: 34
Update:
The OP says in a comment that he wants "maximum of day and night ànd record" -- I really don't understand what he means by that.
Here is my attempt at guessing:
max(
(/*/record/max,
/*/(day|night)/*/(average+max-above-average, average+min-above-average)
)
)
When implanted in the XSLT transformation (above), this produces:
max: 64
An XSLT 1.0 approach:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="yes" omit-xml-declaration="yes"/>
<xsl:template match="/*">
<xsl:variable name="recordMax" select="record/max" />
<xsl:variable name="dayMax">
<xsl:apply-templates select="day" mode="max" />
</xsl:variable>
<xsl:variable name="nightMax">
<xsl:apply-templates select="night" mode="max" />
</xsl:variable>
<xsl:call-template name="Max">
<xsl:with-param name="v1" select="$recordMax" />
<xsl:with-param name="v2">
<xsl:call-template name="Max">
<xsl:with-param name="v1" select="$dayMax" />
<xsl:with-param name="v2" select="$nightMax" />
</xsl:call-template>
</xsl:with-param>
</xsl:call-template>
</xsl:template>
<xsl:template match="*[set]" mode="max">
<xsl:apply-templates select="set" mode="max">
<xsl:sort select="average + max-above-average"
data-type="number"
order="descending" />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="set" mode="max">
<xsl:if test="position() = 1">
<xsl:value-of select="average + max-above-average" />
</xsl:if>
</xsl:template>
<xsl:template name="Max">
<xsl:param name="v1" />
<xsl:param name="v2" />
<xsl:choose>
<xsl:when test="not($v2 > $v1)">
<xsl:value-of select="$v1" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$v2" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
When run on your sample input, the result is:
60
Having a little trouble with XSLT.. I think I may be going about it completely the wrong way..
Trying to display the customer name with the SKU of items with special 1 status in a line...
then the customer with the special 2 items etc. then part 2(which I haven't started) the items without a status by themselves
so for this XML file the output would be
Joe prod1 //special1
Joe prod3 //special2
Joe prod2 //no status
Joe prod4 //no status
Joe prod5 //no status
John Smith prod6 prod8 //special1
John Smith prod7 //no status
John Smith prod9 //no status
John Smith prod10 //no status
It kind of works at the moment but the problem is that if there is no special1 or special2 I can't figure out how to make it not print the Customer name..
and I'm not sure how to display the ones with no status afterwards either - any help would be much appreciated!
XML:
<customer>
<name>Joe</name>
<order>
<item>
<SKU>prod1</SKU>
<status>special1</status>
</item>
<item>
<SKU>prod2</SKU>
</item>
<item>
<SKU>prod3</SKU>
<status>special2</status>
</item>
<item>
<SKU>prod4</SKU>
</item>
<item>
<SKU>prod5</SKU>
</item>
</order>
</customer>
<customer
<name>John Smith</name>
<order>
<item>
<SKU>prod6</SKU>
<status>special1</status>
</item>
<item>
<SKU>prod7</SKU>
</item>
<item>
<SKU>prod8</SKU>
<status>special1</status>
</item>
<item>
<SKU>prod9</SKU>
</item>
<item>
<SKU>prod10</SKU>
</item>
</order>
XSLT:
<!DOCTYPE xsl:stylesheet[ <!ENTITY nl "
"> ]>
<xsl:template match="customer">
<xsl:value-of select="name" /><xsl:apply-templates select="order/item[status='special1']" /><xsl:text>&nl;</xsl:text>
<xsl:value-of select="name" /><xsl:apply-templates select="order/item[status='special2']" /><xsl:text>&nl;</xsl:text>
</xsl:template>
<xsl:template match="item[status='special1']"><xsl:text> </xsl:text><xsl:value-of select="SKU" /></xsl:template>
<xsl:template match="item[status=special2']"><xsl:text> </xsl:text><xsl:value-of select="SKU" /></xsl:template>
<xsl:template match="text()"/>
Your simplest option is just an xsl:if
<xsl:template match="customer">
<xsl:if test="order/item[status='special1']">
<xsl:value-of select="name" /><xsl:apply-templates select="order/item[status='special1']" /><xsl:text>&nl;</xsl:text>
</xsl:if>
<xsl:if test="order/item[status='special2']">
<xsl:value-of select="name" /><xsl:apply-templates select="order/item[status='special2']" /><xsl:text>&nl;</xsl:text>
</xsl:if>
</xsl:template>
I am assuming that you do not know a priori the set of different status. So, if you wan't your XML to be maintainable (without having to change it each time that you add a different status) you could use the following solution:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" />
<!-- Use the following key for grouping -->
<xsl:key name="status-key"
match="item"
use="status" />
<!-- Cache the following operation to avoid doing it several times in the future.
You can improve performance by changing // to a fixed path in your XML where
all the items are (e.g. /customers/customer/order/item) -->
<xsl:variable name="item-group"
select="//item[generate-id(.) = generate-id(key('status-key', status)[1])]" />
<xsl:template match="customer">
<!-- Obtain the following values before losing the current context -->
<xsl:variable name="current-id" select="generate-id(.)" />
<xsl:variable name="current-name" select="name" />
<!-- Display the products with a status defined -->
<xsl:for-each select="$item-group">
<!-- Obtain list of status for this costumer -->
<xsl:variable name="customer-status"
select="key('status-key', status)[generate-id(../..) = $current-id]" />
<!-- Print the status information if the costumer has at least one status -->
<xsl:if test="$customer-status">
<!-- Display the name of the costumer -->
<xsl:value-of select="$current-name" />
<!-- Group the product by status -->
<xsl:for-each select="$customer-status">
<xsl:value-of select="concat(' ', SKU)" />
</xsl:for-each>
<!-- Output the status -->
<xsl:value-of select="concat(' //', status, '
')" />
</xsl:if>
</xsl:for-each>
<!-- Display the prodcuts without status -->
<xsl:for-each select="order/item[not(status)]">
<xsl:value-of select="concat($current-name, ' ', SKU, ' //no-status
')" />
</xsl:for-each>
</xsl:template>
<xsl:template match="text()" />
</xsl:stylesheet>
This is an example of a 'grouping' problem. You are trying to group items by a combination of customer and status. The approach you take to solve this differs by whether you are using XSLT 1.0 or XSLT 2.0. In XSLT 1.0 you would use a technique called Muenchian Grouping. You start off by defing a key to hold the nodes you are grouping. In this case, you are grouping by customer and item
<xsl:key name="items" match="item" use="concat(generate-id(../..), '|', status)" />
Then, for each customer element you match, you would get the distinct status elements of each item like so:
<xsl:apply-templates select="order/item
[status != '']
[generate-id() = generate-id(key('items', concat(generate-id(../..), '|', status))[1])]" />
Essentially what this is doing is looking at each item for the customer and selecting the one that occurs first in the key you defined for the status element.
Then, in the template that matches the items with a status, you can then get the individual items with the same status like so:
<xsl:apply-templates select="key('items', concat(generate-id(../..), '|', status))/SKU" />
Selecting items with no status though is much easier
<xsl:apply-templates select="order/item[not(status != '')]" />
Here is the full XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:key name="items" match="item" use="concat(generate-id(../..), '|', status)" />
<xsl:template match="customer">
<xsl:apply-templates select="order/item[status != ''][generate-id() = generate-id(key('items', concat(generate-id(../..), '|', status))[1])]" />
<xsl:apply-templates select="order/item[not(status != '')]" />
</xsl:template>
<xsl:template match="item[status !='']">
<xsl:value-of select="../../name" />
<xsl:apply-templates select="key('items', concat(generate-id(../..), '|', status))/SKU" />
<xsl:value-of select="concat(' //', status, '
')" />
</xsl:template>
<xsl:template match="item">
<xsl:value-of select="concat(../../name, ' ', SKU, ' // no status
')" />
</xsl:template>
<xsl:template match="SKU">
<xsl:value-of select="concat(' ', .)" />
</xsl:template>
</xsl:stylesheet>
When applied to you XML, the following is output:
Joe prod1 //special1
Joe prod3 //special2
Joe prod2 // no status
Joe prod4 // no status
Joe prod5 // no status
John Smith prod6 prod8 //special1
John Smith prod7 // no status
John Smith prod9 // no status
John Smith prod10 // no status
If you were using XSLT2.0 it becomes a bit simpler to follow, because you can make us of xsl:for-each-group to handle the grouping:
<xsl:for-each-group select="order/item[status != '']" group-by="status">
And then to get the items with the group, you use the current-group() function
<xsl:apply-templates select="current-group()/SKU" />
Here is the full XSLT2.0 stylesheet which should also output the same results:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="customer">
<xsl:for-each-group select="order/item[status != '']" group-by="status">
<xsl:value-of select="../../name" />
<xsl:apply-templates select="current-group()/SKU" />
<xsl:value-of select="concat(' //', status, '
')" />
</xsl:for-each-group>
<xsl:apply-templates select="order/item[not(status != '')]" />
</xsl:template>
<xsl:template match="item">
<xsl:value-of select="concat(../../name, ' ', SKU, ' // no status
')" />
</xsl:template>
<xsl:template match="SKU">
<xsl:value-of select="concat(' ', .)" />
</xsl:template>
</xsl:stylesheet>
I am transforming following XML to generate HTML.
XML
<clause code="section6">
<variable col="1" name="R1C1" row="1">Water</variable>
<variable col="2" name="R1C2" row="1">true</variable>
<variable col="1" name="R2C1" row="2">Gas</variable>
<variable col="2" name="R2C2" row="2"></variable>
<variable col="1" name="R3C1" row="3">Petrol</variable>
<variable col="2" name="R3C2" row="3">true</variable>
<clause>
XSLT
1: <xsl:for-each select="$clause/variable[#col='1']">
2: <xsl:sort select="#row" data-type="number"/>
3: <xsl:variable name="row-id" select="#row"/>
4: <xsl:variable name="row" select="$clause/variable[#row=$row-id]"/>
5: <xsl:if test="$clause/variable[#col='2' and #row=$row-id]='true'">
6: <xsl:value-of name="row-no" select="concat(position(), ') ')"/>
7: <xsl:value-of select="$clause/variable[#col='1' and #row=$row-id]"/>
8: </xsl:if>
9: </xsl:for-each>
The transformation works fine and shows result 1) Water 3) Petrol
The issue is sequence number. You can see condition on Line 5 filters rows that only have 'true' value in col 2 and position() used for displaying sequence number. I cannot have running counter in XLST.
I was wondering if I can add condition of Line 5 with for-each at Line 1. The result with above example should be 1) Water 2) Patrol any advice?
Does this do what you want?
I drive the for-each selection on col 2 being true. That way position, which equals where we are in the selected set of nodes will equal 2 not 3
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:template match="/">
<xsl:for-each select="clause/variable[#col='2' and text()='true']">
<xsl:sort select="#row" data-type="number"/>
<xsl:variable name="row-id" select="#row"/>
<xsl:value-of select="concat(position(), ') ')"/>
<xsl:value-of select="/clause/variable[#col='1' and #row=$row-id]"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Though I'd probably use templates in preference to the for-each and use current() so that we don't need the row-id variable:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:template match="/">
<xsl:apply-templates select="clause/variable[#col='2' and text()='true']">
<xsl:sort select="#row" data-type="number"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="clause/variable[#col='2' and text()='true']">
<xsl:value-of select="concat(position(), ') ')"/>
<xsl:value-of select="/clause/variable[#col='1' and #row=current()/#row]"/>
</xsl:template>
</xsl:stylesheet>
I don't think you can express this with a single XPath 1.0 expression.
In XSLT 1.0 I will use keys and the solution becomes short, elegant and efficient:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:key name="kVarCol2" match="variable[#col=2]" use="#row"/>
<xsl:template match="/*">
<xsl:for-each select="variable[#col='1'][key('kVarCol2', #row)='true']">
<xsl:sort select="#row" data-type="number"/>
<xsl:value-of select="concat('
', position(), ') ', .)"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML document (corrected to be made well-formed):
<clause code="section6">
<variable col="1" name="R1C1" row="1">Water</variable>
<variable col="2" name="R1C2" row="1">true</variable>
<variable col="1" name="R2C1" row="2">Gas</variable>
<variable col="2" name="R2C2" row="2"></variable>
<variable col="1" name="R3C1" row="3">Petrol</variable>
<variable col="2" name="R3C2" row="3">true</variable>
</clause>
the wanted, correct result is produced:
1) Water
2) Petrol
II. XPath 2.0 (single expression) solution:
for $i in 1 to max(/*/*/#row/xs:integer(.))
return
/*/variable[#row eq string($i)]
[#col eq '1'
and
../variable
[#row eq string($i)
and
#col eq '2'
and
. eq 'true'
]
]
/concat('
', position(), ') ', .)
When this XPath 2.0 expression is evaluated on the same XML document (above), the result is the same wanted string:
1) Water
2) Petrol