RE: [xsl] grouping/position problem

Subject: RE: [xsl] grouping/position problem
From: "Michael Kay" <mike@xxxxxxxxxxxx>
Date: Sat, 23 Apr 2005 15:32:42 +0100
On entry to xsl:for-each-group, the context item (.) is the first item in
the group, by definition. So the expression (. is current-group()[1]) will
always be true. Changing "." to current() doesn't help because at the
outermost level of an XPath expression, current() is the same as ".".

I'm having to go back to your original post now to see what you're actually
trying to do.

Let's start by looking at your existing code. I've changed it so that the
XSLT code uses the element and attribute names that actually appear in the
XML source, please check I've got this right:

<list>
   <item author="five" year="2001"/>
   <item author="three" year="2003"/>
   <item author="four" year="2002"/>
   <item author="two" year="1998"/>
   <item author="one" year="2005"/>
   <item author="two" year="2000"/>
   <item author="four" year="1999"/>
</list>

   <xsl:template match="list" mode="sort_author-year">
     <xsl:for-each-group select="item" group-by="@author">
       <xsl:sort select="current-grouping-key()"/>
       <xsl:variable name="author-position" select="position()"/>
       <xsl:variable name="shorten-author" as="xs:boolean"
select="$author-position > 1" />
       <xsl:for-each-group select="current-group()" group-by="@year">
         <xsl:sort select="current-grouping-key()" />
	   <xsl:variable name="year">
           <xsl:value-of select="current-grouping-key()"/>
	     <xsl:if test="last() > 1">
	       <xsl:number value="position()" format="a"/>
	     </xsl:if>
	   </xsl:variable>
         <xsl:for-each select="current-group()">
           <xsl:apply-templates select=".">
             <xsl:with-param name="year" select="$year"/>
             <xsl:with-param name="shorten-author"
select="$shorten-author"/>
           </xsl:apply-templates>
         </xsl:for-each>
       </xsl:for-each-group>
     </xsl:for-each-group>
   </xsl:template>

Within xsl:for-each-group, you are processing a group of items. "." refers
to the first item in the group, position() refers to the position of this
group within the sequence of groups (it's 1 for the first group, 2 for the
second, and so on). last() is the number of groups. 

So this code:

   <xsl:variable name="author-position" select="position()"/>
   <xsl:variable name="shorten-author" as="xs:boolean"
select="$author-position > 1" />

sets $shorten-author to true if you're processing the second or subsequent
group of same-author items. I don't think this is what you intended - you
wanted it true for the second and subsequent items within each same-author
group. (You're only executing this code once for each group of same-author
items, so position() can't vary from one item in this group to another!)

The position of an author within a group of same-author items is given by
the value of position() in the inner loop, that is, the loop in which you
iterate over the same-author items.

Your data doesn't actually have an example of two items in which the author
and year are the same, nor do you give the required output, so I'm now
having to make guesses. Lets say you have:

<list>
   <item author="one" year="2001"/>
   <item author="one" year="2001"/>
   <item author="one" year="2002"/>
</list>

Do you want $shorten-author to be true only for the second item, or for the
third one as well? I'm assuming only for the second: that is, you're
concerned with the position within the group of same-author-same-year items.
This is simply the value of position() within the innermost group. 

This suggests that the code you want is:

   <xsl:template match="list" mode="sort_author-year">
     <xsl:for-each-group select="item" group-by="@author">
       <xsl:sort select="current-grouping-key()"/>
       <xsl:for-each-group select="current-group()" group-by="@year">
         <xsl:sort select="current-grouping-key()" />
	   <xsl:variable name="year">
           <xsl:value-of select="current-grouping-key()"/>
	     <xsl:if test="last() > 1">
	       <xsl:number value="position()" format="a"/>
	     </xsl:if>
	   </xsl:variable>
         <xsl:for-each select="current-group()">
           <xsl:variable name="author-position" select="position()"/>
           <xsl:variable name="shorten-author" as="xs:boolean"
select="$author-position > 1" />
           <xsl:apply-templates select=".">
             <xsl:with-param name="year" select="$year"/>
             <xsl:with-param name="shorten-author"
select="$shorten-author"/>
           </xsl:apply-templates>
         </xsl:for-each>
       </xsl:for-each-group>
     </xsl:for-each-group>
   </xsl:template>

You could equally well compute position() within the match="item" template
rule, in which case you could change the code 

        <xsl:for-each select="current-group()">
           <xsl:variable name="author-position" select="position()"/>
           <xsl:variable name="shorten-author" as="xs:boolean"
select="$author-position > 1" />
           <xsl:apply-templates select=".">
             <xsl:with-param name="year" select="$year"/>
             <xsl:with-param name="shorten-author"
select="$shorten-author"/>
           </xsl:apply-templates>
         </xsl:for-each>

to read

          <xsl:apply-templates select="current-group()">
             <xsl:with-param name="year" select="$year"/>
          </xsl:apply-templates>

If my assumption is wrong, and you want $shorten-author to be true for
subsequent items with the same author whether or not the year is the same,
you need to move the variable into the code that processes all the
same-author items and groups them by year; you will also need to sort by
year at this level so you get the position in the sorted order.

Michael Kay
http://www.saxonica.com/

P.S. I thought you solved this problem months ago?



> -----Original Message-----
> From: Bruce D'Arcus [mailto:bdarcus@xxxxxxxxx] 
> Sent: 23 April 2005 11:10
> To: xsl-list@xxxxxxxxxxxxxxxxxxxxxx
> Subject: Re: [xsl] grouping/position problem
> 
> 
> On Apr 23, 2005, at 4:48 AM, Michael Kay wrote:
> 
> > Assuming $NiQ is a member of current-group(), then you only need to 
> > test
> > "$NiQ is not the first in the group", because that implies 
> that there 
> > is
> > more than one node in the group. So:
> >
> > test="not($NiQ is current-group()[1])
> 
> Thanks.
> 
> But now a dumb question: how do I specify $NiQ?  What the expression 
> (or other code) that allows me to define "the member of a 
> current-group()"?  Here's the relevant bit, but I've got it wrong:
> 
>    <xsl:template match="mods:modsCollection" mode="sort_author-year">
>      <xsl:variable name="bibrefs" select="mods:mods"/>
>      <xsl:for-each-group select="$bibrefs" 
> group-by="bib:grouping-key(.)">
>        <xsl:sort select="current-grouping-key()"/>
>        <xsl:variable name="author-shorten" select="test="not(. is 
> current-group()[1])"/>
>        <!-- next group -->
> 
> Bruce

Current Thread