XSLT grouping items by count

Tag: xslt , xslt-grouping Author: GUAILANG Date: 2013-04-29

I have the following list of items.

<items>
    <item type="Type1">Item1</item>
    <item type="Type2">Item2<item>
    <item type="Type2">Item3<item>
    <item type="Type3">Item4<item>
    <item type="Type3">Item5<item>
    <item type="Type1">Item6<item>
    <item type="Type3">Item7<item>
    <item type="Type1">Item8<item>
    <item type="Type2">Item9<item>
    <item type="Type1">Item10<item>
<items>

I'm having trouble figuring out the XSLT required so that the above are displayed in groups of Type1(x1), Type2(x2), Type3(x4), where the counts are the number in parenthesis or less. In other words, the goal is to create a repeating pattern: the next item of Type1 if any of those remain, then the next two items of Type2 or fewer if less than two remain, then the next four items of Type3 or fewer if less than four remain.

So the desired output would look something like the below:

<div class="Items">
    <div class="Type1">Item1</div>
    <div class="Type2">Item2</div>
    <div class="Type2">Item3</div>
    <div class="Type3">Item4</div>
    <div class="Type3">Item5</div>
    <div class="Type3">Item7</div>
    <div class="Type1">Item6</div>
    <div class="Type2">Item9</div>
    <div class="Type1">Item8</div>
    <div class="Type1">Item10</div>
</div>

From the above output, you can see that the ordering has changed. i.e. there is <=1 Type 1, followed by <=2 Type2, followed by <=4 Type3, and this pattern repeats itself. I suppose the items will need to be grouped into the pattern described and repeat itself until the full list if items are exhausted. I hope I make sense.

Can anyone provide the required XSLT or some pointers please?

Thanks, John.

Best Answer

Please give this a whirl:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" indent="yes"/>
  <xsl:key name="kType2Group" match="item[@type = 'Type2']"
           use="floor(count(preceding-sibling::item[@type = 'Type2']) div 2) + 1"/>
  <xsl:key name="kType3Group" match="item[@type = 'Type3']"
           use="floor(count(preceding-sibling::item[@type = 'Type3']) div 4) + 1"/>

  <xsl:template match="@* | node()" name="Copy">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="/*">
    <xsl:copy>
      <xsl:apply-templates select="item[@type = 'Type1']" />
    </xsl:copy>
  </xsl:template>

  <xsl:template match="item[@type = 'Type1']">
    <xsl:call-template name="Copy" />
    <xsl:apply-templates select="key('kType2Group', position())" />
    <xsl:apply-templates select="key('kType3Group', position())" />
  </xsl:template>
</xsl:stylesheet>

When run on your input XML, the result is:

<items>
  <item type="Type1">Item1</item>
  <item type="Type2">Item2</item>
  <item type="Type2">Item3</item>
  <item type="Type3">Item4</item>
  <item type="Type3">Item5</item>
  <item type="Type3">Item7</item>
  <item type="Type1">Item6</item>
  <item type="Type2">Item9</item>
  <item type="Type1">Item8</item>
  <item type="Type1">Item10</item>
</items>

The keys in this case are used to separate the Type2 and Type3 items into groups, so that they can be retrieved by their group number (1, 2, 3, etc.) the logic for determining the group numbers is to count the number of preceding items of the same type, divide that number by the number of items in each group, round down, and add one. So for example, this calculation carried out for the first four Type2s would be:

floor(0 div 2) + 1 = floor(0) + 1 = 0 + 1 = 1
floor(1 div 2) + 1 = floor(0.5) + 1 = 0 + 1 = 1
floor(2 div 2) + 1 = floor(1) + 1 = 1 + 1 = 2
floor(3 div 2) + 1 = floor(1.5) + 1 = 1 + 1 = 2

and so on.

The general logic of the XSLT is:

  • Match the root element, copy it, and apply templates to all Type1 items.
  • The template for Type1 items:
    • Copies the item itself.
    • Uses the key to apply templates to the Type2 items in its group (position() will be 1 for the first Type1 item, 2 for the second, and so on.
    • Uses the key to apply templates to the Type3 items in its group
  • The first template, the identity template, does the job of copying anything that doesn't have any other template to match it.

comments:

Thanks @JLRishe. That worked when I put it through xslttest.appspot.com. As you can probably tell, I'm not very good with XSLT. It'd be great if you can give a me a bit of a walkthrough your solution? Especially around the use of keys.
Added some elaboration above.
Awesome! This is some powerful stuff. I will try to apply this to the scenario I have at hand. Thanks again.
Hi @JLRishe, I find that this solution only works when there are more Type1 items than the groups. eg. If I only had 1 Type1 item, it would only show a group of Type 2 and Type 3, but not the remaining Type2 and Type 3 groups, given that there are more of course. Similarly, in the case where there are no Type1 items, and only Type2 and Type3 items, this XSLT would display nothing. Any ideas how we can get around this? Many thanks.
Good point. I will come up with an improved approach in the next 24 hours. Sorry for the oversight.