4

Using the below XML, I need to figure out which person worked more hours in each site. For instance in the below XML, person 1 worked 8 hours in site 1 but person 2 worked only 6 hours. So result should contain person 1 and site 1 in transformed XML. If the hours are equal, select first person.

EDIT: I want this to be implemented using XSLT 1.0.

<root>
    <WorkSite Person="P1" Site="S1">
        <Hours>8</Hours>
    </WorkSite>
    <WorkSite Person="P1" Site="S2">
        <Hours>2</Hours>
    </WorkSite>
    <WorkSite Person="P1" Site="S3">
        <Hours>9</Hours>
    </WorkSite>
    <WorkSite Person="P2" Site="S1">
        <Hours>6</Hours>
    </WorkSite>
    <WorkSite Person="P2" Site="S2">
        <Hours>10</Hours>
    </WorkSite>
    <WorkSite Person="P2" Site="S3">
        <Hours>2</Hours>
    </WorkSite>
</root>

XSLT transform result should be like this:

<root> 
    <WorkSite Person="P1" Site="S1"/>  
    <WorkSite Person="P2" Site="S2"/> 
    <WorkSite Person="P1" Site="S3"/> 
</root>
Dour High Arch
  • 21,513
  • 29
  • 75
  • 90
Amzath
  • 3,159
  • 10
  • 31
  • 43

3 Answers3

7

This XSLT 1.0 transformation:

<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="kSiteByName" match="@Site" use="."/>

 <xsl:key name="kWorksiteBySite"
   match="WorkSite" use="@Site"/>

 <xsl:variable name="vSites" select=
  "/*/*/@Site[generate-id()
             =
              generate-id(key('kSiteByName',.)[1])
              ]"
  />

 <xsl:template match="/">
  <root>
    <xsl:for-each select="$vSites">
      <xsl:for-each select="key('kWorksiteBySite', .)">
        <xsl:sort select="Hours" data-type="number"
         order="descending"/>
        <xsl:if test="position()=1">
         <xsl:copy>
           <xsl:copy-of select="@*"/>
         </xsl:copy>
        </xsl:if>
      </xsl:for-each>
    </xsl:for-each>
  </root>
 </xsl:template>
</xsl:stylesheet>

when applied on the provided XML document:

<root>
    <WorkSite Person="P1" Site="S1">
        <Hours>8</Hours>
    </WorkSite>
    <WorkSite Person="P1" Site="S2">
        <Hours>2</Hours>
    </WorkSite>
    <WorkSite Person="P1" Site="S3">
        <Hours>9</Hours>
    </WorkSite>
    <WorkSite Person="P2" Site="S1">
        <Hours>6</Hours>
    </WorkSite>
    <WorkSite Person="P2" Site="S2">
        <Hours>10</Hours>
    </WorkSite>
    <WorkSite Person="P2" Site="S3">
        <Hours>2</Hours>
    </WorkSite>
</root>

produces the wanted, correct result:

<root>
    <WorkSite Person="P1" Site="S1"/>
    <WorkSite Person="P2" Site="S2"/>
    <WorkSite Person="P1" Site="S3"/>
</root>

Do note:

  1. The use of the Muenchian method for grouping to find all different Site values.

  2. The way maximum is found by sorting in descending order and getting the first result from the sorted node-list.

Dimitre Novatchev
  • 240,661
  • 26
  • 293
  • 431
2
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

   <xsl:template match="/">
      <root>
         <xsl:for-each-group select="*/WorkSite" group-by="@Site">
            <WorkSite Person="{(current-group()[Hours = max(current-group()/Hours)])[1]/@Person}" Site="{current-grouping-key()}" />
         </xsl:for-each-group>
      </root>
   </xsl:template>

</xsl:stylesheet>
Max Toro
  • 28,282
  • 11
  • 76
  • 114
  • Thank you for the answer. But I need this xslt version 1.0. – Amzath Aug 11 '10 at 19:19
  • +1 for XSLT 2.0 solution. A minor: I think you can replace `.//WorkSite` by `*/WorkSite` avoiding `//` operator. Also, to honor this `if the(re) are equal(s), select top person` you should wrap your sequence and filter the first, otherwise it will output the sequence. –  Aug 11 '10 at 20:29
0

An XSLT 1.0 solution. This stylesheet:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:key name="BySite" match="WorkSite" use="@Site"/>
 <xsl:template match="root">
  <root>
   <xsl:for-each select="/*/WorkSite[count(.|key('BySite',@Site)[1])=1]">
    <WorkSite Person="{key('BySite',@Site)
                       [not(key('BySite',@Site)/Hours 
                            > Hours)]/@Person}"
              Site="{@Site}" />
   </xsl:for-each>
  </root>
 </xsl:template>
</xsl:stylesheet>

Output:

<root>
 <WorkSite Person="P1" Site="S1" />
 <WorkSite Person="P2" Site="S2" />
 <WorkSite Person="P1" Site="S3" />
</root>
  • This is OK, but has an undesirable performance characteristics (O(N^2)). – Dimitre Novatchev Aug 11 '10 at 20:52
  • (Where did my comment go?) @Dimitre: Yes, that would be the worst scenario if the processor does not perform node set comparison optimization. –  Aug 12 '10 at 12:49
  • @Alejandro: Can you please provide the explanation for your Code. – Gracious Mar 07 '11 at 12:16
  • @anuj: This use the XPath 1.0 maximum expresion translated as "there is no one greater than me", instead of the better XSLT 1.0 maximum idiom wich is in Dimitre's answer. –  Mar 07 '11 at 20:46