I would like to sort elements based on a derived value based on child elements. The derived value cannot be calculated using XPath (sum
, concat
, etc), but can with XSL (xsl:choose
, xsl:if
, etc).
I would use the EXSLT function extension but it is not available. The environment is XSLT 1.0, Xalan-C++ version 1.10 with the EXSLT common and set extensions.
EDIT Changed the example to emphasize that the derived value I need to group by cannot be calculated with simple node/xpath functions in xsl:sort
statements.
My goal is to list current medications before inactive ones, sorted by descending start date. The logic to determine if a medication is current depends on if it was canceled, hasn't expired, and some other business logic.
Given this XML:
<?xml version="1.0"?>
<medications>
<medication>
<name>med1</name>
<status>canceled</status>
<startTime>2012-02-01T00:00:00Z</startTime>
<endTime>2012-12-31T00:00:00Z</endTime>
<!-- other elements omitted -->
</medication>
<medication>
<name>med2</name>
<status />
<startTime>2012-01-01T00:00:00Z</startTime>
<endTime>2012-01-07T00:00:00Z</endTime>
<!-- other elements omitted -->
</medication>
<medication>
<name>med3</name>
<status />
<startTime>2012-01-01T00:00:00Z</startTime>
<!-- other element omitted -->
</medication>
</medications>
The stylesheet will produce a sorted list of medications including information omitted from the example data (ordering doctor, pharmacy location, etc) and data from a parent node (patient address, primary care physician, etc). For this example, I'll just produce a simple sorted list that shows the medication node can be traversed:
<?xml version="1.0" encoding="utf-8"?>
<medications>
<medication isCurrent="1" name="med3" />
<medication isCurrent="0" name="med1" />
<medication isCurrent="0" name="med2" />
</medications>
The best I could come up with is to pre-calculate the derived value into an EXSLT node-set (along with other values needed for sort), and use a key to lookup the medication element by generate-id:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exsl="http://exslt.org/common"
exclude-result-prefixes="exsl">
<xsl:output encoding="utf-8" method="xml" indent="yes" />
<xsl:key name="medications-by-id" match="medication" use="generate-id()" />
<xsl:variable name="medication-sorter">
<xsl:for-each select="//medication">
<item id="{generate-id(.)}">
<xsl:attribute name="isCurrent">
<xsl:apply-templates mode="isCurrentMedication" select="." />
</xsl:attribute>
<xsl:attribute name="startTime">
<xsl:value-of select="startTime/text()" />
</xsl:attribute>
</item>
</xsl:for-each>
</xsl:variable>
<xsl:template match="medications">
<!-- hardcoded key lookup works -->
<hardcoded><xsl:value-of select="key('medications-by-id',generate-id(medication[2]))/name/text()"/></hardcoded>
<!-- but key lookup from the sort helper does not -->
<medications>
<xsl:for-each select="exsl:node-set($medication-sorter)/item">
<xsl:sort select="@isCurrent" order="descending" />
<xsl:sort select="@startTime" order="descending" />
<medication>
<xsl:attribute name="isCurrent">
<xsl:value-of select="@isCurrent" />
</xsl:attribute>
<xsl:attribute name="name">
<xsl:value-of select="key('medications-by-id',@id)/name/text()" />
</xsl:attribute>
</medication>
</xsl:for-each>
</medications>
</xsl:template>
<xsl:template mode="isCurrentMedication" match="medication">
<xsl:choose>
<xsl:when test="(status/text()='canceled') or (status/text()='discontinued') or (status/text()='inactive')">0</xsl:when>
<xsl:otherwise>
<!-- omitted date checking logic not relevent to this question, so just hardcoded -->
<xsl:choose>
<xsl:when test="name/text()='med2'">0</xsl:when>
<xsl:when test="name/text()='med3'">1</xsl:when>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
However, this didn't work as expected. When looking up the key with generate-id(medication[2]) the node is valid and the name is output, but not work when called using the @id from the node set, even though the values appear to be exactly the same:
<?xml version="1.0" encoding="utf-8"?>
<medications>
<hardcoded>med2</hardcoded>
<medication isCurrent="1" name="" />
<medication isCurrent="0" name="" />
<medication isCurrent="0" name="" />
</medications>
Also tested this with Xalan for Java 2.7.1 with the same result.
I can get around this by including a copy-of
the medication element in the $medication-sorter node-set, but then the parent context is lost and there are times where my styleheet will need that.
Is there another way to approach sorting/grouping on a value that must be calculated using a xsl:template
?