动态分组

Grouping dynamically

我正在尝试通过对相邻节点求和来将数据分组在一起。例子

<root>
    <row id="AAA" val="2"/>
    <row id="BBB" val="3"/>
    <row id="CCC" val="1"/>
    <row id="DDD" val="4"/>
    <row id="EEE" val="6"/>
    <row id="FFF" val="3"/>
    <row id="GGG" val="6"/>
    <row id="HHH" val="8"/>
    <row id="III" val="3"/>
    <row id="JJJ" val="4"/>
    <row id="KKK" val="2"/>
    <row id="LLL" val="1"/>
</root>

假设我的参数为 10,那么每次值总和为 10 或小于 10 时,都应将它们组合在一起。 结果应该是

<root>
    <grouped>
        <row id="AAA" val="2"/>
        <row id="BBB" val="3"/>
        <row id="CCC" val="1"/>
        <row id="DDD" val="4"/>
    </grouped>
    <grouped>
        <row id="EEE" val="6"/>
        <row id="FFF" val="3"/>
    </grouped>
    <grouped>
        <row id="GGG" val="6"/>
    </grouped>
    <grouped>
        <row id="HHH" val="8"/>
    </grouped>
    <grouped>
        <row id="III" val="3"/>
        <row id="JJJ" val="4"/>
        <row id="KKK" val="2"/>
        <row id="LLL" val="1"/>
    </grouped>
</root>

我尝试使用 group-adjacent 和 sum(current/@val + following-sibling::row/@val le 10) 然后尝试 group-by(sum(@val)) 但我可以看到我的基本方法是不正确的。现在我想知道,这是否可能。所以我想我应该请教专家!

谢谢!

在 XSLT 1 中,您可以使用兄弟递归,在 XSLT 3 中,使用起来更简单但有点冗长 xsl:iterate:

  <xsl:template match="root">
      <xsl:copy>
          <xsl:iterate select="row">
              <xsl:param name="sum" as="xs:integer" select="0"/>
              <xsl:param name="group" as="element(row)*" select="()"/>
              <xsl:on-completion>
                  <xsl:if test="$group">
                      <group>
                          <xsl:copy-of select="$group"/>
                      </group>
                  </xsl:if>
              </xsl:on-completion>
              <xsl:variable name="current-sum" select="$sum + xs:integer(@val)"/>
              <xsl:if test="$current-sum > 10">
                  <group>
                    <xsl:copy-of select="$group"/>
                  </group>
              </xsl:if>
              <xsl:next-iteration>
                  <xsl:with-param name="sum" select="if ($current-sum > 10) then xs:integer(@val) else $current-sum"/>
                  <xsl:with-param name="group" select="if ($current-sum > 10) then . else ($group, .)"/>
              </xsl:next-iteration>
          </xsl:iterate>
      </xsl:copy>
  </xsl:template>

https://xsltfiddle.liberty-development.net/6pS2B6o

作为替代方案,您可以使用累加器将 @val 值和 "remembers" 相加,当 "group" 已建立时,然后在分组中您可​​以使用 group-starting-with 检查累加器:

  <xsl:param name="max" as="xs:integer" select="10"/>

  <xsl:mode on-no-match="shallow-copy" use-accumulators="#all"/>

  <xsl:output method="xml" indent="yes"/>

  <xsl:accumulator name="window" as="item()*" initial-value="()">
      <xsl:accumulator-rule match="root" select="(0, true())"/>
      <xsl:accumulator-rule match="root/row"
        select="let $val := xs:integer(@val),
                    $sum := $value[1],
                    $window-start := $value[2],
                    $current-sum := $sum + $val
                return
                    if ($current-sum gt $max)
                    then ($val, true())
                    else ($current-sum, false())"/>
  </xsl:accumulator>

  <xsl:template match="root">
      <xsl:copy>
          <xsl:for-each-group select="row" group-starting-with="*[accumulator-before('window')[2]]">
              <grouped>
                  <xsl:apply-templates select="current-group()"/>
              </grouped>
          </xsl:for-each-group>
      </xsl:copy>
  </xsl:template>

https://xsltfiddle.liberty-development.net/6pS2B6o/1

你甚至可以让它流式传输(在 Michael Kay 的帮助下):

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="#all" version="3.0">

    <xsl:param name="max" as="xs:integer" select="10"/>

    <xsl:mode on-no-match="shallow-copy" use-accumulators="#all" streamable="yes"/>

    <xsl:output method="xml" indent="yes"/>

    <xsl:accumulator name="window" as="item()*" initial-value="()" streamable="yes">
        <xsl:accumulator-rule match="root" select="(0, true())"/>
        <xsl:accumulator-rule match="root/row"
            select="
                let $val := xs:integer(@val),
                    $sum := $value[1],
                    $window-start := $value[2],
                    $current-sum := $sum + $val
                return
                    if ($current-sum gt $max)
                    then
                        ($val, true())
                    else
                        ($current-sum, false())"
        />
    </xsl:accumulator>

    <xsl:template match="root">
        <xsl:copy>
            <xsl:for-each-group select="row"
                group-starting-with="*[boolean(accumulator-before('window')[2])]">
                <grouped>
                    <xsl:apply-templates select="current-group()"/>
                </grouped>
            </xsl:for-each-group>
        </xsl:copy>
    </xsl:template>

</xsl:stylesheet>

xsl:for-each-group 指令无法满足此要求。

除了 Martin 的建议,另一种 3.0 方法是 fold-left:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  xmlns:array="http://www.w3.org/2005/xpath-functions/array"
  exclude-result-prefixes="#all"
  version="3.0">

  <xsl:param name="max" as="xs:integer" select="10"/>

  <xsl:template match="root">
    <xsl:copy>
      <xsl:variable name="groups" select="
        fold-left(row, ([]), function($groups, $next) {
           if (sum(head($groups)?*/@val) + $next/@val le $max)
           then (array:append(head($groups), $next), tail($groups))
           else ([$next], $groups)
        }) => reverse()"/>
      <xsl:for-each select="$groups">
        <grouped>
          <xsl:copy-of select="?*"/>
        </grouped>
      </xsl:for-each>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

这会将组构建为一系列数组,每组一个数组,最初顺序相反:回调函数对每一行执行一次,如果总数在你的阈值之内,否则它会开始一个新的组。

(为什么要倒序?主要是因为 head()tail() 很方便,而获取最后一项和 "all except the last" 没有等效项)。