Reputation: 17152
Given a list of elements with a date attribute, for example,
<foo>
<bar date="2001-04-15"/>
<bar date="2002-01-01"/>
<bar date="2005-07-04"/>
<bar date="2010-11-10"/>
</foo>
I would like to retrieve the element closest to but not after a given date using XSLT.
Calling this function with parameter "2008-01-01" should print <bar date="2005-07-04">
. Assume the context node is already <foo>
.
I'm not sure what would be easier, but I could also set up three attributes: day, month, year, instead of having one date attribute.
Upvotes: 4
Views: 627
Reputation: 52878
Here's an XSLT 2.0 option...
XML Input
<foo>
<bar date="2001-04-15"/>
<bar date="2005-07-04"/>
<bar date="2002-01-01"/>
<bar date="2010-11-10"/>
</foo>
XSLT 2.0
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:param name="threshold" select="xs:date('2008-01-01')"/>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="foo">
<xsl:variable name="closestDate" as="node()*">
<xsl:apply-templates select="bar[$threshold >= xs:date(@date)]">
<xsl:sort select="@date" data-type="text"/>
</xsl:apply-templates>
</xsl:variable>
<xsl:copy-of select="$closestDate[last()]"/>
</xsl:template>
</xsl:stylesheet>
XML Output
<bar date="2005-07-04"/>
Explanation of "foo" template...
<xsl:template match="foo">
<!--First a variable named 'closestDate' is created by doing an
'xsl:apply-templates' to all 'bar' elements that have a '@date'
attribute that is less than or equal to the 'threshold' parameter
(which is '2008-01-01' in the example). Notice that both '@date'
and '$threshold' are cast as 'xs:date' so that the date comparison
will work correctly. Also, we use the 'as="node()*"' attribute to
cast the variable as zero or more nodes() so that each individual
'bar' can be accessed individually.-->
<xsl:variable name="closestDate" as="node()*">
<xsl:apply-templates select="bar[$threshold >= xs:date(@date)]">
<!--This 'xsl:sort' is used to put all the 'bar' elements in order
based on the '@date' attribute.-->
<xsl:sort select="@date" data-type="text"/>
</xsl:apply-templates>
</xsl:variable>
<!--What we end up with for the 'closestDate' variable is this:
<bar date="2001-04-15"/>
<bar date="2002-01-01"/>
<bar date="2005-07-04"/>
In the following 'xsl:copy-of', we choose the last node
in 'closestDate'.-->
<xsl:copy-of select="$closestDate[last()]"/>
</xsl:template>
Upvotes: 2
Reputation: 122394
For XSLT 1.0 this is tricky, as it doesn't support dates as first class values, nor lexicographic comparisons of strings. And it doesn't (per spec) support constructing a node set in a temporary variable and then extracting individual nodes from that set. Nontheless what you want is possible with a few tricks.
The fact that you have your dates as YYYY-MM-DD means that if you strip out the hyphens and treat the resulting string as a number, sorting these numbers in numerical order gives the same result as sorting the original dates in chronological order. And although XSLT doesn't have updateable variables, you can get a similar effect by having a template which recursively applies itself to sibling nodes one by one, passing the state down in template parameters.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:param name="targetDate" select="20080101" />
<xsl:template match="foo">
<!-- find the first "candidate" bar whose date is before the target date -->
<xsl:apply-templates select="(bar[translate(@date, '-', '') < $targetDate])[1]" />
</xsl:template>
<xsl:template match="bar">
<xsl:param name="closest" select="." />
<!-- find the next candidate bar whose date is before the target date -->
<xsl:variable name="nextCandidate"
select="(following-sibling::bar[translate(@date, '-', '') < $targetDate])[1]" />
<xsl:choose>
<xsl:when test="$nextCandidate">
<xsl:choose>
<xsl:when test="translate($nextCandidate/@date, '-', '') > translate($closest/@date, '-', '')">
<!-- $nextCandidate is closer to the target than the current $closest -->
<xsl:apply-templates select="$nextCandidate">
<xsl:with-param name="closest" select="$nextCandidate" />
</xsl:apply-templates>
</xsl:when>
<xsl:otherwise>
<!-- current $closest is still the closest -->
<xsl:apply-templates select="$nextCandidate">
<xsl:with-param name="closest" select="$closest" />
</xsl:apply-templates>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:otherwise>
<!-- no more candidates, so $closest is the node we require -->
<xsl:copy-of select="$closest" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Upvotes: 3