How to XPath sum() all previous nodes in a XSL for

2019-05-25 09:08发布

问题:

The source XML (this is just foobar data, in reality it is thousands of rows wich can be both positive and negative):

<accounting>
    <entry id="1">
        <accounting_date>2010-10-29</accounting_date>
        <transfer_date>2010-10-29</transfer_date>
        <description>Start balance</description>
        <vat>0</vat>
        <sum>87287</sum>
    </entry>
    <entry id="2">
        <accounting_date>2011-01-24</accounting_date>
        <transfer_date>2011-02-17</transfer_date>
        <description>Bill 1</description>
        <vat>175</vat>
        <sum>875</sum>
    </entry>
    <entry id="3">
        <accounting_date>2011-01-31</accounting_date>
        <transfer_date>2011-01-18</transfer_date>
        <description>Bill 2</description>
        <vat>350</vat>
        <sum>1750</sum>
    </entry>
</accounting>

I want to transform this XML to an HTML table to display to the user. Most of the transformation is just putting values in the right places, but the balance-field is giving me headache.

My XSLT (that does not work):

<table>
    <tr>
        <th>Accounting date</th>
        <th>Description</th>
        <th>Sum</th>
        <th>Balanche</th>
    </tr>
    <xsl:for-each select="/accounting/entry">
        <tr>
            <td><xsl:value-of select="accounting_date" /></td>
            <td><xsl:value-of select="description" /></td>
            <td><xsl:value-of select="sum" /></td>
            <td><xsl:value-of select="sum(../entry[position() &lt; current()/position()]/sum)" /></td><!-- This XPath is the problem! -->
        </tr>
    </xsl:for-each>
</table>

Expected result:

<table>
    <tr>
        <th>Accounting date</th>
        <th>Description</th>
        <th>Sum</th>
        <th>Balanche</th>
    </tr>
    <tr>
        <td>2010-10-29</td>
        <td>Start balance</td>
        <td>87287</td>
        <td>87287</td>
    </tr>
    <tr>
        <td>2011-01-24</td>
        <td>Bill 1</td>
        <td>875</td>
        <td>88162</td>
    </tr>
    <tr>
        <td>2011-01-31</td>
        <td>Bill 2</td>
        <td>1750</td>
        <td>89912</td>
    </tr>
</table>

Chrome is blank, and Firefox gives me:

Error loading stylesheet: XPath parse failure: Name or Nodetype test expected:

I'm stuck, please help. :)

回答1:

The best solution might depend a bit on whether you are using XSLT 1.0 or XSLT 2.0. You really need to say, since at present there's a roughly even mix of both in use in the field. (It seems you're running it in the browser, which suggests you want a 1.0 solution, so that's what I've given you).

But either way, recursion is your friend. In this case, "sibling recursion" where you write a template to process an entry, and it does apply-templates to process the next entry, passing the total so far as a parameter: something like this

<xsl:template match="entry">
  <xsl:param name="total-so-far" select="0"/>
         <tr>
            <td><xsl:value-of select="accounting_date" /></td>
            <td><xsl:value-of select="description" /></td>
            <td><xsl:value-of select="sum" /></td>
            <td><xsl:value-of select="$total-so-far + sum"/></td><
        </tr>  
    <xsl:apply-templates select="following-sibling::entry[1]">
      <xsl:with-param name="total-so-far" select="$total-so-far + sum"/>
    </xsl:apply-templates>
</xsl:template>

Then you need to start the process off with

<xsl:template match="accounting">
 <table>
   <xsl:apply-templates select="entry[1]"/>
 </table>
</xsl:template>

If there are thousands of rows then this could cause stack overflow in an XSLT processor that doesn't do tail call optimisation. I've no idea whether the XSLT processors in today's browsers implement this optimisation or not.



回答2:

Alternatively you can use the preceding-sibling axes

<table>
    <tr>
        <th>Accounting date</th>
        <th>Description</th>
        <th>Sum</th>
        <th>Balanche</th>
    </tr>
    <xsl:for-each select="/accounting/entry">
        <tr>
            <td>
                <xsl:value-of select="accounting_date" />
            </td>
            <td>
                <xsl:value-of select="description" />
            </td>
            <td>
                <xsl:value-of select="sum" />
            </td>
            <td>
                <xsl:value-of select="sum(preceding-sibling::*/sum)+sum" />
            </td>
        </tr>
    </xsl:for-each>
</table>


回答3:

In addition to the correct answer by @Michael Kay, here is a general template/function from FXSL to use for computing running totals. Its DVC variant will never (for practical purposes) crash due to stack overflow. With DVC (Divide and Conquer) recursion, processing a sequence of 1000000 (1M) items requires maximum stack depth of only 19.

Here is an example of using the scanl template:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:f="http://fxsl.sf.net/"
xmlns:myAdd="f:myAdd"
xmlns:myParam="f:myParam"
>
  <xsl:import href="scanlDVC.xsl"/>
  <xsl:output omit-xml-declaration="yes" indent="yes"/>

  <myAdd:myAdd/>

  <myParam:myParam>0</myParam:myParam>

  <xsl:template match="/">

    <xsl:variable name="vFun" select="document('')/*/myAdd:*[1]"/>
    <xsl:variable name="vZero" select="document('')/*/myParam:*[1]"/>


    <xsl:call-template name="scanl">
      <xsl:with-param name="pFun" select="$vFun"/>
      <xsl:with-param name="pQ0" select="$vZero" />
      <xsl:with-param name="pList" select="/*/num"/>
    </xsl:call-template>
  </xsl:template>

  <xsl:template match="myAdd:*" mode="f:FXSL">
    <xsl:param name="pArg1" select="0"/>
    <xsl:param name="pArg2" select="0"/>

    <xsl:value-of select="$pArg1 + $pArg2"/>
  </xsl:template>
</xsl:stylesheet>

When this transformation is applied on the following XML file:

<nums>
  <num>01</num>
  <num>02</num>
  <num>03</num>
  <num>04</num>
  <num>05</num>
  <num>06</num>
  <num>07</num>
  <num>08</num>
  <num>09</num>
  <num>10</num>
</nums>

the correct result (running totals) is produced:

<el>0</el>
<el>1</el>
<el>3</el>
<el>6</el>
<el>10</el>
<el>15</el>
<el>21</el>
<el>28</el>
<el>36</el>
<el>45</el>
<el>55</el>

Using it for the provided XML document:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:f="http://fxsl.sf.net/"
xmlns:myAdd="f:myAdd"
xmlns:myParam="f:myParam"
>
  <xsl:import href="scanlDVC.xsl"/>
  <xsl:output omit-xml-declaration="yes" indent="yes"/>

  <myAdd:myAdd/>

  <myParam:myParam>0</myParam:myParam>

  <xsl:template match="/">

    <xsl:variable name="vFun" select="document('')/*/myAdd:*[1]"/>
    <xsl:variable name="vZero" select="document('')/*/myParam:*[1]"/>


    <xsl:call-template name="scanl">
      <xsl:with-param name="pFun" select="$vFun"/>
      <xsl:with-param name="pQ0" select="$vZero" />
      <xsl:with-param name="pList" select="/*/*/sum"/>
    </xsl:call-template>
  </xsl:template>

  <xsl:template match="myAdd:*" mode="f:FXSL">
    <xsl:param name="pArg1" select="0"/>
    <xsl:param name="pArg2" select="0"/>

    <xsl:value-of select="$pArg1 + $pArg2"/>
  </xsl:template>
</xsl:stylesheet>

and the correct result is produced::

<el>0</el>
<el>87287</el>
<el>88162</el>
<el>89912</el>


标签: xslt xpath