Sum with transformations
Imagine a situation where you are provided with the following set of data:
<books>
<book>
<title>Gut: The Inside Story of Our Body's Most Underrated Organ</title>
<price>18.00$</price>
</book>
<book>
<title>American Gods</title>
<price>19.00$</price>
</book>
<book>
<title>Going Postal</title>
<price>20.00$</price>
</book>
<book>
<title>And the Band Played On: Politics, People, and the AIDS Epidemic</title>
<price>21.00$</price>
</book>
</books>
... and asked to compute the total of the prices of these books.
Your realization of the problem might be deferred until the moment you shove the data down the throat of the sum
function but there is no escaping it then. The exception invalid value for cast/constructor clearly indicates that the sum function cannot handle the dollar signs in the prices.
Sure, you could argue that this is really poorly designed XML
. And I would be the first one to agree! But the beauty of working in the realm of XML
is that the structure of the data is often up to someone else and therefore, as far as the poor receiving side is concerned, fixed.
Let's write a function that deals with the issue:
<!--
- Template that allows us to sum a list of numbers that include
- the given currency sign. It does so by stripping the given sign(s)
- of each and every number and then appending it to the result at the
- end of its recursive execution.
-->
<xsl:template name="utils:sumWithCurrency">
<xsl:param name="list" />
<xsl:param name="currency" />
<xsl:param name="total" select="0" />
<xsl:choose>
<!-- If there are any elements in the list, add the next one
to the current total and call recursively. -->
<xsl:when test="$list">
<xsl:call-template name="utils:sumWithCurrency">
<!-- Use all but the first element as the next list. -->
<xsl:with-param name="list" select="$list[position() > 1]"/>
<xsl:with-param name="currency" select="$currency"/>
<!-- Add the first element to the total while stripping the
currency sign off of it. -->
<xsl:with-param
name="total"
select="$total + number(translate($list[1], $currency, ''))"/>
</xsl:call-template>
</xsl:when>
<!-- No elements in the list -> append the currency sign and return. -->
<xsl:otherwise>
<xsl:value-of select="concat($total, $currency)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!--
- Companion function to the template of the identical name enabling us to write
- `<xsl:value-of select="utils:sumWithCurrency(/books/book/price, '$')" />`
- instead of having to invoke the actual template via `<xsl:call-template...`.
-->
<xsl:function name="utils:sumWithCurrency">
<xsl:param name="list" />
<xsl:param name="currency" />
<xsl:call-template name="utils:sumWithCurrency">
<xsl:with-param name="list" select="$list" />
<xsl:with-param name="currency" select="$currency" />
</xsl:call-template>
</xsl:function>
And voilà, you can now sum up the prices using the following:
<xsl:value-of select="utils:sumWithCurrency(/books/book/price, '$')" />
Naturally, similar challenges with different specifics could be solved by adjusting the summing function (or making it more general).