XSLT1.0排序后如何获取下一个排序后的节点?
解决XSLT 1.0中排序后获取下一条节点及按员工每日分组的问题
你遇到的问题很典型:following-sibling是基于原始文档的节点结构,和<xsl:sort>在for-each里生成的临时排序顺序完全无关,所以排序后用它拿到的还是原文档里的下一个节点,不是排序后的。在XSLT 1.0里,我们可以通过先预先生成排序后的节点集,再结合位置索引来解决这个问题,同时用Muenchian分组法实现按员工每日一行的需求。
核心思路
- 预先生成排序后的节点集:把所有
Record节点按你的规则排序后,存到一个变量里,后续遍历的就是已经排好序的节点集合。 - 用位置索引获取下一个节点:遍历排序后的节点集时,用
position()函数获取当前位置,然后通过$sorted-records[position() + 1]就能拿到排序后的下一个节点。 - Muenchian分组实现按员工每日聚合:创建一个复合键(员工编号+日期),按这个键分组,每个组对应一个员工的一天,然后在组内处理最多3条记录,同时计算当日总工时。
完整XSLT代码示例
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" indent="yes"/> <!-- Muenchian分组的复合键:员工编号+日期,确保同一员工同一天的记录归为一组 --> <xsl:key name="emp-day-group" match="Record" use="concat(EmployeeNumber, '|', TimeStartRaw)"/> <!-- 预先生成排序后的所有Record节点集 --> <xsl:variable name="sorted-records"> <xsl:for-each select="/Export/Record"> <xsl:sort select="EmployeeNumber" order="ascending"/> <xsl:sort select="concat(substring(TimeStartRaw,7,4),substring(TimeStartRaw,4,2),substring(TimeStartRaw,1,2))" order="ascending"/> <xsl:sort select="translate(TimeStartRaw1,':','')" order="ascending"/> <xsl:copy-of select="."/> </xsl:for-each> </xsl:variable> <!-- 将结果树片段转换为可遍历的节点集(XSLT 1.0需依赖EXSLT扩展,主流处理器均支持) --> <xsl:variable name="sorted-node-set" select="exsl:node-set($sorted-records)/Record" xmlns:exsl="http://exslt.org/common"/> <xsl:template match="/"> <Export> <!-- 按员工+日期分组,仅处理每组的第一个节点,避免重复生成每日记录 --> <xsl:for-each select="$sorted-node-set[generate-id() = generate-id(key('emp-day-group', concat(EmployeeNumber, '|', TimeStartRaw))[1])]"> <DailyRecord> <EmployeeNumber><xsl:value-of select="EmployeeNumber"/></EmployeeNumber> <Date><xsl:value-of select="TimeStartRaw"/></Date> <!-- 计算当日该员工的总工时 --> <TotalHours><xsl:value-of select="sum(key('emp-day-group', concat(EmployeeNumber, '|', TimeStartRaw))/Punch_nethours)"/></TotalHours> <!-- 输出当日最多3条打卡记录 --> <Punches> <xsl:for-each select="key('emp-day-group', concat(EmployeeNumber, '|', TimeStartRaw))"> <xsl:if test="position() <= 3"> <Punch> <StartTime><xsl:value-of select="TimeStartRaw1"/></StartTime> <EndTime><xsl:value-of select="TimeEndRaw"/></EndTime> <Hours><xsl:value-of select="Punch_nethours"/></Hours> </Punch> </xsl:if> </xsl:for-each> </Punches> <!-- 获取排序后的下一条记录(判断是否存在,避免越界) --> <xsl:variable name="current-pos" select="position()"/> <xsl:if test="$sorted-node-set[$current-pos + 1]"> <NextRecordEmployee><xsl:value-of select="$sorted-node-set[$current-pos + 1]/EmployeeNumber"/></NextRecordEmployee> <NextRecordDate><xsl:value-of select="$sorted-node-set[$current-pos + 1]/TimeStartRaw"/></NextRecordDate> </xsl:if> </DailyRecord> </xsl:for-each> </Export> </xsl:template> </xsl:stylesheet>
关键部分解释
- 排序后的节点集处理:
我们先把排序后的Record节点复制到变量sorted-records中,由于XSLT 1.0直接存储的是结果树片段,需要用exsl:node-set()将其转换为可遍历的节点集,主流XSLT 1.0处理器(如Saxon、Xalan)都支持这个EXSLT扩展。 - Muenchian分组逻辑:
通过concat(EmployeeNumber, '|', TimeStartRaw)创建复合分组键,确保同一员工同一天的记录被分到一组;用generate-id()判断当前节点是否为组内第一个节点,避免重复生成每日记录。 - 排序后下一条节点获取:
遍历分组后的节点时,用position()记录当前组的位置,通过$sorted-node-set[$current-pos + 1]即可拿到排序后的下一个节点,同时增加了存在性判断,防止越界报错。 - 每日最多3条记录限制:
在组内遍历打卡记录时,用position() <= 3过滤,只保留前3条记录。
输出结果示例
针对你提供的输入数据,输出结构如下:
<Export> <DailyRecord> <EmployeeNumber>17177006</EmployeeNumber> <Date>02.03.2020</Date> <TotalHours>0.5</TotalHours> <Punches> <Punch> <StartTime>08:00</StartTime> <EndTime>08:15</EndTime> <Hours>0.25</Hours> </Punch> <Punch> <StartTime>11:00</StartTime> <EndTime>11:15</EndTime> <Hours>0.25</Hours> </Punch> </Punches> <NextRecordEmployee>17177006</NextRecordEmployee> <NextRecordDate>03.03.2020</NextRecordDate> </DailyRecord> <DailyRecord> <EmployeeNumber>17177006</EmployeeNumber> <Date>03.03.2020</Date> <TotalHours>0.25</TotalHours> <Punches> <Punch> <StartTime>08:00</StartTime> <EndTime>08:15</EndTime> <Hours>0.25</Hours> </Punch> </Punches> </DailyRecord> </Export>
这样既解决了排序后获取下一条节点的问题,又实现了按员工每日聚合、最多3条记录的需求,完全用XSLT 1.0原生(加常用扩展)实现,不需要依赖C#。
内容的提问来源于stack exchange,提问作者Itsallgonepearshaped
相关产品推荐
相关产品推荐

