代码之家  ›  专栏  ›  技术社区  ›  Tomalak

复杂的xslt递归树转换

  •  1
  • Tomalak  · 技术社区  · 15 年前

    (注意:如果您在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类别树。每一行表示上一行的子级,其中子类别按其各自的位置附加到父级-如果其位置为>=左侧的父级,右侧的下一个父级,则它们是子级( 如果 有一个)。

    我希望输出是一棵树,按标签分组( 仅在所述父子关系内 ,但通过表格)。例如,“第1类”分别存在于“A”类和“B类”中。

    有n个表,n行,每个表有n个单元格。您可以将输入数据结构想象为一个3d立方体,每个表代表不同年份的大致相同的数据。

    如果只有一个表(“年”),我可以做上述的事情,但是对于n个表,我有一个很大的问题,找到一个应用的方法,因为不同的立场,否则平等的父母。

    最后,我对一个无扩展函数的xslt 1.0解决方案感兴趣,尽管任何(算法)帮助都是非常感谢的。这个问题困扰了我很长一段时间,我似乎无法把我的头围绕它。我觉得必须有一个干净的解决方案,我只是看不到。我相信这可以通过一个递归模板、几个键和一些非常聪明的xpath来完成。

    不过,我想这个问题是关于风滚草徽章的材料。:-)

    2 回复  |  直到 15 年前
        1
  •  1
  •   kdgregory    15 年前

    因此,每行中的单元格实际上是前一行中单元格的子单元格,其中子单元格的“pos”值是父单元格的“pos”值,<前一行中后续父单元格(如果有)的“pos”值?

    如果你一心想用xsl来实现这个功能,那么 take a look 下一个同级节点 轴。然而,这将是一个混乱的选择路径。可能更容易进行两种转换:一种是将平面数据组织为层次结构格式,另一种是生成最终输出。

    我个人认为我应该编写一个程序,显式地处理dom树,特别是如果这是一次性操作。我怀疑它写起来也会很快,而且在选择逻辑中没有机会出现奇怪的角落案例。

        2
  •  1
  •   Chris Nielsen    15 年前

    这是我第一次尝试这个。这看起来很简单,直到我遇到了递归分组的麻烦。我考虑过使用节点集,但我认为这是对xsl 1.0的扩展,对吧?所以,没有为这个设置节点?如果没有这个,它会像这样:

    1. 找到所有根节点,以不同的方式处理它们(它们没有父节点,因此适用不同的规则)。

    2. 对于每个这样的节点,遍历每个//cell节点并测试它是否可能是当前节点的直接子节点。如果是个孩子,我们还得把它分组,所以…

    3. 沿着前面的轴爬行穿过每个/细胞节点。对于前面的每个//cell节点,查看它是否也可能是在步骤2中找到的父节点的直接子节点。

    4. 在找到前面的//cell节点(它是子节点)时,将前面子节点的标签与步骤2中找到的子节点进行比较。如果它们相等,则不输出任何内容(因为此标签是以前输出的)。否则…

    5. 开始输出子节点。再次在每个//cell节点中爬行,以定位该标签的所有位置,其中该标签是在步骤2中找到的父节点的子节点。

    6. 递归:返回步骤2,使用这个子节点作为新的父节点。

    在msxml2上测试,看起来它正在生成您要求的内容。它似乎可以工作,因为我知道,即使我再添加一些<row>元素,它也应该可以工作。写起来确实很有趣,它让我思考了一些在使用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元素来帮助按标签查找子节点。做了一些其他的优化——希望不会降低可读性。

    想法/评论?