XSLT 3-уровневая группировка по атрибутам - PullRequest
4 голосов
/ 04 июня 2009

ОК, я ЗНАЮ, что вариации на это были заданы и даны ответы; Я читал их весь день, но я все еще застрял. Итак, вот что:

Мне нужно создать сводный список в HTML из некоторого XML.

Учитывая этот XML:

<Root><!-- yes, I know I don't need a 'Root' element! Legacy code... -->
  <Plans>
    <Plan AreaID="1" UnitID="83">
      <Part ID="9122" Name="foo" />
      <Part ID="9126" Name="bar" />
    </Plan>
    <Plan AreaID="1" UnitID="86">
      <Part ID="8650" Name="baz" />
    </Plan>
    <Plan AreaID="2" UnitID="26">
      <Part ID="215" Name="quux" />
    </Plan>
    <Plan AreaID="1" UnitID="95">
      <Part ID="7350" Name="meh" />
    </Plan>
  </Plans>
</Root>

Мне нужно выбросить:

<ol>
  <li>Area 1: 
    <ol><!-- units in Area 1 -->
      <li>Unit 83: 
        <ol>
          <li>Part 9122 (foo)</li>
          <li>Part 9126 (bar)</li>
        </ol>
      </li>
      <li>Unit 86: 
        <ol>
          <li>Part 8650 (baz)</li>
        </ol>
      <li>Unit 95: 
        <ol>
          <li>Part 7350 (meh)</li>
        </ol>
      </li>
    </ol><!-- /units in Area 1-->
  </li>
  <li>Area 2: 
    <ol><!-- units in Area 2 -->
      <li>Unit 26: 
        <ol>
          <li>Part 215 (quux)</li>
        </ol>
      </li>
    </ol><!-- /units in Area 2-->
  </li>
</ol>

У меня работает внешняя группировка - я получаю элементы списка верхнего уровня для областей 1 и 2. Но я не могу получить последовательности единиц в областях - я либо не получаю вывод, либо повторяю то же значение , Я даже не дошел до уровня Партии: - (

Я работал над таблицей стилей, подобной этой:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
<xsl:output method="html" indent="yes"/>

<xsl:key name="kAreaID" match="Plan" use="@AreaID" />
<xsl:key name="kUnitID" match="Plan" use="@UnitID" />

<xsl:template match="/Root/Plans">
<html><head><title>test grouping</title></head>
<body>
  <ol>
    <xsl:for-each select="./Plan[generate-id(.) = 
                      generate-id( key( 'kAreaID', @AreaID )[1] )]"
    >
      <xsl:sort order="ascending" select="./@AreaID" />
      <li>Area <xsl:value-of select="@AreaID"/>: 
        <ol>
          <xsl:for-each select="key( 'kUnitID', @UnitID )">
            <li>Unit <xsl:value-of select="@UnitID"/>: 
              <ol>
                <li>(Parts go here...)</li>
              </ol>
            </li>
          </xsl:for-each>
        </ol>
      </li>
    </xsl:for-each>
  </ol>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

Любая помощь очень ценится!

Ответы [ 4 ]

18 голосов
/ 05 июня 2009

Вот решение по группировке по Мюнхену, которое вы ищете.

Исходя из исходного предоставленного вами XML, я подумал, что группировки по AreaID будет достаточно, но оказывается, что также необходима вторая группировка по UnitID.

Вот мое модифицированное решение XSLT 1.0. Это не намного сложнее, чем оригинальное решение:

<xsl:stylesheet
  version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>

  <xsl:key name="kPlanByArea" match="Plan" 
           use="@AreaID" />
  <xsl:key name="kPlanByAreaAndUnit" match="Plan" 
           use="concat(@AreaID, ',', @UnitID)" />

  <xsl:template match="/">
    <xsl:apply-templates select="Root/Plans" />
  </xsl:template>

  <!-- main template -->
  <xsl:template match="Plans">
    <ol>
      <!-- group by '{@AreaID}' (note the template mode!) -->
      <xsl:apply-templates mode="area-group" select="
        Plan[
          generate-id()
          =
          generate-id(
            key('kPlanByArea', @AreaID)[1]
          )
        ]
      ">
        <xsl:sort select="@AreaID" data-type="number" />
      </xsl:apply-templates>
    </ol>
  </xsl:template>

  <!-- template to output each '{@AreaID}' group -->
  <xsl:template match="Plan" mode="area-group">
    <li>
      <xsl:value-of select="concat('Area ', @AreaID)" />
      <ol>
        <!-- group by '{@AreaID},{@UnitID}' -->
        <xsl:apply-templates mode="unit-group" select="
          key('kPlanByArea', @AreaID)[
            generate-id()
            =
            generate-id(
              key('kPlanByAreaAndUnit', concat(@AreaID, ',', @UnitID))[1]
            )
          ]
        ">
          <xsl:sort select="@UnitID" data-type="number" />
        </xsl:apply-templates>
      </ol>
    </li>
  </xsl:template>

  <!-- template to output each '{@AreaID},{@UnitID}' group -->
  <xsl:template match="Plan" mode="unit-group">
    <li>
      <xsl:value-of select="concat('Unit ', @UnitID)" />
      <ol>
        <xsl:apply-templates select="
          key('kPlanByAreaAndUnit', concat(@AreaID, ',', @UnitID))/Part
        ">
          <xsl:sort select="@UnitID" data-type="number" />
        </xsl:apply-templates>
      </ol>
    </li>
  </xsl:template>

  <!-- template to output Parts into a list -->
  <xsl:template match="Part">
    <li>
      <xsl:value-of select="concat('Part ', @ID, ' (', @Name ,')')" />
    </li>
  </xsl:template>

</xsl:stylesheet>

Так как в вашем XML его нет, я добавил UnitID для группировки по:

<Plan AreaID="1" UnitID="86">
  <Part ID="8651" Name="zzz" />
</Plan>

А вот и вывод:

<ol>
  <li>Area 1
    <ol>
      <li>Unit 83
        <ol>
          <li>Part 9122 (foo)</li>
          <li>Part 9126 (bar)</li>
        </ol>
      </li>
      <li>Unit 86
        <ol>
          <li>Part 8650 (baz)</li>
          <li>Part 8651 (zzz)</li>
        </ol>
      </li>
      <li>Unit 95
        <ol>
          <li>Part 7350 (meh)</li>
        </ol>
      </li>
    </ol>
  </li>
  <li>Area 2
    <ol>
      <li>Unit 26
        <ol>
          <li>Part 215 (quux)</li>
        </ol>
      </li>
    </ol>
  </li>
</ol>

Поскольку вам кажется, что вам трудно работать с ключом XSL, вот моя попытка объяснения:

<xsl:key> абсолютно эквивалентен ассоциативному массиву (map, hash, как вы его называете), известному многим языкам программирования. Это:

<xsl:key name="kPlanByAreaAndUnit" match="Plan" 
         use="concat(@AreaID, ',', @UnitID)" />

генерирует структуру данных, которая может быть выражена в JavaScript следующим образом:

var kPlanByAreaAndUnit = {
  "1,83": ['array of all <Plan> nodes with @AreaID="1" and @UnitID="83"'],
  "1,86": ['array of all <Plan> nodes with @AreaID="1" and @UnitID="86"'],
  /* ... */
  "1,95": ['array of all <Plan> nodes with @AreaID="1" and @UnitID="95"']
};

Функция доступа к структуре данных называется key(). Итак, это выражение XPath:

key('kPlanByAreaAndUnit', concat(@AreaID, ',', @UnitID))

является логическим эквивалентом (опять же в JavaScript):

kPlanByAreaAndUnit[this.AreaID + ',' + this.UnitID];

возвращает массив (набор узлов, точнее) всех узлов, соответствующих заданной ключевой строке (ключ всегда является строкой). Этот набор узлов может использоваться как любой другой набор узлов в XSLT, т. Е. Тот, который вы получаете с помощью «традиционного» XPath. Это означает, что вы можете применять к нему условия (предикаты):

<!-- first node only... -->
key('kPlanByAreaAndUnit', concat(@AreaID, ',', @UnitID))[1]

<!-- nodes that have <Part> children only... -->
key('kPlanByAreaAndUnit', concat(@AreaID, ',', @UnitID))[Part]

или используйте его как базу для навигации по XPath:

<!-- the actual <Part> children of matched nodes... -->
key('kPlanByAreaAndUnit', concat(@AreaID, ',', @UnitID))/Part

и так далее. Это также означает, что мы можем использовать его как выражение «select» для <xsl:apply-templates>, и мы можем использовать его как основу для группировки. Что подводит нас к сути вышеуказанной таблицы стилей (если вы обернули голову вокруг этой таблицы, вы также поняли и остальное решение):

key('kPlanByArea', @AreaID)[
  generate-id()
  =
  generate-id(
    key('kPlanByAreaAndUnit', concat(@AreaID, ',', @UnitID))[1]
  )
]

Снова в JavaScript это можно выразить как:

// the result will be a node-set, so we prepare an array
var selectedNodes = [];

// "key('kPlanByArea', @AreaID)"
var nodeSet = kPlanByArea[this.AreaID];

// "[...]" - the [] actually triggers a loop that applies 
// the predicate expression to all nodes in the set, so we do:
for (var i = 0; i < nodeSet.length; i++) {
   // use the current node for any calculations
   var c = nodeSet[i];
   if (
     // if the current node === the *first* node in kPlanByAreaAndUnit...
     generateId(c)
     ==
     generateId(kPlanByAreaAndUnit[c.AreaID + ',' + c.UnitID][0])
   ) {
     // ...include it in the resulting selection
     selectedNodes.push(c)
   }
}

После того как выражение выполнено, выбираются только те узлы, которые являются соответствующими первыми с заданной комбинацией «AreaID, UnitID» - фактически мы сгруппировали их по их комбинации «AreaID, UnitID».

Применение шаблона к этому набору узлов приводит к тому, что каждая комбинация появляется только один раз. Затем My <xsl:template match="Plan" mode="unit-group"> снова получает полный список для получения полного вывода для каждой группы.

Я надеюсь, что использование JavaScript для объяснения концепции было полезной идеей.

1 голос
/ 04 июня 2009

Не думаю, что вам вообще нужно использовать ключ kUnitID . Вместо этого замените следующую строку ...

<xsl:for-each select="key( 'kUnitID', @UnitID )">

.. с этой строкой, которая должна зациклить все части, соответствующие текущему AreaID

<xsl:for-each select="key( 'kAreaID', @AreaID )">

И в этом цикле, для вашего (Детали идут сюда ...) кода, вы можете просто зациклить части

<xsl:for-each select="Part">
   <li>Part (<xsl:value-of select="@ID" />)</li>
</xsl:for-each>
1 голос
/ 05 июня 2009

Ну, я на время отказался от ключей и мюнхенской группировки. Я едва понимаю это, и взлом на это не принес желаемых результатов. Я понимаю рекурсию, хотя и пошел с этим рекурсивным подходом, который дает желаемый результат. Я нашел это в http://www.biglist.com/lists/xsl-list/archives/200412/msg00865.html

Поток обсуждения предупреждает, что производительность сильно сказывается на большом вводе по сравнению с мюнхенским подходом, и приведенное ниже решение является многословным и повторяющимся (я мог бы выполнить его рефакторинг, чтобы сделать его меньше и сложнее для понимания ;-) но 1) на самом деле работает для меня, и 2) для моей настоящей задачи входные наборы довольно малы, не более дюжины или около того нижестоящих узлов Part.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <!-- recursive grouping http://www.biglist.com/lists/xsl-list/archives/200412/msg00865.html -->

  <xsl:template match="//Plans">
    <html>
      <head>
        <title>test grouping</title>
      </head>
      <body>
        <ol>
          <xsl:call-template name="PlanGrouping">
            <xsl:with-param name="list" select="Plan"/>
          </xsl:call-template>
        </ol>
      </body>
    </html>
  </xsl:template>

  <xsl:template name="PlanGrouping">
    <xsl:param name="list"/>
    <!-- Selecting the first Area ID as group identifier and the group itself-->
    <xsl:variable name="group-identifier" select="$list[1]/@AreaID"/>
    <xsl:variable name="group" select="$list[@AreaID = $group-identifier]"/>
    <!-- Do some work for the group -->
    <li>
      Area <xsl:value-of select="$group-identifier"/>:
      <ol>
        <xsl:call-template name="AreaGrouping">
          <xsl:with-param name="list" select="$list[(@AreaID = $group-identifier)]"/>
        </xsl:call-template>
      </ol>
    </li>
    <!-- If there are other groups left, calls itself -->
    <xsl:if test="count($list)>count($group)">
      <xsl:call-template name="PlanGrouping">
        <xsl:with-param name="list" select="$list[not(@AreaID = $group-identifier)]"/>
      </xsl:call-template>
    </xsl:if>
  </xsl:template>

  <xsl:template name="AreaGrouping">
    <xsl:param name="list"/>
    <!-- Selecting the first Unit ID as group identifier and the group itself-->
    <xsl:variable name="group-identifier" select="$list[1]/@UnitID"/>
    <xsl:variable name="group" select="$list[@UnitID = $group-identifier]"/>
    <!-- Do some work for the group -->
    <li>
      Unit <xsl:value-of select="$group-identifier"/>:
      <ol>
        <xsl:call-template name="Parts">
          <xsl:with-param name="list" select="$list[(@UnitID = $group-identifier)]"/>
        </xsl:call-template>
      </ol>
    </li>
    <!-- If there are other groups left, calls itself -->
    <xsl:if test="count($list)>count($group)">
      <xsl:call-template name="AreaGrouping">
        <xsl:with-param name="list" select="$list[not(@UnitID = $group-identifier)]"/>
      </xsl:call-template>
    </xsl:if>
  </xsl:template>

  <xsl:template name="Parts">
    <xsl:param name="list"/>
    <xsl:for-each select="$list/Part">
      <li>
        Part <xsl:value-of select="@ID"/> (<xsl:value-of select="@Name"/>)
      </li>
    </xsl:for-each>
  </xsl:template>

</xsl:stylesheet>
0 голосов
/ 04 июня 2009

Это делает то, что вы хотите, но с рекурсией, а не группировкой. Извините, я все еще учусь использовать группировку:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html" indent="yes"/>

  <xsl:key name="kAreaID" match="Plan" use="@AreaID" />
  <xsl:key name="kUnitID" match="Plan" use="@UnitID" />

  <xsl:template match="/Root/Plans">
    <html>
      <head>
        <title>test grouping</title>
      </head>
      <body>
        <ol>
          <xsl:for-each select="./Plan[generate-id(.) = 
                      generate-id( key( 'kAreaID', @AreaID )[1] )]"    >
            <xsl:sort order="ascending" select="./@AreaID" />
            <xsl:variable name="curArea" select="@AreaID"/>

            <li>
              Area <xsl:value-of select="$curArea"/>:
              <ol>
                <xsl:for-each select="ancestor::Root/Plans/Plan[@AreaID = $curArea]">
                  <xsl:variable name="curUnit" select="@UnitID"/>
                  <li>
                    Unit <xsl:value-of select="$curUnit"/>:
                    <ol>
                        <xsl:for-each select="ancestor::Root/Plans/Plan[@AreaID = $curArea and @UnitID = $curUnit]/Part">
                          <li>
                            Part <xsl:value-of select="concat(@ID, '  (', @Name, ')')"/>
                          </li>
                        </xsl:for-each>
                    </ol>
                  </li>
                </xsl:for-each>
              </ol>
            </li>
          </xsl:for-each>
        </ol>
      </body>
    </html>
  </xsl:template>
</xsl:stylesheet>
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...