XSLT 基于输入 XML 的十进制格式

发布于 2024-07-16 03:42:34 字数 3684 浏览 4 评论 0原文

我遇到的情况是,我的 XSLT 文件应有条件地显示价格和小数,具体取决于输入 XML 是否包含小数。 因此,我可以接收具有两种类型值的 XML 文件 - XML 将包含所有以小数形式格式化的价格,最多两位(我将其称为“Decimal-XML”),或者价格将四舍五入为最接近的整数(我将其称为“Decimal-XML”)一个“整数 XML”)。

我的问题是,我需要在 XSLT 文件中尽可能少地重构,但允许它们以与 XML 输入相同的格式将转换应用到 XHTML。 为了实现这一目标,我向我的团队实施并建议了三个准则:

  1. 在计算值进行计算或将值存储在变量中时,删除所有 format-number() 函数调用。 请改用 number()。 然而,某些条件适用于该规则(见下文)。
  2. 当要显示值时,请使用 format-number(, '#.##') 格式。 这应该确保整数或小数值将按照 XML 中最初的形式显示。
  3. 对于可选标签(例如“Discount”),即使仅计算值,也请使用 format-number(, '0.00') 函数。 这是必要的,因为如果标签不存在,尝试获取值将给出 NaN 结果。

下面是 XSLT 的说明性示例:

  <x:stylesheet version="1.0" xmlns:x="http://www.w3.org/1999/XSL/Transform">
  <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>

正如 HTML 注释所指出的,它适用于大多数情况,但并非全部。

我想使用单个表达式来格式化与输入相同的所有价格,而不是在任何地方应用“when-otherwise”构造,这还需要传递到 XSLT 的布尔参数来确定显示格式。 XSLT 目前的状态是,无论输入如何,所有数字都会使用 format-number(, '0') 进行四舍五入。

我该如何实现这个目标?


编辑: Dimitre 发表评论后,我决定创建一个示例 XML(以及上面的 XSLT),以便专家可以轻松尝试。

示例 XML(此包含小数):

<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>

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">
  <x:template match="/">
    <html>
      <body>
        <table border="1" width="60%">
          <tr>
            <th>Simple</th>
            <th>number()</th>
            <th>format-number(<expression>, '0.00')</th>
            <th>format-number(<expression>, '#.##')</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>

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

全部不再 2024-07-23 03:42:34

如果我理解正确,您希望:

  • “Integer-XML”始终显示为四舍五入的数字,没有小数位
  • “Decimal-XML”始终显示两位小数,
  • 不使用进行格式化的模板,但是XPath 单行语句

最接近您想要的的是这个表达式:

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

由于 XPath 1.0 中没有易于使用的条件表达式 (XPath 2.0 有 if/then/else),折衷方案是这样:

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

但是,这个“失败”(截掉小数)当 $cost$extraCharges$discount 加起来为整数时。

没有吸引力的替代方法是使用贝克尔方法(以 Oliver Becker of < a href="http://www.informatik.hu-berlin.de/" rel="noreferrer">HU Berlin):

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

从技术上讲,这是一句简单的话。 两个 sub-string() 部分基于 $condition 是互斥的,因此 concat() 只会返回一个值或另一个值。

实际上(特别是对于您的情况),这将是一个难以管理的混乱,缓慢且完全隐藏了意图:

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, '#.##'))
  )
)

由于您声明所有输入值都包含小数,或者都不包含小数,因此上面的表达式仅检查 $cost 包含小数点。 我猜检查 $extraCharges$discount 也是正确的。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文