Tricky XSLT рекурсивное преобразование дерева - PullRequest
1 голос
/ 10 июля 2009

(Примечание: если вы хорошо справляетесь с функциональным языком программирования, отличным от XSLT, эта проблема все еще может быть для вас - это не обязательно XSLT, что делает это сложным для меня.) Как бы я преобразовал это:

<!-- note that empty cells are implicit and therefore nonexistent -->
<tables>
  <table pos="1">
    <row pos="1">
      <cell pos="1">Category A</cell>
      <cell pos="10">Category B</cell>
    </row>
    <row pos="2">
      <cell pos="1">Sub-Category 1</cell>
      <cell pos="4">Sub-Category 2</cell>
      <cell pos="6">Sub-Category 3</cell>
      <cell pos="10">Sub-Category 1</cell>
    </row>
  </table>
  <table pos="2">
    <row pos="1">
      <cell pos="1">Category A</cell>
      <cell pos="11">Category B</cell>
    </row>
    <row pos="2">
      <cell pos="1">Sub-Category 1</cell>
      <cell pos="2">Sub-Category 2</cell>
      <cell pos="4">Sub-Category 3</cell>
      <cell pos="10">Sub-Category 4</cell>
      <cell pos="11">Sub-Category 1</cell>
      <cell pos="12">Sub-Category 2</cell>
    </row>
  </table>
</tables>

К этому:

<tree>
  <node label="Category A">
    <positions>
      <pos table="1" row="1" cell="1" />
      <pos table="2" row="1" cell="1" />
    </positions>
    <node label="Sub-Category 1">
      <positions>
        <pos table="1" row="2" cell="1" />
        <pos table="2" row="2" cell="1" />
      </positions>
    </node>
    <node label="Sub-Category 2">
      <positions>
        <pos table="1" row="2" cell="4" />
        <pos table="2" row="2" cell="2" />
      </positions>
    </node>
    <node label="Sub-Category 3">
      <positions>
        <pos table="1" row="2" cell="6" />
        <pos table="2" row="2" cell="4" />
      </positions>
    </node>
    <node label="Sub-Category 4">
      <positions>
        <pos table="2" row="2" cell="10" />
      </positions>
    </node>
  </node>
  <node label="Category B">
    <positions>
      <pos table="1" row="1" cell="10" />
      <pos table="2" row="1" cell="11" />
    </positions>
    <node label="Sub-Category 1">
      <positions>
        <pos table="1" row="2" cell="10" />
        <pos table="2" row="2" cell="11" />
      </positions>
    </node>
    <node label="Sub-Category 2">
      <positions>
        <pos table="2" row="2" cell="12" />
      </positions>
    </node>
  </node>
</tree>

Обратите внимание, что каждая таблица представляет произвольно глубокое дерево 2D-категорий. Каждая строка представляет дочерние элементы предыдущего ряда, где дочерние категории прикрепляются к родителям по их соответствующим позициям - они являются дочерними, если их позиция> = родительский слева и <следующий родительский справа (<em>, если есть один).

Я бы хотел, чтобы выходные данные представляли собой дерево, сгруппированное по меткам ( только в рамках описанных родительско-дочерних отношений , но через таблицы). Например, «Подкатегория 1» существует отдельно как для «Категории A», так и для «Категории B».

Существует n таблиц с n строками по n ячеек в каждой. Вы можете представить структуру входных данных в виде трехмерного куба, каждая таблица представляет примерно одни и те же данные за разные годы.

Я могу сделать вышеописанное, если бы это была всего одна таблица («год»), но с n таблицами у меня возникла большая проблема с поиском способа применения этой вещи из-за различий в позициях равных родителей.

В конце концов меня интересует решение XSLT 1.0 без функции расширения, хотя любая (алгоритмическая) помощь очень ценится. Эта проблема преследует меня довольно долгое время, и я, кажется, не могу обернуть ее вокруг. Я чувствую, что для этого должно быть чистое решение, я просто не вижу этого. Я уверен, что это можно сделать с помощью одного рекурсивного шаблона, нескольких ключей и действительно умного XPath.

Полагаю, этот вопрос является материалом для значка с завитками. : -)

Ответы [ 2 ]

1 голос
/ 15 июля 2009

Вот моя первая попытка этого. Это выглядело достаточно легко, пока я не столкнулся с проблемой рекурсивной группировки. Я думал об использовании набора узлов, но я думаю, что это расширение XSL 1.0, верно? Итак, для этого нет набора узлов? Без этого все выглядит примерно так:

  1. Найти все корневые узлы, обращаться с ними по-разному (у них нет родителей, и поэтому применяются разные правила).

  2. Для каждого такого узла просмотрите каждый узел // ячейки и проверьте, не является ли он прямым потомком текущего узла. Если это может быть ребенок, мы все равно должны сгруппировать его, так что ...

  3. Просматривать каждый // узел ячейки вдоль предыдущей оси. Для каждого такого предыдущего // узла ячейки посмотрите, может ли он быть также прямым потомком родительского узла, найденного на шаге 2.

  4. Найдя предыдущий // узел ячейки, который является дочерним узлом, сравните метку предыдущего дочернего элемента с дочерним, найденным на шаге 2. Если они равны, ничего не выводите (поскольку эта метка была выход раньше). В противном случае ...

  5. Начать вывод дочернего узла. Выполните обход каждого узла // ячейки еще раз, чтобы найти все позиции этой метки, в которой он является дочерним узлом родительского узла, найденного на шаге 2.

  6. Recurse: вернитесь к шагу 2, используя этот дочерний узел в качестве нового родительского узла.

Протестировано на MSXML2, похоже, оно генерирует то, что вы просили. Кажется, он работает, поскольку я понимаю, что он должен работать, даже если я добавлю еще элементов. Это было, конечно, весело писать, и это заставило меня задуматься о направлениях, о которых я обычно не думаю, работая с XSL. Я думаю, это хорошо ...? Опять же, может быть, это пример проблемы, которая МОЖЕТ быть решена с помощью XSL, но может быть решена легче другим способом.

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" indent="yes"/>

    <xsl:key name="roots" match="/tables/table/row[1]/cell" use="text()" />
    <xsl:key name="cell-by-label" match="/tables/table/row/cell" use="text()"/>
    <xsl:variable name="cells" select="/tables/table/row/cell" />

    <xsl:template match="/tables">

        <xsl:variable name="rootCells" select="/tables/table/row[1]/cell"/>

        <tree>
            <!-- Work on each root-level nodes first. -->
            <xsl:apply-templates
                mode="root"
                select="$rootCells[count(.|key('roots', text())[1]) = 1]" />
        </tree>
    </xsl:template>

    <!--
    Root level cells are handled differently. They have no available $parent,
    for one thing. Because of that, it seems simpler to handle them as an
    exception here, rather than peppering the mode="crawl" with exceptions
    for parentless nodes.
    -->
    <xsl:template match="cell" mode="root">
        <node label="{.}">

            <!--
            Get a list of everywhere that this cell is found.
            We are looking for only other root-level cells here.
            -->
            <positions>
                <xsl:for-each select="key('roots', text())">
                    <pos table="{../../@pos}" row="{../@pos}" cell="{@pos}"/>
                </xsl:for-each>
            </positions>

            <!--
            Locate all child nodes, across all tables in which this node is found.
            A node is a child node if:
            1. It is in a row directly following the row in which this cell is found.
            2. If the @pos is >= the @pos of the parent
            3. If the @pos is < the @pos of the parent to the right (if there is a parent to the right)
               Note: Meeting the above conditions is not difficult; it's grouping at this
                     point that gives trouble. If the problem permitted extension functions
                     to XSL 1.0, then perhaps we could generate a node-set and group that.
                     I've not tried that way, since I'm under the impression that a node-set
                     would be an extension to 1.0, and therefore, "cheating."
                     However, if we could generate a node-set and group based on that,
                     then the following block selects the correct nodes (I think):

            <xsl:for-each select="$matches">
                <xsl:variable name="childRow" select="../following-sibling::row[1]"/>
                <xsl:variable name="Lparent" select="@pos"/>
                <xsl:variable name="Rparent" select="following-sibling::cell[1]/@pos"/>
                <xsl:choose>
                <xsl:when test="$Rparent">
                    <xsl:apply-templates
                        select="$childRow/cell[
                                @pos &gt;= $Lparent
                            and @pos &lt; $Rparent]"
                        mode="child" />
                </xsl:when>
                <xsl:otherwise>
                    <xsl:apply-templates
                        select="$childRow/cell
                            [@pos &gt;= $Lparent]"
                        mode="child"/>
                </xsl:otherwise>
                </xsl:choose>
            </xsl:for-each>

            Without using a node-set, I'll try to solve this by crawling over every
            table/row/cell and test each one, every time.  Not pretty by a long shot.
            But hey, memory and processors are getting cheaper every day.
            -->

            <xsl:apply-templates select="$cells" mode="crawl">
            <xsl:with-param name="parent" select="."/>
            <xsl:with-param name="task">Group children by their labels</xsl:with-param>
            </xsl:apply-templates>

        </node>
    </xsl:template>

    <xsl:template match="cell" mode="child">
    <xsl:param name="parent"/>

        <node label="{.}">

            <positions>
                <xsl:apply-templates mode="crawl"
                    select="key('cell-by-label', text())">
                <xsl:with-param name="parent" select="$parent"/>
                <xsl:with-param name="child"  select="."/>
                <xsl:with-param name="task">Find all positions of a child</xsl:with-param>
                </xsl:apply-templates>
            </positions>

            <!-- And.... Recursion Start Now! -->
            <xsl:apply-templates select="$cells" mode="crawl">
            <xsl:with-param name="parent" select="."/>
            <xsl:with-param name="task">Group children by their labels</xsl:with-param>
            </xsl:apply-templates>

        </node>

    </xsl:template>

    <xsl:template match="cell" mode="crawl">
    <xsl:param name="parent"/>
    <xsl:param name="child"/>
    <xsl:param name="task"/>

        <xsl:variable name="parentRow"
            select="generate-id(../preceding-sibling::row[1])"/>

        <xsl:variable name="parentCell"
            select="key('cell-by-label', $parent/text())
                [$parentRow = generate-id(..)]" />

        <xsl:variable name="RparentPos"
            select="$parentCell/following-sibling::cell[1]/@pos"/>

        <!--
        This cell is a child if it is in a row directly following a row
        in which the parent cell's text value made an appearance.
        <xsl:if test="$parentCell">

        This cell is a child if it's @pos is >= the parent cell's pos
        <xsl:if test="@pos &gt;= $parentCell/@pos">

        If there is a parent cell to the right of this cell's parent cell,
        this this cell is a child only if its @pos is < the right-parent
        cell's @pos.
        <xsl:if test="not($RparentPos) or @pos &lt; $RparentPos">
        -->

        <xsl:if test="
            $parentCell
            and (@pos &gt;= $parentCell/@pos)
            and (not($RparentPos) or @pos &lt; $RparentPos)">

            <xsl:choose>

            <!--
            If our task is to determine whether there are any nodes prior to
            the given child node in the document order which are also
            children of the parent and which have the same label value, we do
            that now. All we really want is to make a mark. We will later use
            string-length to see if we made any marks here or not.
            -->
            <xsl:when test="$task = 'Are there prior children with equal labels?'">
                Yes
            </xsl:when>

            <!--
            Here, our task is to generate the <pos> nodes of the children.
            -->
            <xsl:when test="$task = 'Find all positions of a child'">
                <pos table="{../../@pos}" row="{../@pos}" cell="{@pos}"/>
            </xsl:when>

            <!--
            If our task is to group children by their labels, we need to know
            if this is the first child node with this particular label. To do
            that, we crawl over all cells along the preceding axis, and if
            they are otherwise potential children (see above block), then a
            mark is made (perhaps several such marks, doesn't matter how many,
            really). If we have any marks when we are done, we know we have
            output this label before, so we don't do it again.
            -->
            <xsl:when test="$task = 'Group children by their labels'">
                <xsl:variable name="priorMatches">
                    <xsl:apply-templates mode="crawl"
                        select="preceding::cell[text() = current()/text()]">
                    <xsl:with-param name="parent" select="$parent"/>
                    <xsl:with-param name="task">Are there prior children with equal labels?</xsl:with-param>
                    </xsl:apply-templates>
                </xsl:variable>

                <xsl:if test="string-length($priorMatches) = 0">
                    <xsl:apply-templates select="." mode="child">
                    <xsl:with-param name="parent" select="$parent"/>
                    </xsl:apply-templates>
                </xsl:if>
            </xsl:when>

            </xsl:choose>
        </xsl:if>

    </xsl:template>
</xsl:stylesheet>

Редактировать 1: немного переформатирован. Добавлено использование нескольких элементов xsl: key, чтобы помочь найти дочерние узлы по их метке. Сделано несколько других оптимизаций - надеюсь, без снижения читабельности.

Мысли / комментарии?

1 голос
/ 10 июля 2009

Таким образом, ячейки в каждой строке на самом деле являются потомками ячеек в предыдущей строке, где значение «pos» дочерней ячейки равно> = значение «pos» родительской ячейки и <значение «pos» последующий родитель (если есть) в предыдущей строке? </p>

Если вы одержимы использованием XSL для этого, тогда взгляните на ось следующего брата . Тем не менее, это будет один грязный путь выбора. Вероятно, проще сделать два преобразования: одно, которое организует плоские данные в иерархический формат, и другое, которое производит окончательный вывод.

Лично я думаю, что написал бы программу, которая явно обрабатывает дерево DOM, особенно если это одноразовая операция. Я подозреваю, что писать было бы так же быстро и не иметь шансов на странные угловые случаи в логике выбора.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...