Типовая задача XSLT- группировка элементов по одному из параметров. Например, есть список сотрудников с указанием подразделений, в которых они работают.
<items>
<item dep="отдел 301" > Иванов И.И.</item>
<item dep="отдел 302" > Петрова П.П.</item>
<item dep="АХО" > Тынгылчав И.И.</item>
<item dep="ВОХР" > Рабинович П.Е.</item>
<item dep="отдел 301" > Вышку Н.Н.</item>
<item dep="ВОХР" > Штырц-Кобер С.С.</item>
<item dep="АХО" > Хидияттулин Г.Г.</item>
<item dep="отдел 302" > Непийпиво С.С.</item>
<item dep="ВОХР" > Говорухо-Отрок С.П.</item>
</items> |
Необходимо сгруппировать всех сотрудников по подразделениям в следующем виде:
<h1>отдел 301</h1>
<ul>
<li> Иванов И.И.</li>
<li> Вышку Н.Н.</li>
</ul>
<h1> отдел 302</h1>
<ul>
<li> Петрова П.П.</li>
<li> Непийпиво С.С.</li>
</ul> |
Ниже рассмотрим три варианта решения задачи. Исходники можно скачать из архива.
Пример I. С вложенными циклами
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output indent="yes" />
<xsl:template match="*">
<-- ПЕРВЫЙ ЦИКЛ - отбор подразделений -- >
<xsl:for-each select="item[not(@dep=preceding-sibling::item/@dep)]" >
<xsl:variable name="dep" select="@dep" />
<h1> <xsl:value-of select="$dep"/> </h1>
<ul>
<-- ВТОРОЙ ЦИКЛ - отбор сотрудников -- >
<xsl:for-each select="../item[@dep = $dep]">
<li> <xsl:value-of select="."/> </li>
</xsl:for-each>
<-- ВТОРОЙ ЦИКЛ - отбор сотрудников -- >
</ul>
</xsl:for-each>
<-- / ПЕРВЫЙ ЦИКЛ - отбор подразделений -- >
</xsl:template>
</xsl:stylesheet>
|
Этот пример вызовет бурное негодование XSLT-пуристов, потому что циклы xsl:for-each нарушают стройную концепцию XSLT, но об этом поговорим отдельно. В остальном пример лаконичен и прост для понимания тех, кто привык к обычным языкам программирования.
Первый цикл (см. комментарии в листинге) формирует список отделов из общего списка сотрудников. Если бы у каждого сотрудника был параметр, указывающий на должность, можно было бы отобрать по этому параметру начальников, а по параметру @dep отобрать название подразделений. Но такого параметра нет. Поэтому для составления списка отделов используется фильтр, отбирающий предшествующих в списке соседей (ось preceding-sibling):
item[not(@dep=preceding-sibling::item/@dep)]
Этот фильтр определяет тех сотрудников, для которых нет предшествующих в списке (preceding-sibling::item) коллег из такого же отдела, т.е. не выполняется условие: @dep=preceding-sibling::item/@dep.
Вложенный цикл xsl:for-each повторно проходит по всему списку сотрудников, и выводит только тех сотрудников, которые принадлежат отделу отобранному в первом цикле
Пример II. С шаблонными правилами и использованием ключей xsl:key
Ключи ( xsl:key) позволяют работать с документами, содержащими перекрестные ссылки и аналогичны ключам БД. Так же как и в БД использование xsl:key ускоряет выборку узлов дерева за счет построения XSLT-парсерами внутренних индексов.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes" />
<!-- формирование ключа привязки сотрудника
к подразделению
-->
<xsl:key name="dep" match="item" use="@dep"/>
<xsl:template match="*">
<xsl:apply-templates />
</xsl:template>
<xsl:template match="item">
<!-- ЦИКЛ 1 (отбор подразделений) -->
<xsl:if test="not(@dep=preceding-sibling::item/@dep)">
<h1> <xsl:value-of select="@dep"/> </h1>
<ul>
<!-- ЦИКЛ 2 (отбор сотрудников ) по ключу
-->
<xsl:apply-templates mode="second" select="key('dep',@dep)"> </xsl:apply-templates>
</ul>
</xsl:if>
<!-- /ЦИКЛ 1 -->
</xsl:template>
<xsl:template match="item" mode="second">
<li> <xsl:value-of select="."/> </li>
</xsl:template>
</xsl:stylesheet>
|
Для того, чтобы можно было отобрать сотрудников, привязанных к одному подразделению, в этом примере используется xsl:ключ
<xsl:key name="dep" match="item" use="@dep"/>
который определяется в самом начале XSLT. Выборку всех сотрудников отдела с названием "ОТДЕЛ" можно составить так:
<xsl:apply-templates select="key('dep','ОТДЕЛ')" /> Но прежде чем применить эту выборку, необходимо определить список отделов. В примере это делается в первом шаблонном правиле, которое мы применяем ко всем сотрудниками <xsl:template match="item"> .
Выборка осуществляется аналогично первому примеру через условный оператор:
<xsl:if test="not(@dep=preceding-sibling::item/@dep)" >
После того как список отделов для отбора в первом шаблонном правиле для item определен, остается применить шаблонное правило для обработки всех сотрудников из этого отдела, используя ранее определенный ключ:
<xsl:apply-templates mode="second" select="key('dep',@dep)" />Обратите внимание на mode="second". Мы создаем два шаблонных правила для обработки элемента списка сотрудников item. Первое правило безусловное <xsl:template match="item"> и работает по умолчанию Оно используется для составления списка отделов. Второе правило для обработки списков сотрудников вызывается с модой (условием) <xsl:template match="item" mode="second"> Такой подход вместо использования циклов (Пример I) считается более препочтительным, но принимать его за догму было бы ошибкой.
Пример III. C генерацией ключей по методу Мюнха
Этот метод назван по имени автора Steve Muench (Мюнх/Мюнч). Он лучше всего демонстрирует изощреннность (если не сказать извращенность) XSL-техник для решения элементарных задач.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:key name="dep" match="item" use="@dep"/>
<xsl:template match="*">
<xsl:apply-templates select="item[generate-id(.)=generate-id(key('dep',@dep))]"/>
</xsl:template>
<xsl:template match="item">
<h1> <xsl:value-of select="@dep"/> </h1>
<ul>
<xsl:apply-templates mode="next" select="key('dep',@dep)"> </xsl:apply-templates>
</ul>
</xsl:template>
<xsl:template match="item" mode="next">
<li> <xsl:value-of select="."/> </li>
</xsl:template>
</xsl:stylesheet>
|
Отличие этого примера от предыдущего в том, что первое шаблонное правило производит выборку отделов, (точнее сотрудников по одному на отдел) без использования циклов и условных операторов, которые должен презирать каждый адепт XSLT. Разберем подробно выражение:
<xsl:apply-templates
select="item[generate-id(.)=generate-id(key('dep',@dep))]"
/> (*) выражение generate-id(.) - формирует уникальный ID для текущего узла (сотрудника).
выражение key('dep','АХО') формирует множество узлов (сотрудников) из отдела с названием "АХО" (административно-хозяйственный отдел)
выражение generate-id(key('dep','ОТДЕЛ')) определяет уникальный ID множества. Уникальный ID множества соответствует ID первого элемента множества. Т.е. ID первого сотрудника из выбранного множества всех сотрудников, работающих в АХО
Таким образом полностью выражение (*) означает отбор тех сотрудников, кто стоит первым из своего отдела в общем списке, т.е. чей generate-id равен generate-id отдела.
Остальное уже было разобранов в примере II.
Любопытно, что чуществует еще один метод Мюнча (Reed and Muench), используемый в биологических исследованиях. Он применяется для определения летальной дозы препарата в биологических исследованиях.
PS
Все что написано в этом блоге относится исключительно к стандарту XSLT1.0, который не предусматривает вменяемых средств для группировки данных. В новом стандарте XSLT2 разработчики ввели дополнительные инструменты для сортировки и группировки данных, но пока стандарт XSLT2, принятый два года назад не успел получить широкой поддержки со стороны производителей софта.