XSLT transformation-grouping and sum

Tag: xslt Author: liydong Date: 2013-06-29

I have a input xml

<students>
  <student>
  <name>John</name>
  <marks>100</marks>
</student>
<student>
  <name>Arvind</name>
  <marks>90</marks>
</student>
<student>
  <name>John</name>
  <marks>100</marks>
</student>
<student>
  <name>Arvind</name>
  <marks>80</marks>
</student>
</students>

I want the above xml to be transformed into

<students>
   <student>
     <name>John</name>
    <totalMarks>200</marks>
   </student>
   <student>
     <name>Arvind</name>
    <totalMarks>170</marks>
  </student>
 </students>

so basically I want to group the input xml based on the student name and get the sum of their marks as well.

Best Answer

In XSLT 1, the Muenchian method of grouping is typically used when there is one key:

t:\ftemp>type students.xml
<students>
  <student>
  <name>John</name>
  <marks>100</marks>
</student>
<student>
  <name>Arvind</name>
  <marks>90</marks>
</student>
<student>
  <name>John</name>
  <marks>100</marks>
</student>
<student>
  <name>Arvind</name>
  <marks>80</marks>
</student>
</students>
t:\ftemp>xslt students.xml students.xsl
<?xml version="1.0" encoding="utf-8"?>
<students>
   <student>
      <name>John</name>
      <totalMarks>200</totalMarks>
   </student>
   <student>
      <name>Arvind</name>
      <totalMarks>170</totalMarks>
   </student>
</students>
t:\ftemp>type students.xsl
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  version="1.0">

<xsl:key name="students-by-name" match="student" use="name"/>

<xsl:output indent="yes"/>

<xsl:template match="students">
  <students>
    <xsl:for-each
        select="student[generate-id(.)=
                        generate-id(key('students-by-name',name)[1])]">
      <student>
        <xsl:copy-of select="name"/>
        <totalMarks>
          <xsl:value-of select="sum(key('students-by-name',name)/marks
                                    [number(.)=number(.)])"/>
        </totalMarks>
      </student>
    </xsl:for-each>
  </students>
</xsl:template>

</xsl:stylesheet>
t:\ftemp>

comments:

In the classroom I also teach the variable-based grouping method, useful when you are creating groups within groups. That can be used when there is only one set of groups, but the Muenchian method is faster.
Hi... G. Ken Holman thanks a lot for your reply..can you kindly explain the select expressions in the for-each and select in the value-of of <totalmarks> element.
The argument to sum() is key('students-by-name',name)/marks[number(.)=number(.)] which is to say: find all students in the key table with the same name as the group's name element, get all of the <marks> element children, and only keep those that are valid numbers. The last bit can be omitted if you have faith that marks are correctly recorded, but if not, then the predicate protects the sum from being spoiled by something that is "not a number" (NaN).
Sorry, I missed that you also asked for the <xsl:for-each> expression. In XSLT 1.0 this is the classic "Muenchian method" for grouping, and it uses a key table. The table is loaded with all students and is indexed by the student name. The predicate ensures that only the first student in document order for each given name is selected. The body of the for-each is then executed once for each group.
Thanks a lot Ken..

Other Answer1

In xlst 2.0 you could use for-each-group:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format">
    <xsl:output method="xml" indent="yes" />

    <xsl:template match="/">
        <students>
            <xsl:apply-templates select="students" />
        </students>
    </xsl:template>

    <xsl:template match="students">
        <xsl:for-each-group select="student" group-by="name">
            <student>
                <name>
                    <xsl:value-of select="current-grouping-key()" />
                </name>
                <totalMarks>
                    <xsl:value-of select="sum(current-group()/marks)" />
                </totalMarks>
            </student>
        </xsl:for-each-group>
    </xsl:template>

</xsl:stylesheet>

comments:

thanks for the reply..is there any way where we can achieve the same using xslt 1.0 I don't have <xsl:for-each-group> available..
Yes it is, @Ken described this method