My XML looks like this
<page>
<orderhistory>
<order>
<ordernr>10610000000001</ordernr>
<orderrecord>
<productcategory>Something</productcategory>
</orderrecord>
<orderrecord>
<productcategory>Something</productcategory>
</orderrecord>
<orderrecord>
<productcategory>Something</productcategory>
</orderrecord>
</order>
<order>
<ordernr>10210000000001</ordernr>
<orderrecord>
<productcategory>Something</productcategory>
</orderrecord>
</order>
<order>
<ordernr>10110000000001</ordernr>
<orderrecord>
<productcategory>Something</productcategory>
</orderrecord>
</order>
<order>
<ordernr>10310000000001</ordernr>
<orderrecord>
<productcategory>Something</productcategory>
</orderrecord>
<orderrecord>
<productcategory>Forms</productcategory>
</orderrecord>
</order>
</orderhistory>
What i want is to show all productcategories that belongs to a single order. so if there are more orderrecords in one order I want to show all categories without duplicates
next the ordernr.
But it looked like this
03-06-2009 10610000000001 something something
03-06-2009 10210000000001 something
03-06-2009 10110000000001 something
03-05-2009 10310000000001 Forms something
I made a XSL to group the productcategories from the orderrecords, so that duplicates are removed. It seemed to work fine, but the problem is that i want it to be grouped by order.
<xsl:key name="orderrecord-by-productcategory" match="orderrecord" use="productcategory" />
<xsl:template match="/page/orderhistory/order">
<xsl:for-each select="orderrecord[generate-id(.) =generate-id(key('artists-by-country', productcategory)[1])]" >
<xsl:value-of select="productcategory" />
</xsl:for-each>
</xsl:template>
My output looks like this now
03-06-2009 10610000000001 something
03-06-2009 10210000000001
03-06-2009 10110000000001
03-05-2009 10310000000001 Forms
but i want it to look like this.
03-06-2009 10610000000001 something
03-06-2009 10210000000001 something
03-06-2009 10110000000001 something
03-05-2009 10310000000001 Forms something
How can i do this?
I think one way to do this is to use a key that is a concetation of the order number and the product category so that duplicate categories for different orders are treated differently
<xsl:key name="prodcat" match="productcategory" use="concat(../../ordernr/text(), concat('-', text()))"/>
This concatenates the order number, a hyphen (probably not needed), and the product category. Your XSL could then look something like this:
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" />
<xsl:key name="prodcat" match="productcategory" use="concat(../../ordernr/text(), concat('-', text()))"/>
<xsl:template match="/page/orderhistory">
<xsl:for-each select="order">
<xsl:value-of select="ordernr" />
<xsl:for-each select="orderrecord/productcategory[generate-id(.) = generate-id( key( 'prodcat', concat(../../ordernr/text(), concat('-', text())) )[1] )]">
<xsl:sort select="text()" />
<xsl:value-of select="." />
</xsl:for-each>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
There aren't any line-breaks or spaces in the output produced, but I hope it gives you the general idea.
Related
I have a question on XSLT. Basically I am look for ...Unique Delivery method ( Mail,Home etc) from last transaction for books having status 'In Stock' from the below xml. For example in the below example 'Mail' and 'Store Pickup' would be the result. Can somebody help me with XSLT ? Thanks in advance.
<BookCollection>
<Book>
<status>In Stock</status>
<name>The Hunt for Red October</name>
<OrderHistory>
<Order>
<transactionid>sssss</transactionid>
<date>2018-03-24</date>
<Delivery>Home</Delivery>
</Order>
<Order>
<transactionid>sssss</transactionid>
<date>2018-04-23</date>
<Delivery>Mail</Delivery>
</Order>
</Orderhistory>
</Book>
<Book>
<name>A case of need</name>
<status>Pending Stock</status>
<OrderHistory>
<Order>
<transactionid>sssss</transactionid>
<date>2018-08-24</date>
<Delivery>Home</Delivery>
</Order>
<Order>
<transactionid>sssss</transactionid>
<date>2018-02-23</date>
<Delivery>Store Pickup</Delivery>
</Order>
</Orderhistory>
</Book>
<Book>
<name>Sharp Objects</name>
<status>In Stock</status>
<OrderHistory>
<Order>
<transactionid>sssss</transactionid>
<date>2018-01-24</date>
<Delivery>Home</Delivery>
</Order>
<Order>
<transactionid>sssss</transactionid>
<date>2018-05-23</date>
<Delivery>Store Pickup</Delivery>
</Order>
</Orderhistory>
</Book>
<Book>
<name>Happy Doomsday</name>
<status>In Stock</status>
<OrderHistory>
<Order>
<transactionid>aaa</transactionid>
<date>2018-01-24</date>
<Delivery>Mail</Delivery>
</Order>
<Order>
<transactionid>bb</transactionid>
<date>2018-03-23</date>
<Delivery>Store Pickup</Delivery>
</Order>
</Orderhistory>
</Book>
.......
.........
</Book>
</BookCollection>
Typically if you want to find distinct items, you can view it as a grouping issue, but you only need to output one value from each group. In XSLT 1.0 you would use a technique called Muenchian Grouping, which would mean defining a key like so:
<xsl:key name="orders" match="Order" use="Delivery" />
However, you have the added complexity of getting the last Order by date. Probably the easiest option is to create a variable holding only the last Order elements for each OrderHistory
<xsl:variable name="Orders">
<xsl:for-each select="//Book[status = 'In Stock']/OrderHistory">
<xsl:for-each select="Order">
<xsl:sort select="date" order="descending" />
<xsl:if test="position() = 1">
<xsl:copy-of select="." />
</xsl:if>
</xsl:for-each>
</xsl:for-each>
</xsl:variable>
However, you need to use an extension function to convert this "Result Tree Fragment" into a node-set which you can then use the Muencjian Grouping technique on. Most like your process support EXSLT, which means adding this namespace
xmlns:exsl="http://exslt.org/common"
Then, to get the distinct values from the variable, you do this...
<xsl:for-each select="exsl:node-set($Orders)/Order[generate-id() = generate-id(key('orders', Delivery)[1])]">
Try this XSLT
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
version="1.0">
<xsl:output method="text" />
<xsl:key name="orders" match="Order" use="Delivery" />
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:variable name="Orders">
<xsl:for-each select="//Book[status = 'In Stock']/OrderHistory">
<xsl:for-each select="Order">
<xsl:sort select="date" order="descending" />
<xsl:if test="position() = 1">
<xsl:copy-of select="." />
</xsl:if>
</xsl:for-each>
</xsl:for-each>
</xsl:variable>
<xsl:template match="/">
<xsl:for-each select="exsl:node-set($Orders)/Order[generate-id() = generate-id(key('orders', Delivery)[1])]">
<xsl:if test="position() > 1">, </xsl:if>
<xsl:value-of select="Delivery" />
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
See this in action at http://xsltfiddle.liberty-development.net/3NzcBtu/2 (which is using Microsoft's XSLTCompiledTransform, which is XSLT 1.0).
Ideally though, you should look into upgrading to XSLT 3.0, if only to stop Martin Honnen getting bored....
You can do that in pure XPath 3 if the higher-order version of the sort function https://www.w3.org/TR/xpath-functions/#func-sort is supported:
distinct-values(
BookCollection/Book[status = 'In Stock']
!
sort(
OrderHistory/Order,
(),
function($order) { xs:date($order/date) }
)[last()]/Delivery
)
A complete XSLT 3 stylesheet that would work with Saxon 9.8 EE or PE or any other XSLT 3 processor supporting the higher-order function feature would be
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
version="3.0">
<xsl:template match="/">
<xsl:value-of
select="distinct-values(
BookCollection/Book[status = 'In Stock'] ! sort(OrderHistory/Order, (), function($order) { xs:date($order/date) })[last()]/Delivery
)"
separator=", "/>
</xsl:template>
</xsl:stylesheet>
In XSLT 3 processors like Saxon 9.8 HE where the higher-order variant of sort is not supported you need to delegate the sorting to your own function:
<xsl:template match="/">
<xsl:value-of
select="distinct-values(
BookCollection/Book[status = 'In Stock'] ! mf:sort(OrderHistory/Order)[last()]/Delivery
)"
separator=", "/>
</xsl:template>
<xsl:function name="mf:sort" as="element(Order)*">
<xsl:param name="orders" as="element(Order)*"/>
<xsl:perform-sort select="$orders">
<xsl:sort select="xs:date(date)"/>
</xsl:perform-sort>
</xsl:function>
Example at https://xsltfiddle.liberty-development.net/bdxtqs.
I am trying to pull nodes out of a node set stored in a variable using the msxsl:node-set() function and am not getting anything. My xml looks like this:
<Root>
<Items olditemnumber="100" newitemnumber="200">
<Item ItemNumber="100" ItemAliasCode="1001" ItemCode="X" />
<Item ItemNumber="100" ItemAliasCode="1002" ItemCode="X" />
<Item ItemNumber="200" ItemAliasCode="2001" ItemCode="X" />
<Item ItemNumber="200" ItemAliasCode="2003" ItemCode="X" />
<Item ItemNumber="100" ItemAliasCode="1003" ItemCode="P" />
<Item ItemNumber="100" ItemAliasCode="1004" ItemCode="P" />
<Item ItemNumber="200" ItemAliasCode="2002" ItemCode="P" />
</Items>
</Root>
In my xslt I try to populate a variable with a subset of the nodes and then call them using the msxsl:node-set() function. This doesn't return anything however.
XSLT looks like this:
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt">
<xsl:template match="//Root">
<xsl:variable name="OldItemNumber" select="/Items/#olditemnumber"/>
<xsl:variable name="NewItemNumber" select="/Items/#newitemnumber"/>
<xsl:variable name="OldItems">
<xsl:value-of select="//Item[#ItemNumber = $OldItemNumber]"/>
</xsl:variable>
<xsl:variable name="NewItems">
<xsl:value-of select="//Item[#ItemNumber = $NewItemNumber]"/>
</xsl:variable>
<xsl:for-each select="msxsl:node-set($OldItems)/Item">
...work
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
The XSLT skips over the for-each loop, though I see in the watch that the the Xpath query grabs the right nodes in assigning the variables. The watch also tells me that the msxsl:node-set() function is undefined. Any help would be appreciated. What am I missing?
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt">
<xsl:template match="//Root">
<xsl:variable name="OldItemNumber" select="/Items/#olditemnumber"/>
<xsl:variable name="NewItemNumber" select="/Items/#newitemnumber"/>
<xsl:variable name="OldItems" select="//Item[#ItemNumber = $OldItemNumber]"/>
<xsl:variable name="NewItems" select="//Item[#ItemNumber = $NewItemNumber]"/>
<xsl:for-each select="$OldItems">
...work
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
msxsl:node-set is for converting a result tree fragment (a.k.a. RTF) to a node set, which is not needed on your case.
xsl:value-of is for creating text nodes, so don't use it for selecting nodes of the input tree that you want to further query/process.
I am trying to get the last value of a node that belongs to a group.
Given the following XML -
<books>
<author>
<name>R.R.</name>
<titles>
<titlename>North</titlename>
<titlename>King</titilename>
</titles>
</author>
<author>
<name>R.L.</name>
<titles>
<titlename>Dragon</titlename>
<titlename>Wild</titilename>
</titles>
</author>
</books>
I assume it would be something like -
<template match="/">
<for-each-group select="books/author" group-by="name">
<lastTitle>
<name><xsl:value-of select="name"/></name>
<title><xsl:value-of select="last()/name"/></title>
</lastTitle
</for-each-group>
<template>
Thus the result would be -
<lastTitle>
<name>R.R</name>
<title>King</title>
</lastTitle>
<lastTitle>
<name>R.L.</name>
<title>Wild</title>
</lastTitle>
How can I produce this result?
Instead of doing this
<xsl:value-of select="last()/name"/>
The expression you are looking for is this:
<xsl:value-of select="titles/titlename[last()]"/>
However, it might be worth pointing out, this isn't really a 'grouping' problem. (It would only be a grouping problems if you have two different author elements with the same name). You can actually just use a simple xsl:for-each here
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<xsl:for-each select="books/author">
<lastTitle>
<name>
<xsl:value-of select="name"/>
</name>
<title>
<xsl:value-of select="titles/titlename[last()]"/>
</title>
</lastTitle>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
I have an xml file i'm receiving and i want to format it using xsl.But the requirement is to
grab the elements and print them out in the order i recieve the xml file.
Looking at the sample below the first special element has got an order element and then the next one doesn't have one and the third one has one.
So i want the output exactly that way.
Thanks i nAdvance
<main>
<submain>
<detail>
<specials>
<spec-qty>1</spec-qty>
<spec-desc> Receivable </spec-desc>
</specials>
<order>
<text>Test</text>
</order>
<specials>
<spec-qty>-1</spec-qty>
<spec-desc>Receivable1 </spec-desc>
</specials>
<specials>
<spec-qty>-1</spec-qty>
<spec-desc> Receivable2 </spec-desc>
</specials>
<order>
<text>Test2</text>
</order>
</detail>
</submain></main>
Output should be:
qty 1 Receivable order:Test qty -1 Receivable1 qty -1 Receivable2 order: Test2
Thanks and sorry for the previous uncompleted code
You can use this template:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<xsl:apply-templates select="//detail/*[self::specials or self::order]"/>
</xsl:template>
<xsl:template match="specials">
<xsl:value-of select="concat('qty ', spec-qty, ' ')"/>
<xsl:value-of select="spec-desc"/>
<xsl:text> </xsl:text>
</xsl:template>
<xsl:template match="order">
<xsl:value-of select="concat('order:', text, ' ')"/>
</xsl:template>
</xsl:stylesheet>
Output:
qty 1 Receivable order:Test qty -1 Receivable1 qty -1 Receivable2 order:Test2
Given the following xml:
<container>
<val>2</val>
<id>1</id>
</container>
<container>
<val>2</val>
<id>2</id>
</container>
<container>
<val>2</val>
<id>3</id>
</container>
<container>
<val>4</val>
<id>1</id>
</container>
<container>
<val>4</val>
<id>2</id>
</container>
<container>
<val>4</val>
<id>3</id>
</container>
I'd like to return something like
2 - 1
2 - 3
4 - 1
4 - 3
Using a nodeset I've been able to get the last occurrence via:
exsl:node-set($list)/container[not(val = following::val)]
but I can't figure out how to get the first one.
To get the first and the last occurrence (document order) in each "<val>" group, you can use an <xsl:key> like this:
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:output method="text" />
<xsl:key name="ContainerGroupByVal" match="container" use="val" />
<xsl:variable name="ContainerGroupFirstLast" select="//container[
generate-id() = generate-id(key('ContainerGroupByVal', val)[1])
or
generate-id() = generate-id(key('ContainerGroupByVal', val)[last()])
]" />
<xsl:template match="/">
<xsl:for-each select="$ContainerGroupFirstLast">
<xsl:value-of select="val" />
<xsl:text> - </xsl:text>
<xsl:value-of select="id" />
<xsl:value-of select="'
'" /><!-- LF -->
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
EDIT #1: A bit of an explanation since this might not be obvious right away:
The <xsl:key> returns all <container> nodes having a given <val>. You use the key() function to query it.
The <xsl:variable> is where it all happens. It reads as:
for each of the <container> nodes in the document ("//container") check…
…if it has the same unique id (generate-id()) as the first node returned by key() or the last node returned by key()
where key('ContainerGroupByVal', val) returns the set of <container> nodes matching the current <val>
if the unique ids match, include the node in the selection
the <xsl:for-each> does the output. It could just as well be a <xsl:apply-templates>.
EDIT #2: As Dimitre Novatchev rightfully points out in the comments, you should be wary of using the "//" XPath shorthand. If you can avoid it, by all means, do so — partly because it potentially selects nodes you don't want, and mainly because it is slower than a more specific XPath expression. For example, if your document looks like:
<containers>
<container><!-- ... --></container>
<container><!-- ... --></container>
<container><!-- ... --></container>
</containers>
then you should use "/containers/container" or "/*/container" instead of "//container".
EDIT #3: An alternative syntax of the above would be:
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:output method="text" />
<xsl:key name="ContainerGroupByVal" match="container" use="val" />
<xsl:variable name="ContainerGroupFirstLast" select="//container[
count(
.
| key('ContainerGroupByVal', val)[1]
| key('ContainerGroupByVal', val)[last()]
) = 2
]" />
<xsl:template match="/">
<xsl:for-each select="$ContainerGroupFirstLast">
<xsl:value-of select="val" />
<xsl:text> - </xsl:text>
<xsl:value-of select="id" />
<xsl:value-of select="'
'" /><!-- LF -->
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Explanation: The XPath union operator "|" combines it's arguments into a node-set. By definition, a node-set cannot contain duplicate nodes — for example: ". | . | ." will create a node-set containing exactly one node (the current node).
This means, if we create a union node-set from the current node ("."), the "key(…)[1]" node and the "key(…)[last()]" node, it's node count will be 2 if (and only if) the current node equals one of the two other nodes, in all other cases the count will be 3.
Basic XPath:
//container[position() = 1] <- this is the first one
//container[position() = last()] <- this is the last one
Here's a set of XPath functions in more detail.
I. XSLT 1.0
Basically the same solution as the one by Tomalak, but more understandable Also it is complete, so you only need to copy and paste the XML document and the transformation and then just press the "Transform" button of your favourite XSLT IDE:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:key name="kContByVal" match="container"
use="val"/>
<xsl:template match="/*">
<xsl:for-each select=
"container[generate-id()
=
generate-id(key('kContByVal',val)[1])
]
">
<xsl:variable name="vthisvalGroup"
select="key('kContByVal', val)"/>
<xsl:value-of select=
"concat($vthisvalGroup[1]/val,
'-',
$vthisvalGroup[1]/id,
'
'
)
"/>
<xsl:value-of select=
"concat($vthisvalGroup[last()]/val,
'-',
$vthisvalGroup[last()]/id,
'
'
)
"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
when this transformation is applied on the originally-provided XML document (edited to be well-formed):
<t>
<container>
<val>2</val>
<id>1</id>
</container>
<container>
<val>2</val>
<id>2</id>
</container>
<container>
<val>2</val>
<id>3</id>
</container>
<container>
<val>4</val>
<id>1</id>
</container>
<container>
<val>4</val>
<id>2</id>
</container>
<container>
<val>4</val>
<id>3</id>
</container>
</t>
the wanted result is produced:
2-1
2-3
4-1
4-3
Do note:
We use the Muenchian method for grouping to find one container element for each set of such elements that have the same value for val.
From the whole node-list of container elements with the same val value, we output the required data for the first container element in the group and for the last container element in the group.
II. XSLT 2.0
This transformation:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xsl:output method="text"/>
<xsl:template match="/*">
<xsl:for-each-group select="container"
group-by="val">
<xsl:for-each select="current-group()[1], current-group()[last()]">
<xsl:value-of select=
"concat(val, '-', id, '
')"/>
</xsl:for-each>
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>
when applied on the same XML document as above, prodices the wanted result:
2-1
2-3
4-1
4-3
Do note:
The use of the <xsl:for-each-group> XSLT instruction.
The use of the current-group() function.