Search code examples
xsltxslt-2.0xslt-groupingmuenchian-grouping

XSLT Multilevel Grouping: xsl:for-each-group with Muenchian method


I am trying to understand grouping in XSLT. I have an XML document that contains the answers to a questionnaire, listed by respondent ("pid"). I wanted to group the answers by question for all respondents so that I could create charts, etc. Here is the basic XML structure:

<survey>
    <answers>
        <pid xml:id="p1">
            <category key="#c1">
                <head>Category Heading</head>
                ... 
                <question key="#q1.4">
                    <answer key="#a2"/>
                    <answer key="#a3"/>
                    <answer key="#a4"/>
                </question
                ...
            </category
            ...
        </pid> 
        <pid xml:id="p2">
            <category key="#c1">
                <head>Category Heading</head>
                ... 
                <question key="#q1.4"> 
                    <answer key="#a2"/>
                    <answer key="#a3"/>
                    <answer key="#a4"/>
                </question
                ...
            </category
            ...
        </pid>   
        <pid xml:id="p3">
            <category key="#c1">
                <head>Category Heading</head>
                ... 
                <question key="#q1.4"> 
                    <answer key="#a2"/>                   
                </question
                ...
            </category
            ...
        </pid>   
        <pid xml:id="p4">
            <category key="#c1">
                <head>Category Heading</head>
             ... 
            <question key="#q1.4">
                <answer key="#a1"/>                                                             
            </question
            ...
            </category
            ...
        </pid>   
    </answers>
</survey>

I also wanted to count the occurrences of each answer across the respondents. I tried using XSLT 2.0's for-each-group by itself, but couldn't get it to work the way I wanted. I ended up combining it with the Muenchian (XSLT 1.0) grouping technique and finally did get it to work. However, I still don't really understand how it's working. Here is the XSLT bit that I used to group the <answer> elements:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xd="http://www.oxygenxml.com/ns/doc/xsl"
    exclude-result-prefixes="#all"
    version="2.0">
    <xsl:output encoding="iso-8859-1" method="text"
        omit-xml-declaration="yes" indent="yes"/>
    <xsl:strip-space elements="*"/>     

    <xsl:key name="answerKey" match="answer" use="@key"/>
    <xsl:key name="answerKey3" match="answer" use="concat(generate-id(..),' ',@key)"/>

    <xsl:template match="/">
    ...
        <xsl:for-each-group select="/survey/answers/pid/category/question" group-by="@key">          
            <xsl:variable name="qKey" select="@key"/>
            <xsl:variable name="x" select="generate-id(.)"/>  
            <xsl:text>
            </xsl:text>
            <xsl:value-of select="substring-after(@key,'#')"/>                                            
            <xsl:for-each-group select="//answer[count(.|key('answerKey3',concat($x,' ',@key))[1])=1][parent::question[@key=$qKey]]" group-by="@key">
            <xsl:text>
            </xsl:text>    
                <xsl:variable name="aID" select="substring-after(@key,'#')"/>                               
                <!-- <xsl:value-of select="key('questionKey2',parent::question/@key)/Answers/Answer[@xml:id=$aID]"/> -->                
                <xsl:value-of select="substring-after(@key,'#')"/>                        
                <xsl:text>,</xsl:text>                                                       
                <xsl:value-of select="count(key('answerKey',@key)[ancestor::question[@key=$qKey]])"/>                 
            </xsl:for-each-group>  
            <xsl:text>
            </xsl:text>
        </xsl:for-each-group>
    </xsl:template>
</xsl:stylesheet>

With this code, I am able to output a comma-separated list of answers and answer counts grouped by question, for example:

q1.4
a1,1
a2,3
a3,2
a4,2

Can anyone explain how this is working, especially the second (nested) for-each-group that uses the Muenchian XPath to group my <answer> elements?

Is there a simpler way to achieve the same result? Thank you!


Solution

  • This can be simplified a bit without the need for muenchian grouping (as lovely as that is).

    You have started correctly, by grouping the questions by their @key.

    <xsl:for-each-group select="/survey/answers/pid/category/question" group-by="@key">
    

    But then, for each question, you need to look at all the answers for that particular question, and group them. You could use a key in this instance, to group all the answers for a given question

    <xsl:key name="answerKey" match="answer" use="../@key"/>
    

    Then, to group the answers by @key, within a given question, you could still use xsl:for-each-group

    <xsl:for-each-group select="key('answerKey', current-grouping-key())" group-by="@key">
    

    i.e Get all answers for the question, and group them by the @key attribute. current-grouping-key() in this instance contains the @key attribute of the current question.

    Finally, it is straight-forward to give the totals for the grouped answers

    <xsl:value-of 
      select="concat(current-grouping-key(), ',', count(current-group()), '&#10;')" />
    

    Here is the full XSLT

    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xd="http://www.oxygenxml.com/ns/doc/xsl" exclude-result-prefixes="#all" version="2.0">
       <xsl:output encoding="iso-8859-1" method="text" omit-xml-declaration="yes" indent="yes"/>
       <xsl:strip-space elements="*"/>
       <xsl:key name="answerKey" match="answer" use="../@key"/>
    
       <xsl:template match="/"> 
          <xsl:for-each-group select="survey/answers/pid/category/question" group-by="@key">
             <xsl:value-of select="concat(current-grouping-key(), '&#10;')" />
             <xsl:for-each-group select="key('answerKey', current-grouping-key())" group-by="@key">
                <xsl:sort select="@key" />
                <xsl:value-of select="concat(current-grouping-key(), ',', count(current-group()), '&#10;')" />
             </xsl:for-each-group>
          </xsl:for-each-group>
       </xsl:template>
    </xsl:stylesheet>
    

    When applied to your XML, the following is output

    #q1.4
    #a1,1
    #a2,3
    #a3,2
    #a4,2