Tillebeck
Tillebeck

Reputation: 3523

XSLT, sort and group by year-date

Regarding Umbraco XSLT version 1.

I have aprox. 150 news items in XML. Lets say like this (all is pseudocode until I get more familiar with this xml/xslt):

<news>
  <data alias=date>2008-10-20</data>
</news>
<news>
  <data alias=date>2009-11-25</data>
</news><news>
  <data alias=date>2009-11-20</data>
</news> etc. etc....

I would like to run through the XML and create html-output as a news archive. Something like (tags not important):

2008
  Jan
  Feb
  ...
2009
  Jan
  Feb
  Mar
  etc. etc.

I can only come up with a nested for-each (pseudocode):

var year_counter = 2002
var month_counter = 1
<xsl:for-each select="./data [@alias = 'date']=year_counter">
  <xsl:for-each select="./data [@alias = 'date']=month_counter">
    <xsl:value-of select="data [@alias = 'date']>
  "...if month_counter==12 end, else month_counter++ ..."
  </xsl:for-each>
"... year_counter ++ ..."
</xsl:for-each>

But a programmer pointet out that looping through 10 years will give 120 loops and that is bad coding. Since I think Umbraco caches the result I am not so concerned, plus in this case there will be a max. of 150 records.

Any clues on how to sort and output many news items and group them in year and group each year in months?

Br. Anders

Upvotes: 2

Views: 5449

Answers (4)

Tomalak
Tomalak

Reputation: 338316

For the following solution I used this XML file:

<root>
  <news>
    <data alias="date">2008-10-20</data>
  </news>
  <news>
    <data alias="date">2009-11-25</data>
  </news>
  <news>
    <data alias="date">2009-11-20</data>
  </news>
  <news>
    <data alias="date">2009-03-20</data>
  </news>
  <news>
    <data alias="date">2008-01-20</data>
  </news>
</root>

and this XSLT 1.0 transformation:

<xsl:stylesheet 
  version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:cfg="http://tempuri.org/config"
  exclude-result-prefixes="cfg"
>
  <xsl:output method="xml" encoding="utf-8" />

  <!-- index news by their "yyyy" value (first 4 chars) -->
  <xsl:key 
    name="kNewsByY"  
    match="news" 
    use="substring(data[@alias='date'], 1, 4)" 
  />
  <!-- index news by their "yyyy-mm" value (first 7 chars) -->
  <xsl:key 
    name="kNewsByYM" 
    match="news" 
    use="substring(data[@alias='date'], 1, 7)" 
  />

  <!-- translation table (month number to name) -->
  <config xmlns="http://tempuri.org/config">
    <months>
      <month id="01" name="Jan" />
      <month id="02" name="Feb" />
      <month id="03" name="Mar" />
      <month id="04" name="Apr" />
      <month id="05" name="May" />
      <month id="06" name="Jun" />
      <month id="07" name="Jul" />
      <month id="08" name="Aug" />
      <month id="09" name="Sep" />
      <month id="10" name="Oct" />
      <month id="11" name="Nov" />
      <month id="12" name="Dec" />
    </months>
  </config>

  <xsl:template match="root">
    <xsl:copy>
      <!-- group news by "yyyy" -->
      <xsl:apply-templates mode="year" select="
        news[
          generate-id()
          =
          generate-id(key('kNewsByY', substring(data[@alias='date'], 1, 4))[1])
        ]
      ">
        <xsl:sort select="data[@alias='date']" order="descending" />
      </xsl:apply-templates>
    </xsl:copy>
  </xsl:template>

  <!-- year groups will be enclosed in a <year> element -->
  <xsl:template match="news" mode="year">
    <xsl:variable name="y" select="substring(data[@alias='date'], 1, 4)" />
    <year num="{$y}">
      <!-- group this year's news by "yyyy-mm" -->
      <xsl:apply-templates mode="month" select="
        key('kNewsByY', $y)[
          generate-id() 
          =
          generate-id(key('kNewsByYM', substring(data[@alias='date'], 1, 7))[1])
        ]
      ">
        <xsl:sort select="data[@alias='date']" order="descending" />
      </xsl:apply-templates>
    </year>
  </xsl:template>

  <!-- month groups will be enclosed in a <month> element -->
  <xsl:template match="news" mode="month">
    <xsl:variable name="ym" select="substring(data[@alias='date'], 1, 7)" />
    <xsl:variable name="m" select="substring-after($ym, '-')" />
    <!-- select the label of the current month from the config -->
    <xsl:variable name="label" select="document('')/*/cfg:config/cfg:months/cfg:month[@id = $m]/@name" />
    <month num="{$m}" label="{$label}">
      <!-- process news of the current "yyyy-mm" group -->
      <xsl:apply-templates select="key('kNewsByYM', $ym)">
        <xsl:sort select="data[@alias='date']" order="descending" />
      </xsl:apply-templates>
    </month>
  </xsl:template>

  <!-- for the sake of this example, news elements will just be copied -->
  <xsl:template match="news">
    <xsl:copy-of select="." />
  </xsl:template>
</xsl:stylesheet>

When the transformation is applied, the following output is produced:

<root>
  <year num="2009">
    <month num="11" label="Nov">
      <news>
        <data alias="date">2009-11-25</data>
      </news>
      <news>
        <data alias="date">2009-11-20</data>
      </news>
    </month>
    <month num="03" label="Mar">
      <news>
        <data alias="date">2009-03-20</data>
      </news>
    </month>
  </year>
  <year num="2008">
    <month num="10" label="Oct">
      <news>
        <data alias="date">2008-10-20</data>
      </news>
    </month>
    <month num="01" label="Jan">
      <news>
        <data alias="date">2008-01-20</data>
      </news>
    </month>
  </year>
</root>

It has the right structure already, you can adapt actual appearance to your own needs.

The solution is a two-phase Muenchian grouping approach. In the first phase, news items are grouped by year, in the second phase by year-month.

Please refer to my explanation of <xsl:key> and key() over here. You don't need to read the other question, though it is a similar problem. Just read the lower part of my answer.

Upvotes: 4

yu_sha
yu_sha

Reputation: 4400

You can't do month_counter++ in XSLT, it's not a procedural language and it's not how XSLT works. So, it's kind of pointless to worry about this being inefficient if this does not work this way.

This looks like a major pain in the neck in XSLT. My XSLT is not fresh enough to try and actually implement it. But here are two ways:

1)

  • use xsl:key to extract all unique years-
  • then iterate through these years. For each year do
  • use xsl:key to extract all months
  • For each month do

2) (seems easier, if it works.)

  • sort them by date, save sorted array in variable
  • iterate this variable (it's important that variable holds sorted array)
  • each time look at preceding-sibling. If its year/month not equal to the current element, write the appropriate header

3) Forget XSLT, use a real programming language.

Upvotes: 0

Ivo
Ivo

Reputation: 3436

in addition to lucero, check out Xsl grouping duplicates problem for avoiding problems with month names being removes

Upvotes: 0

Lucero
Lucero

Reputation: 60236

What you need is the so-called Muenchian Grouping method, which addresses exactly this problem/pattern for XSLT.

Basically, it groups by finding unique keys and looping over the entries contained in the key being used.

Upvotes: 2

Related Questions