#xslt #xpath
#xslt #xpath
Вопрос:
У меня есть:
<node handle="full"/>
<node handle="full"/>
<node handle="left"/>
<node handle="right"/>
<node handle="top-left"/>
<node handle="top-right"/>
<node handle="bottom"/>
<node handle="full"/>
<node handle="full"/>
Мне нужно сгруппировать эти узлы на основе следующей логики:
full
должно быть само по себе.left
должна группироваться с минимум 1 и максимум 2 дополнительными узлами типаright
,top-right
,bottom-right
.right
должна группироваться с минимум 1 и максимум 2 дополнительными узлами типаleft
,top-left
,bottom-left
.top-left
должна группироваться минимум с 2 и максимум с 3 узлами типаbottom-right
,top-right
,bottom-right
,bottom
.- …
Очевидно, что если я начну с left
и following-sibling
есть right
, процесс должен сбросить и продолжить со следующего элемента.
Таким образом, результат должен выглядеть следующим образом:
<group>
<node handle="full"/>
</group>
<group>
<node handle="full"/>
</group>
<group>
<node handle="left"/>
<node handle="right"/>
</group>
<group>
<node handle="top-left"/>
<node handle="top-right"/>
<node handle="bottom"/>
</group>
<group>
<node handle="full"/>
</group>
<group>
<node handle="full"/>
</group>
Существует ли эффективный (как для людей, так и для машин) способ справиться с этим или им следует управлять в каждом конкретном случае в коде?
РЕДАКТИРОВАТЬ 1:
Я думаю, что я мог бы определить свой набор правил следующим образом, а затем сравнить с ним в каждом конкретном случае:
<xsl:variable name="layouts">
<opt start="left" min="1" max="2">
<allow pos="right" value="2"/>
<allow pos="top-right" value="1"/>
<allow pos="bottom-right" value="1"/>
</opt>
<opt start="right" min="1" max="2">
<allow pos="left" value="2"/>
<allow pos="top-left" value="1"/>
<allow pos="bottom-left" value="1"/>
</opt>
</xsl:variable>
Я бы использовал max
оценку, из которой я бы вычел value
из каждого добавленного элемента.
Как это выглядит?
ПРАВКА 2:
Решение, которое я нашел, сработало за несколько мгновений до того, как Тим Си опубликовал свой ответ.
Первое различие, которое я вижу между ними, заключается в том, что моя версия ограничивает допустимые начальные элементы для последовательности (слева, вверху слева). Я больше не знаю, хорошо ли это или ограничение, которое я ввел, чтобы избежать сопоставления узлов, которые уже стали частью последовательности.
В любом случае, aswer Тима намного элегантнее, чем у меня.
<!--
For each /item:
- see it it's one of the starting points of a sequence:
- Not "full"
- Left, Top-Left
- if full, just return the element
- if not full and not a starting point, skip it, since it means it being added by the previous item.
- if not full and either of the starting points, kick into a recursion loop in a separate template:
- store the item's current "score" (2 or 1 for single-quadrant images)
- recur through the following-siblings with a counter for position(), checking if they are in the allowed list, and decreasing the "score" counter.
- every time a match is found:
- recreate the "allow" list, minus the current match, and pass the updated list to the next iteration
- decrease the counter
- if the iteration completes, reaching zero, return the position() of the last matched item
- if during the iteration, while the score is still >0, a match is not found, return false(). Our sequence is broken, we have a user error.
- the calling template (the one matching *every* item) checks whether the returned result is >0 or false()
- if >0 returns a copy of every node up the number specified by >0
- if false() print out and error, suggesting possible sequences.
-->
<xsl:variable name="layouts">
<start handle="left" score="2"> <!-- The starting score which we'll subtract from on every iteration -->
<allow handle="right" value="2"/> <!-- the acceptable position which we'll check against on every iteration -->
<allow handle="top-right" value="1"/> <!-- the value for each position which we'll subtract from the <start> score -->
<allow handle="bottom-right" value="1"/>
</start>
<start handle="top-left" score="3">
<allow handle="right" value="2"/>
<allow handle="bottom-left" value="1"/>
<allow handle="top-right" value="1"/>
<allow handle="bottom-right" value="1"/>
</start>
<start handle="full" score="0"/> <!-- Position which are not acceptable as the start of a sequence are scored 0 -->
<start handle="right" score="0"/>
<start handle="top-right" score="0"/>
<start handle="bottom-right" score="0"/>
<start handle="bottom-left" score="0"/>
</xsl:variable>
<!-- Applied to every /item -->
<xsl:template mode="imagewraps" match="item">
<xsl:param name="i" select="position()"/>
<xsl:variable name="nodeName" select="name(.)"/>
<xsl:variable name="layout" select="exsl:node-set($layouts)"/>
<xsl:variable name="position" select="position/item/@handle"/>
<xsl:variable name="score" select="$layout/start[@handle = $position]/@score"/>
<xsl:variable name="allowList" select="$layout/start[@handle = $position]"/>
<!-- This variable will store the final result of the recursion lanunched from within.
The returned value will be a number, indication the position of the last node that is part of the sequence -->
<xsl:variable name="sequenceFound">
<xsl:if test="$score > 0">
<xsl:apply-templates mode="test" select="parent::node()/*[name() = $nodeName][$i 1]">
<xsl:with-param name="i" select="$i 1"/>
<xsl:with-param name="score" select="$score"/>
<xsl:with-param name="allowList" select="$allowList"/>
</xsl:apply-templates>
</xsl:if>
</xsl:variable>
<div style="border: 1px solid red">
<xsl:choose>
<!-- If the $score is 0 and the position is 'full' just return a copy if the current node -->
<xsl:when test="$score = 0 and $position = 'full'">
<xsl:copy-of select="."/>
</xsl:when>
<!-- if the $score is greater than 0, return a copy of the current node
and the siblings the follow, up to the value stored in $sequenceFound -->
<xsl:when test="$score > 0">
<xsl:choose>
<!-- Actually do the above only if $sequenceFound didn't end up being 0
(it currently never does, but good to have as an option to handle errors in here) -->
<xsl:when test="$sequenceFound != 0">
<xsl:copy-of select="."/>
<xsl:copy-of select="following-sibling::*[$sequenceFound - $i >= position()]"/>
</xsl:when>
</xsl:choose>
</xsl:when>
<!-- If the first item is wrong, let jsut say it -->
<xsl:when test="$score = 0 and position() > 1">
<xsl:message>The first item should either be "full", "left", "top-left".</xsl:message>
</xsl:when>
</xsl:choose>
</div>
</xsl:template>
<xsl:template mode="test" match="*">
<xsl:param name="i"/>
<xsl:param name="score"/>
<xsl:param name="allowList"/>
<xsl:variable name="this" select="."/>
<xsl:variable name="nodeName" select="name()"/>
<xsl:variable name="position" select="position/item/@handle"/>
<xsl:variable name="isInAllowList" select="count($allowList/allow[@handle = $position]) > 0"/>
<xsl:variable name="value">
<xsl:if test="$isInAllowList">
<xsl:value-of select="$allowList/allow[@handle = $position]/@value"/>
</xsl:if>
</xsl:variable>
<xsl:variable name="allowListMinusMatched">
<xsl:if test="$isInAllowList">
<xsl:copy-of select="$allowList/allow[@handle != $position]"/>
</xsl:if>
</xsl:variable>
<xsl:choose>
<xsl:when test="$isInAllowList">
<xsl:choose>
<!-- if we've not ran out of loops, continue -->
<xsl:when test="($score - $value) > 0">
<xsl:apply-templates mode="test" select="parent::node()/*[name() = $nodeName][$i 1]">
<xsl:with-param name="i" select="$i 1"/>
<xsl:with-param name="allowList" select="$allowListMinusMatched"/>
<xsl:with-param name="score" select="$score - $value"/>
</xsl:apply-templates>
</xsl:when>
<xsl:when test="($score - $value) = 0">
<xsl:value-of select="$i"/>
</xsl:when>
</xsl:choose>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="layout" select="exsl:node-set($layouts)"/>
<xsl:variable name="allowed" select="$layout/start[@handle = $position]"/>
<xsl:message>Bombing out. Wrong Sequence.</xsl:message>
<xsl:message>
Items allowed after "<xsl:value-of select="$allowed/@handle"/>" are:
<xsl:for-each select="$allowed/allow">
<xsl:value-of select="@handle"/>
<xsl:if test="count($allowed/allow) > position()">, </xsl:if>
<xsl:if test="count($allowed/allow) = position()">.</xsl:if>
</xsl:for-each>
</xsl:message>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
Ответ №1:
Мне удалось найти решение для этого, но я не был полностью уверен в «минимальном» требовании. Тем не менее, я опубликую то, что я сделал до сих пор, если это поможет….
Во-первых, я определил xsl:ключ для каждого из правил. Например, для левого правила:
<xsl:key name="left"
match="node[@handle='right' or @handle='top-right' or @handle='bottom-right']"
use="generate-id(preceding-sibling::node[@handle='left'][1])"/>
Итак, в этом случае он сопоставляет правый, верхний правый и нижний правый элементы и связывает их с самым предыдущим левым элементом. Обратите внимание, что это позволит получить больше элементов, которые вам нужны, если между левым и правым элементом будет нижний элемент. Это будет рассмотрено чуть позже…
Затем я создал шаблон для сопоставления первого узла в группе. Первоначально я определил переменную, содержащую следующие узлы, используя соответствующий ключ xsl: (где $maximum — переменная, содержащая максимальное количество узлов)
<xsl:variable name="followingNodes"
select="key(@handle, generate-id())[position() amp;<= $maximum]" />
Однако это приведет к обнаружению элементов, между которыми может быть недопустимый элемент. Чтобы остановить это, я сначала вычислил положение текущего узла…
<xsl:variable name="position" select="count(preceding-sibling::node)"/>
Затем я мог бы проверить, равна ли позиция узла в ключе позиции относительно текущего узла, и в этом случае перед ними нет недопустимых элементов.
<xsl:variable
name="followingNodes"
select="key(@handle, generate-id())[position() amp;<= $maximum][count(preceding-sibling::node) - $position amp;<= position()]"/>
Вот полный XSLT…
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:key name="left"
match="node[@handle='right' or @handle='top-right' or @handle='bottom-right']"
use="generate-id(preceding-sibling::node[@handle='left'][1])"/>
<xsl:key name="right"
match="node[@handle='left' or @handle='top-left' or @handle='bottom-left']"
use="generate-id(preceding-sibling::node[@handle='right'][1])"/>
<xsl:key name="top-left"
match="node[@handle='bottom-right' or @handle='top-right' or @handle='bottom-right' or @handle='bottom']"
use="generate-id(preceding-sibling::node[@handle='top-left'][1])"/>
<xsl:template match="nodes">
<xsl:apply-templates select="node[1]" mode="first"/>
</xsl:template>
<xsl:template match="node[@handle='full']" mode="first">
<group>
<xsl:copy-of select="."/>
</group>
<xsl:apply-templates select="following-sibling::node[1]" mode="first"/>
</xsl:template>
<xsl:template match="node" mode="first">
<xsl:variable name="maximum">
<xsl:choose>
<xsl:when test="@handle='top-left'">3</xsl:when>
<xsl:otherwise>2</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="position" select="count(preceding-sibling::node)"/>
<xsl:variable name="followingNodes" select="key(@handle, generate-id())[position() amp;<= $maximum][count(preceding-sibling::node) - $position amp;<= position()]"/>
<group>
<xsl:copy-of select="."/>
<xsl:apply-templates select="$followingNodes"/>
</group>
<xsl:apply-templates select="following-sibling::node[count($followingNodes) 1]" mode="first"/>
</xsl:template>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Когда это применяется к следующему входному XML…
<nodes>
<node handle="full"/>
<node handle="full"/>
<node handle="left"/>
<node handle="right"/>
<node handle="top-left"/>
<node handle="top-right"/>
<node handle="bottom"/>
<node handle="full"/>
<node handle="full"/>
</nodes>
Результат выглядит следующим образом:
<group>
<node handle="full"/>
</group>
<group>
<node handle="full"/>
</group>
<group>
<node handle="left"/>
<node handle="right"/>
</group>
<group>
<node handle="top-left"/>
<node handle="top-right"/>
<node handle="bottom"/>
</group>
<group>
<node handle="full"/>
</group>
<group>
<node handle="full"/>
</group>
Комментарии:
1. Это выглядит действительно умно! Я нашел другое решение, которое выглядит менее экономичным и не использует ключи. Я добавляю это к своему собственному вопросу. Мне было бы любопытно узнать, что вы об этом думаете.
2. Требование «min» бесполезно. Я думал, что мне это нужно, чтобы установить диапазон для рекурсии. Но, очевидно, мы оба справились без этого.