XSLT 1.0 - Multiple Child Nodes Formatted Output - xslt

I can do this with For-Each but having some problems with Apply_Templates.
The XML Data contains varying child nodes but I want to display all child nodes with their parents information.
<orders>
<order>
<customer>1</customer>
<items>
<item>
<name>Widget A</name>
<price>10</price>
<code>1A</code>
</item>
</items>
</order>
<order>
<customer>2</customer>
<items>
<item>
<name>Widget A</name>
<price>10</price>
<code>1A</code>
</item>
</items>
<items>
<item>
<name>Widget B</name>
<price>20</price>
<code>2B</code>
</item>
</items>
</order>
<order>
<customer>3</customer>
<items>
<item>
<name>Widget A</name>
<price>10</price>
<code>1A</code>
</item>
</items>
<items>
<item>
<name>Widget B</name>
<price>10</price>
<code>1A</code>
</item>
</items>
<items>
<item>
<name>Widget C</name>
<price>30</price>
<code>3C</code>
</item>
</items>
</order>
As you can see, there are multiple item per customer per order. I want the output to look like this:
Customer, Name, Price, Code
1, Widget A, 10, 1A
2, Widget A, 10, 1A
2, Widget B, 20, 2B
3, Widget A, 10, 1A
3, Widget B, 20, 2B
3, Widget C, 30, 3C
When I to apply-templates, I can only get the first item to display but not the others. If you think I should post my XSL, let me know but I think it should be simple just stuck. I want to get away from using for-each.

You can try this way :
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="no"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/">
<xsl:text>Customer, Name, Price ,Code
</xsl:text>
<xsl:apply-templates select="//item"/>
</xsl:template>
<xsl:template match="item">
<xsl:value-of select="../../customer"/>
<xsl:text>, </xsl:text>
<xsl:value-of select="name"/>
<xsl:text>, </xsl:text>
<xsl:value-of select="price"/>
<xsl:text>, </xsl:text>
<xsl:value-of select="code"/>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
First template match the root element and print header in the output document.
The 2nd template match item elements and output corresponding comma-separated values. This way you can have rows from each item in the output document without using for-each construct.

I believe a truly template-based approach would look like this:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="utf-8"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/orders">
<xsl:text>Customer, Name, Price, Code
</xsl:text>
<xsl:apply-templates select="order/items/item"/>
</xsl:template>
<xsl:template match="item">
<xsl:value-of select="../../customer"/>
<xsl:text>, </xsl:text>
<xsl:apply-templates/>
<xsl:if test="position()!=last()">
<xsl:text>
</xsl:text>
</xsl:if>
</xsl:template>
<xsl:template match="item/*">
<xsl:value-of select="."/>
<xsl:if test="position()!=last()">
<xsl:text>, </xsl:text>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
However, as I mentioned in the comment to your question, this serves no discernible purpose, other than breaking the code into discontinuous chunks that are hard to follow.

Related

xslt group by function or append to array

I have collection of items:
<items>
<result>
<item>
<product_name>some description</product_name>
</item>
<item>
<product_name>some similar description</product_name>
</item>
<item>
<product_name>other product size 1</product_name>
</item>
<item>
<product_name>other product size 2</product_name>
</item>
</result>
</items>
I also have some external function strdist:string-distance that compares previous product to current one and if there is some match then it returns true. Based on this returned value I'd like to:
add the current element of for-each to the group of previous element when returned value is true
close previous group, create a new group and add current element of for-each loop to it when returned value is false
I'm little bit struggling with the process of how to create the groups and add there elements in for-loop.
Here is my template
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:strdist="http://example.com/string-distance"
exclude-result-prefixes="strdist">
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>
<xsl:template match="/">
<xsl:variable name="items" as="element()*">
<xsl:perform-sort select="items/result/item">
<xsl:sort select="./product_name"/>
</xsl:perform-sort>
</xsl:variable>
<xsl:variable name="groupedItems" as="element()*">
<groups>
<xsl:for-each select="$items">
<xsl:variable name="position" select="position()"/>
<xsl:variable name="currentProductName" select="./product_name/text()"/>
<xsl:choose>
<xsl:when test="$position -1 = 0">
<xsl:element name="group"/>
<xsl:message select="concat($position, ' # ', $currentProductName)"/>
</xsl:when>
<xsl:when test="$position -1 > 0">
<xsl:variable name="previousProductName"
select="$items[position() = $position -1]/product_name/text()"/>
<xsl:message
select="concat($position, ' # ', $previousProductName, ' # ', $currentProductName)"/>
<xsl:message select="strdist:string-distance($previousProductName, $currentProductName)"/>
</xsl:when>
</xsl:choose>
</xsl:for-each>
</groups>
</xsl:variable>
<xsl:message select="$groupedItems" />
</xsl:template>
</xsl:stylesheet>
Finally I'd like to have something like this:
<items>
<result>
<group>
<item>
<product_name>some description</product_name>
</item>
<item>
<product_name>some similar description</product_name>
</item>
</group>
<group>
<item>
<product_name>other product size 1</product_name>
</item>
<item>
<product_name>other product size 2</product_name>
</item>
</group>
</result>
</items>
I'm using xslt 2.0 with saxon-he 10.3.
Here is the example using for-each-group group-starting-with and a sample function:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:mf="http://example.com/mf"
exclude-result-prefixes="#all"
version="3.0">
<xsl:mode on-no-match="shallow-copy"/>
<xsl:output method="xml" indent="yes"/>
<xsl:function name="mf:string-distance" as="xs:boolean">
<xsl:param name="name1" as="xs:string"/>
<xsl:param name="name2" as="xs:string"/>
<xsl:sequence select="tokenize($name1)[1] = tokenize($name2)[1]"/>
</xsl:function>
<xsl:template match="result">
<xsl:copy>
<xsl:for-each-group select="item" group-starting-with="item[not(mf:string-distance(product_name, preceding-sibling::item[1]/product_name))]">
<group>
<xsl:apply-templates select="current-group()"/>
</group>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Seems to give the wanted result at https://xsltfiddle.liberty-development.net/bEJbVrR.

xslt order and grouping with for-each-group

I'm trying to get my head over nested grouping and sorting while using for-each-group.
My idea is to order and group items at first by producer. Then when I have this producer groups I'd like to sort each of them by code. However currently the order of code doesn't work as I'd like to. In following example the problem is with item with code=01001-064-03. It should be grouped together with all other items whose code starts with 01001 but it isn't. If I move entire item/code[text()='01001-064-03'] (the last one) to the beginning of xml then grouping works ok.
Please what is my issue here?
Thanks
<items>
<change_date>#11.11.2020 7:42:13</change_date>
<result>
<item>
<code>01001-064-01</code>
<producer>prod1</producer>
</item>
<item>
<code>01001-064-02</code>
<producer>prod1</producer>
</item>
<item>
<code>def</code>
<producer>prod1</producer>
</item>
<item>
<code>ghi</code>
<producer>prod2</producer>
</item>
<item>
<code>jkl</code>
<producer>prod3</producer>
</item>
<item>
<code>abc</code>
<producer>prod3</producer>
</item>
<item>
<code>def</code>
<producer>prod4</producer>
</item>
<item>
<code>ghi</code>
<producer>prod4</producer>
</item>
<item>
<code>jkl</code>
<producer>prod5</producer>
</item>
<item>
<code>01001-064-03</code>
<producer>prod1</producer>
</item>
</result>
</items>
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:math="http://www.w3.org/2005/xpath-functions/math"
xmlns:map="http://www.w3.org/2005/xpath-functions/map"
xmlns:array="http://www.w3.org/2005/xpath-functions/array"
xmlns:mf="http://example.com/mf"
exclude-result-prefixes="#all"
version="3.0">
<xsl:mode on-no-match="shallow-copy"/>
<xsl:output method="xml" indent="yes" html-version="5"/>
<xsl:function name="mf:same-product" as="xs:boolean">
<xsl:param name="left" as="xs:string"/>
<xsl:param name="right" as="xs:string"/>
<xsl:variable name="leftParsed" select="mf:get-regexp-group($left, 1)"/>
<xsl:variable name="rightParsed" select="mf:get-regexp-group($right, 1)"/>
<xsl:sequence select="matches($leftParsed, $rightParsed)"/>
</xsl:function>
<xsl:function name="mf:get-regexp-group" as="xs:string">
<xsl:param name="text" as="xs:string"/>
<xsl:param name="groupNumber" as="xs:integer"/>
<xsl:variable name="result">
<xsl:analyze-string select="$text" regex="(^[a-zA-Z0-9]+)(.*)">
<xsl:matching-substring>
<xsl:value-of select="regex-group($groupNumber)"/>
</xsl:matching-substring>
</xsl:analyze-string>
</xsl:variable>
<xsl:sequence select="$result"/>
</xsl:function>
<xsl:template match="items">
<xsl:apply-templates />
</xsl:template>
<xsl:template match="change_date"/>
<xsl:template match="result">
<data>
<xsl:for-each-group select="item" group-by="producer">
<xsl:sort select="producer"/>
<xsl:for-each-group select="current-group()" group-starting-with="item[not(mf:same-product(code, preceding-sibling::item[1]/code))]">
<xsl:sort select="code"/>
<group>
<xsl:apply-templates select="current-group()" />
</group>
</xsl:for-each-group>
</xsl:for-each-group>
</data>
</xsl:template>
<xsl:template match="item">
<xsl:copy-of select="."/>
</xsl:template>
</xsl:stylesheet>
fiddle example here
I'm using xslt 2.0 with saxon-he 10.3.
EDIT:
So as #michael.hor257k asked for better explanation I'll try to do my best:
Each item is product. This product has producer and has code (product code). I want to group all products of producer by code. However codes are not same for similar products so the similarity is matched by function mf:same-product. For example two similar products could be 01001-064-01 and 01001-064-02 here I check the first prefix 01001 and if it matches it means both products should be added to same group.
expected result should look like this:
<?xml version="1.0" encoding="UTF-8"?>
<data>
<group>
<item>
<code>01001-064-01</code>
<producer>prod1</producer>
</item>
<item>
<code>01001-064-02</code>
<producer>prod1</producer>
</item>
<item>
<code>01001-064-03</code>
<producer>prod1</producer>
</item>
</group>
<group>
<item>
<code>def</code>
<producer>prod1</producer>
</item>
</group>
<group>
<item>
<code>ghi</code>
<producer>prod2</producer>
</item>
</group>
<group>
<item>
<code>abc</code>
<producer>prod3</producer>
</item>
</group>
<group>
<item>
<code>jkl</code>
<producer>prod3</producer>
</item>
</group>
<group>
<item>
<code>def</code>
<producer>prod4</producer>
</item>
</group>
<group>
<item>
<code>ghi</code>
<producer>prod4</producer>
</item>
</group>
<group>
<item>
<code>jkl</code>
<producer>prod5</producer>
</item>
</group>
</data>
Perhaps a composite group-by suffices:
<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:mode on-no-match="shallow-skip"/>
<xsl:output method="xml" indent="yes"/>
<xsl:template match="change_date"/>
<xsl:template match="result">
<data>
<xsl:for-each-group select="item" composite="yes" group-by="producer, code => replace('[^a-z0-9].*$', '', 'i')">
<xsl:sort select="producer"/>
<xsl:sort select="code"/>
<group>
<xsl:apply-templates select="current-group()" />
</group>
</xsl:for-each-group>
</data>
</xsl:template>
<xsl:template match="item">
<xsl:copy-of select="."/>
</xsl:template>
</xsl:stylesheet>
https://xsltfiddle.liberty-development.net/ei5R4uT/10
That is XSLT 3 which Saxon 9.8 and later (e.g. Saxon 10) support, if you really need to do it with an XSLT 2.0 processor then a nested for-each-group group-by or a concatenated grouping key can achieve the same as the composite grouping key in the above XSLT 3.
My idea is to order and group items at first by producer. Then when I have this producer groups I'd like to sort each of them by code.
If that's all you want to do, why isn't it sufficient to do:
XSLT 3.0
<xsl:stylesheet version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:mode on-no-match="shallow-copy"/>
<xsl:template match="result">
<xsl:for-each-group select="item" group-by="producer">
<xsl:sort select="producer"/>
<group>
<xsl:apply-templates select="current-group()">
<xsl:sort select="code"/>
</xsl:apply-templates>
</group>
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>
I'm using xslt 2.0 with saxon-he 10.3.
Actually, you are using XSLT 3.0.

Proper use of for-each-group in XSLT

I have the following xml sample:
<items>
<item>
<item_id>1</item_id>
<item_name>item 1</item_name>
<group_id>1</group_id>
<group_name>group 1</group_name>
</item>
<item>
<item_id>2</item_id>
<item_name>item 2</item_name>
<group_id>1</group_id>
<group_name>group 1</group_name>
</item>
<item>
<item_id>3</item_id>
<item_name>item 3</item_name>
<group_id>2</group_id>
<group_name>group 2</group_name>
</item>
</items>
which I need transformed into the following csv format:
1,item 1
2,item 2
3,item 3
1,group 1
2,group 2
In the xml, item_id will always be followed by item_name. The item name will not always be concat('item_',#). It could be an description such as 'toothpaste'. Items will never be repeated in the file but occasionally the group_id->group_name pairing will not always be 1-1. In this case, it is preferred to take the first pairing.
I am accomplishing this using a 'for-each-group' statement but it seems a bit hacky. Are there any downsides to this approach? What are some better ways to accomplish this?
<xsl:template match="/">
<xsl:call-template name="list_format">
<xsl:with-param name="list" select="'item'"/>
</xsl:call-template>
<xsl:call-template name="list_format">
<xsl:with-param name="list" select="'group'"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="list_format">
<xsl:param name="list"/>
<xsl:variable name="linefeed" select="'
'"></xsl:variable>
<xsl:variable name="list_id" select="concat($list,'_id')"></xsl:variable>
<xsl:variable name="list_name" select="concat($list,'_name')"></xsl:variable>
<xsl:for-each-group select="/items/item" group-by="*[name()=$list_id]">
<xsl:sort select="*[name()=$list_id]"/>
<xsl:value-of select="*[name()=$list_id]"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="*[name()=$list_name]"/>
<xsl:value-of select="$linefeed"/>
</xsl:for-each-group>
</xsl:template>
hmmm .. not really a downside but it doesn't work in XSLT 1.0 as it doesn't support for-each-group :-/
I don't think you would need XSLT 1.0 solution .. but still am posting my work :) Check this out!
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/items">
<xsl:call-template name="items_grouping">
<xsl:with-param name="list" select="'item'"/>
</xsl:call-template>
<xsl:call-template name="items_grouping">
<xsl:with-param name="list" select="'group'"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="items_grouping">
<xsl:param name="list"/>
<xsl:variable name="linefeed" select="'
'"></xsl:variable>
<xsl:variable name="list_id" select="concat($list,'_id')"></xsl:variable>
<xsl:variable name="list_name" select="concat($list,'_name')"></xsl:variable>
<xsl:for-each select="item">
<xsl:for-each select="*[name()=$list_id and not(.= ../preceding-sibling::item/*[name()=$list_id]/.)]">
<xsl:value-of select="concat(.,',')"/>
<xsl:value-of select="../*[name()=$list_name]"/>
<xsl:value-of select="$linefeed"/>
</xsl:for-each>
</xsl:for-each>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
Used same methodology of yours .. only change is 'preceding-sibling'
and also for this code, order of child-nodes of <item> doesn't matter
Here is a simple and short XSLT 2.0 (as requested) solution:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/">
<xsl:apply-templates/>
<xsl:for-each-group select="/*/*/group_id" group-by=".">
<xsl:value-of separator="," select="../*[starts-with(name(), 'group_')]"/>
<xsl:text>
</xsl:text>
</xsl:for-each-group>
</xsl:template>
<xsl:template match="item">
<xsl:value-of separator="," select="*[starts-with(name(), 'item_')]"/>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
when this transformation is applied on the provided XML document:
<items>
<item>
<item_id>1</item_id>
<item_name>item 1</item_name>
<group_id>1</group_id>
<group_name>group 1</group_name>
</item>
<item>
<item_id>2</item_id>
<item_name>item 2</item_name>
<group_id>1</group_id>
<group_name>group 1</group_name>
</item>
<item>
<item_id>3</item_id>
<item_name>item 3</item_name>
<group_id>2</group_id>
<group_name>group 2</group_name>
</item>
</items>
the wanted, correct result is produced:
1,item 1
2,item 2
3,item 3
1,group 1
2,group 2

XSLT 1.0: apply-templates and template mode

I have the following XML:
<?xml version="1.0" encoding="UTF-8"?>
<Order>
<Item>
<RECORD_ID>RECORD_ID</RECORD_ID>
<ENTITY_CODE>ENTITY_CODE</ENTITY_CODE>
<USER_CODE>USER_CODE</USER_CODE>
<RECORD_DATE>RECORD_DATE</RECORD_DATE>
<ITEM_CODE>ITEM_CODE</ITEM_CODE>
<LINE_QUANTITY>LINE_QUANTITY</LINE_QUANTITY>
<LINE_FREE_STOCK>LINE_FREE STOCK</LINE_FREE_STOCK>
<LINE_PRICE>LINE_PRICE</LINE_PRICE>
<LINE_DISCOUNT_PERCENT>LINE_DISCOUNT PERCENT</LINE_DISCOUNT_PERCENT>
</Item>
<Item>
<RECORD_ID>9046</RECORD_ID>
<ENTITY_CODE>12010601</ENTITY_CODE>
<USER_CODE>122</USER_CODE>
<RECORD_DATE>2011-08-24</RECORD_DATE>
<ITEM_CODE>804-008165</ITEM_CODE>
<LINE_QUANTITY>2</LINE_QUANTITY>
<LINE_FREE_STOCK>1</LINE_FREE_STOCK>
</Item>
<Item>
<RECORD_ID>9046</RECORD_ID>
<ENTITY_CODE>12010601</ENTITY_CODE>
<USER_CODE>122</USER_CODE>
<RECORD_DATE>2011-08-24</RECORD_DATE>
<ITEM_CODE>804-008161</ITEM_CODE>
<LINE_QUANTITY>1</LINE_QUANTITY>
<LINE_FREE_STOCK>1</LINE_FREE_STOCK>
</Item>
<Item>
<RECORD_ID>9046</RECORD_ID>
<ENTITY_CODE>12010601</ENTITY_CODE>
<USER_CODE>122</USER_CODE>
<RECORD_DATE>2011-08-24</RECORD_DATE>
<ITEM_CODE>804-008225</ITEM_CODE>
<LINE_QUANTITY>5</LINE_QUANTITY>
</Item>
</Order>
Sometimes within the item tag I have the element <LINE_FREE_STOCK>. If that occurs I have to create an additional position in the output XML.
Now I came up with this style sheet:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output encoding="UTF-8" method="xml" indent="yes"/>
<xsl:template match="/">
<ORDERS05>
<IDOC BEGIN="1">
<xsl:apply-templates select="Order"/>
</IDOC>
</ORDERS05>
</xsl:template>
<xsl:template match="Order">
<Header>
<xsl:value-of select="'some header data'"/>
</Header>
<xsl:apply-templates select="Item[position() >1]"/>
<xsl:apply-templates select="Item[position() >1 and child::LINE_FREE_STOCK]" mode="freestock"/>
</xsl:template>
<xsl:template match="Item">
<position>
<item>
<number><xsl:value-of select="ITEM_CODE"/></number>
<quantity><xsl:value-of select="LINE_QUANTITY"/></quantity>
</item>
</position>
</xsl:template>
<xsl:template match="Item[position() >1 and child::LINE_FREE_STOCK]" mode="freestock">
<position>
<item>
<number><xsl:value-of select="ITEM_CODE"/></number>
<freestock_quant><xsl:value-of select="LINE_FREE_STOCK"/></freestock_quant>
</item>
</position>
</xsl:template>
</xsl:stylesheet>
It creates this (simplified) wanted output:
<?xml version="1.0" encoding="UTF-8"?>
<ORDERS05>
<IDOC BEGIN="1">
<Header>some header data</Header>
<position>
<item>
<number>804-008165</number>
<quantity>2</quantity>
</item>
</position>
<position>
<item>
<number>804-008161</number>
<quantity>1</quantity>
</item>
</position>
<position>
<item>
<number>804-008225</number>
<quantity>5</quantity>
</item>
</position>
<position>
<item>
<number>804-008165</number>
<freestock_quant>1</freestock_quant>
</item>
</position>
<position>
<item>
<number>804-008161</number>
<freestock_quant>1</freestock_quant>
</item>
</position>
</IDOC>
</ORDERS05>
804-008165 and 804-008161 show up twice - once as a standard item and once as the free stock item with the respective quantities.
But did I forget anything here? Is there some sort of pitfall I don't see?
Is that XSLT robust enough?
As others have noted, the problem is in this code:
<xsl:apply-templates select="Item"/>
<xsl:apply-templates select="Item[child::LINE_FREE_STOCK]" mode="freestock"/>
If there is a child Item that has a child LINE_FREE_STOCK, templates would be applied on this Item element twice -- here is how you get the repetitions in the output.
The transformation can be significantly shortened and it doesn't need modes or explicit conditional instructions at all:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<ORDERS05>
<IDOC BEGIN="1">
<xsl:apply-templates select="Order"/>
</IDOC>
</ORDERS05>
</xsl:template>
<xsl:template match="Order">
<Header>
<xsl:value-of select="'some header data'"/>
</Header>
<xsl:apply-templates select="Item[position() >1]"/>
</xsl:template>
<xsl:template match="Item">
<position>
<item>
<number>
<xsl:value-of select="ITEM_CODE"/>
</number>
<xsl:apply-templates select=
"self::node()[not(LINE_FREE_STOCK)]/LINE_QUANTITY
|
LINE_FREE_STOCK"/>
</item>
</position>
</xsl:template>
<xsl:template match="LINE_QUANTITY">
<quantity>
<xsl:value-of select="."/>
</quantity>
</xsl:template>
<xsl:template match="LINE_FREE_STOCK">
<freestock_quant>
<xsl:value-of select="."/>
</freestock_quant>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML document:
<Order>
<Item>
<RECORD_ID>RECORD_ID</RECORD_ID>
<ENTITY_CODE>ENTITY_CODE</ENTITY_CODE>
<USER_CODE>USER_CODE</USER_CODE>
<RECORD_DATE>RECORD_DATE</RECORD_DATE>
<ITEM_CODE>ITEM_CODE</ITEM_CODE>
<LINE_QUANTITY>LINE_QUANTITY</LINE_QUANTITY>
<LINE_FREE_STOCK>LINE_FREE STOCK</LINE_FREE_STOCK>
<LINE_PRICE>LINE_PRICE</LINE_PRICE>
<LINE_DISCOUNT_PERCENT>LINE_DISCOUNT PERCENT</LINE_DISCOUNT_PERCENT>
</Item>
<Item>
<RECORD_ID>9046</RECORD_ID>
<ENTITY_CODE>12010601</ENTITY_CODE>
<USER_CODE>122</USER_CODE>
<RECORD_DATE>2011-08-24</RECORD_DATE>
<ITEM_CODE>804-008165</ITEM_CODE>
<LINE_QUANTITY>2</LINE_QUANTITY>
<LINE_FREE_STOCK>1</LINE_FREE_STOCK>
</Item>
<Item>
<RECORD_ID>9046</RECORD_ID>
<ENTITY_CODE>12010601</ENTITY_CODE>
<USER_CODE>122</USER_CODE>
<RECORD_DATE>2011-08-24</RECORD_DATE>
<ITEM_CODE>804-008161</ITEM_CODE>
<LINE_QUANTITY>1</LINE_QUANTITY>
<LINE_FREE_STOCK>1</LINE_FREE_STOCK>
</Item>
<Item>
<RECORD_ID>9046</RECORD_ID>
<ENTITY_CODE>12010601</ENTITY_CODE>
<USER_CODE>122</USER_CODE>
<RECORD_DATE>2011-08-24</RECORD_DATE>
<ITEM_CODE>804-008225</ITEM_CODE>
<LINE_QUANTITY>5</LINE_QUANTITY>
</Item>
</Order>
the wanted, correct result is produced:
<ORDERS05>
<IDOC BEGIN="1">
<Header>some header data</Header>
<position>
<item>
<number>804-008165</number>
<freestock_quant>1</freestock_quant>
</item>
</position>
<position>
<item>
<number>804-008161</number>
<freestock_quant>1</freestock_quant>
</item>
</position>
<position>
<item>
<number>804-008225</number>
<quantity>5</quantity>
</item>
</position>
</IDOC>
</ORDERS05>
This is because you have two match templates for Item:
<xsl:template match="Item">
<xsl:if test="position() > 1">
<position>
<item>
<number><xsl:value-of select="ITEM_CODE"/></number>
<quantity><xsl:value-of select="LINE_QUANTITY"/></quantity>
</item>
</position>
</xsl:if>
</xsl:template>
<xsl:template match="Item[child::LINE_FREE_STOCK]" mode="freestock">
<xsl:if test="position() > 1">
<position>
<item>
<number><xsl:value-of select="ITEM_CODE"/></number>
<freestock_quant><xsl:value-of select="LINE_FREE_STOCK"/></freestock_quant>
</item>
</position>
</xsl:if>
</xsl:template>
First the default Item template matches and then the Item's with LINE_FREE_STOCK also matches the Item with child LINE_FREE_STOCK template, hence the duplicate for Item's with LINE_FREE_STOCK.
Instead why not just use one template, like this:
<xsl:template match="Item">
<xsl:if test="position() > 1">
<position>
<item>
<number><xsl:value-of select="ITEM_CODE"/></number>
<xsl:choose>
<xsl:when test="child::LINE_FREE_STOCK">
<freestock_quant><xsl:value-of select="LINE_FREE_STOCK"/></freestock_quant>
</xsl:when>
<xsl:otherwise>
<quantity><xsl:value-of select="LINE_QUANTITY"/></quantity>
</xsl:otherwise>
</xsl:choose>
</item>
</position>
</xsl:if>
</xsl:template>
Using the single template your Order template is also simplified:
<xsl:template match="Order">
<Header>
<xsl:value-of select="'some header data'"/>
</Header>
<xsl:apply-templates select="Item"/>
</xsl:template>
This way you do not need to use Modes either.
It's not clear what is the wanted output. Perhaps you want:
<xsl:apply-templates select="Item[not(LINE_FREE_STOCK)"/>
<xsl:apply-templates select="Item[LINE_FREE_STOCK]" mode="freestock"/>
in place of your
<xsl:apply-templates select="Item"/>
<xsl:apply-templates select="Item[child::LINE_FREE_STOCK]" mode="freestock"/>
You would need an addition filtering
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output encoding="UTF-8" method="xml" indent="yes"/>
<xsl:template match="/">
<ORDERS05>
<IDOC BEGIN="1">
<xsl:apply-templates select="Order"/>
</IDOC>
</ORDERS05>
</xsl:template>
<xsl:template match="Order">
<Header>
<xsl:value-of select="'some header data'"/>
</Header>
<xsl:apply-templates select="Item[not(child::LINE_FREE_STOCK)]"/>
<xsl:apply-templates select="Item[child::LINE_FREE_STOCK]" mode="freestock"/>
</xsl:template>
<xsl:template match="Item">
<xsl:if test="position() > 1">
<position>
<item>
<number><xsl:value-of select="ITEM_CODE"/></number>
<quantity><xsl:value-of select="LINE_QUANTITY"/></quantity>
</item>
</position>
</xsl:if>
</xsl:template>
<xsl:template match="Item[child::LINE_FREE_STOCK]" mode="freestock">
<xsl:if test="position() > 1">
<position>
<item>
<number><xsl:value-of select="ITEM_CODE"/></number>
<freestock_quant><xsl:value-of select="LINE_FREE_STOCK"/></freestock_quant>
</item>
</position>
</xsl:if>
</xsl:template>
</xsl:stylesheet>

Doing a pivot using XSLT

I have an xml file like this:
<root>
<item>
<name>one</name>
<status>good</status>
</item>
<item>
<name>two</name>
<status>good</status>
</item>
<item>
<name>three</name>
<status>bad</status>
</item>
<item>
<name>four</name>
<status>ugly</status>
</item>
<item>
<name>five</name>
<status>bad</status>
</item>
</root>
I want to transform this using XSLT to get something like:
<root>
<items><status>good</status>
<name>one</name>
<name>two</name>
</items>
<items><status>bad</status>
<name>three</name>
<name>five</name>
</items>
<items><status>ugly</status>
<name>four</name>
</items>
</root>
In other words, I get a list of items, each with a status, and I want to turn it into a list of statuses, each with a list of items.
My initial thought was to do apply-templates matching each status type in turn, but that means I have to know the complete list of statuses. Is there a better way to do it?
Thanks for any help.
Muench to the rescue!
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" encoding="UTF-8"/>
<xsl:key name="muench" match="/root/item/status" use="."/>
<xsl:template match="/">
<root>
<xsl:for-each select="/root/item/status[generate-id() = generate-id(key('muench',.)[1])]">
<xsl:call-template name="pivot">
<xsl:with-param name="status" select="."/>
</xsl:call-template>
</xsl:for-each>
</root>
</xsl:template>
<xsl:template name="pivot">
<xsl:param name="status"/>
<items>
<status><xsl:value-of select="$status"/></status>
<xsl:for-each select="/root/item[status=$status]">
<name><xsl:value-of select="name"/></name>
</xsl:for-each>
</items>
</xsl:template>
</xsl:stylesheet>
Yes, this can be done in XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<!-- -->
<xsl:key name="kStatByVal"
match="status" use="."/>
<!-- -->
<xsl:key name="kItemByStat"
match="item" use="status"/>
<!-- -->
<xsl:variable name="vDoc" select="/"/>
<xsl:template match="/">
<top>
<xsl:for-each select=
"/*/*/status[generate-id()
=
generate-id(key('kStatByVal',.)[1])
]">
<items>
<status><xsl:value-of select="."/></status>
<xsl:for-each select="key('kItemByStat', .)">
<xsl:copy-of select="name"/>
</xsl:for-each>
</items>
</xsl:for-each>
</top>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the original XML document:
<root>
<item>
<name>one</name>
<status>good</status>
</item>
<item>
<name>two</name>
<status>good</status>
</item>
<item>
<name>three</name>
<status>bad</status>
</item>
<item>
<name>four</name>
<status>ugly</status>
</item>
<item>
<name>five</name>
<status>bad</status>
</item>
</root>
The wanted result is produced:
<top>
<items>
<status>good</status>
<name>one</name>
<name>two</name>
</items>
<items>
<status>bad</status>
<name>three</name>
<name>five</name>
</items>
<items>
<status>ugly</status>
<name>four</name>
</items>
</top>
Do note the use of:
The Muenchian method for grouping
The use of <xsl:key> and the key() function
It depends about your xslt engine. If you're using xslt 1.0 without any extension, then your approach is certainly the best.
On the other side, if you're allowed to use exslt (especially the node-set extension) or xslt 2.0, then you could do it in a more generic way:
Collect all the available statuses
Create a node-set from the obtained result
Iterating on this node set, create your pivot by filtering you status base on the current element in your iteration.
But before doing that, consider that it may be overkill if you only have a few set of statuses and that adding another status is quite rare.
In XSLT 2.0 you can replace the muenchian grouping by its standard grouping mechanism. Applied to the given answer the xslt would look like:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" encoding="UTF-8"/>
<xsl:key name="muench" match="/root/item/status" use="."/>
<xsl:template match="/">
<root>
<xsl:for-each-group select="/root/item/status" group-by="key('muench', .)">
<xsl:call-template name="pivot">
<xsl:with-param name="status" select="."/>
</xsl:call-template>
</xsl:for-each-group>
</root>
</xsl:template>
<xsl:template name="pivot">
<xsl:param name="status"/>
<items>
<status><xsl:value-of select="$status"/></status>
<xsl:for-each select="/root/item[status=$status]">
<name><xsl:value-of select="name"/></name>
</xsl:for-each>
</items>
</xsl:template>
</xsl:stylesheet>