Xslt for-each and key match - xslt

I have two xml data sources like this:
<orders>
<order>
<ordernumber>123</ordernumber>
<subtotal>20</subtotal>
<total>23.5</total>
</order>
<order>
<ordernumber>234</ordernumber>
<subtotal>19</subtotal>
<total>26.5</total>
</order>
</orders>
<orderitems>
<item>
<ordernumber>123</ordernumber>
<productname>test1</productname>
<sku>s9sdidk</sku>
<item>
<item>
<ordernumber>123</ordernumber>
<productname>test2</productname>
<sku>123232</sku>
<item>
<item>
<ordernumber>234</ordernumber>
<productname>test3</productname>
<sku>s9sd2d32k</sku>
<item>
<item>
<ordernumber>234</ordernumber>
<productname>test4</productname>
<sku>s9swe23</sku>
<item>
</orderitems>
and then I need to use xslt to group items by order number and get an output like this:
productname sku
test1 s9sdidk
test2 123232
---------------------------------
subtotal: 20
total: 23.5
productname sku
test3 s9sd2d32k
test4 s9swe23
---------------------------------
subtotal: 19
total: 26.5
I need to use html tags so I can't use for-each-group thing...please help!
thanks in advance.

This stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:key name="kItemByOrdernumber" match="item" use="ordernumber"/>
<xsl:variable name="vSource2" select="document('source2.xml')"/>
<xsl:template match="text()"/>
<xsl:template match="order/ordernumber">
<xsl:variable name="vCurrent" select="."/>
<xsl:text>productname sku
</xsl:text>
<xsl:for-each select="$vSource2">
<xsl:apply-templates
select="key('kItemByOrdernumber',$vCurrent)"/>
</xsl:for-each>
<xsl:value-of
select="concat('---------------------------------','
',
' subtotal: ',../subtotal,'
',
' total: ',../total,'
')"/>
</xsl:template>
<xsl:template match="item">
<xsl:value-of select="concat(productname,' ',
sku,'
')"/>
</xsl:template>
</xsl:stylesheet>
With first document as input and second document as source2.xml external input, output:
productname sku
test1 s9sdidk
test2 123232
---------------------------------
subtotal: 20
total: 23.5
productname sku
test3 s9sd2d32k
test4 s9swe23
---------------------------------
subtotal: 19
total: 26.5

This transformation:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:key name="kitemByOrder" match="item" use="ordernumber"/>
<xsl:param name="pmaxSize" select="100"/>
<xsl:template match="*">
<xsl:apply-templates />
</xsl:template>
<xsl:template match="order">
productname<xsl:text> </xsl:text>sku
<xsl:apply-templates select="key('kitemByOrder', ordernumber)"/>
---------------------------------
subtotal: <xsl:value-of select="subtotal"/>
total: <xsl:value-of select="total"/>
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template match="item">
<xsl:value-of select="concat(productname, ' ', sku, '
')"/>
</xsl:template>
<xsl:template match="orderitems"/>
</xsl:stylesheet>
when applied on the provided XML document (corrected as it was severely non-well-formed):
<t>
<orders>
<order>
<ordernumber>123</ordernumber>
<subtotal>20</subtotal>
<total>23.5</total>
</order>
<order>
<ordernumber>234</ordernumber>
<subtotal>19</subtotal>
<total>26.5</total>
</order>
</orders>
<orderitems>
<item>
<ordernumber>123</ordernumber>
<productname>test1</productname>
<sku>s9sdidk</sku>
</item>
<item>
<ordernumber>123</ordernumber>
<productname>test2</productname>
<sku>123232</sku>
</item>
<item>
<ordernumber>234</ordernumber>
<productname>test3</productname>
<sku>s9sd2d32k</sku>
</item>
<item>
<ordernumber>234</ordernumber>
<productname>test4</productname>
<sku>s9swe23</sku>
</item>
</orderitems>
</t>
produces the wanted, correct result:
productname sku
test1 s9sdidk
test2 123232
---------------------------------
subtotal: 20
total: 23.5
productname sku
test3 s9sd2d32k
test4 s9swe23
---------------------------------
subtotal: 19
total: 26.5
Note: You may use two different XML documents for the two data sources, but the processing is essentially the same, with the exception that you need to address one of them using the document() function.

Related

XSLT: Trying to combine two nodes based on the value of an element of those two nodes

I am struggling with XSLT and have been for days. I'm in the process of modifying a previous coworkers python code that transforms many different JSON files into xml and finally into kml. I'm within a hairs breadth of wrapping this up and, of course, the one part I can't get my head around is what's left.
I have an xml file with this structure:
<?xml version="1.0" ?>
<document xmlns="http://ws.wso2.org/dataservice">
<group>
<display_name>Housing</display_name>
<id>Housing</id>
<item>
<id>5063</id>
<image_url>images/5063.jpg</image_url>
<latitude>40.354007</latitude>
<longitude>-74.666675</longitude>
<name>Stanworth Apartments</name>
</item>
.
. (Many items omitted)
.
</group>
<group>
<display_name>Buildings</display_name>
<id>Building</id>
<item>
<id>5025</id>
<image_url>images/5025.jpg</image_url>
<latitude>40.350066</latitude>
<longitude>-74.603464</longitude>
<name>Lyman Spitzer Building</name>
<name_alt>LSB</name_alt>
<organization_id>ORG418</organization_id>
</item>
.
. (Many items omitted)
.
</group>
<group>
.
. (Many groups omitted)
.
</group>
<group>
<display_name>Accessible Features</display_name>
<id>Entryway</id>
<item>
<description>Accessible entryway</description>
<id>E028</id>
<latitude>40.349159</latitude>
<longitude>-74.658629</longitude>
<name>E028</name>
</item>
<item>
<description>Accessible entryway</description>
<id>E029</id>
<latitude>40.349398</latitude>
<longitude>-74.658517</longitude>
<name>E029</name>
</item>
</group>
<group>
<display_name>Accessible Features</display_name>
<id>Route</id>
<item>
<description>Accessible pathway</description>
<id>R054</id>
<name>R054</name>
<steps>-74.66032495749012,40.3489269473544</steps>
<steps>-74.6602836233495,40.34888813533125</steps>
</item>
<item>
<description>Accessible pathway</description>
<id>R055</id>
<name>R055</name>
<steps>-74.66023036637355,40.34884827131961</steps>
<steps>-74.66018651597699,40.34881015960344</steps>
</item>
<item>
<description>Accessible pathway</description>
<id>R072</id>
<name>R072</name>
<steps>-74.66101885775542,40.34737535360176</steps>
<steps>-74.6610915120654,40.34740600913134</steps>
<steps>-74.66187000551304,40.34717392492537</steps>
</item>
</group>
</document>
Each "group" is transformed into a Folder in the final KML file.
<Folder id="Housing">
<name>Housing</name>
<Placemark id="_0288">
.
. (Many lines omitted)
.
The goal is to create one Folder "id='Accessible" with the contents of two groups. The group with id='Entryway' and the group with id='Route. The desired output would be:
<Folder id="Accessible">
<name>Accessible Features</name>
<Placemark id="_E001">
<name>E001</name>
<description><![CDATA[<div><p>Accessible entryway</p></div>]]></description>
<styleUrl>#entryway</styleUrl>
<Point>
<coordinates>-74.663266, 40.348289,0</coordinates>
</Point>
</Placemark>
<Placemark id="_E002">
<name>E002</name>
<description><![CDATA[<div><p>Accessible entryway</p></div>]]></description>
<styleUrl>#entryway</styleUrl>
<Point>
<coordinates>-74.662252, 40.348057,0</coordinates>
</Point>
</Placemark>
.
. then have the items from the group with id='Route'
.
<Placemark id="_R002">
<name>Accessible Routes</name>
<description><![CDATA[<div><p>Accessible pathway</p></div>]]></description>
<styleUrl/>
<Style>
<LineStyle>
<color>FFFF0000</color>
<width>4</width>
</LineStyle>
</Style>
<LineString>
<coordinates>
-74.65135187837255,40.34699608960065
-74.65134698312161,40.34698651192196
</coordinates>
</LineString>
</Placemark>
<Placemark id="_R003">
<name>Accessible Routes</name>
<description><![CDATA[<div><p>Accessible pathway</p></div>]]></description>
<styleUrl/>
<Style>
<LineStyle>
<color>FFFF0000</color>
<width>4</width>
</LineStyle>
</Style>
<LineString>
<coordinates>
-74.65135184561255,40.34699603789065
-74.65134698312256 44.34698634192100
</coordinates>
</LineString>
</Placemark>
.
. more than 66,000 lines omitted
.
</Folder>
I've written the XSLT to transform the XML into these KML Folders and the only thing left to do is get them under the same folder.
What I've been trying to do is move all of the items from the group with id='Route' into the group with id='Entryway.
In my xslt file is an apply-templates at the group nodes.
<xsl:apply-templates select="ds:group">
<xsl:sort select="ds:display_name"/>
</xsl:apply-templates>
This is picked up by the template match for each group.
<xsl:template match="ds:group">
At which point I'm lost. I'll post my code but it is only going to confuse and depress you.
<xsl:template match="ds:group">
<xsl:choose>
<xsl:when test="not(ds:id = 'Route') and not(ds:id = 'Entryway')">
<Folder id="{ds:id}">
<name>
<xsl:value-of select="ds:display_name"/>
</name>
<xsl:apply-templates select="ds:item">
<xsl:sort select="ds:name"/>
</xsl:apply-templates>
</Folder>
</xsl:when>
<xsl:when test="ds:id = 'Entryway'">
<Folder id='Accessible'>
<name>
<xsl:value-of select="ds:display_name"/>
</name>
<xsl:apply-templates select="ds:item">
<xsl:sort select="ds:name"/>
</xsl:apply-templates>
</Folder>
</xsl:when>
<xsl:when test="ds:id = 'Route'">
<!-- Copy all of current node to Entryway node -->
</xsl:when>
<xsl:otherwise>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
I don't think I'm going about this the right way. By the time the XSLT process gets to the group with id='Route' the KML Folder for Entryway has already been written. I'm at a dead end. Can I union two groups together based on the value of "id"? Conceptually the idea would be: <xsl:template match="ds:id='Route' | ds:id='Entryway'"> But that doesn't even compile.
Can I copy all of the elements of the group (id='Route') to the group (id='Entryway') after the first group has been processed?
Thank you in advance for your time and attention.
George
I think you need to "step" in at the document level and do e.g.
<xsl:template match="ds:document">
<Folder id='Accessible'>
<xsl:apply-templates select="ds:group[ds:id = 'Entryway' or ds:id = 'Route']"/>
</Folder>
<!-- process other ds:group here as well e.g.
<xsl:apply-templates select="ds:group[not(ds:id = 'Entryway' or ds:id = 'Route')]"/>
-->
</xsl:template>
AFAICT you want to do something like:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ns0="http://ws.wso2.org/dataservice"
exclude-result-prefixes="ns0">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/ns0:document">
<Folders>
<xsl:variable name="accessible" select="ns0:group[ns0:id='Entryway' or ns0:id ='Route']" />
<xsl:if test="$accessible">
<Folder id="Accessible">
<name>Accessible Features</name>
<xsl:apply-templates select="$accessible/ns0:item"/>
</Folder>
</xsl:if>
<xsl:apply-templates select="ns0:group[not(ns0:id='Entryway' or ns0:id ='Route')]"/>
</Folders>
</xsl:template>
<xsl:template match="ns0:group">
<Folder id="{ns0:id}">
<name>
<xsl:value-of select="display_name"/>
</name>
<xsl:apply-templates select="ns0:item"/>
</Folder>
</xsl:template>
<xsl:template match="ns0:item">
<Placemark id="{ns0:id}">
<name>
<xsl:value-of select="ns0:name"/>
</name>
<!-- more here -->
</Placemark>
</xsl:template>
</xsl:stylesheet>
<xsl:template match="/ds:document">
<kml>
<Document>
<name>Campus Map</name>
<xsl:apply-templates select="ds:group[ds:id != 'Entryway' and ds:id != 'Route']">
<xsl:sort select="ds:display_name"/>
</xsl:apply-templates>
<Folder id='Accessible'>
<name>Accessible Feature</name>
<xsl:apply-templates select="ds:group[ds:id = 'Entryway' or ds:id = 'Route']"/>
</Folder>
</Document>
</kml>
</xsl:template>
<xsl:template match="ds:group[ds:id = 'Entryway' or ds:id = 'Route']">
<xsl:apply-templates select="ds:item"/>
</xsl:template>
<xsl:template match="ds:group[ds:id != 'Entryway' and ds:id != 'Route']">
<Folder id="{ds:id}">
<name>
<xsl:value-of select="ds:display_name"/>
</name>
<xsl:apply-templates select="ds:item">
<xsl:sort select="ds:name"/>
</xsl:apply-templates>
</Folder>
</xsl:template>

In XSLT replace value

I have this XML document :Now I want to replace LineNo so that the output will be line no will 1 ,2 . I have tried some thing like this.
<xsl:value-of select="replace( '000010',1)"/>
<Rder>
<Order>
<OrderNo>458</OrderNo>
<LineNo>000010</LineNo>
<SerialNO>96</SerialNO>
<VNo>543</VNo>
</Order>
<Order>
<OrderNo>458</OrderNo>
<LineNo>000020</LineNo>
<SerialNO>32</SerialNO>
<VNo>543</VNo>
</Order>
</Rder>
I want to replace the value of LineNo= 000010 ,000020 by 1,2 in XSLT below one i have tried.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" indent="yes" />
<xsl:key name="orders" match="Order" use="OrderNo" />
<xsl:template match="/*">
<SalesOrders>
<xsl:for-each select="Rder/Order[generate-id() = generate-id(key('orders', OrderNo)[1])]">
<Order VNo="{VNo}" OrderNo="{OrderNo}">
<OrderLines>
<xsl:apply-templates select="key('orders', OrderNo)" />
</OrderLines>
</Order>
</xsl:for-each>
</SalesOrders>
</xsl:template>
<xsl:template match="Order">
<OrderLine LineNo="{LineNo}" SerialNO="{SerialNO}"/>
</xsl:template>
</xsl:stylesheet>
Actually I getting those lineno details in same format i have tried couple cases its doesn't giving that expected format.
Any help would be appreciated.
Why don't you do simply:
<xsl:template match="Order">
<OrderLine LineNo="{position()}" SerialNO="{SerialNO}"/>
</xsl:template>
or:
<xsl:template match="Order">
<OrderLine LineNo="{number(LineNo) div 10}" SerialNO="{SerialNO}"/>
</xsl:template>

XSLT a little confused

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>

How to use XPath/Xsl to compare child element with the same child element under a different parent?

I need to use xsl/xpath (version 1.0) to do something special (for simplifying, say insert some dummy text) when the value of SupplierId changes. I need to handle 3 variations;
Do something when on the first Order
(the first occurence of SupplierId)
Do somwthing when on OrderId O3 (SupplierId changed from S1 to S2)
Do something when on the last Order (the last occurence of SupplierId)
.
<?xml version="1.0" encoding="utf-8"?>
<Orders>
<Order>
<OrderId>O1</OrderId>
<SupplierId>S1</SupplierId>
</Order>
<Order>
<OrderId>O2</OrderId>
<SupplierId>S1</SupplierId>
</Order>
<Order>
<OrderId>O3</OrderId>
<SupplierId>S2</SupplierId>
</Order>
<Order>
<OrderId>O4</OrderId>
<SupplierId>S2</SupplierId>
</Order>
<Order>
<OrderId>O5</OrderId>
<SupplierId>S2</SupplierId>
</Order>
</Orders>
I've tried using preceding-sibling, following-sibling, etc, but haven't found out of it yet. I'd appreciate any help on this newbie question.
Wally
This is one natural and easy solution:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="Order[1]">
First OrderId = <xsl:text/>
<xsl:value-of select="OrderId"/>
</xsl:template>
<xsl:template match="Order[last()]">
Last OrderId = <xsl:text/>
<xsl:value-of select="OrderId"/>
</xsl:template>
<xsl:template match=
"Order[not(position() = 1)]
[not(SupplierId
=
preceding-sibling::Order[1]/SupplierId
)
]">
Changes in Order OrderId = <xsl:text/>
<xsl:value-of select="OrderId"/>
SupplierId = <xsl:text/>
<xsl:value-of select="SupplierId"/>
Previous Order OrderId = <xsl:text/>
<xsl:value-of select=
"preceding-sibling::Order[1]/OrderId"/>
SupplierId = <xsl:text/>
<xsl:value-of select=
"preceding-sibling::Order[1]/SupplierId"/>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
When this transformation is applied on the provided XML document:
<Orders>
<Order>
<OrderId>O1</OrderId>
<SupplierId>S1</SupplierId>
</Order>
<Order>
<OrderId>O2</OrderId>
<SupplierId>S1</SupplierId>
</Order>
<Order>
<OrderId>O3</OrderId>
<SupplierId>S2</SupplierId>
</Order>
<Order>
<OrderId>O4</OrderId>
<SupplierId>S2</SupplierId>
</Order>
<Order>
<OrderId>O5</OrderId>
<SupplierId>S2</SupplierId>
</Order>
</Orders>
the desired result is produced:
First OrderId = O1
Changes in Order OrderId = O3
SupplierId = S2
Previous Order OrderId = O2
SupplierId = S1
Last OrderId = O5
Assuming a root element Orders, the XPath expressions matching each condition become:
a. first Order (the first occurence of SupplierId)
XPath 1.0 - /Orders/Order[SupplierId][1]
XPath 2.0 - /Orders/Order[exists(SupplierId)][1]
b. On OrderId O3 (SupplierId changed from S1 to S2)
/Orders/Order[OrderId = 'O3' and SupplierId = 'S2']
c. On the last Order (the last occurence of SupplierId)
/Orders/Order[SupplierId][last()]
You can use recursion to step down the list of orders one by one, comparing the previous value to the current value as you go (the solution implies that document order is correct already, because it uses the following-sibling axis):
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:template match="/Orders">
<xsl:apply-templates select="Order[1]" mode="watch_SupplierId" />
</xsl:template>
<xsl:template match="Order" mode="watch_SupplierId">
<xsl:param name="PrevValue" select="''" />
<xsl:if test="string(SupplierId) != $PrevValue">
<xsl:call-template name="DoSomething" />
</xsl:if>
<xsl:variable name="next" select="following-sibling::Order[1]" />
<xsl:choose>
<xsl:when test="$next">
<xsl:apply-templates select="$next" mode="watch_SupplierId">
<xsl:with-param name="PrevValue" select="string(SupplierId)" />
</xsl:apply-templates>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="concat('last Order found: ', OrderId, '
')" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="DoSomething">
<xsl:value-of select="concat(
'#SupplierId changed to "'
, SupplierId/text()
, '" in Order '
, OrderId
, '
'
)" />
</xsl:template>
</xsl:stylesheet>
Outputs:
#SupplierId changed to "S1" in Order O1
#SupplierId changed to "S2" in Order O3
last Order found: O5

How to apply an alphanumeric sort in XSLT

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>