XSL 排序由句点分隔的数字

XSL Sort Numbers Separated by Periods

我对用句点分隔的数字排序有问题(例如 1、2.1、1.1、1.3)。我在这里 XSL recursive sort 找到了解决方案。这就是我需要的,略有不同。 在我的 xml 中,标签就像

 <root>
   <row>
       <col name="rank"/>
       <name>A</name>
       <val>1.1</val>
   </row>
   <row>
       <col name="rank"/>
       <name>B</name>
       <val>1</val>
   </row>
   <row>
       <col name="level"/>
       <name>C</name>
       <val>test</val>
   </row>
   <row>
       <col name="rank"/>
       <name>D</name>
       <val>1.2.2</val>
   </row>
   <row>
       <col name="rank"/>
       <name>E</name>
       <val>1.2.1</val>
    </row>
   <row>
       <col name="rank"/>
       <name>F</name>
       <val>1.2</val>
    </row>
 </root>

我想根据 "val" 标签对 col/@name = "rank" 的所有行进行排序。是否可以仅修改链接问题中接受的答案来获得输出?如果没有,是否有 xsl 版本 1(如果有 none 则为 2)的任何解决方案。 我需要的输出是这样的:

<ul>
   <li>1 - B
     <ul>
       <li>1.1 - A</li>
       <li>1.2 - F
          <ul>
             <li>1.2.1 - E</li>
             <li>1.2.2 - D</li>
          </ul>
       </li>
    </ul>      
   </li>
</ul>

更新 I:所以根据 michael.hor257k 的回答,这就是我一直在寻找的解决方案

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html" version="5.0" encoding="UTF-8" indent="yes"/>
  <xsl:template match="/root">
    <html>
    <body>
      <ul>
        <xsl:apply-templates select="row[not(contains(val, '.'))][contains(col/@name, 'rank')]">
          <xsl:sort select="val" data-type="number" order="ascending"/>
        </xsl:apply-templates>
      </ul>
    </body>    
    </html>      
  </xsl:template>

  <xsl:template match="row">
    <li>
      <xsl:variable name="parent" select="concat(val, '.')" />
      <xsl:value-of select="./name"/> - <xsl:value-of select="./val"/>
      <xsl:if test="../row[starts-with(val, $parent)][not(contains(substring-after(val, $parent), '.'))][contains(col/@name, 'rank')]">
        <ul>
          <xsl:apply-templates select="../row[starts-with(val, $parent)][not(contains(substring-after(val, $parent), '.'))][contains(col/@name, 'rank')]">
            <xsl:sort select="substring-after(val, $parent)" data-type="number" order="ascending"/>
          </xsl:apply-templates>
        </ul>
      </xsl:if>
    </li>
  </xsl:template>
</xsl:stylesheet> 

更新 II:感谢 Dimitre Novatchev,我有一个更好的解决方案,我认为这是最好的答案。所以,我试图理解它,之后我会检查它是否为已接受的答案。

更新 III:我接受了 michael.hor257k 发布的答案,因为这是我需要的。我知道我的 xml 中没有排名顺序的跳跃,但正如 Dimitre Novatchev 提到的那样,如果有,比如有 1.3.2 而没有 1.3,这个解决方案就会有问题,你可以使用完整的答案Dimitre Novachev 发布的。

知道关卡的数量吗?如果是这样,在 XSLT 2.0 中您可以使用

<xsl:apply-templates select="row[col/@name = 'rank']">
<xsl:sort select="xs:integer(tokenize(val, '\'.')[1])"/>
<xsl:sort select="xs:integer(tokenize(val, '\'.')[2])"/>
<xsl:sort select="xs:integer(tokenize(val, '\'.')[3])"/>
</xsl:apply-templates/>

三个级别。在 XSLT 3.0 中,您甚至可以使用任何级别的 sort 函数来完成此操作:<xsl:apply-templates select="sort(row[col/@name = 'rank'], function($row) { tokenize($row/val, '\.')!xs:integer(.) })"> 尽管您也想嵌套,但我认为使用递归函数来执行

<xsl:for-each-group select="$rows" group-by="xs:integer(tokenize(val, '\.')[1])"><xsl:sort select="current-grouping-key()"/>...</xsl:for-each-group>

比纯排序更适合 XSLT 2.0 或 3.0。

一个完整的样式表是

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:mf="http://example.com/mf"
    exclude-result-prefixes="xs mf" version="2.0">

    <xsl:output method="html" indent="yes"/>

    <xsl:function name="mf:nest" as="element()*">
        <xsl:param name="rows" as="element(row)*"/>
        <xsl:sequence select="mf:nest($rows, 1)"/>
    </xsl:function>

    <xsl:function name="mf:nest" as="element()*">
        <xsl:param name="rows" as="element(row)*"/>
        <xsl:param name="level" as="xs:integer"/>
        <xsl:for-each-group select="$rows" group-by="xs:integer(tokenize(val, '\.')[$level])">
            <xsl:sort select="current-grouping-key()"/>
            <li>
                <xsl:variable name="item" select="current-group()[not(tokenize(val, '\.')[$level + 1])]"/>
                <xsl:value-of select="$item/concat(name, ' - ', val)"/>
                <xsl:if test="current-group()[2]">
                    <ul>
                        <xsl:sequence select="mf:nest(current-group() except $item, $level + 1)"/>
                    </ul>
                </xsl:if>
            </li>
        </xsl:for-each-group>
    </xsl:function>

    <xsl:template match="root">
        <ul>
            <xsl:sequence select="mf:nest(row[col/@name = 'rank'])"/>
        </ul>
    </xsl:template>

</xsl:stylesheet>

它转换输入

<root>
    <row>
        <col name="rank"/>
        <name>A</name>
        <val>1.1</val>
    </row>
    <row>
        <col name="rank"/>
        <name>B</name>
        <val>1</val>
    </row>
    <row>
        <col name="level"/>
        <name>C</name>
        <val>test</val>
    </row>
    <row>
        <col name="rank"/>
        <name>D</name>
        <val>1.2.2</val>
    </row>
    <row>
        <col name="rank"/>
        <name>E</name>
        <val>1.2.1</val>
    </row>
    <row>
        <col name="rank"/>
        <name>foo</name>
        <val>2</val>
    </row>
    <row>
        <col name="rank"/>
        <name>bar</name>
        <val>1.10</val>
    </row>
    <row>
        <col name="rank"/>
        <name>F</name>
        <val>1.2</val>
    </row>
    <row>
        <col name="rank"/>
        <name>F</name>
        <val>1.10.1</val>
    </row>
</root>

进入结果

<ul>
   <li>B - 1
      <ul>
         <li>A - 1.1</li>
         <li>F - 1.2
            <ul>
               <li>E - 1.2.1</li>
               <li>D - 1.2.2</li>
            </ul>
         </li>
         <li>bar - 1.10
            <ul>
               <li>F - 1.10.1</li>
            </ul>
         </li>
      </ul>
   </li>
   <li>foo - 2</li>
</ul>

考虑以下样式表:

XSLT 1.0

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

<xsl:template match="/root">
    <list>
        <xsl:apply-templates select="row[not(contains(val, '.'))]">
            <xsl:sort select="val" data-type="number" order="ascending"/>
        </xsl:apply-templates>
    </list>
</xsl:template>

<xsl:template match="row">
    <xsl:variable name="parent" select="concat(val, '.')" />
    <item val="{val}">
        <xsl:apply-templates select="../row[starts-with(val, $parent)][not(contains(substring-after(val, $parent), '.'))]">
            <xsl:sort select="substring-after(val, $parent)" data-type="number" order="ascending"/>
        </xsl:apply-templates>
    </item>
</xsl:template>

</xsl:stylesheet> 

应用于以下输入示例:

XML

<root>
   <row>
       <col name="rank"/>
       <name>A</name>
       <val>1.1</val>
   </row>
   <row>
       <col name="rank"/>
       <name>B</name>
       <val>1</val>
   </row>
   <row>
       <col name="rank"/>
       <name>F</name>
       <val>1.10</val>
    </row>
   <row>
       <col name="level"/>
       <name>C</name>
       <val>2</val>
   </row>
   <row>
       <col name="rank"/>
       <name>D</name>
       <val>1.2.2</val>
   </row>
   <row>
       <col name="rank"/>
       <name>E</name>
       <val>1.2.1</val>
    </row>
   <row>
       <col name="rank"/>
       <name>F</name>
       <val>1.2</val>
    </row>
 </root>

结果将是:

<?xml version="1.0" encoding="UTF-8"?>
<list>
   <item val="1">
      <item val="1.1"/>
      <item val="1.2">
         <item val="1.2.1"/>
         <item val="1.2.2"/>
      </item>
      <item val="1.10"/>
   </item>
   <item val="2"/>
</list>

这是递归工作的,并且对级别数没有限制。但请注意,每个项目(不包含点的 "ancestor" 项目除外)都必须有父项。

I have a problem with sorting numbers, separated by periods (e.g. 1, 2.1, 1.1, 1.3). I found a solution here XSL recursive sort.

第一部分排序

很容易将原来的解决方案应用到新的案例中。 与接受的答案不同,此解决方案正确排序 XML 文档,其中有

<val>1.3.2</val>

但是没有

<val>1.3</val>

有关将排序结果转换为所需的嵌套列表结构的信息,请参见第二部分。


<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

    <xsl:template match="root">
        <xsl:copy>
            <xsl:apply-templates select="row">
                <xsl:sort select="substring-before(concat(val, '.'), '.')" 
                     data-type="number"/>
            </xsl:apply-templates>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="row">
        <xsl:param name="prefix" select="''"/>
        <xsl:choose>
            <!-- end of recursion, there isn't any more row with more chunks -->
            <xsl:when test="val = substring($prefix, 1, string-length($prefix)-1)">
                <xsl:copy-of select="."/>
            </xsl:when>
            <xsl:otherwise>
                <xsl:variable name="chunk" select=
                 "substring-before(concat(substring-after(val, $prefix), '.'), '.')"/>
                <!-- this tests for grouping row with same prefix, to skip duplicates -->
                <xsl:if test=
                "not(preceding-sibling::row[starts-with(val, concat($prefix, $chunk))])">
                    <xsl:variable name="new-prefix" 
                                          select="concat($prefix, $chunk, '.')"/>
                    <xsl:apply-templates select=
              "../row[starts-with(val, $new-prefix) or val = concat($prefix, $chunk)]">
                        <xsl:sort select= 
              "substring-before(concat(substring-after(val, $new-prefix), '.'), '.')" 
                                  data-type="number"/>
                        <xsl:with-param name="prefix" select="$new-prefix"/>
                    </xsl:apply-templates>
                </xsl:if>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
</xsl:stylesheet>

当此转换应用于以下 XML 文档时 -- 注意有 <val>1.3.2</val> 但没有 <val>1.3</val> 并且接受的答案不会产生正确的结果——实际上删除了整个 <row> 具有 <val>1.3.2</val> child:

<root>
    <row>
        <col name="rank"/>
        <name>A</name>
        <val>1.1</val>
    </row>
    <row>
        <col name="rank"/>
        <name>B</name>
        <val>1</val>
    </row>
    <row>
        <col name="rank"/>
        <name>F</name>
        <val>1.10</val>
    </row>
    <row>
        <col name="level"/>
        <name>C</name>
        <val>2</val>
    </row>
    <row>
        <col name="rank"/>
        <name>D</name>
        <val>1.2.2</val>
    </row>
    <row>
        <col name="rank"/>
        <name>D</name>
        <val>1.3.2</val>
    </row>
    <row>
        <col name="rank"/>
        <name>E</name>
        <val>1.2.1</val>
    </row>
    <row>
        <col name="rank"/>
        <name>F</name>
        <val>1.2</val>
    </row>
</root>

产生了想要的、正确排序的结果:

<root>
   <row>
      <col name="rank"/>
      <name>B</name>
      <val>1</val>
   </row>
   <row>
      <col name="rank"/>
      <name>A</name>
      <val>1.1</val>
   </row>
   <row>
      <col name="rank"/>
      <name>F</name>
      <val>1.2</val>
   </row>
   <row>
      <col name="rank"/>
      <name>E</name>
      <val>1.2.1</val>
   </row>
   <row>
      <col name="rank"/>
      <name>D</name>
      <val>1.2.2</val>
   </row>
   <row>
      <col name="rank"/>
      <name>D</name>
      <val>1.3.2</val>
   </row>
   <row>
      <col name="rank"/>
      <name>F</name>
      <val>1.10</val>
   </row>
   <row>
      <col name="level"/>
      <name>C</name>
      <val>2</val>
   </row>
</root>

最后,再进行一次重构:消除所有 XSLT 条件运算符:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output omit-xml-declaration="yes" indent="yes"/>
    <xsl:strip-space elements="*"/>

    <xsl:template match="/*">
        <xsl:copy>
            <xsl:apply-templates select="row">
                <xsl:sort select="substring-before(concat(val, '.'), '.')" 
                          data-type="number"/>
            </xsl:apply-templates>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="row">
        <xsl:param name="prefix" select="''"/>
        <xsl:variable name="vHasChildren" select=
                           "not(val = substring($prefix, 1, string-length($prefix)-1))"/>
        <xsl:copy-of select="self::node()[not($vHasChildren)]"/>

        <xsl:variable name="chunk" 
             select="substring-before(concat(substring-after(val, $prefix), '.'), '.')"/>
        <xsl:variable name="new-prefix" select="concat($prefix, $chunk, '.')"/>
        <xsl:apply-templates select= "self::node()
            [$vHasChildren 
           and not(preceding-sibling::row[starts-with(val, concat($prefix, $chunk))])
            ]
             /../row[starts-with(val, $new-prefix) or val = concat($prefix, $chunk)]">
            <xsl:with-param name="prefix" select="$new-prefix"/>
            <xsl:sort data-type="number" select=
                "substring-before(concat(substring-after(val, $new-prefix), '.'), '.')"/>
        </xsl:apply-templates>
    </xsl:template>
</xsl:stylesheet>

第二部分:将排序后的平面结果转换为嵌套列表结构

在这里,我们从第 I 部分中生成的转换结果开始,并从中生成所需的嵌套列表结构。到目前为止,我们得到的排序结果是:

<root>
   <row>
      <col name="rank"/>
      <name>B</name>
      <val>1</val>
   </row>
   <row>
      <col name="rank"/>
      <name>A</name>
      <val>1.1</val>
   </row>
   <row>
      <col name="rank"/>
      <name>F</name>
      <val>1.2</val>
   </row>
   <row>
      <col name="rank"/>
      <name>E</name>
      <val>1.2.1</val>
   </row>
   <row>
      <col name="rank"/>
      <name>D</name>
      <val>1.2.2</val>
   </row>
   <row>
      <col name="rank"/>
      <name>D</name>
      <val>1.3.2</val>
   </row>
   <row>
      <col name="rank"/>
      <name>F</name>
      <val>1.10</val>
   </row>
   <row>
      <col name="level"/>
      <name>C</name>
      <val>2</val>
   </row>
</root>

我们使用这个转换:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

  <xsl:template match="/*">
    <list>
      <xsl:apply-templates select=
      "row[not(substring-before(concat(val, '.'), '.') 
              = substring-before(concat(preceding-sibling::row[1]/val,'.'),'.'))]">
       <xsl:with-param name="pPrefix" select="''"/>
      </xsl:apply-templates>
    </list>
  </xsl:template>


  <xsl:template match="row">
    <xsl:param name="pPrefix"/>
    <item val="{val}">
      <xsl:variable name="vnewPrefix" select="concat($pPrefix, val, '.')"/>
      <xsl:variable name="vcurrentVal" select="val"/>
      <xsl:apply-templates select="following-sibling::row
                  [starts-with(val, concat($vcurrentVal,'.'))
                 and
                   (string-length(val) - string-length(translate(val,'.','')) 
                    = 1 + string-length($vcurrentVal) - string-length(translate($vcurrentVal,'.','')
                    )
                   or
                    not(starts-with(val, 
                                    concat($vnewPrefix, 
                                           substring-before(concat(substring-after(preceding-sibling::row[1]/val, $vnewPrefix),'.'),'.'),
                                                            '.')
                                    )
                        )
                   )
                  ]">
         <xsl:with-param name="pPrefix" select="$vnewPrefix"/>
      </xsl:apply-templates>
    </item>
  </xsl:template>
</xsl:stylesheet>

对上述 XML 文档应用此转换的结果是想要的嵌套列表结构:

<list>
   <item val="1">
      <item val="1.1"/>
      <item val="1.2">
         <item val="1.2.1"/>
         <item val="1.2.2"/>
      </item>
      <item val="1.3.2"/>
      <item val="1.10"/>
   </item>
   <item val="2"/>
</list>

我们可以类似地生成想要的 HTML,使用这个转换:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

  <xsl:template match="/*">
    <ul>
      <xsl:apply-templates select=
      "row[not(substring-before(concat(val, '.'), '.') 
              = substring-before(concat(preceding-sibling::row[1]/val,'.'),'.'))]">
       <xsl:with-param name="pPrefix" select="''"/>
      </xsl:apply-templates>
    </ul>
  </xsl:template>

  <xsl:template match="row">
    <xsl:param name="pPrefix"/>
    <li> <xsl:value-of select="concat(val, ' - ', name, '&#xA;')"/>
      <xsl:variable name="vnewPrefix" select="concat($pPrefix, val, '.')"/>
      <xsl:variable name="vcurrentVal" select="val"/>

      <xsl:variable name="vnextInChain" select=
      "following-sibling::row
                  [starts-with(val, concat($vcurrentVal,'.'))
                 and
                   (string-length(val) - string-length(translate(val,'.','')) 
                    = 1 + string-length($vcurrentVal) - string-length(translate($vcurrentVal,'.','')
                    )
                   or
                    not(starts-with(val, 
                                    concat($vnewPrefix, 
                                           substring-before(concat(substring-after(preceding-sibling::row[1]/val, $vnewPrefix),'.'),'.'),
                                                            '.')
                                    )
                        )
                   )
                  ]"/>

      <xsl:if test="$vnextInChain">
       <ul>
        <xsl:apply-templates select="following-sibling::row
                  [starts-with(val, concat($vcurrentVal,'.'))
                 and
                   (string-length(val) - string-length(translate(val,'.','')) 
                    = 1 + string-length($vcurrentVal) - string-length(translate($vcurrentVal,'.','')
                    )
                   or
                    not(starts-with(val, 
                                    concat($vnewPrefix, 
                                           substring-before(concat(substring-after(preceding-sibling::row[1]/val, $vnewPrefix),'.'),'.'),
                                                            '.')
                                    )
                        )
                   )
                  ]">
         <xsl:with-param name="pPrefix" select="$vnewPrefix"/>
        </xsl:apply-templates>
       </ul>
      </xsl:if>
    </li>
  </xsl:template>
</xsl:stylesheet>

将此转换应用于平面排序结果时,会生成所需的 HTML 结果:

<ul>
    <li>1 - B
        <ul>
            <li>1.1 - A
            </li>
            <li>1.2 - F
                <ul>
                    <li>1.2.1 - E
                    </li>
                    <li>1.2.2 - D
                    </li>
                </ul></li>
            <li>1.3.2 - D
            </li>
            <li>1.10 - F
            </li>
        </ul></li>
    <li>2 - C
    </li>
</ul>

检查您选择的 XSLT 处理器是否支持数字排序规则(一种将连续数字序列视为数字的字符串排序方式,因此 "Chapter 2" 在 "Chapter 10" 之前排序)。例如,对于 XSLT 3.0 和 XPath 3.1 中定义的 UCA 归类 URI,这将是

    <xsl:sort select="val" 
         collation="http://www.w3.org/2013/collation/UCA?numeric=yes"/>

数字排序规则在撒克逊语中也已经使用了一些年

collation="http://saxon.sf.net/collation?alphanumeric=yes"

如果您能够利用它,这里还有另一个候选者:XPath 3.1 中的新 fn:sort 函数允许使用复合排序键,因此您可以编写

sort(val, function($x){tokenize($x, '\.')!number()})