xslt on distinct nodes? - xslt

I have the following schema:
<parent>
<child id="1" name="Child 1 Version 1" />
</parent>
<parent>
<child id="2" name="Child 2 Version 1" />
</parent>
<parent>
<child id="1" name="Child 1 Version 2" />
</parent>
I want to handle only the last node for each id. Below is what I have tried based on some reading:
<xsl:for-each select="//parent/child">
<xsl:sort select="#id"/>
<xsl:if test="not(#id=following-sibling::*/#id)">
<xsl:element name="child">
<xsl:value-of select="#name"/>
</xsl:element>
</xsl:if>
</xsl:for-each>
But it does not seem to work. My output still contains all three elements. Any ideas on what I can do to correct my issue?

That I want to only handle the last
node for each id. Below is what I have
tried based on some reading:
<xsl:for-each select="//parent/child">
<xsl:sort select="#id"/>
<xsl:if test="not(#id=following-sibling::*/#id)">
<xsl:element name="child">
<xsl:value-of select="#name"/>
</xsl:element>
</xsl:if>
</xsl:for-each>
But it does not seem to work. My
output still contains all three of the
elements. Any ideas on what I can do
to correct my issue?
The problem with this code is that even though the nodes are in a sorted node-set, their following-sibling s are still the ones in the document.
In order for this code to work, one would first create an entirely new document in which the nodes are sorted in the desired way, then (in XSLT 1.0 it is necessary to use the xxx:node-set() extension on the produced RTF to make it an ordinary XML document) on this document the nodes have their siblings as desired.
Solution:
This transformation presents one possible XSLT 1.0 solution that does not require the use of extension functions:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="kchildById" match="child" use="#id"/>
<xsl:template match="/*">
<t>
<xsl:apply-templates select=
"*/child[generate-id()
=
generate-id(key('kchildById',
#id)[last()]
)
]
"/>
</t>
</xsl:template>
<xsl:template match="child">
<child>
<xsl:value-of select="#name"/>
</child>
</xsl:template>
</xsl:stylesheet>
when applied on the provided XML fragment (wrapped in a top element to become well-formed XML document and adding a second version for id="2"):
<t>
<parent>
<child id="1" name="Child 1 Version 1" />
</parent>
<parent>
<child id="2" name="Child 2 Version 1" />
</parent>
<parent>
<child id="1" name="Child 1 Version 2" />
</parent>
<parent>
<child id="2" name="Child 2 Version 2" />
</parent>
</t>
produces the wanted result:
<t>
<child>Child 1 Version 2</child>
<child>Child 2 Version 2</child>
</t>
Do note: the use of the Muenchian method for grouping.

This stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:key name="kParentByChildId" match="parent" use="child/#id"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="parent[count(.|key('kParentByChildId',
child/#id)[last()]) != 1]"/>
</xsl:stylesheet>
Output:
<root>
<parent>
<child id="2" name="Child 2 Version 1"></child>
</parent>
<parent>
<child id="1" name="Child 1 Version 2"></child>
</parent>
</root>
Note. Grouping by #id, selecting last of the group.
Edit: Just in case this is confusing. Above stylesheet means: copy everything execpt those child not having the last #id of the same kind. So, it's not selecting the last of the group, but as reverse logic, striping not last in the group.
Second. Why yours is not working? Well, because of the following-sibling axis. Your method for finding the first of a kind is from an old time where there was few processor implementing keys. Now those days are gone.
So, this stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:template match="t">
<xsl:for-each select="parent/child">
<xsl:sort select="#id"/>
<xsl:if test="not(#id=following::child/#id)">
<xsl:element name="child">
<xsl:value-of select="#name"/>
</xsl:element>
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Output:
<child>Child 1 Version 2</child>
<child>Child 2 Version 1</child>
Note: following axis, because child elements have not siblings.

Related

How to group parent elements based on the child elements in XSLT

I am tring to group multiple elements based on the child element that has the same value.
Each Child1 that has the same value should be inside of a node called Group. See desired output below.
Here is a sample XML
`<?xml version="1.0" encoding="utf-8"?>
<Root>
<Parent>
<Child1>0123</Child1>
<Child2>KIK</Child2>
<Child3>YAM</Child3>
</Parent>
<Parent>
<Child1>0123</Child1>
<Child2>FIS</Child2>
<Child3>BOL</Child3>
</Parent>
<Parent>
<Child1>0123</Child1>
<Child2>TOC</Child2>
<Child3>INO</Child3>
</Parent>
<Parent>
<Child1>456</Child1>
<Child2>CHI</Child2>
<Child3>KEN</Child3>
</Parent>
<Parent>
<Child1>456</Child1>
<Child2>ALA</Child2>
<Child3>KING</Child3>
</Parent>
</Root>`
I want the output to be like the following:
`<?xml version="1.0" encoding="utf-8"?>
<Root>
<GROUP>
<Parent>
<Child1>0123</Child1>
<Child2>KIK</Child2>
<Child3>YAM</Child3>
</Parent>
<Parent>
<Child1>0123</Child1>
<Child2>FIS</Child2>
<Child3>BOL</Child3>
</Parent>
<Parent>
<Child1>0123</Child1>
<Child2>TOC</Child2>
<Child3>INO</Child3>
</Parent>
</GROUP>
<GROUP>
<Parent>
<Child1>456</Child1>
<Child2>CHI</Child2>
<Child3>KEN</Child3>
</Parent>
<Parent>
<Child1>456</Child1>
<Child2>ALA</Child2>
<Child3>KING</Child3>
</Parent>
</GROUP>
</Root>`
I was only able to copy the whole code and not able to group it based on the values on Child1.
Sharing my solution below:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()" />
</xsl:copy>
</xsl:template>
<xsl:template match="Root">
<Root>
<xsl:for-each-group select="Parent" group-by="Child1">
<xsl:choose>
<xsl:when test="count(current-group()) gt 1">
<Group>
<xsl:apply-templates select="current-group()" />
</Group>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="current-group()" />
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</Root>
</xsl:template>
</xsl:stylesheet>

XSLT Muenchian Grouping on different elements based on a common attribute

I am given XML similar to the following that I need to process.
<root>
<Header/>
<Customer id="1" date="13/04/2014"/>
<Account id="1" date="14/04/2014"/>
<Account id="1" date="01/06/2015"/>
<Address id="1" date="14/04/2014"/>
<Customer id="2" date="12/08/2015"/>
<Account id="2" date="13/08/2015"/>
<Address id="2" date="13/08/2015"/>
<Address id="2" date="03/09/2015"/>
<Address id="2" date="27/01/2017"/>
<Customer id="3" date="04/10/2015"/>
<Customer id="3" date="01/02/2017"/>
<Account id="3" date="05/10/2015"/>
<Address id="3" date="08/10/2015"/>
<Address id="3" date="03/09/2016"/>
</root>
All of the nodes have more attributes but I stripped them off. Each element has an id and a date.If there are duplicate elements that have the same id then the one with the most recent date is considered valid and the older one should be ignored.
If the older ones can be stripped out at the same time I would like to output it into something like this.
<Customers>
<Customer id="1">
<Account/>
<Address/>
</Customer>
<Customer id="2">
<Account/>
<Address/>
</Customer>
<Customer id="3">
<Account/>
<Address/>
</Customer>
</Customers>
If not then it is fine to process the file in two transforms (one to group them by customer id and each customer have multiple Account/Address fields, then in the other transform remove the older entries)
The source XML has close to a million entries so performance is an issue. The transform taking a few minutes is fine, but any more than 15 will not work.
I currently have the following XSLT
<?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" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:key name="nodes-by-id" match="//root/*" use="#id"/>
<xsl:template match="root">
<Customers>
<xsl:for-each select="*[count(. | key('nodes-by-id', #id)[1]) = 1]">
<xsl:variable name="current-grouping-key" select="#id"/>
<xsl:variable name="current-group" select="key('nodes-by-id', $current-grouping-key)"/>
<Customer>
<xsl:attribute name="id">
<xsl:value-of select="$current-grouping-key"/>
</xsl:attribute>
<CustomerElements>
<xsl:for-each select="$current-group/Customer">
<CustomerElement>
<xsl:attribute name="date">
<xsl:value-of select="#date"/>
</xsl:attribute>
</CustomerElement>
</xsl:for-each>
</CustomerElements>
<xsl:apply-templates select="$current-group"/>
</Customer>
</xsl:for-each>
</Customers>
</xsl:template>
</xsl:stylesheet>
Currently this just tries to group all of the elements by their id, then output all of the Customer elements. I get the following:
<Customers>
<Customer id="">
<CustomerElements/>
</Customer>
<Customer id="1">
<CustomerElements/>
</Customer>
<Customer id="2">
<CustomerElements/>
</Customer>
<Customer id="3">
<CustomerElements/>
</Customer>
</Customers>
I get the customer with the blank ID because I don't ignore the header row. My real question is why does the $current-group variable not contain any elements?
Also any tips on how to ignore the header row, and to filter out entries with the older dates.
I got everything sorted. This is a segment of the XSLT I used. More info in the XML comments.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:key name="nodes-by-id" match="//root/*" use="#id"/>
<xsl:template match="PR-030">
<CustomerMeters>
<!-- Using select="Customer[cou.... instead of select="*[cou... will couse it to ignore the header. However it requres
the Customer element to be the first element for the icp in the xml. -->
<xsl:for-each select="Customer[count(. | key('nodes-by-id', #id)[1]) = 1]">
<xsl:variable name="current-grouping-key" select="#id"/>
<xsl:variable name="current-group" select="key('nodes-by-id', $current-grouping-key)"/>
<xsl:variable name="current-group-sorted">
<!-- If we sort all nodes by date order, then we can fetch the first Address/Customer/etc... from this group and we will have the latest-->
<xsl:for-each select="$current-group">
<!-- year -->
<xsl:sort select="substring(#date, 7, 4)" order="descending" data-type="number"/>
<!-- month -->
<xsl:sort select="substring(#date, 4, 2)" order="descending" data-type="number"/>
<!-- day -->
<xsl:sort select="substring(#date, 1, 2)" order="descending" data-type="number"/>
<xsl:copy-of select="current()" />
</xsl:for-each>
</xsl:variable>
<Customer>
<!-- In here I can get what I want from the current-group-sorted varaible-->
<!-- Because they are in date order I can just get the first occurance and it will be the most recent-->
<someField>
<xsl:value-of select="$current-group-sorted/*[self::Account][1]/#someAttribute"/>
</someField>
</Customer>
</xsl:for-each>
</CustomerMeters>
</xsl:template>
</xsl:stylesheet>

Add mandatory nodes with XSLT

I am facing an xslt/xpath problem and hope someone could help, in a few words here is what I try to achieve.
I have to transform an XML document where some nodes may be missing, these missing nodes are mandatory in the final result. I have the set of mandatory node names available in an xsl:param.
The base document is:
<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="TRANSFORM.xslt"?>
<BEGIN>
<CLIENT>
<NUMBER>0021732561</NUMBER>
<NAME1>John</NAME1>
<NAME2>Connor</NAME2>
</CLIENT>
<PRODUCTS>
<PRODUCT_ID>12</PRODUCT_ID>
<DESCRIPTION>blah blah</DESCRIPTION>
</PRODUCTS>
<PRODUCTS>
<PRODUCT_ID>13</PRODUCT_ID>
<DESCRIPTION>description ...</DESCRIPTION>
</PRODUCTS>
<OPTIONS>
<OPTION_ID>1</OPTION_ID>
<DESCRIPTION>blah blah blah ...</DESCRIPTION>
</OPTIONS>
<PROMOTIONS>
<PROMOTION_ID>1</PROMOTION_ID>
<DESCRIPTION>blah blah blah ...</DESCRIPTION>
</PROMOTIONS>
</BEGIN>
Here is the stylesheet so far:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fn="http://www.w3.org/2005/xpath-functions">
<xsl:output method="xml" encoding="UTF-8" indent="yes"/>
<xsl:param name="mandatoryNodes" as="xs:string*" select=" 'PRODUCTS', 'OPTIONS', 'PROMOTIONS' "/>
<xsl:template match="/">
<xsl:apply-templates select="child::node()"/>
</xsl:template>
<xsl:template match="node()">
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="BEGIN">
<xsl:element name="BEGIN">
<xsl:for-each select="$mandatoryNodes">
<!-- If there is no node with this name -->
<xsl:if test="count(*[name() = 'current()']) = 0">
<xsl:element name="{current()}" />
</xsl:if>
</xsl:for-each>
<xsl:apply-templates select="child::node()"/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
I tried the transformation in XML Spy, the xsl:iftest failed saying that 'current item is PRODUCTS of type xs:string.
I've tried the same xsl:if outside of a for-each and it seems to work ... what am I missing ?
Inside of <xsl:for-each select="$mandatoryNodes"> the context item is a string but you want to access the primary input document and its nodes so you need to store that document or the template's context node in a variable and use that e.g.
<xsl:template match="BEGIN">
<xsl:variable name="this" select="."/>
<xsl:element name="BEGIN">
<xsl:for-each select="$mandatoryNodes">
<!-- If there is no child node of `BEGIN` with this name -->
<xsl:if test="count($this/*[name() = current()]) = 0">
<xsl:element name="{current()}" />
</xsl:if>
</xsl:for-each>
<xsl:apply-templates select="child::node()"/>
</xsl:element>
</xsl:template>

XSLT multiple stylesheets

I have the following xml
<TopLevel>
<data m="R263">
<s ut="263firstrecord" lt="2013-02-16T09:21:40.393" />
<s ut="263secondrecord" lt="2013-02-16T09:21:40.393" />
</data>
<data m="R262">
<s ut="262firstrecord" lt="2013-02-16T09:21:40.393" />
<s ut="262secondrecord" lt="2013-02-16T09:21:40.393" />
</data>
</TopLevel>
I have some XSLT that does the call template but it's not itterating correctly.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="data">
<xsl:value-of select="#m" />
<xsl:variable name="vYourName" select="#m"/>
<xsl:choose>
<xsl:when test="#m='R262'">
<xsl:call-template name="R262"/>
</xsl:when>
</xsl:choose>
<xsl:choose>
<xsl:when test="#m='R263'">
<xsl:call-template name="R263"/>
</xsl:when>
</xsl:choose>
</xsl:template>
<xsl:template name="R262">
<xsl:for-each select="/TopLevel/data/s">
Column1=<xsl:value-of select="#ut" />
Column2=<xsl:value-of select="#lt" />
</xsl:for-each>
</xsl:template>
<xsl:template name="R263">
<xsl:for-each select="/TopLevel/data/s">
Column1=<xsl:value-of select="#ut" />
Column2=<xsl:value-of select="#lt" />
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
This gives me 8 records insead of the 4 (<s> level) records. I know it has to do with my iteration ... but I am not sure how to address this.
I am also aware of the apply stylesheets but I couldn't unravel that mystery either ... If someone can help me with XSLT that will only process everything from <TopLevel> to <\TopLevel> checking the value of m at the <data> level and applying the stylesheet at the <s> level for each <s> record I will be greateful beyond belief.
I don't know what output you want to produce, but I suspect you want to replace
<xsl:for-each select="/TopLevel/data/s">
by
<xsl:for-each select="s">
that is, you only want to process the "s" elements within the "data" you are currently processing, rather than selecting all the "s" elements in the whole document.
Why not do this using apply-templates?
<xsl:template match="data">
...
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="s[../#m='R262']">
...
</xsl:template>
<xsl:template match="s[../#m='R263']">
...
</xsl:template>
If you want to use match template and apply-templates you could do the following which gives you also a text output just like your stylesheet does. So this XSLT applied to your original source XML:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="data">
<xsl:value-of select="#m"/>
<xsl:apply-templates select="s"/>
</xsl:template>
<xsl:template match="s">
Column1=<xsl:value-of select="#ut"/>
Column2=<xsl:value-of select="#lt"/>
</xsl:template>
</xsl:stylesheet>
gives you this output:
<?xml version="1.0" encoding="UTF-8"?>
R263
Column1=263firstrecord
Column2=2013-02-16T09:21:40.393
Column1=263secondrecord
Column2=2013-02-16T09:21:40.393
R262
Column1=262firstrecord
Column2=2013-02-16T09:21:40.393
Column1=262secondrecord
Column2=2013-02-16T09:21:40.393
You basically only match on the s and give out the attributes "ut" and "lt". You can also output XML which would look better.
Using this XSLT:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<root>
<xsl:apply-templates/>
</root>
</xsl:template>
<xsl:template match="data">
<list>
<xsl:apply-templates select="s"/>
</list>
</xsl:template>
<xsl:template match="s">
<xsl:element name="record">
<xsl:attribute name="m">
<xsl:value-of select="parent::data/#m"/>
</xsl:attribute>
<item>Column1=<xsl:value-of select="#ut"/></item>
<item>Column2=<xsl:value-of select="#lt"/></item>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
will give you this nice XML output:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<list>
<record m="R263">
<item>Column1=263firstrecord</item>
<item>Column2=2013-02-16T09:21:40.393</item>
</record>
<record m="R263">
<item>Column1=263secondrecord</item>
<item>Column2=2013-02-16T09:21:40.393</item>
</record>
</list>
<list>
<record m="R262">
<item>Column1=262firstrecord</item>
<item>Column2=2013-02-16T09:21:40.393</item>
</record>
<record m="R262">
<item>Column1=262secondrecord</item>
<item>Column2=2013-02-16T09:21:40.393</item>
</record>
</list>
You have to adapt the original XSLT a little bit to get a nice XML structure. Also when matching s you "climb" up to element data to get the R-numbers for your attribute values.
The template matching root you need for a proper XML root element. <list> you could also get rid off then you have <record> as child of <root>.

Insert XML element using XSLT in exisiting node

I have the following XML document:
<root someAttribute="someValue" />
Now I want to add a tag using XSLT, so that the document will look like this:
<root someAttribute="someValue">
<item>TEXT</item>
</root>
If I repeat to use the XSLT once more it should just add another item:
<root someAttribute="someValue">
<item>TEXT</item>
<item>TEXT</item>
</root>
It sound's so easy, does it not? Here is the best I got after trying a ton of things:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0" >
<xsl:param name="message" />
<xsl:output method="xml" encoding="utf-8"/>
<xsl:template match="/*">
<xsl:copy>
<xsl:copy-of select="*"/>
<item>
<xsl:value-of select="$message" />
</item>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
It does /nearly/ what I have asked for, except that it "forgets" the attributes of the root element. I have found a number of other solutions here on stackoverflow and elsewhere that have in common with my solution that they loose the attributes of the root element. How can I fix that?
You're currently transforming only child nodes, not attributes.
<xsl:template match="root">
<xsl:copy>
<xsl:copy-of select="node()|#*"/> <!-- now does attrs too -->
<item>
<xsl:value-of select="$message" />
</item>
</xsl:copy>
</xsl:template>