Reputation: 94
I want to use XPath in XSLT to select nodes with conditions based on attribute values.
To illustrate my question, I have a short example XML instance like below:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<elementA fID="2013_4_20150722_0" dateTime="2015-07-13T01:04:20+02:00"/>
<elementA fID="2013_4_20150721_0" dateTime="2015-07-13T01:04:20+02:00"/>
<elementA fID="2013_4_20150721_0" dateTime="2015-07-20T14:14:22+02:00"/>
</root>
And I want to select all elementA
nodes with following conditions:
fID
is uniqueelementA
nodes with the same fID
attribute value, then only the one with newest dateTime
will be selected.So in my example I want to select the first and third elementA
.
How can I achieve this with XPath 2.0 in XSLT 2.0?
Upvotes: 2
Views: 851
Reputation: 243579
Here is a pure, single and efficient (no sorting) XPath 2.0 expression, which selects the wanted elements:
for $fid in distinct-values(/*/*/@fID),
$maxtime in max(/*/*[@fID eq $fid]/@dateTime/xs:dateTime(.))
return
(/*/*[@fID eq $fid and xs:dateTime(@dateTime) eq $maxtime])[1]
Here is a proof, in which XSLT is used just to copy the result of evaluating the expression to the output:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="/">
<xsl:sequence select=
"for $fid in distinct-values(/*/*/@fID),
$maxtime in max(/*/*[@fID eq $fid]/@dateTime/xs:dateTime(.))
return
(/*/*[@fID eq $fid and xs:dateTime(@dateTime) eq $maxtime])[1]
"/>
</xsl:template>
</xsl:stylesheet>
When the above transformation is applied on this source XML document:
<root>
<elementA fID="2013_4_20150722_0" dateTime="2015-07-13T01:04:20+02:00"/>
<elementA fID="2013_4_20150721_0" dateTime="2015-07-13T01:04:20+02:00"/>
<elementA fID="2013_4_20150721_0" dateTime="2015-07-20T12:14:22+00:00"/>
<elementA fID="2013_4_20150721_0" dateTime="2015-07-20T14:14:22+02:00"/>
<elementA fID="2013_4_20150721_0" dateTime="2015-07-20T14:14:22+02:00"/>
</root>
the wanted, correct result is produced:
<elementA fID="2013_4_20150722_0" dateTime="2015-07-13T01:04:20+02:00"/>
<elementA fID="2013_4_20150721_0" dateTime="2015-07-20T12:14:22+00:00"/>
Note on efficiency:
This XPath expression only uses the max()
function, which is
O(N)
-- better than the O(N*log(N)) of a solution that uses sorting.
Upvotes: 2
Reputation: 167716
I would do the grouping and sorting in XSLT 2.0, if you want to have that then available in XPath you could write a user-defined function wrapping the functionality:
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:mf="http://example.com/mf"
exclude-result-prefixes="xs mf">
<xsl:output indent="yes"/>
<xsl:function name="mf:group-and-sort" as="element(elementA)*">
<xsl:param name="input" as="element(elementA)*"/>
<xsl:for-each-group select="$input" group-by="@fID">
<xsl:variable name="sorted-group" as="element(elementA)*">
<xsl:perform-sort select="current-group()">
<xsl:sort select="xs:dateTime(@dateTime)" order="descending"/>
</xsl:perform-sort>
</xsl:variable>
<xsl:sequence select="$sorted-group[1]"/>
</xsl:for-each-group>
</xsl:function>
<xsl:template match="root">
<xsl:copy>
<xsl:variable name="max-elementAs" select="mf:group-and-sort(elementA)"/>
<xsl:copy-of select="$max-elementAs"/>
</xsl:copy>
</xsl:template>
</xsl:transform>
Online example is at http://xsltransform.net/jyH9rNb.
Upvotes: 1