Search code examples
xsltsortingumbracoforeachxslt-grouping

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


Solution

  • 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.