Re: [xsl] XSLT conditional sorting problem

Subject: Re: [xsl] XSLT conditional sorting problem
From: Jeni Tennison <jeni@xxxxxxxxxxxxxxxx>
Date: Tue, 11 Nov 2003 09:06:44 +0000
Hi Ned,

> When I encounter an album by various artists, I'd like to handle it
> as below, but was unable to figure out how to correctly do this:
>
> "Various Artists" - Album_1
>         Artist - Song_1
>         Artist - Song-2
>         ...

You're using Muenchian Grouping to group the <song> elements based on
their album, and applying templates to the first <song> in a
particular album in 'albums' mode.

Within that template, you can (and do) get hold of all the songs on
the album using:

  key('albums', @album)

Hold that in a variable called songs:

  <xsl:variable name="songs" select="key('albums', @album)" />

Now, if it's true that an @artist attribute of one of the <song>
elements in $songs is *not*equal*to* another @artist attribute of one
of the <song> elements in $songs, then you have an album by various
artists. You can test this with:

  $songs/@artist != $songs/@artist

Note that the != comparison uses "existential semantics" -- it returns
true if any of the comparisons between the artist attributes returns
true, so if *any* artist is not the same as *any* other artist.

So you can use this test to determine whether to put "Various Artists"
or use the name of the only artist:

  <xsl:choose>
    <xsl:when test="$songs/@artist != $songs/@artist">
      <xsl:text>Various Artists</xsl:text>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="@artist" />
    </xsl:otherwise>
  </xsl:choose>

You can also use the test to determine a mode in which to apply
templates to the songs, or to pass a parameter into a template,
whichever suits you better. For example:

  <ul title="Songs">
    <xsl:apply-templates select="$songs" mode="songs">
      <xsl:sort select="@tracknr" data-type="number" />
      <xsl:with-param name="show-artist"
        select="$songs/@artist != $songs/@artist" />
    </xsl:apply-templates>
  </ul>

with:

<xsl:template match="song" mode="songs">
  <xsl:param name="show-artist" select="false()" />
  <li title="(Track {@tracknr})">
    <xsl:if test="$show-artist">
      <xsl:value-of select="@artist"/>
      <xsl:text> - </xsl:text>
    </xsl:if>
    <xsl:value-of select="@title"/>
  </li>
</xsl:template>

> One additional thing, if anyone's feeling particularly intelligent
> and helpful - I would be interested to see how I'd sort my XML data
> into nested lists on both artist and album, as such:
>
> Artist_1
>         Album_1
>                 Song_1
>                 Song_2
>                 ...
>         Album_2
>         ...
> Artist_2
> ...

To do that, you need two levels of Muenchian Grouping. Define the two
keys, one that indexes the <song> elements by artist:

<xsl:key name="songs-by-artist" match="song" use="@artist" />

and another that indexes the <song> elements by artist and album by
concatenating the two values:

<xsl:key name="songs-by-artist-and-album" match="song"
  select="concat(@artist, ' - ', @album)" />

At the first level, you want to apply templates to the first <song> by
a particular artist, in 'artists' mode:

<xsl:template match="music">
  <ul title="Songs">
    <xsl:apply-templates mode="artists"
      select="song[generate-id(.) =
                   generate-id(key('songs-by-artist',@artist)[1])]" />
  </ul>
</xsl:template>

The template for 'artists' mode needs to give the artist, and then
apply templates to the first <song> element with that artist on each
particular album, in 'albums' mode, using the second key:

<xsl:template match="song" mode="artists">
  <li>
    <xsl:value-of select="@artist" />
    <xsl:variable name="songs"
                  select="key('songs-by-artist', @artist)" />
    <ul title="Songs">
      <xsl:apply-templates mode="albums"
        select="$songs[generate-id(.) =
                       generate-id(key('songs-by-artist-and-album',
                                       concat(@artist, ' - ', @album))[1])]" />
    </ul>
  </li>
</xsl:template>

The template for 'albums' mode is similar to what you have already,
except that it uses the 'songs-by-artist-and-album' key. A simplified
version would be:

<xsl:template match="song" mode="albums">
  <li>
    <xsl:value-of select="@album" />
    <xsl:variable name="songs"
      select="key('songs-by-artist-and-album',
                  concat(@artist, ' - ', @album))" />
    <ul title="Songs">
      <xsl:apply-templates select="$songs" mode="songs">
        <xsl:sort select="@tracknr" data-type="number" />
      </xsl:apply-templates>
    </ul>
  </li>
</xsl:template>

I don't know how you want to combine this two-level grouping with the
special handling of albums by "Various Artists" -- whether you want
the album to be listed under the artist or you want a special "Various
Artists" category -- but hopefully you should be able to work out how
to do it. If you need more help, let us know.

Cheers,

Jeni

---
Jeni Tennison
http://www.jenitennison.com/


 XSL-List info and archive:  http://www.mulberrytech.com/xsl/xsl-list


Current Thread