XSLT 1.0:分组和删除重复项

3

我有一个 XML 分组的挑战,需要按照以下方式进行分组和去重:

<Person>
<name>John</name>
<date>June12</date>
<workTime taskID=1>34</workTime>
<workTime taskID=1>35</workTime>
<workTime taskID=2>12</workTime>
</Person>
<Person>
<name>John</name>
<date>June13</date>
<workTime taskID=1>21</workTime>
<workTime taskID=2>11</workTime>
<workTime taskID=2>14</workTime>
</Person>

请注意,对于特定的名称/任务ID/日期出现次数,只会选择第一个。 在这个例子中,
<workTime taskID=1>35</workTime> 
<workTime taskID=2>14</workTime> 

将被搁置。

以下是预期输出:

<Person>
<name>John</name>
<taskID>1</taskID>
<workTime>
<date>June12</date>
<time>34</time>
</worTime>
<workTime>
<date>June13</date>
<time>21</time>
</worTime>
</Person>
<Person>
<name>John</name>
<taskID>2</taskID>
<workTime>
<date>June12</date>
<time>12</time>
</worTime>
<workTime>
<date>June13</date>
<time>11</time>
</worTime>
</Person>

我尝试使用下面的键在XSLT 1.0中使用Muenchian分组:

<xsl:key name="PersonTasks" match="workTime" use="concat(@taskID, ../name)"/>

但是我该如何仅挑选第一个出现的

呢?
concat(@taskID, ../name, ../date)

似乎我需要两个级别的密钥!

这个问题既有趣又困难(+1)。请查看我的答案,其中包含了一个高效而简洁的解决方案。 - Dimitre Novatchev
3个回答

2

这个转换

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

 <xsl:key name="kwrkTimeByNameTask" match="workTime"
  use="concat(../name, '+', @taskID)"/>

 <xsl:key name="kDateByName" match="date"
  use="../name"/>

 <xsl:key name="kwrkTimeByNameTaskDate" match="workTime"
  use="concat(../name, '+', @taskID, '+', ../date)"/>

 <xsl:template match="/">
   <xsl:for-each select=
    "*/*/workTime
           [generate-id()
           =
            generate-id(key('kwrkTimeByNameTask',
                             concat(../name, '+', @taskID)
                            )[1]
                        )
           ]
    ">
      <xsl:sort select="../name"/>
      <xsl:sort select="@taskID" data-type="number"/>

      <xsl:variable name="vcurTaskId" select="@taskID"/>
      <Person>
        <name><xsl:value-of select="../name"/></name>
        <taskID><xsl:value-of select="@taskID"/></taskID>

          <xsl:for-each select=
           "key('kDateByName', ../name)
                  [key('kwrkTimeByNameTaskDate',
                       concat(../name, '+', current()/@taskID, '+', .)
                      )
                  ]
           ">
             <workTime>
               <date><xsl:value-of select="."/></date>
               <time>
                <xsl:value-of select=
                 "key('kwrkTimeByNameTaskDate',
                  concat(../name, '+', $vcurTaskId, '+', .)
                 )"/>
               </time>
             </workTime>
          </xsl:for-each>
      </Person>
   </xsl:for-each>
 </xsl:template>
</xsl:stylesheet>

当应用于提供的XML(已校正多个问题以成为格式良好的XML)时:

<t>
    <Person>
        <name>John</name>
        <date>June12</date>
        <workTime taskID="1">34</workTime>
        <workTime taskID="1">35</workTime>
        <workTime taskID="2">12</workTime>
    </Person>
    <Person>
        <name>John</name>
        <date>June13</date>
        <workTime taskID="1">21</workTime>
        <workTime taskID="2">11</workTime>
        <workTime taskID="2">14</workTime>
    </Person>
</t>

产生所需的、正确的结果:

<Person>
   <name>John</name>
   <taskID>1</taskID>
   <workTime>
      <date>June12</date>
      <time>34</time>
   </workTime>
   <workTime>
      <date>June13</date>
      <time>21</time>
   </workTime>
</Person>
<Person>
   <name>John</name>
   <taskID>2</taskID>
   <workTime>
      <date>June12</date>
      <time>12</time>
   </workTime>
   <workTime>
      <date>June13</date>
      <time>11</time>
   </workTime>
</Person>

解释:

  1. 首先,我们使用Muenchian方法对../name@taskID这两个唯一的元素对获取所有workTime元素进行分组。

  2. 我们按照../name@taskID的顺序排序。

  3. 对于每个这样的workTime,我们获取所有列在该workTime../name下的date元素,并仅保留那些date元素,其中有一个workTime具有相同的../date../name

  4. 在前面的步骤中,我们使用了两个不同的辅助键'kDateByName'通过它们的../name索引所有date元素,而'kwrkTimeByNameTaskDate'通过它们的../name../date@taskID索引所有workTime元素。

因此,以下内容的意思是:

          <xsl:for-each select=
           "key('kDateByName', ../name)
                  [key('kwrkTimeByNameTaskDate',
                       concat(../name, '+', current()/@taskID, '+', .)
                      )
                  ]
           ">

是:

对于每个日期名称,使得一个workTime存在于该名称日期@taskID(在外部<xsl:for-each>的当前workTime中),执行此<xsl:for-each>指令中的内容。


你能稍微解释一下你的解决方案的设计吗?看起来很简洁,但我想尽可能多地学习它。谢谢。 - Daniel
@Daniel:我添加了一个解释。 - Dimitre Novatchev
我在想是否最好使用简单的Muenchian分组,然后检查前面的兄弟节点是否有重复。这会是一个好的解决方案吗? - Daniel
@Daniel:如果我们拥有键的能力,为什么还要回到兄弟节点比较呢? - Dimitre Novatchev

1

只是为了好玩,另一种使用两个键的解决方案。这个样式表:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:key name="kWorkTimeByName-TaskID" match="workTime" 
              use="concat(../name,'++',@taskID)"/>
    <xsl:key name="kWorkTimeByName-Date-TaskID" match="workTime" 
              use="concat(../name,'++',../date,'++',@taskID)"/>
    <xsl:template match="/">
        <xsl:variable name="vAllWorkTime" select="*/*/workTime"/>
        <result>
            <xsl:for-each select="$vAllWorkTime
                        [count(.|key('kWorkTimeByName-TaskID',
                                         concat(../name,'++',@taskID))[1])=1]">
                <xsl:sort select="../name"/>
                <xsl:sort select="@taskID" data-type="number"/>
                <Person>
                    <xsl:copy-of select="../name"/>
                    <taskID>
                        <xsl:value-of select="@taskID"/>
                    </taskID>
                    <xsl:for-each select="$vAllWorkTime
                          [count(.|key('kWorkTimeByName-Date-TaskID',
                               concat(current()/../name,'++',
                                   ../date,'++',current()/@taskID))[1])=1]">
                        <xsl:sort select="../date"/>
                        <xsl:copy>
                            <xsl:copy-of select="../date"/>
                            <time>
                                <xsl:value-of select="."/>
                            </time>
                        </xsl:copy>
                    </xsl:for-each>
                </Person>
            </xsl:for-each>
        </result>
    </xsl:template>
</xsl:stylesheet>

输出:

<result>
    <Person>
        <name>John</name>
        <taskID>1</taskID>
        <workTime>
            <date>June12</date>
            <time>34</time>
        </workTime>
        <workTime>
            <date>June13</date>
            <time>21</time>
        </workTime>
    </Person>
    <Person>
        <name>John</name>
        <taskID>2</taskID>
        <workTime>
            <date>June12</date>
            <time>12</time>
        </workTime>
        <workTime>
            <date>June13</date>
            <time>11</time>
        </workTime>
    </Person>
</result>

我在想是否最好使用简单的Muenchian分组,然后检查前面的兄弟节点是否有重复。这会是一个好的解决方案吗? - Daniel
在连接字符串时,“++”、“+”或不加符号有什么区别? - Daniel
@Daniel:关于分隔符字符串:它只需要是一个既不在键中的字符串,所以大多数时候可以把Dimitre的评论当做玩笑。关于分组:你正在按名称和任务进行分组,然后按日期进行分组(因此键变为名称、任务和日期);如果您仅使用最后一个当前组的所有节点或仅使用第一个节点,对算法逻辑没有影响。 - user357812

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接