Какое-то древнее дерьмо

Живёт тупо потому, что за хостинг уплачено на годы вперед

Еще один пост про группировку в XSLT 1.0

19 ноября 2010 · Комментариев: 4 · XSLT/XPath, Рецептарий

В этом посте мы рассмотрим, сюрприз, группировку посредством XSLT 1.0. Тема уже обжевана изрядно, но не лишать же мне вас удовольствия ознакомиться с ней и в этом уютном бложике.

Допустим, с сервера пришел совершенно бесталанный XML.

<marks>
    <mark>Buick</mark>
    <mark>Citroen</mark>
    <mark>Daewoo</mark>	
    <mark>Chery</mark>
    <mark>Audi</mark>
    <mark>Chevrolet</mark>
    <mark>Alfa Romeo</mark>
    <mark>BMW</mark>
    <mark>Cadillac</mark>
    <mark>Daihatsu</mark>
    <mark>Aston Martin</mark>
    <mark>Dodge</mark>
    <mark>Bentley</mark>
    <mark>Chrysler</mark>
</marks>

Мы хотим из него получить алфавитный список, группированный по первой букве и отсортированный в нужном порядке:

<alphabet-listing>
    <letter name="A">
        <mark>Alfa Romeo</mark>
        <mark>Aston Martin</mark>
        <mark>Audi</mark>
    </letter>
    <letter name="B">
        <mark>Bentley</mark>
        <mark>BMW</mark>
        <mark>Buick</mark>
    </letter>
    <letter name="C">
        <mark>Cadillac</mark>
        <mark>Chery</mark>
        <mark>Chevrolet</mark>
        <mark>Chrysler</mark>
        <mark>Citroen</mark>
    </letter>
    <letter name="D">
        <mark>Daewoo</mark>
        <mark>Daihatsu</mark>
        <mark>Dodge</mark>
    </letter>
</alphabet-listing>

Для решения задачи нам потребуется метод, броско именуемый организованной преступной группировкой Мюнха.

Основан он на том, что если функции generate-id() передать некую выборку узлов, то она вернет id первого из них согласно порядку следования в дереве. А для каждого конкретного узла id уникален, то есть у ноды будет один и тот же id вне зависимости от того, где функция была к этой ноде применена. Конкретнее, если мы попросим id от ноды <mark>Buick</mark> в двух разных шаблонах, то полученный идентификатор совпадет.

<xsl:template match="marks">
    <id-comparison>
        <buick-id><xsl:value-of select="generate-id(mark[1])"/></buick-id>
        <xsl:apply-templates select="mark[text() = 'Buick']"/>
    </id-comparison>
</xsl:template>	

<xsl:template match="mark">
    <this-id><xsl:value-of select="generate-id()"/></this-id>
</xsl:template>
<id-comparison>
    <buick-id>idmarkx2x3</buick-id>
    <this-id>idmarkx2x3</this-id>
</id-comparison>

С этим разобрались. Вернемся к нашим баранам.

Что нам нужно для очеловечивания исходного XML? Во-первых, нам потребуется задать ключ.

<xsl:key name="marks-by-letter" match="marks/mark" use="substring(.,1,1)"/>

В данном случае ключ смотрит на узлы mark, используя первую букву от их значения.

Создадим xsl, который выбирает первый из множества элементов, соответствующих этому ключу.

...
<xsl:template match="marks">
    <alphabet-listing>
        <xsl:apply-templates select="mark[generate-id(.) = generate-id(key('marks-by-letter',substring(.,1,1)))]"/>
    </alphabet-listing>
</xsl:template>	
	
<xsl:template match="mark">
    <letter name="{substring(.,1,1)}">
        <xsl:copy-of select="."/>
    </letter>
</xsl:template>	

Второй шаблон описательный и не заслуживает рассмотрения.

В первом же шаблоне мы берем первую букву значения текущего (поскольку дело происходит в предикате, то мы находимся в контексте mark) узла и смотрим, первая ли это нода с такой первой буквой в нашем “массиве” узлов. То есть во второй шаблон попадет первый узел, начинающийся с “A”, первый узел, начинающийся с “B”, и так далее.

<alphabet-listing>
    <letter name="B">
        <mark>Buick</mark>
    </letter>
    <letter name="C">
        <mark>Citroen</mark>
    </letter>
    <letter name="D">
        <mark>Daewoo</mark>
    </letter>
    <letter name="A">
        <mark>Audi</mark>
    </letter>
</alphabet-listing>

Как видите, первым в исходном XML идет Buick, соответственно порядок следования нас не устраивает как несоотвествующий алфавиту.

Решается это настолько просто, что аж противно. Нужен нам для этого только первый шаблон. Мы используем элемент xsl:sort в совершенно голом виде, пользуясь тем фактом, что дефолтным типом данных для него является строка, а порядком сортировки — возрастание.

...
<xsl:template match="marks">
    <alphabet-listing>
        <xsl:apply-templates select="mark[generate-id(.) = generate-id(key('marks-by-letter',substring(.,1,1)))]">
            <xsl:sort/> <!-- добавлено -->
        </xsl:apply-templates>
    </alphabet-listing>
</xsl:template>	
...
<alphabet-listing>
    <letter name="A">
        <mark>Audi</mark>
    </letter>
    <letter name="B">
        <mark>Buick</mark>
    </letter>
    <letter name="C">
        <mark>Citroen</mark>
    </letter>
    <letter name="D">
        <mark>Daewoo</mark>
    </letter>
</alphabet-listing>

Дальше нам остается только взять во втором шаблоне по ключу все ноды, значения которых начинаются с соответствующей буквы. Не забываем при этом про алфавитный порядок, а также создаем шаблон, пробрасывающий саму марку.

Окончательный код:

<xsl:key name="marks-by-letter" match="marks/mark" use="substring(.,1,1)"/>

<xsl:template match="marks">
    <alphabet-listing>
        <xsl:apply-templates select="mark[generate-id(.) = generate-id(key('marks-by-letter',substring(.,1,1)))]">
            <xsl:sort/>
        </xsl:apply-templates>
    </alphabet-listing>
</xsl:template>	
	
<xsl:template match="mark">
    <letter name="{substring(.,1,1)}">
        <xsl:apply-templates select="key('marks-by-letter',substring(.,1,1))" mode="second"> <!-- изменено -->
            <xsl:sort/>
        </xsl:apply-templates>
    </letter>
</xsl:template>	

<xsl:template match="mark" mode="second"> <!-- добавлено -->
    <xsl:copy-of select="."/>
</xsl:template>

и результат:

<alphabet-listing>
    <letter name="A">
        <mark>Alfa Romeo</mark>
        <mark>Aston Martin</mark>
        <mark>Audi</mark>
    </letter>
    <letter name="B">
        <mark>Bentley</mark>
        <mark>BMW</mark>
        <mark>Buick</mark>
    </letter>
    <letter name="C">
        <mark>Cadillac</mark>
        <mark>Chery</mark>
        <mark>Chevrolet</mark>
        <mark>Chrysler</mark>
        <mark>Citroen</mark>
    </letter>
    <letter name="D">
        <mark>Daewoo</mark>
        <mark>Daihatsu</mark>
        <mark>Dodge</mark>
    </letter>
</alphabet-listing>

Засим на сегодня всё.

Добавлю только, что в XSLT 2.0 есть ништяк xsl:for-each-group, который, при наличии такой роскоши как вторая версия, следует незамедлительно использовать.

P.S. Кстати, самая веская причина для подобных постов — вспомнить и не забыть разные интересные штуки, которые ты когда-либо делал. Даже если про них написано уже миллион раз.

Теги:

Комментариев: 4 ↓

  • Сергей

    Сурово. Только ХЗ на практике такое часто ли придется применять. Имхо, правильнее на сервере генерировать более удобный XML

  • Flack

    Капитаним, шеф.

  • Александр

    Добрый день. Могли бы Вы мне помочь. Ну никак не удается вывести элементы без повторения для своего xml.
    Сам xml. Нужно вывести список валют без повторения для каждого региона.
    Заранее спасибо, очень поможете!

  • Flack

    Александр, конкретизируйте. Выложите куда-нибудь на pastebin пример входного XML и желаемый результат.

Оставить комментарий