Peter
Peter

Reputation: 5

Help with XSLT to transform XML: grouping and selecting elements with max value

I have the current XML:

<DocumentElement>
  <Customer>
    <CustomerId>2315</CustomerId>
    <Date>2011-04-28 14:14:00</Date>   
    <VersionNumber>1</VersionNumber>
    <GUID>2E05DE20-02A0-425D-944D-65E5E744FF8A</GUID>
  </Customer>
  <Customer>
    <CustomerId>2316</CustomerId>
    <Date>2011-04-28 15:03:00</Date>
    <VersionNumber>2</VersionNumber>
    <GUID>2E05DE20-02A0-425D-944D-65E5E744FF8A</GUID>
  </Customer>
  <Customer>
    <CustomerId>2317</CustomerId>
    <Date>2011-04-28 15:03:00</Date>
    <VersionNumber>1</VersionNumber>
    <GUID>9995DE20-02A0-425D-944D-65E5E744FF8A</GUID>
  </Customer>
</DocumentElement>

What I am trying to do is to filter out one element of each GUID with the highest version number, i.e, transform the document above to look like:

<DocumentElement>
  <Customer>
    <CustomerId>2316</CustomerId>
    <Date>2011-04-28 15:03:00</Date>
    <VersionNumber>2</VersionNumber>
    <GUID>2E05DE20-02A0-425D-944D-65E5E744FF8A</GUID>
  </Customer>
  <Customer>
    <CustomerId>2317</CustomerId>
    <Date>2011-04-28 15:03:00</Date>
    <VersionNumber>1</VersionNumber>
    <GUID>9995DE20-02A0-425D-944D-65E5E744FF8A</GUID>
  </Customer>
</DocumentElement>

Anyone who can point me in the right direction on where to start?

Thanks in advance.

Upvotes: 0

Views: 727

Answers (2)

mousio
mousio

Reputation: 10337

I would start with declaring a key on GUID, this enables to easily process distinct GUIDs; then, per GUID, sort the corresponding Customer elements on their VersionNumber and simply copy the first (xslt-1.0):

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>
    <xsl:key name="byGUID" match="Customer" use="GUID"/>
    <xsl:template match="/*">
        <xsl:copy>
            <!-- process first Customer for each distinct GUID -->
            <xsl:apply-templates select="//Customer[generate-id()=generate-id(key('byGUID',GUID))]"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="*">
        <!-- sort and process all Customers with the same GUID -->
        <xsl:apply-templates select="key('byGUID',GUID)" mode="copyFirst">
            <xsl:sort select="VersionNumber" order="descending"/>
        </xsl:apply-templates>
    </xsl:template>
    <xsl:template match="*" mode="copyFirst">
        <!-- only copy the first (highest VersionNumber) -->
        <xsl:if test="position()=1">
            <xsl:copy-of select="."/>
        </xsl:if>
    </xsl:template>
</xsl:stylesheet> 

Using this intersection of nodesets is another way to process the first Customer for each distinct GUID:

<xsl:apply-templates select="//Customer[count(.|key('byGUID',GUID)[1])=1]"/>

Upvotes: 1

MarcoS
MarcoS

Reputation: 13574

As an exercise, I've tried to solve this using XSLT 2.0 and XPath 2.0:

<xsl:stylesheet version="2.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <xsl:output method="xml" indent="yes" omit-xml-declaration="no" />

  <xsl:template match="/DocumentElement">
   <xsl:copy>
    <xsl:for-each-group select="Customer" group-by="GUID">
     <xsl:copy-of select="current-group()
                          [VersionNumber=max(current-group()/VersionNumber)]" />
    </xsl:for-each-group>
   </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

Major differences:

  • with XSLT 2.0 you don't need to define an xsl:key as in this other answer, but you can use xsl:for-each-group ... group-by="GUID"

  • with XPath 2.0 you have the fn:max(...) function

Upvotes: 3

Related Questions