Группировка динамически - PullRequest
1 голос
/ 16 июня 2020

Я пытаюсь сгруппировать данные, суммируя соседний узел. Пример

<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-смежным с помощью sum (current / @ val + following-sibling :: row / @ val le 10), затем попробовал group-by (sum (@val )) но я вижу, что мой подход basi c неверен. Теперь мне интересно, возможно ли это вообще. Я решил спросить у экспертов!

Спасибо!

Ответы [ 2 ]

4 голосов
/ 16 июня 2020

Инструкция xsl:for-each-group не может справиться с этим требованием.

Помимо предложений Мартина, другой подход версии 3.0 - складывание влево:

<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>

Это создает группы как последовательность массивов, по одному массиву на группу, первоначально в обратном порядке: функция обратного вызова выполняется один раз для каждой строки и добавляет строку в первую (т.е. последнюю) группу, если сумма находится в пределах вашего порога, в противном случае запускается новый group.

(Почему в обратном порядке? Во многом потому, что head() и tail() удобны, и нет эквивалента для получения последнего элемента и «все, кроме последнего»).

4 голосов
/ 16 июня 2020

В 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 и «запоминает», когда «группа» была создана, затем в группировке вы можете использовать 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

Вы даже можете сделать это потоковым (с помощью Майкла Кея):

<?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>
...