动态分组
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" 没有等效项)。
我正在尝试通过对相邻节点求和来将数据分组在一起。例子
<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" 没有等效项)。