如何在XSLT中对值进行分组和求和

15
对于每个“机构”节点,我需要找到具有相同key1、key2、key3值的“stmt”元素,并仅输出一个“stmt”节点,其中“comm”和“prem”值求和。对于该“机构”内不与任何其他基于key1、key2和key3匹配的“stmt”元素匹配的任何“stmt”元素,我需要按原样输出它们。因此,在转换后,第一个“机构”节点将只有两个“stmt”节点(一个已求和),而第二个“机构”节点将按原样传递,因为键不匹配。XSLT 1.0或2.0的解决方案都可以,虽然我的样式表当前是1.0。请注意,机构节点可能具有任意数量的“stmt”元素,这些元素具有需要分组和求和的匹配键以及任意数量的不匹配键。
<statement>
<agency>
    <stmt>
        <key1>1234</key1>
        <key2>ABC</key2>
        <key3>15.000</key3>
        <comm>75.00</comm>
        <prem>100.00</prem>
    </stmt>
    <stmt>
        <key1>1234</key1>
        <key2>ABC</key2>
        <key3>15.000</key3>
        <comm>25.00</comm>
        <prem>200.00</prem>
    </stmt>
    <stmt>
        <key1>1234</key1>
        <key2>ABC</key2>
        <key3>17.50</key3>
        <comm>25.00</comm>
        <prem>100.00</prem>
    </stmt>
</agency>
<agency>
    <stmt>
        <key1>5678</key1>
        <key2>DEF</key2>
        <key3>15.000</key3>
        <comm>10.00</comm>
        <prem>20.00</prem>
    </stmt>
    <stmt>
        <key1>5678</key1>
        <key2>DEF</key2>
        <key3>17.000</key3>
        <comm>15.00</comm>
        <prem>12.00</prem>
    </stmt>
</agency>


好问题(+1)。请查看我的答案,其中包含完整的XSLT 1.0解决方案。 - Dimitre Novatchev
3个回答

16

以下是一个 XSLT 2.0 的解决方案:

<xsl:stylesheet version="2.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:xs="http://www.w3.org/2001/XMLSchema"
 exclude-result-prefixes="xs"
 >
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:template match="node()|@*">
   <xsl:copy>
    <xsl:apply-templates select="node()|@*"/>
   </xsl:copy>
 </xsl:template>

 <xsl:template match="agency">
  <agency>
   <xsl:for-each-group select="stmt" group-by=
    "concat(key1, '+', key2, '+', key3)">

    <stmt>
      <xsl:copy-of select=
       "current-group()[1]/*[starts-with(name(),'key')]"/>

       <comm>
         <xsl:value-of select="sum(current-group()/comm)"/>
       </comm>
       <prem>
         <xsl:value-of select="sum(current-group()/prem)"/>
       </prem>
    </stmt>
   </xsl:for-each-group>
  </agency>
 </xsl:template>
</xsl:stylesheet>

1
concat(key1,key2,key3) 在某些情况下会失败,例如 key1="1A" key2="B" key3="1.000"key1="1" key2="AB" key3="1.000"... 我认为在没有深入了解字符串内容(或其限制)的情况下进行字符串拼接是错误的。 - Lucero
@Lucero:再次感谢,concat 没有问题——这是我疏忽了的地方——今天整天我都感到非常困——现在已经纠正过来了。如果这个更正能够满足您,请告诉我。这种更正在这类解决方案中很典型。 - Dimitre Novatchev
@Dimitre,与其他“concat”问题相比,这里的“+”不是一个合适的分隔符,因为XML数据理论上可能会有带有“+”的键字符串 - 想想“key1 =“1 +”key2 =“2”和“key1 =“1”key2 =“+2”。所以我的意思是,只有在您知道分隔符永远不会成为连接数据的一部分时,才应该进行连接。 - Lucero
@Lucero:虽然原则上是正确的,但使用此方法的人都清楚可能存在的问题。只有他们知道数据的值空间,通常可以做出明智的选择。所有与xslt相关的论坛上的解决方案都使用“|”作为分隔符,尽管人们知道在某些情况下这可能不是一个好选择。无论如何,感谢您的坚持提醒,尽管这并不是什么新鲜事。 - Dimitre Novatchev
@Lucero:(续)我一直使用“+”符号,因为我认为它(象征性地)表达了连接操作的本质。如果这个问题对你来说真的很重要,为什么不使用你认为非常罕见的字符串呢?比如:'!+|@#$%^*()'。无论如何,感谢你坚持提醒,尽管这并不是什么新鲜事。 - Dimitre Novatchev
1
@Dimitre,你写道“使用这种方法的人都很清楚可能存在的问题”。在像SO这样的网站上,提问者和搜索该网站的人可能不知道这种技术,因此我认为在使用特定解决方案时,让读者了解任何限制或需要注意的事项非常重要。我只是想指出这一点。 - Lucero

9
在XSLT 1.0中,使用Muenchian方法进行分组(使用复合键)。
这个转换:
<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:key name="kStmtByKeys" match="stmt"
      use="concat(generate-id(..), key1, '+', key2, '+', key3)"/>

 <xsl:template match="node()|@*">
   <xsl:copy>
    <xsl:apply-templates select="node()|@*"/>
   </xsl:copy>
 </xsl:template>

 <xsl:template match="agency">
   <agency>
    <xsl:for-each select=
     "stmt[generate-id()
          =
           generate-id(key('kStmtByKeys',
                           concat(generate-id(..), key1, '+', key2, '+', key3)
                           )[1]
                       )
           ]
     ">
      <xsl:variable name="vkeyGroup" select=
       "key('kStmtByKeys', concat(generate-id(..), key1, '+', key2, '+', key3))"/>

     <stmt>
      <xsl:copy-of select="*[starts-with(name(), 'key')]"/>
      <comm>
       <xsl:value-of select="sum($vkeyGroup/comm)"/>
      </comm>
      <prem>
       <xsl:value-of select="sum($vkeyGroup/prem)"/>
      </prem>
     </stmt>
    </xsl:for-each>
   </agency>
 </xsl:template>
</xsl:stylesheet>

应用于提供的XML文档时,可以生成所需的结果。
<statement>
    <agency>
        <stmt>
            <key1>1234</key1>
            <key2>ABC</key2>
            <key3>15.000</key3>
            <comm>100</comm>
            <prem>300</prem>
        </stmt>
        <stmt>
            <key1>1234</key1>
            <key2>ABC</key2>
            <key3>17.50</key3>
            <comm>25</comm>
            <prem>100</prem>
        </stmt>
    </agency>
    <agency>
        <stmt>
            <key1>5678</key1>
            <key2>DEF</key2>
            <key3>15.000</key3>
            <comm>10</comm>
            <prem>20</prem>
        </stmt>
        <stmt>
            <key1>5678</key1>
            <key2>DEF</key2>
            <key3>17.000</key3>
            <comm>15</comm>
            <prem>12</prem>
        </stmt>
    </agency>
</statement>

如果我理解正确的话,你的解决方案在另一个带有相同键的机构拥有 "stmt" 节点时会出现问题。对我来说,由于存在多个机构,使用全局键的 Muenchian 方法将无法工作。 - Lucero
@Lucero:非常好的观察,谢谢。现在已经更正了,我仍然使用复合键的Muenchian方法。 - Dimitre Novatchev
嗯,通过这种方法生成复合键是否保证在所有情况下都能产生所需的结果呢?例如,如果一个键是 concat('1', '23'),而另一个键是 concat('12', '3')(你懂的),这可能会根据输入文档和 XSLT 处理器产生问题。 - Lucero
谢谢你们两位提供详细的答案和注意事项。连接符可以处理我的数据。我会仔细研究这些选项,以确定最适合我当前和未来数据的方案。 - johkar
@Lucero:你是否注意到在concat()函数参数中的“断开”的'+'这就是确保避免任何冲突的关键,当然我们必须确保选择一个在真正的数据值连接形成密钥时,永远不会成为结束和/或开始的字符串。 - Dimitre Novatchev
1
@Dimitre,是的,我看到了,但我手头没有generate-id()函数输出的确切规范,这也是为什么我把它写成一个问题(“在所有情况下都是这样吗?”)。但你是对的,“+”字符不允许作为生成的ID的一部分,这使它成为这里的合适分隔符。http://www.w3.org/TR/xslt#function-generate-id - Lucero

1
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
    <xsl:output method="xml" indent="yes"/>

    <xsl:template match="/|*">
        <xsl:copy>
            <xsl:apply-templates select="*" />
        </xsl:copy>
    </xsl:template>

    <xsl:template match="stmt">
        <xsl:variable name="stmtGroup" select="../stmt[(key1=current()/key1) and (key2=current()/key2) and (key3=current()/key3)]" />
        <xsl:if test="generate-id()=generate-id($stmtGroup[1])">
            <xsl:copy>
                <key1>
                    <xsl:value-of select="key1"/>
                </key1>
                <key2>
                    <xsl:value-of select="key2"/>
                </key2>
                <key3>
                    <xsl:value-of select="key3"/>
                </key3>
                <comm>
                    <xsl:value-of select="format-number(sum($stmtGroup/comm), '#.00')"/>
                </comm>
                <prem>
                    <xsl:value-of select="format-number(sum($stmtGroup/prem), '#.00')"/>
                </prem>
            </xsl:copy>
        </xsl:if>
    </xsl:template>
</xsl:stylesheet>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接