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

Posted

技术标签:

【中文标题】如何在 XSLT 中对值进行分组和求和【英文标题】:How to group and sum values in XSLT 【发布时间】:2011-02-15 15:47:19 【问题描述】:

对于每个“代理”节点,我需要找到具有相同 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 解决方案。 【参考方案1】:

还有一个 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>

【讨论】:

concat(key1,key2,key3) 在某些情况下会失败,例如key1="1A" key2="B" key3="1.000"key1="1" key2="AB" key3="1.000"...我觉得在不了解其内容(或限制)的情况下连接字符串是错误的。 @Lucero:再次感谢,concat 没有任何问题——它是从我身上滑落的东西——我今天一整天都感觉很困——现在已经更正了。请让我知道更正是否让您满意。这种更正是此类解决方案中的典型特征。 @Dimitre,与其他 concat 问题相比,+ 在这里不是合适的分隔符,因为 XML 数据理论上很可能包含带有 + 的关键字符串 -想想key1="1+" key2="2"key1="1" key2="+2"。所以我的说法是,只有当你知道分隔符永远不会成为连接数据的一部分时,你才应该连接。 @Lucero:虽然原则上这是正确的,但使用这种方法的人很清楚可能存在的问题。只有他们知道他们数据的价值空间,他们通常可以在知情的情况下进行选择。 xslt 相关论坛上的所有解决方案都使用"|" 作为中断字符串,尽管人们知道在某些情况下这可能不是一个好的选择。无论如何,感谢您的坚持提醒,虽然这不是什么新鲜事。 @Dimitre,您写道“使用这种方法的人都清楚可能出现的问题”。在像 SO 这样的网站上,提出问题的人和搜索该网站的人都不知道该技术,我觉得让读者意识到使用特定解决方案时要记住的任何限制或事项很重要。我只是想指出这一点。【参考方案2】:

在 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:很好的观察,谢谢。现在已更正,我仍在使用带有复合键的 Muenchian 方法。 嗯,这种生成复合键的方式是否保证在所有情况下都能提供想要的结果?如果一个键是 concat('1', '23') 而另一个是 concat('12', '3')(你明白了),这可能会根据输入文档和 XSLT 处理器产生问题。 感谢您提供详细的答案和陷阱。连接适用于我的数据。我将仔细研究这些选项,以确定我当前和未来数据的最佳选择。 @Dimitre,是的,我看到了,但我手头没有 generate-id() 函数输出的确切规范,这也是为什么把它写成一个问题(“在所有情况?”)。但你是对的,“+”字符不允许作为生成的 ID 的一部分,这使它成为此处合适的分隔符。 w3.org/TR/xslt#function-generate-id【参考方案3】:
<?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>

【讨论】:

以上是关于如何在 XSLT 中对值进行分组和求和的主要内容,如果未能解决你的问题,请参考以下文章

Scala:如何按键分组并在 scala 中对值求和并以预期的返回类型返回列表

如何使用 XSLT 从电子邮件地址中对值进行子串化

在 XSLT 中对记录进行分组时如何避免 O(n^2) 复杂性?

在 BIRT 中对值进行单列求和

我如何在熊猫中分组然后对值求和? [复制]

如何在下面的 XSLT 1.0 代码中进行分组。需要按 TaxRateCode 分组