2
votes

I'm new with xsl and faced an issue with counting nodes in multiple xml documents. Here is my XSLT fragment:

<xsl:variable name="count">
    <xsl:for-each select="document(./log/@file)/testResults/*[not(@lb = preceding::*/@lb)]">
            <xsl:value-of select="count(../*[@lb = current()/@lb])"/>
    </xsl:for-each>
</xsl:variable>

where ./log/@file maches few xml documents. An xml document sample:

<testResults version="1.2">
        <sample t="63" lt="0" ts="1343919489839" s="true" lb="jp@gc - Dummy Sampler" rc="200" rm="OK" tn="Thread Group 1-1" dt="text" by="114"/>
        <sample t="62" lt="0" ts="1343919489903" s="true" lb="jp@gc - Dummy Sampler" rc="200" rm="OK" tn="Thread Group 1-1" dt="text" by="114"/>
        <sample t="58" lt="0" ts="1343919490063" s="true" lb="jp@gc - Dummy Sampler" rc="200" rm="OK" tn="Thread Group 1-1" dt="text" by="114"/>
        <sample t="13" lt="0" ts="1343919490210" s="true" lb="jp@gc - Dummy Sampler" rc="200" rm="OK" tn="Thread Group 1-1" dt="text" by="114"/>
        <sample t="37" lt="0" ts="1343919490223" s="true" lb="jp@gc - Dummy Sampler" rc="200" rm="OK" tn="Thread Group 1-1" dt="text" by="114"/>
</testResults>

I have same structure for all parsed documents.

And an issue finally... count function returns count for one document correctly. But next count result will concatinated to previous one. My target is to sum the results of each iteration.

So if I'll have 15 maches for 1st iteration and 4 matches for second iteration count variable will be set to 154.

Could you please help with this?

P.s. I'm counting child elements of testResults grouping by lb attribute

P.p.s xsl version is 1.0

Thanks, Valeriy

1
Could you, please, explain exactly which nodes are you counting for each document? This isn't clear but is necessary to know in order to give an answer. Please, edit the question and add this missing information.Dimitre Novatchev

1 Answers

1
votes

I would go with a pipe-line design. In the first stage, for a given document, collect all the @lb counts, with something like so...

<xsl:variable name="phase-1-output">
  <xsl:apply-templates select="..some-expression.../@file" mode="phase-1">
</xsl:variable>

<template match="@file" mode="phase-1">
 <xsl:apply-templates select="document(.)/testResults/sample" mode="phase-1" />
</template>

<xsl:template match="*" mode="phase-1" />

<xsl:template match="testResults/*[not(@lb = preceding::*/@lb)]" mode="phase-1" >
 <lb-group key="{@lb}">
  <xsl:number count="../*[@lb = current()/@lb]" />
 <lb-group>
</xsl:variable>

This gives us a variable ($phase-1-output) which contains a list of elements (lb-group) containing a count and a key.Replace the select expression in the first template with what-ever you need for your problem space.

You may have @lb values which are shared across documents, and I presume that you want these grouped together and summed. So in phase 2, you apply the same grouping and counting technique as you did in phase 1, except that the input comes from the $phase-1-output variable, and you will be summing, instead of counting. To access the lb-groups within $phase-1-output, you will need to use the node-set() function.

Let me know if this is enough, or you want a full style-sheet.


Update

The OP has asked for a full style-sheet so here it is. Due to the lack of supplied appropriate sample data, I made up a couple of sample input documents with the same salient features as the OP provided one, but cut down and simplified as is appropriate for a Q & A site.

Input

Suppose we have 2 input files. The first file, with URL in1.xml has this content:

<testResults category="citrus">
 <sample lb="lemon" />
 <sample lb="lemon" />
 <sample lb="green apple" />
 <sample lb="green apple" />
 <sample lb="green apple" />
</testResults>

And another file, with URL in2.xml has this content:

<testResults category="green food">
 <sample lb="green apple" />
 <sample lb="celery soup" />
 <sample lb="peas" />
 <sample lb="peas" />
</testResults>

The OP's stated requirement is to ...

count child elements of testResults grouping by lb attribute

Required output

Thus the required output will be as follows. I invented the non-informational structure because the OP forgot to supply it.

<root>
   <lb-group lb-key="lemon">2</lb-group>
   <lb-group lb-key="green apple">4</lb-group>
   <lb-group lb-key="celery soup">1</lb-group>
   <lb-group lb-key="peas">2</lb-group>
</root>

The reader will notice that there are 4 green apples. 3 are from the first input document, and 1 is from the second. I have assumed that the OP wanted counting across file boundaries. If separation is required, that is to say, counting on a strictly per-file basis, please let me know.

The solution

On the Saxon XSLT processor, in backwards compatibility mode, this result can be achieved by the following XSLT 1.0 style-sheet, which implements the aforementioned pipe-line design.

<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:so="http://stackoverflow.com/questions/11847434"
  xmlns:exslt="http://exslt.org/common"
  exclude-result-prefixes="xsl so exslt">
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes" />
<xsl:strip-space elements="*" />

<xsl:variable name="test-result-files">
 <so:file file="in1.xml" />
 <so:file file="in2.xml" />
</xsl:variable>

<xsl:template match="/" >
 <root>
  <xsl:variable name="phase-1-output" >
    <xsl:apply-templates select="document('')/*/xsl:variable
      [@name='test-result-files']/so:file/@file" mode="phase-1" />
  </xsl:variable>
  <xsl:apply-templates select="$phase-1-output/lb-group" mode="phase-2" />
 </root>
</xsl:template>

<xsl:template match="@file" mode="phase-1">
 <xsl:apply-templates select="document(.)/testResults/sample" mode="phase-1" />
</xsl:template>

<xsl:template match="*" mode="phase-1" />

<xsl:template match="testResults/*[not(@lb = following::*/@lb)]" mode="phase-1" >
 <xsl:variable name="lb-key" select="@lb" />
 <lb-group lb-key="{$lb-key}">
  <xsl:number count="*[@lb = $lb-key]" />
 </lb-group>
</xsl:template>    

<xsl:template match="*" mode="phase-2" />

<xsl:template match="lb-group[not(@lb-key = following::*/@lb-key)]" mode="phase-2">
 <xsl:copy>
  <xsl:copy-of select="@*" />
  <xsl:value-of select="sum(../*[@lb-key=current()/@lb-key])" />
 </xsl:copy>
</xsl:template>

</xsl:stylesheet> 

Caveat

Depending on what your XSLT engine is, you may have to replace the line...

  <xsl:apply-templates select="$phase-1-output/lb-group" mode="phase-2" />

...with...

  <xsl:apply-templates select="xslt:node-set($phase-1-output)/lb-group" mode="phase-2" />

...or the Microsoft equivalent, if using the MS processor.