views:

1778

answers:

1

I have a situation where my XSLT files should display prices alongwith decimals conditionally, depending on whether the input XML contains decimals or not. So, I can receive XML files with two types of values - XML will contain all prices formatted with decimals upto two places (I call this one "Decimal-XML") or the prices will be rounded off to the nearest integer (I call this one "Integer-XML").

My problem is that I need to refactor as little as possible in the XSLT files and yet allow them to apply the transformation to XHTML in the same format as the XML input. In order to accomplish this, I implemented and suggested three guidelines to my team:

  1. Remove all format-number() function calls when the value is being computed for calculations or stored in a variable. Use number(<value>) instead. However, certain conditions apply to this rule (See below).
  2. When the value is to be displayed, use the format-number(<value>, '#.##') format. This should ensure that integer or decimal values will be displayed as originally present in the XML.
  3. For optional tags (such as "Discount"), use the format-number(<value>, '0.00') function even if the value is only being computed. This is necessary because if the tag is absent, trying to obtain a value will give an NaN result.

Here is a illustrative example of the XSLT:

  <x:stylesheet version="1.0" xmlns:x="http://www.w3.org/1999/XSL/Transform"&gt;
  <x:template match="/">
    <html>
      <body>
        <table border="1" width="60%">
          <tr>
            <th>Simple</th>
            <th>number()</th>
            <th>format-number(&lt;expression&gt;, '0.00')</th>
            <th>format-number(&lt;expression&gt;, '#.##')</th>
          </tr>
          <x:apply-templates />
        </table>
      </body>
    </html>
  </x:template>

  <x:template match="Item">
    <x:variable name="qty" select="number(@numItems)" />
    <x:variable name="cost" select="number(ItemCost) * $qty" />
    <x:variable name="extraCharges" select="(number(Tax) + number(TxnFee)) * $qty"/>
    <x:variable name="discount" select="format-number(Discount, '0.00') * $qty"/>
    <tr>
      <td>
      <!-- Works for Integer-XML, but values in Decimal-XML are
      *sometimes* rendered upto 14 decimal places. Even though Quickwatch
      shows it correctly in the debugger in VS. I cannot figure out what's
      special about the error cases. -->
        <x:value-of select="$cost + $extraCharges - $discount"/>
      </td>
      <td>
        <!-- Works same as the above case. -->
        <x:value-of select="number($cost + $extraCharges - $discount)"/>
      </td>
      <td>
      <!-- Works for Decimal-XML, but values in Integer-XML are always
      rendered with decimal digits. -->
        <x:value-of select="format-number(($cost + $extraCharges - $discount), '0.00')"/>
      </td>
      <td>
      <!-- Works for Integer-XML, but some values in Decimal-XML are
      rendered incorrectly. For example, 95.20 is rendered as 95.2;
      95.00 is rendered as 95 -->
        <x:value-of select="format-number(($cost + $extraCharges - $discount), '#.##')"/>
      </td>
    </tr>
  </x:template>
</x:stylesheet>

As the HTML comments note, it works in most cases but not all.

I would like to use a single expression to format all prices same as the input, rather than applying "when-otherwise" constructs everywhere which would additionally require a boolean parameter passed to the XSLT to determine the display format. The present state of the XSLT is that all numbers are rounded off regardless of the input, using format-number(<expression>, '0').

How do I accomplish this?


Edit: After Dimitre's comment, I decided to create a sample XML (and the XSLT above) so the experts can try it out easily.

Sample XML (This one contains decimals):

<ShoppingList>
  <Item numItems="2">
    <ItemCost>10.99</ItemCost>
    <Tax>3.99</Tax>
    <TxnFee>2.99</TxnFee>
    <Discount>2.99</Discount>
  </Item>
  <Item numItems="4">
    <ItemCost>15.50</ItemCost>
    <Tax>5.50</Tax>
    <TxnFee>3.50</TxnFee>
    <Discount>3.50</Discount>
  </Item>
</ShoppingList>
+2  A: 

If I understand you correctly, you want:

  • "Integer-XML" always to show up as a rounded number, no decimal places
  • "Decimal-XML" always to show up with two decimal places
  • not to use a template that does the formatting, but an XPath one-liner

The closest you have gotten to what you want is this expression:

<xsl:value-of select="
  format-number($cost + $extraCharges - $discount, '0.00')
" />

Since there is no easy to use conditional expression in XPath 1.0 (XPath 2.0 has if/then/else), a compromise would be this:

<xsl:value-of select="
  substring-before(
    concat(format-number($cost + $extraCharges - $discount, '0.00'), '.00')
    , '.00'
  )
" />

However, this "fails" (cutting off the decimals) when $cost, $extraCharges and $discount add up to a round number.

The unattractive alternative is to use Becker's method (named after Oliver Becker of HU Berlin):

concat(
  substring($s1, 1, number($condition)      * string-length($s1)),
  substring($s2, 1, number(not($condition)) * string-length($s2))
)

Technically, that's a one-liner. The two sub-string() parts are mutually exclusive based on $condition, so concat() will only return the one value or the other.

Practically (especially for your case), it's going to be an unmanageable mess, slow and entirely concealing the intention:

concat( 
  substring(
    format-number($cost + $extraCharges - $discount, '0.00')
    , 1
    , number(
      contains($cost, '.')
    ) * string-length(format-number($cost + $extraCharges - $discount, '0.00'))
  ),
  substring(
    format-number($cost + $extraCharges - $discount, '#.##')
    , 1
    , number(
      not(contains($cost, '.')) 
    ) * string-length(format-number($cost + $extraCharges - $discount, '#.##'))
  )
)

Since you stated that either all input values contained decimals, or none of them, the above expression only checks if $cost contains the decimal dot. It would be correct to check $extraCharges and $discount as well, I guess.

Tomalak