views:

568

answers:

4

Hello

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

+2  A: 

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.

Lucero
Having up voted this and gone to check something it turns out that I'm not actually using the method - I was doing something not right! I'm running the crude solution: <xsl:for-each select="//date[not(@year=preceding::date/@year)]"> wrapping <xsl:for-each select="//date[@year = current()/@year][not(@month=preceding::date[@year = current()/@year]/@month)]"> - but for the size of data I have (and you have) it works!
Murph
Thanks for the link, Lureco. I have started the reading about Muenchian grouping. And thanks for your comment Murph, that is a good base for the "quick-and-dirty" solution that I will start out with until I get the "Muenchian grouping" to work. BR. Anders
Tillebeck
A: 

in addition to lucero, check out http://stackoverflow.com/questions/950050/xsl-grouping-duplicates-problem for avoiding problems with month names being removes

Ivo
A: 
yu_sha
+1  A: 

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"&gt;
    <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.

Tomalak
Interesting use of the `document()` function (empty URL returns XSL transformation document - I'd expected to get the document being processed instead). Is this documented somewhere and portable across different 1.0 compliant XSLT engines?
Lucero
This is documented, standard behavior. All processors will behave like this.
Tomalak
Wow. Thanks a lot. I am new with XSLT and guess there would be a build in function or so :-) You work seems as a complete solution and I have started to include your solution on the webpage. Still in progress though, but I will mark this as an answer and get on with reading about Muenchian grouping and integrating your code into my macro. BR and thanks, Anders
Tillebeck
You are welcome. :) In XSLT 2.0 much has been improved in regard to grouping, it is much more natural than in 1.0. But as far as I understand, Umbraco does not support 2.0, right?
Tomalak
Yes, Umbraco is only supporting version 1.0. And I heard something about Microsoft trying to push their LINQ2XMl instead of developing support for for XSLT 2.0 into the .net framework (that Umbraco is based on) no clue if it is right, I just try to pass on the input I got from a programmer :-)
Tillebeck
After knowing what to search for (Muenchian) I came across this link with an Muenchian example for Umbraco. So it should be possible: http://our.umbraco.org/wiki/how-tos/xslt-useful-tips-and-snippets/grouping-output
Tillebeck
That link of yours demonstrates default Muenchian grouping, nothing Umbraco specific. Any XSLT 1.0 compliant engine can do this sort of thing.
Tomalak