XSLT, sort and group by year-date

孤人 提交于 2019-12-01 00:41:42
Tomalak

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.

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.

Ivo

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

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!