Главная » XSLT » Создание обобщенных функций агрегирования элементов

0

Задача

Требуется создать повторно используемые шаблоны для выполнения широ­кого спектра операций агрегирования набора узлов.

Решение

В обобщенном расширяемом решении применяется метод пометки узлов, описанный во введении к этой главе.

<xsl:stylesheet version="1.0"

xmlns:xsl="http://www.w3.org/1999/XSL/Transform"

xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic"

xmlns:aggr="http://www.ora.com/XSLTCookbook/namespaces/aggregate"

extension-element-prefixes="generic">

<xsl:variable name="generic:public-generics"

select="document(”)/*/generic:*"/>

<xsl:variable name="generic:generics" select="$generic:public-generics"/>

<!– Примитивные обобщенные функции от x –>

<generic:func name="identity"/>

<xsl:template match="generic:func[@name=’identity’]"> <xsl:param name="x"/> <xsl:value-of select="$x"/> </xsl:template>

<generic:func name="square"/>

<xsl:template match="generic:func[@name=’square’]"> <xsl:param name="x"/> <xsl:value-of select="$x * $x"/> </xsl:template>

<generic:func name="cube"/>

<xsl:template match="generic:func[@name=’cube’]"> <xsl:param name="x"/>

<xsl:value-of select="$x * $x * $x"/> </xsl:template>

<generic:func name="incr" param1="1"/>

<xsl:template match="generic:func[@name=’incr’]"> <xsl:param name="x"/>

<xsl:param name="param1" select="@param1"/> <xsl:value-of select="$x + $param1"/> </xsl:template>

<!– Примитивные обобщенные агрегаторы –>

<generic:aggr-func name="sum" identity="0"/> <xsl:template match="generic:aggr-func[@name=’sum’]"> <xsl:param name="x"/> <xsl:param name="accum"/> <xsl:value-of select="$x + $accum"/> </xsl:template>

<generic:aggr-func name="product" identity="1"/> <xsl:template match="generic:aggr-func[@name=’product’]"> <xsl:param name="x"/> <xsl:param name="accum"/> <xsl:value-of select="$x * $accum"/> </xsl:template>

<!– Обобщенный шаблон агрегирования –> <xsl:template name="generic:aggregation"> <xsl:param name="nodes"/>

<xsl:param name="aggr-func" select=" ‘sum’ "/> <xsl:param name="func" select=" ‘identity’ "/> <xsl:param name="func-param1"

select="$generic:generics[self::generic:func and

@name = $func]/@param1"/>

<xsl:param name="i" select="1"/> <xsl:param name="accum"

select="$generic:generics[self::generic:aggr-func and

@name = $aggr-func]/@identity"/>

<xsl:choose> <xsl:when test="$nodes">

<!– Вычислить func($x) –> <xsl:variable name="f-of-x"> <xsl:apply-templates

select="$generic:generics[self::generic:func and

@name = $func]"> <xsl:with-param name="x" select="$nodes[1]"/> <xsl:with-param name="i" select="$i"/>

<xsl:with-param name="param1" select="$func-param1"/> </xsl:apply-templates> </xsl:variable>

<!– Агрегировать текущее значение $f-of-x с $accum –> <xsl:variable name="temp"> <xsl:apply-templates

select="$generic:generics[self::generic:aggr-func and @name = $aggr-func]"> <xsl:with-param name="x" select="$f-of-x"/> <xsl:with-param name="accum" select="$accum"/> <xsl:with-param name="i" select="$i"/> </xsl:apply-templates> </xsl:variable>

<!– Хвостовая рекурсия для обработки остальных узлов

с использованием position( ) –> <xsl:call-template name="generic:aggregation">

<xsl:with-param name="nodes" select="$nodes[position( )!=1]"/> <xsl:with-param name="aggr-func" select="$aggr-func"/> <xsl:with-param name="func" select="$func"/> <xsl:with-param name="func-param1" select="$func-param1"/> <xsl:with-param name="i" select="$i + 1"/> <xsl:with-param name="accum" select="$temp"/> </xsl:call-template> </xsl:when> <xsl:otherwise>

<xsl:value-of select="$accum"/> </xsl:otherwise> </xsl:choose> </xsl:template>

</xsl:stylesheet>

Обобщенный код состоит из трех основных частей.

Первая часть – это помеченные обобщенные функции от одной переменной x. Они позволяют выполнять операции агрегирования, применяя функции к узлам из входного набора. Простейшей из них является функция identity, применя­емая, когда нужно агрегировать сами значения входных узлов. Также определе­ны функции для возведения в квадрат, в куб и для увеличения на заданную ве­личину.

Вторая часть – помеченные обобщенные функции-агрегаторы. Мы реализова­ли два типичных агрегатора: сумма и произведение. Импортирующая таблица может добавить и другие.

Третья часть – обобщенный алгоритм агрегирования. В качестве параметров он принимает набор агрегируемых узлов, имя функции-агрегатора (по умолчанию sum) и имя функции, применяемой к одному узлу (по умолчанию identity). Параметр $i нужен для отслеживания позиции текущего обрабатываемого узла; он передается и агрегатору, и функции обработки узла на случай, если вдруг пона­добится. В параметре $accum хранится текущее значение агрегата. Отметим, что по умолчанию этот параметр инициализируется значением атрибута @identity, который хранится в метке функции-агрегатора. Такая инициализация демонст­рирует присущую обобщенному подходу возможность ассоциировать метадан­ные с метками. Это отдаленно напоминает классы-характеристики, часто приме­няемые при обобщенном программировании на C++.

Чтобы разобраться в этом коде, мы начнем с простого приложения, которое пользуется готовыми средствами агрегирования и расширяет их. См. пример 16.1.

Пример 16.1. Использование и расширение обобщенного агрегирования

<xsl:stylesheet version="1.0"

xmlns:xsl="http://www.w3.org/1999/XSL/Transform"

xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic" xmlns:aggr="http://www.ora.com/XSLTCookbook/namespaces/aggregate" extension-element-prefixes="generic aggr">

<xsl:import href="aggregation.xslt"/>

<xsl:output method="xml" indent="yes"/>

<!– Расширить набор обобщенных функций –>

<xsl:variable name="generic:generics" select="$generic:public-generics | document(”)/*/generic:*"/>

<!– Добавить обобщенную функцию для вычисления обратного значения –> <generic:func name="reciprocal"/>

<xsl:template match="generic:func[@name=’reciprocal’]"> <xsl:param name="x"/> <xsl:value-of select="1 div $x"/> </xsl:template>

<!– Добавить обобщенный агрегатор для вычисления минимального –Ю <!– и максимального узла в наборе –> <generic:aggr-func name="min" identity=""/> <xsl:template match="generic:aggr-func[@name=’min’]"> <xsl:param name="x"/> <xsl:param name="accum"/> <xsl:choose>

<xsl:when test="$accum = @identity or $accum >= $x">

<xsl:value-of select="$x"/> </xsl:when> <xsl:otherwise>

<xsl:value-of select="$accum"/> </xsl:otherwise> </xsl:choose> </xsl:template>

<generic:aggr-func name="max" identity=""/> <xsl:template match="generic:aggr-func[@name=’max’]"> <xsl:param name="x"/> <xsl:param name="accum"/> <xsl:choose>

<xsl:when test="$accum = @identity or $accum &lt; $x">

<xsl:value-of select="$x"/> </xsl:when> <xsl:otherwise>

<xsl:value-of select="$accum"/> </xsl:otherwise> </xsl:choose> </xsl:template>

<!– Тестируем функциональность агрегирования –> <xsl:template match="numbers">

<results>

<!– Сумма чисел –> <sum>

<xsl:call-template name="generic:aggregation">

<xsl:with-param name="nodes" select="number"/> </xsl:call-template> </sum>

<!– Сумма квадратов –> <sumSq>

<xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="func" select=" ‘square’ "/> </xsl:call-template> </sumSq>

<!– Произведение обратных значений –> <prodRecip>

<xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="aggr-func" select=" ‘product’ "/> <xsl:with-param name="func" select=" ‘reciprocal’ "/> </xsl:call-template> </prodRecip>

<!– Максимум –> <max>

<xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="aggr-func" select=" ‘max’ "/> </xsl:call-template> </max>

<!– Минимум –> <min>

<xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="aggr-func" select=" ‘min’ "/> </xsl:call-template> </min>

</results>

</xsl:template>

</xsl:stylesheet>

В примере 16.1 показано, как добавлять новые функции, применяемые к эле­менту, и функции-агрегаторы к тем, что уже включены в таблицу aggregation.xslt. Быть может, вы не думали, что минимум и максимум можно вычислить с помо­щью этого обобщенного кода, однако, как видите, это совсем просто. Протестируем код на следующих входных данных:

<numbers>

<number>1</number> <number>2</number> <number>3</number> </numbers>

В результате получаем:

<?xml version="1.0" encoding="utf-8"?> <results>

<sum>6</sum>

<sumSq>14</sumSq>

<prodRecip>0.1666666666666666 6</prodRecip> <max>3</max> <min>1</min> </results>

Обсуждение

Приведенный в разделе «Решении» пример – это лишь верхушка айсберга. С помощью обобщенного агрегирования можно делать и куда более интересные вещи. Например, нигде не сказано, что агрегировать можно только числа. Ниже показано, что этот обобщенный код применим также и к строкам:

<strings>

<string>camel</string> <string>through</string> <string>the</string> <string>eye</string> <string>of</string> <string>needle</string> </strings>

<!DOCTYPE stylesheet [

<!ENTITY % standard SYSTEM "../strings/standard.ent"> %standard;

]>

<xsl:stylesheet version="1.0"

xmlns:xsl="http://www.w3.org/1999/XSL/Transform"

xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic" extension-element-prefixes="generic">

<xsl:import href="aggregation.xslt"/>

<xsl:output method="xml" indent="yes"/>

<!– Расширить набор обобщенных функций –>

<xsl:variable name="generic:generics" select="$generic:public-generics | document(”)/*/generic:*"/>

<!– Добавить обобщенную функцию для преобразования первого символа $x в верхний регистр –> <generic:func name="upperFirst"/>

<xsl:template match="generic:func[@name=’upperFirst’]"> <xsl:param name="x"/> <!— О том, что такое LOWER_TO_UPPER, см. рецепт 2.8 —>

<xsl:variable name="upper"

select="translate(substring($x,1,1),&LOWER_TO_UPPER;)"/> <xsl:value-of select="concat($upper, substring($x,2))"/> </xsl:template>

<!– Добавить обощенный агрегатор для конкатенации строк –> <generic:aggr-func name="concat" identity=""/> <xsl:template match="generic:aggr-func[@name=’concat’]"> <xsl:param name="x"/> <xsl:param name="accum"/>

<xsl:value-of select="concat($accum,$x)"/> </xsl:template>

<!– Тестируем функциональность агрегирования –> <xsl:template match="strings">

<results>

<camelCase>

<xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="string"/> <xsl:with-param name="aggr-func" select=" ‘concat’ "/> <xsl:with-param name="func" select=" ‘upperFirst’ "/> </xsl:call-template> </camelCase>

</results>

</xsl:template>

</xsl:stylesheet>

<results>

<camelCase>CamelThroughTheEyeOfNeedle</camelCase> </results>

С помощью агрегирования можно вычислить также и статистические функции: среднее и дисперсию. Нам понадобится параметр $i. Для вычисления дисперсии придется проявить изобретательность; в параметре $accum необходимо хранить три величины: сумму, сумму квадратов и текущее значение агрегата. Сделать это позволит элемент с атрибутом. Единственный недостаток этого решения – необхо­димость использовать функцию node-set() в XSLT 1.0:

<?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0"

xmlns:xsl="http://www.w3.org/1999/XSL/Transform"

xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/generic" xmlns:exslt="http://exslt.org/common" extension-element-prefixes="generic exslt">

<xsl:import href="aggregation.xslt"/>

<xsl:output method="xml" indent="yes"/>

<!– Расширить набор обобщенных функций –>

<xsl:variable name="generic:generics" select="$generic:public-generics | document(”)/*/generic:*"/>

<!— Добавить обощенный агрегатор для вычисления дисперсии —> <generic:aggr-func name="avg" identity="0"/> <xsl:template match="generic:aggr-func[@name=’avg’]"> <xsl:param name="x"/> <xsl:param name="accum"/> <xsl:param name="i"/>

<xsl:value-of select="(($i – 1) * $accum + $x) div $i"/> </xsl:template>

<generic:aggr-func name="variance" identity=""/> <xsl:template match="generic:aggr-func[@name=’variance’]"> <xsl:param name="x"/> <xsl:param name="accum"/> <xsl:param name="i"/>

<xsl:choose>

<xsl:when test="$accum = @identity">

<!– Инициализировать, сумму, сумму квадратов

и дисперсию. Дисперсия одного числа равна 0. –> <variance sum="{$x}" sumSq="{$x * $x}">0</variance> </xsl:when> <xsl:otherwise>

<!– Используем node-set для преобразования $accum

в набор узлов, содержащий элемент, представляющий дисперсию. –> <xsl:variable name="accumElem" select="exslt:node-set($accum)/*"/> <!– Добавить к сумме значение $x –>

<xsl:variable name="sum" select="$accumElem/@sum + $x"/> <!– Добавить к сумме квадрат значения $x –>

<xsl:variable name="sumSq" select="$accumElem/@sumSq + $x * $x"/> <!– Возвращаем элемент, у которого есть атрибуты для представления суммы и суммы квадратов, а текущая величина

дисперсии хранится в виде значения элемента. –> <variance sum="{$sum}" sumSq="{$sumSq}"> <xsl:value-of

select="($sumSq – ($sum * $sum) div $i) div ($i – 1)’ </variance> </xsl:otherwise> /xsl:choose>

</xsl:template>

<xsl:template match="numbers"> <results>

<!– Среднее –> <avg>

<xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="aggr-func" select=" ‘avg’ "/> </xsl:call-template> </avg>

<!– Среднее квадратов –> <avgSq>

<xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="func" select=" ‘square’ "/> <xsl:with-param name="aggr-func" select=" ‘avg’ "/> </xsl:call-template> </avgSq>

<!– Дисперсия –> <variance>

<xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="number"/> <xsl:with-param name="aggr-func" select=" ‘variance’ "/> </xsl:call-template> </variance>

</results>

</xsl:template>

В следующем примере показано, как применить агрегирование для вычисле­ния суммы полиморфных функций:

<?xml version="1.0" encoding="UTF-8"?>

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/ Transform" xmlns:generic="http://www.ora.com/XSLTCookbook/namespaces/ generic" xmlns:aggr="http://www.ora.com/XSLTCookbook/namespaces/ aggregate" xmlns:exslt="http://exslt.org/common" extension-element- prefixes="generic aggr">

<xsl:import href="aggregation.xslt"/>

<xsl:output method="xml" indent="yes"/>

<!– Расширить набор обобщенных функций –> <xsl:variable name="generic:generics"

select="$generic:public-generics | document(”)/*/generic:*"/>

<!– Расширить примитивы для вычисления комиссии –> <generic:func name="commission"/>

<xsl:template match="generic:func[@name=’commission’]"> <xsl:param name="x"/>

<!— отложить фактическое вычисление полиморфного шаблона с помощью —> <!– режима commission –>

<xsl:apply-templates select="$x" mode="commission"/> </xsl:template>

<!– По умолчанию продавец получает 2% комиссионных без основного оклада –>

<xsl:template match="salesperson" mode="commission">

<xsl:value-of select="0.02 * sum(product/@totalSales)"/> </xsl:template>

<!— продавцы ранга > 4 получают оклад $10000.00 + 0.5% комиссионных –> <xsl:template match="salesperson[@seniority > 4]" mode="commission" priority="1">

<xsl:value-of select="10000.00 + 0.05 * sum(product/@totalSales)"/> </xsl:template>

<!– продавцы ранга > 8 получают оклад, равный (ранг * $10000.00) + 0.8% комиссионных –>

<xsl:template match="salesperson[@seniority > 8]" mode="commission" priority="2">

<xsl:value-of select="@seniority * 2000.00 + 0.08 * sum(product/@totalSales)"/>

</xsl:template>

<xsl:template match="salesBySalesperson"> <results> <result>

<xsl:text>Oбщая сумма комиссионных = </xsl:text> <xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="*"/> <xsl:with-param name="aggr-func" select=" ‘sum’ "/> <xsl:with-param name="func" select=" ‘commission’ "/> </xsl:call-template> </result> <result>

<xsl:text>Mинимальная комиссия = </xsl:text> <xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="*"/> <xsl:with-param name="aggr-func" select=" ‘min’ "/> <xsl:with-param name="func" select=" ‘commission’ "/> </xsl:call-template> </result> <result>

<xsl:text>Mаксимальная комиссия = </xsl:text> <xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="*"/> <xsl:with-param name="aggr-func" select=" ‘max’ "/> <xsl:with-param name="func" select=" ‘commission’ "/> </xsl:call-template> </result> <result>

<xsl:text>Cредняя комиссия = </xsl:text> <xsl:call-template name="generic:aggregation"> <xsl:with-param name="nodes" select="*"/> <xsl:with-param name="aggr-func" select=" ‘avg’ "/> <xsl:with-param name="func" select=" ‘commission’ "/> </xsl:call-template> </result> <result>

<xsl:text>Cредняя сумма продаж = </xsl:text> <xsl:call-template name="generic:aggregation">

<xsl:with-param name="nodes" select="*/product/@totalSales"/> <xsl:with-param name="aggr-func" select=" ‘avg’ "/> </xsl:call-template> </result> <result>

<xsl:text>Mинимальная сумма продаж = </xsl:text> <xsl:call-template name="generic:aggregation">

<xsl:with-param name="nodes" select="*/product/@totalSales"/> <xsl:with-param name="aggr-func" select=" ‘min’ "/> </xsl:call-template> </result> <result>

<xsl:text>Mаксимальная сумма продаж = </xsl:text> <xsl:call-template name="generic:aggregation">

<xsl:with-param name="nodes" select="*/product/@totalSales"/> <xsl:with-param name="aggr-func" select=" ‘max’ "/> </xsl:call-template> </result> </results> </xsl:template>

</xsl:stylesheet>

Применив эту таблицу стилей к файлу salesBySalesperson.xml (см. главу 4), получим:

<results xmlns:exslt="http://exslt.org/common">

<result>Общая сумма комиссионных = 471315</result> <result>Mинимальная комиссия = 19 60 0</result> <result>Mаксимальная комиссия = 3 64 4 4 0</result> <result>Средняя комиссия = 117 82 8.7 5</result> <result>Средняя сумма продаж = 5 8 4 6 3 6.3 63 63 6 3 63 6</result> <result>Mинимальная сумма продаж = 55 0 0.0 0</result> <result>Mаксимальная сумма продаж = 2 92 0 0 0 0.0 0</result>

</results>

В этом разделе показано, что многие рецепты, которые мы реализовали по от­дельности в главе 3, можно легко выразить в терминах единого обобщенного шаб­лона. На самом деле, этот шаблон можно применить к вычислению бесконечного разнообразия агрегирующих функций над наборами узлов. К сожалению, такая гибкость и общность даются не бесплатно. Обобщенная реализация как правило работает процентов на 40 медленнее написанного вручную решения. Если быст­родействие – важнейший фактор, то стоит прибегнуть к оптимизированному руч­ному коду. Если же нужно быстро реализовать сложный XSLT-сценарий, в кото­ром выполняется много операций агрегирования, то применение обобщенного подхода может заметно ускорить разработку1. Наибольший эффект вы сможете получить, заранее создав широкий набор готовых обобщенных элементов и функ- ций-агрегаторов. В полный вариант таблицы aggregation.xslt, который вы найдете на сопроводительном сайте книги (http://www.oreilly.com/catalog/xsltckbk), уже включены все функции из этого примера (и ряд других).

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

<xsl:template name="generic:reverse-aggregation"> <xsl:param name="nodes"/>

<xsl:param name="aggr-func" select=" ‘sum’ "/> <xsl:param name="func" select=" ‘identity’ "/>

<xsl:param name="func-param1" select="$generic:generics[self::generic:func

and @name = $func]/@param1"/> <xsl:param name="i" select="1"/>

<xsl:param name="accum" select="$generic:generics[self::generic:aggr-func and @name = $aggr-func]/@identity"/>

<xsl:choose>

<xsl:when test="$nodes">

<!– Вычислить func($x) –> <xsl:variable name="f-of-x"> <xsl:apply-templates select=

"$generic:generics[self::generic:func and @name = $func]"> <xsl:with-param name="x" select="$nodes[last( )]"/>

<xsl:with-param name="i" select="$i"/> </xsl:apply-templates> </xsl:variable>

<!— Агрегировать текущее значение $f-of-x с $accum —> <xsl:variable name="temp"> <xsl:apply-templates

select="$generic:generics[self::generic:aggr-func and @name = $aggr-func]"> <xsl:with-param name="x" select="$f-of-x"/> <xsl:with-param name="accum" select="$accum"/> <xsl:with-param name="i" select="$i"/> </xsl:apply-templates> </xsl:variable>

<xsl:call-template name="generic:reverse-aggregation"> <xsl:with-param name="nodes"

select="$nodes[position( )!=last( )]"/>

<xsl:with-param name="aggr-func" select="$aggr-func"/>

<xsl:with-param name="func" select="$func"/> <xsl:with-param name="func-param1" select="$func-param1"/> <xsl:with-param name="i" select="$i + 1"/> <xsl:with-param name="accum" select="$temp"/> </xsl:call-template> </xsl:when> <xsl:otherwise>

<xsl:value-of select="$accum"/> </xsl:otherwise> </xsl:choose> </xsl:template>

См. также

В библиотеке FXSL (раздел «См. также» во введении к этой главе) имеются фун­кции fold и foldr, аналогичные generic:aggregation и generic:reverse- aggregation соответственно.

Мангано Сэл  XSLT. Сборник рецептов. – М.: ДМК Пресс, СПБ.: БХВ-Петербург, 2008. – 864 с.: ил.

По теме:

  • Комментарии