Search code examples
xmlxsltcountingmuenchian-grouping

Need XSLT transform to count elements returned from applied template


I have a very similar problem to this: Need XSLT transform to remove duplicate elements - sorted by an attribute. I took the solution from the best answer and slightly modified it. I have to count the statuses, but taking into account only the latest results.

My template looks like this:

<xsl:template match='Results/Result' mode='countstatus'>
    <xsl:param name='status' select='"Pass"'/>
    <xsl:for-each select="key('sn-key', SerialNumber)">
        <xsl:sort select='./Date' order='descending'/>
        <xsl:if test='((position() = 1) and (./Status=$status))'>
            <xsl:copy-of select="."/>
        </xsl:if>
    </xsl:for-each>
</xsl:template>

I don't want to display all the matched results but only the number of items returned after this template is applied.

My key definition is <xsl:key name='sn-key' match='Results/Result' use='SerialNumber'/> and I call this template with <xsl:apply-templates select='Results/Result[generate-id() = generate-id(key("sn-key", SerialNumber)[1])]' mode='countstatus'/>.

EDIT

With fresh mind I see that my question is a bit unclear. Here are more details.

My input looks like this:

<Results>
    <Result ID="0">
        <SerialNumber>3333</SerialNumber>
        <Status>Fail</Status>
        <Date>21</Date>
    </Result>
    <Result ID="1">
        <SerialNumber>1111</SerialNumber>
        <Status>Fail</Status>
        <Date>34</Date>
    </Result>
    <Result ID="2">
        <SerialNumber>1111</SerialNumber>
        <Status>Pass</Status>
        <Date>67</Date>
    </Result>
    <Result ID="3">
        <SerialNumber>2222</SerialNumber>
        <Status>Fail</Status>
        <Date>40</Date>
    </Result>
    <Result ID="4">
        <SerialNumber>1111</SerialNumber>
        <Status>Fail</Status>
        <Date>55</Date>
    </Result>
    <Result ID="5">
        <SerialNumber>1111</SerialNumber>
        <Status>Fail</Status>
        <Date>88</Date>
    </Result>
    <Result ID="6">
        <SerialNumber>2222</SerialNumber>
        <Status>Fail</Status>
        <Date>22</Date>
    </Result>
    <Result ID="7">
        <SerialNumber>1111</SerialNumber>
        <Status>Fail</Status>
        <Date>86</Date>
    </Result>
    <Result ID="8">
        <SerialNumber>3333</SerialNumber>
        <Status>Pass</Status>
        <Date>99</Date>
    </Result>
</Results>

I would like to count e.g. how many times the Fail status occurs, but considering only the latest results for SerialNumber. The template I showed earlier displays all the latest results for SerialNumber (but for status Pass). I would like to display just the number. In example when looking for the number of fails I would like to display 2 instead of [1111 Fail 88, 2222 Fail 40].

My transformation look like this now (but I need something else):

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

<xsl:key name='sn-key' match='Results/Result' use='SerialNumber' />

<xsl:template match="/">
    <xsl:apply-templates select="Results/Result[generate-id()=generate-id(key('sn-key', SerialNumber)[1])]"/>
</xsl:template>

<xsl:template match="Results/Result">
    <xsl:param name='status' select='"Fail"'/>
    <xsl:for-each select="key('sn-key', SerialNumber)">
        <xsl:sort select="./Date" order="descending"/>
        <xsl:if test='((position() = 1) and (./Status=$status))'>
            <xsl:value-of select="."/><br />
        </xsl:if>
    </xsl:for-each>
</xsl:template>

</xsl:stylesheet>

Solution

  • --- edited in response to clarification ---

    I would like to count e.g. how many times the Fail status occurs, but considering only the latest results for SerialNumber.

    I believe that translates to:

    • group the Results by SerialNumber;
    • count the groups whose most recent Result has Status of "Fail"

    The exact method depends on whether the Results are listed in chronological order in the input XML.

    If the answer is yes, you can simply modify the Muenchian method to select the last (instead of the usual first) Result in each group, add another predicate to keep only those whose Status is "Fail" and count this set. This is a one-liner:

    XSLT 1.0

    <xsl:stylesheet version="1.0" 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
    
    <xsl:key name="result-by-sn" match="Result" use="SerialNumber" />
    
    <xsl:template match="/Results">
        <output>
            <xsl:value-of select="count(Result[count(. | key('result-by-sn', SerialNumber)[last()]) = 1][Status='Fail'])"/>
        </output>
    </xsl:template>
    
    </xsl:stylesheet>
    

    If the answer is no, then you have no choice but to actually perform the grouping, sorting and filtering into a variable, then count the result:

    XSLT 1.0

    <xsl:stylesheet version="1.0" 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
    
    <xsl:key name="result-by-sn" match="Result" use="SerialNumber" />
    
    <xsl:template match="/Results">
        <xsl:variable name="temp">
            <xsl:for-each select="Result[count(. | key('result-by-sn', SerialNumber)[1]) = 1]">
                <xsl:for-each select="key('result-by-sn', SerialNumber)">
                    <xsl:sort select="Date" order="descending"/>
                    <xsl:if test="position()=1 and Status='Fail'">x</xsl:if>
                </xsl:for-each>
            </xsl:for-each>
        </xsl:variable>
        <output>
            <xsl:value-of select="string-length($temp)"/>
        </output>
    </xsl:template>
    
    </xsl:stylesheet>