dingjun
dingjun

Reputation: 94

How to select nodes with conditions based on attributes

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:

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

Answers (2)

Dimitre Novatchev
Dimitre Novatchev

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

Martin Honnen
Martin Honnen

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

Related Questions