How to Group in XSLT

If you search on the Internet in general, you will see that there is an XSLT 2.0 function called <xsl:for-each-group>, unfortunately the XSLT templating engine used by Spira is based on Microsoft .NET which still only supports XSLT 1.0.

Luckily there is a solution for grouping data that only relies on XSLT 1.0 functions, call the Muenchian grouping approach.

This means we use the following construct:

<xsl:key name="groups" match="/RequirementData/Requirement" use="ComponentName" />

   <xsl:template match="/RequirementData">
    <xsl:apply-templates select="Requirement[generate-id() = generate-id(key('groups', ComponentName)[1])]"/>
  </xsl:template>
 
 <xsl:template match="Requirement"> 
	<h1><xsl:value-of select="ComponentName"/></h1>
    <table id="{ComponentName}" class="DataGrid" style="width:100%">
      <tr>
        <th>Req #</th>
        <th>Name</th>
        <th>Type</th>
        <th>Priority</th>
        <th>Status</th>
        <th>Author</th>
        <th>Owner</th>
        <th>Creation Date</th>
        <th>Last Modified</th>
        <th>Release #</th>
      </tr>
      <xsl:for-each select="key('groups', ComponentName)">

... rest of report template, to display requirement fields

      </xsl:for-each>
    </table>
  </xsl:template>

This will loop through the Component fields, find each unique value, and then display a table of matching requirements under each component.

You can also do the same thing for a custom field (e.g. "Difficulty") by replacing ComponentName with something like:

CustomProperties/CustomProperty[Alias='Difficulty']/Value

Putting this all together would give:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
 <xsl:key name="groups" match="/RequirementData/Requirement" use="ComponentName" />

   <xsl:template match="/RequirementData">
    <xsl:apply-templates select="Requirement[generate-id() = generate-id(key('groups', ComponentName)[1])]"/>
  </xsl:template>
 
 <xsl:template match="Requirement"> 
	<h1><xsl:value-of select="ComponentName"/></h1>
    <table id="{ComponentName}" class="DataGrid" style="width:100%">
      <tr>
        <th>Req #</th>
        <th>Name</th>
        <th>Type</th>
        <th>Priority</th>
        <th>Status</th>
        <th>Author</th>
        <th>Owner</th>
        <th>Creation Date</th>
        <th>Last Modified</th>
        <th>Release #</th>
      </tr>
      <xsl:for-each select="key('groups', ComponentName)">
        <tr>
          <td>
            <xsl:value-of select="RequirementId"/>
          </td>
          <td>
            <xsl:attribute name="style">
              padding-left: <xsl:value-of select="string-length(IndentLevel)*2"/>px;
            </xsl:attribute>
            <xsl:if test="IsSummary='True'">
              <b>
                <xsl:value-of select="Name"/>
              </b>
            </xsl:if>
            <xsl:if test="IsSummary='False'">
              <xsl:value-of select="Name"/>
            </xsl:if>
          </td>
          <td>
            <xsl:value-of select="RequirementTypeName"/>
          </td>
          <td>
            <xsl:value-of select="ImportanceName"/>
          </td>
          <td>
            <xsl:value-of select="RequirementStatusName"/>
          </td>
          <td>
            <xsl:value-of select="AuthorName"/>
          </td>
          <td>
            <xsl:value-of select="OwnerName"/>
          </td>
          <td class="Date">
            <xsl:call-template name="format-date">
              <xsl:with-param name="datetime" select="CreationDate" />
            </xsl:call-template>
          </td>
          <td class="Date">
            <xsl:call-template name="format-date">
              <xsl:with-param name="datetime" select="LastUpdateDate" />
            </xsl:call-template>
          </td>

        </tr>
      </xsl:for-each>
    </table>
  </xsl:template>
  <xsl:template name="format-date">
    <xsl:param name="datetime"/>
    <xsl:variable name="date" select="substring-before($datetime, 'T')" />
    <xsl:variable name="year" select="substring-before($date, '-')" />
    <xsl:variable name="month" select="substring-before(substring-after($date, '-'), '-')" />
    <xsl:variable name="day" select="substring-after(substring-after($date, '-'), '-')" />
    <xsl:variable name="time" select="substring-before(substring-after($datetime, 'T'), '.')" />
    <xsl:variable name="monthname">
      <xsl:choose>
        <xsl:when test="$month='01'">
          <xsl:value-of select="'Jan'"/>
        </xsl:when>
        <xsl:when test="$month='02'">
          <xsl:value-of select="'Feb'"/>
        </xsl:when>
        <xsl:when test="$month='03'">
          <xsl:value-of select="'Mar'"/>
        </xsl:when>
        <xsl:when test="$month='04'">
          <xsl:value-of select="'Apr'"/>
        </xsl:when>
        <xsl:when test="$month='05'">
          <xsl:value-of select="'May'"/>
        </xsl:when>
        <xsl:when test="$month='06'">
          <xsl:value-of select="'Jun'"/>
        </xsl:when>
        <xsl:when test="$month='07'">
          <xsl:value-of select="'Jul'"/>
        </xsl:when>
        <xsl:when test="$month='08'">
          <xsl:value-of select="'Aug'"/>
        </xsl:when>
        <xsl:when test="$month='09'">
          <xsl:value-of select="'Sep'"/>
        </xsl:when>
        <xsl:when test="$month='10'">
          <xsl:value-of select="'Oct'"/>
        </xsl:when>
        <xsl:when test="$month='11'">
          <xsl:value-of select="'Nov'"/>
        </xsl:when>
        <xsl:when test="$month='12'">
          <xsl:value-of select="'Dec'"/>
        </xsl:when>
        <xsl:otherwise>
          <xsl:value-of select="''" />
        </xsl:otherwise>
      </xsl:choose>
    </xsl:variable>
    <xsl:value-of select="concat($day, '-' ,$monthname, '-', $year , ' ', $time)" />
  </xsl:template>
</xsl:stylesheet>

Sample Output

When you run this report you will get something like:

Custom Property Version

The full version using the "Difficulty" custom property would be:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
 <xsl:key name="groups" match="/RequirementData/Requirement" use="CustomProperties/CustomProperty[Alias='Difficulty']/Value" />

   <xsl:template match="/RequirementData">
    <xsl:apply-templates select="Requirement[generate-id() = generate-id(key('groups', CustomProperties/CustomProperty[Alias='Difficulty']/Value)[1])]"/>
  </xsl:template>
 
 <xsl:template match="Requirement"> 
	<h1><xsl:value-of select="CustomProperties/CustomProperty[Alias='Difficulty']/Value"/></h1>
    <table id="{CustomProperties/CustomProperty[Alias='Difficulty']/Value}" class="DataGrid" style="width:100%">
      <tr>
        <th>Req #</th>
        <th>Name</th>
        <th>Type</th>
        <th>Priority</th>
        <th>Status</th>
        <th>Author</th>
        <th>Owner</th>
        <th>Creation Date</th>
        <th>Last Modified</th>
        <th>Release #</th>
      </tr>
      <xsl:for-each select="key('groups', CustomProperties/CustomProperty[Alias='Difficulty']/Value)">
        <tr>
          <td>
            <xsl:value-of select="RequirementId"/>
          </td>
          <td>
            <xsl:attribute name="style">
              padding-left: <xsl:value-of select="string-length(IndentLevel)*2"/>px;
            </xsl:attribute>
            <xsl:if test="IsSummary='True'">
              <b>
                <xsl:value-of select="Name"/>
              </b>
            </xsl:if>
            <xsl:if test="IsSummary='False'">
              <xsl:value-of select="Name"/>
            </xsl:if>
          </td>
          <td>
            <xsl:value-of select="RequirementTypeName"/>
          </td>
          <td>
            <xsl:value-of select="ImportanceName"/>
          </td>
          <td>
            <xsl:value-of select="RequirementStatusName"/>
          </td>
          <td>
            <xsl:value-of select="AuthorName"/>
          </td>
          <td>
            <xsl:value-of select="OwnerName"/>
          </td>
          <td class="Date">
            <xsl:call-template name="format-date">
              <xsl:with-param name="datetime" select="CreationDate" />
            </xsl:call-template>
          </td>
          <td class="Date">
            <xsl:call-template name="format-date">
              <xsl:with-param name="datetime" select="LastUpdateDate" />
            </xsl:call-template>
          </td>

        </tr>
      </xsl:for-each>
    </table>
  </xsl:template>
  <xsl:template name="format-date">
    <xsl:param name="datetime"/>
    <xsl:variable name="date" select="substring-before($datetime, 'T')" />
    <xsl:variable name="year" select="substring-before($date, '-')" />
    <xsl:variable name="month" select="substring-before(substring-after($date, '-'), '-')" />
    <xsl:variable name="day" select="substring-after(substring-after($date, '-'), '-')" />
    <xsl:variable name="time" select="substring-before(substring-after($datetime, 'T'), '.')" />
    <xsl:variable name="monthname">
      <xsl:choose>
        <xsl:when test="$month='01'">
          <xsl:value-of select="'Jan'"/>
        </xsl:when>
        <xsl:when test="$month='02'">
          <xsl:value-of select="'Feb'"/>
        </xsl:when>
        <xsl:when test="$month='03'">
          <xsl:value-of select="'Mar'"/>
        </xsl:when>
        <xsl:when test="$month='04'">
          <xsl:value-of select="'Apr'"/>
        </xsl:when>
        <xsl:when test="$month='05'">
          <xsl:value-of select="'May'"/>
        </xsl:when>
        <xsl:when test="$month='06'">
          <xsl:value-of select="'Jun'"/>
        </xsl:when>
        <xsl:when test="$month='07'">
          <xsl:value-of select="'Jul'"/>
        </xsl:when>
        <xsl:when test="$month='08'">
          <xsl:value-of select="'Aug'"/>
        </xsl:when>
        <xsl:when test="$month='09'">
          <xsl:value-of select="'Sep'"/>
        </xsl:when>
        <xsl:when test="$month='10'">
          <xsl:value-of select="'Oct'"/>
        </xsl:when>
        <xsl:when test="$month='11'">
          <xsl:value-of select="'Nov'"/>
        </xsl:when>
        <xsl:when test="$month='12'">
          <xsl:value-of select="'Dec'"/>
        </xsl:when>
        <xsl:otherwise>
          <xsl:value-of select="''" />
        </xsl:otherwise>
      </xsl:choose>
    </xsl:variable>
    <xsl:value-of select="concat($day, '-' ,$monthname, '-', $year , ' ', $time)" />
  </xsl:template>
</xsl:stylesheet>

Running this version gives the following output: