How to split a grouping into 3 parts

H

Hvid Hat

Hi

At first, I thought I could only solve my problem with a C# method inside
my XSLT but I'm beginning to think it might be possible with XSLT only. So
I'm trying, but I need help :) How can I split a grouping into 3 parts?

I've got the following grouping of countries which is working fine:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:eek:utput method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:key name="x" match="Country" use="." />
<xsl:template match="Persons">
<xsl:for-each select="Person[generate-id(Country) = generate-id(key('x',
Country)[1])]">
<xsl:value-of select="Country" /><br />
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

But how can I split the grouping into 3 parts. Say there's 11 (unique) countries.
How can I split them into 3 parts so I can produce 3 lists of 4, 4 and 3
elements, e.g.

<ul>
<li>Argentina</li>
<li>Belgium</li>
<li>Canada</li>
<li>Denmark</li>
</ul>
<ul>
<li>England</li>
<li>France</li>
<li>Greece</li>
<li>Hungary</li>
</ul>
<ul>
<li>Iceland</li>
<li>Japan</li>
<li>Kenya</li>
</ul
 
P

Pavel Lepin

Hvid Hat said:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:eek:utput method="xml" version="1.0" encoding="UTF-8"
indent="yes"/> <xsl:key name="x" match="Country" use="."
/> <xsl:template match="Persons">
<xsl:for-each select="Person[generate-id(Country) =
generate-id(key('x',
Country)[1])]">
<xsl:value-of select="Country" /><br />
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

But how can I split the grouping into 3 parts. Say there's
11 (unique) countries. How can I split them into 3 parts
so I can produce 3 lists of 4, 4 and 3 elements, e.g.

Define a grouping key that would allow you to select only
first occurrences of country elements in your document.
Then use count() and position() XPath functions to split it
into n chunks.
 
H

Hvid Hat

Hello Pavel,
Define a grouping key that would allow you to select only first
occurrences of country elements in your document. Then use count() and
position() XPath functions to split it into n chunks.

I figured, I had to use a combination of position(), count() and the mod
operator to achieve it, but I'm still not comfortable enough with XSLT to
know how to go about it. I've looked at http://www.dpawson.co.uk/xsl/sect2/N4486.html
for grouping by position without luck. I'm just not into the XSLT way of
thinking (yet).

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:eek:utput method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:key name="x" match="Country" use="."/>
<xsl:template match="Persons">
<xsl:for-each select="Person[generate-id(Country) = generate-id(key('x',
Country)[1])]">
<xsl:value-of select="position()"/> of <xsl:value-of select="last()"/> -
<xsl:value-of select="Country"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet
 
M

Martin Honnen

Hvid said:
I figured, I had to use a combination of position(), count() and the mod
operator to achieve it, but I'm still not comfortable enough with XSLT
to know how to go about it. I've looked at
http://www.dpawson.co.uk/xsl/sect2/N4486.html for grouping by position
without luck. I'm just not into the XSLT way of thinking (yet).

Here is an example XML document:

<root>
<country>USA</country>
<country>Germany</country>
<country>France</country>
<country>Russia</country>
<country>Spain</country>
<country>Russia</country>
<country>Italy</country>
<country>Canada</country>
<country>USA</country>
<country>Portugal</country>
<country>France</country>
<country>Denmark</country>
<country>Sweden</country>
</root>

Here is an XSLT 1.0 stylesheet that uses Muenchian grouping to save the
unique countries in a variable, then the exslt:node-set function
(supported by XslCompiledTransform) to get a node-set to be split into
three components:

<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exslt="http://exslt.org/common"
exclude-result-prefixes="exslt"
version="1.0">

<xsl:eek:utput method="html" indent="yes"/>

<xsl:param name="number-of-parts" select="3"/>

<xsl:key name="by-country" match="country" use="."/>

<xsl:variable
name="unique-countries-rtf">
<xsl:for-each
select="/root/country[generate-id() =
generate-id(key('by-country', .)[1])]">
<xsl:copy-of select="."/>
</xsl:for-each>
</xsl:variable>

<xsl:variable
name="unique-countries"
select="exslt:node-set($unique-countries-rtf)/country"/>

<xsl:variable name="items-per-list"
select="ceiling(count($unique-countries) div $number-of-parts)"/>

<xsl:template match="root">
<div>
<xsl:apply-templates
select="$unique-countries[position() mod $items-per-list = 1]"
mode="group"/>
</div>
</xsl:template>

<xsl:template match="country" mode="group">
<ul>
<xsl:apply-templates
select=". | following-sibling::country[position() &lt;
$items-per-list]"
mode="item"/>
</ul>
</xsl:template>

<xsl:template match="country" mode="item">
<li>
<xsl:value-of select="."/>
</li>
</xsl:template>

</xsl:stylesheet>

Result looks like this:

<div>
<ul>
<li>USA</li>
<li>Germany</li>
<li>France</li>
<li>Russia</li>
</ul>
<ul>
<li>Spain</li>
<li>Italy</li>
<li>Canada</li>
<li>Portugal</li>
</ul>
<ul>
<li>Denmark</li>
<li>Sweden</li>
</ul>
</div>


If you don't want to use exslt:node-set then I think you need to chain
two stylesheets where the first creates a list with the unique countries
and the second splits them into three groups.
 
P

Pavel Lepin

Martin Honnen said:
Here is an example XML document:

<root>
<country>USA</country>
<country>Germany</country>
<country>France</country>
<country>Russia</country>
<country>Spain</country>
<country>Russia</country>
<country>Italy</country>
<country>Canada</country>
<country>USA</country>
<country>Portugal</country>
<country>France</country>
<country>Denmark</country>
<country>Sweden</country>
</root>

[transformation snipped]
If you don't want to use exslt:node-set then I think you
need to chain two stylesheets where the first creates a
list with the unique countries and the second splits them
into three groups.

Not at all.

<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:eek:utput method="xml" indent="yes"/>
<xsl:key name="countries" match="/root/country"
use="string(.)"/>
<xsl:key name="unique-countries" match="/root/country"
use="count(.|key('countries',.)[1])"/>
<xsl:variable name="unique-countries"
select="key('unique-countries',1)"/>
<xsl:variable name="parts" select="3"/>
<xsl:template match="root">
<result>
<xsl:apply-templates select="$unique-countries"
mode="group">
<xsl:with-param name="parts" select="$parts"/>
<xsl:with-param name="elts"
select="$unique-countries"/>
</xsl:apply-templates>
</result>
</xsl:template>
<xsl:template match="/root/country" mode="group">
<xsl:param name="parts"/>
<xsl:param name="elts"/>
<xsl:variable name="total" select="count($elts)"/>
<xsl:variable name="per-group"
select="ceiling($total div $parts)"/>
<xsl:variable name="pos" select="position()-1"/>
<xsl:variable name="group"
select="floor($pos div $per-group)"/>
<xsl:if test="$pos mod $per-group=0">
<group>
<xsl:apply-templates
select=
"
$elts
[floor((position()-1) div $per-group)=$group]
"/>
</group>
</xsl:if>
</xsl:template>
<xsl:template match="/root/country">
<list-elt>
<xsl:apply-templates/>
</list-elt>
</xsl:template>
</xsl:stylesheet>

Xalan will choke on this due to key() invocation in a use
attribute of an xsl:key element, of course. libxslt and
Saxon won't.
 
M

Martin Honnen

Pavel said:
Xalan will choke on this due to key() invocation in a use
attribute of an xsl:key element, of course. libxslt and
Saxon won't.

Saxon 6 complains too: "key() function cannot be used here". .NET's
XslCompiledTransform also complains: "The 'key()' function cannot be
used in 'use' and 'match' attributes of 'xsl:key' element.".
And with Saxon 9 and XSLT 2 I don't think you need keys at all, you can
use xsl:for-each-group and temporary trees if needed.
 
P

Pavel Lepin

Martin Honnen said:
Saxon 6 complains too: "key() function cannot be used
here". .NET's XslCompiledTransform also complains: "The
'key()' function cannot be used in 'use' and 'match'
attributes of 'xsl:key' element.". And with Saxon 9 and
XSLT 2 I don't think you need keys at all, you can use
xsl:for-each-group and temporary trees if needed.

The differences are due to unclear status of E13 in XSLT 1.0
Errata. Frankly, I think the idea is ridiculous. The WG
tried to remove an incredibly useful feature, using 'cycle
prevention' as a justification. Hell, why not disallow
recursion? it might be infinite, after all.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,744
Messages
2,569,484
Members
44,903
Latest member
orderPeak8CBDGummies

Latest Threads

Top