Я преобразую XML в XML с помощью XSLT 1.0. Мне нужно сделать как минимум два прохода, чтобы я мог вычислить промежуточные итоги при первом проходе и использовать их для вывода итогов в XML - чтобы они появлялись перед промежуточными итогами в структуре XML.
У меня есть следующий входной XML:
<?xml version = "1.0" encoding = "UTF-8"?>
<invoice>
<orders>
<order id = "1">
<lineitem id = "1">
<item quantity = "2" price = "100"/>
<item quantity = "3" price = "50"/>
</lineitem>
</order>
<order id = "2">
<lineitem id = "1">
<item quantity = "5" price = "20"/>
<item quantity = "1" price = "100"/>
</lineitem>
</order>
</orders>
</invoice>
Что я хочу изменить, добавив промежуточные итоги в существующий элемент и добавив новый элемент статистики вверху:
<?xml version = "1.0" encoding = "UTF-8"?>
<invoice>
<statistics>
<totalPrice>550</totalPrice>
<totalQuantity>11</totalQuantity>
</statistics>
<orders>
<order id = "1">
<lineitem id = "1">
<item quantity = "2" price = "100" subtotal = "200"/>
<item quantity = "3" price = "50" subtotal = "150"/>
</lineitem>
</order>
<order id = "2">
<lineitem id = "1">
<item quantity = "5" price = "20" subtotal = "100"/>
<item quantity = "1" price = "100" subtotal = "100"/>
</lineitem>
</order>
</orders>
</invoice>
Однако у меня есть только половина решения: я могу добавить subtotals, но не знаю, как добавить элемент statistics.
<?xml version = "1.0" encoding = "UTF-8"?><invoice>
<orders totalPrice = "550.00">
<order id = "1">
<lineitem id = "1">
<item quantity = "2" price = "100" subtotal = "200.00"/>
<item quantity = "3" price = "50" subtotal = "150.00"/>
</lineitem>
</order>
<order id = "2">
<lineitem id = "1">
<item quantity = "5" price = "20" subtotal = "100.00"/>
<item quantity = "1" price = "100" subtotal = "100.00"/>
</lineitem>
</order>
</orders>
</invoice>
На данный момент это мой XSLT:
<xsl:stylesheet xmlns:xsl = "http://www.w3.org/1999/XSL/Transform" version = "1.0"
xmlns:exsl = "http://exslt.org/common"
extension-element-prefixes = "exsl">
<xsl:output method = "xml" indent = "yes"/>
<!-- Handling of order items -->
<xsl:template match = "item">
<xsl:variable name = "subtotal" select = "@price * @quantity" />
<item price = "{@price}" quantity = "{@quantity}" subtotal = "{format-number($subtotal,'0.00')}" />
</xsl:template>
<!-- Root template. Calculates total based on subtotals -->
<xsl:template match = "orders">
<xsl:variable name = "processedOrders">
<xsl:apply-templates/>
</xsl:variable>
<orders totalPrice = "{format-number(sum(exsl:node-set($processedOrders)//item/@subtotal),'0.00')}">
<xsl:copy-of select = "exsl:node-set($processedOrders)/*"/>
</orders>
</xsl:template>
<!-- Identity template -->
<xsl:template match = "@*|node()">
<xsl:copy>
<xsl:apply-templates select = "@*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Я думаю, мне нужно переместить processedOrders, чтобы сделать его глобальной переменной, которую я могу использовать в двух местах:
<xsl:template match = "orders">), в котором exsl:node-set используется для суммирования промежуточных итогов и получения totalPrice.statistics, который будет делать что-то подобное для вывода данных.Однако похоже, что способ создания переменной не работает, поскольку шаблон заказов не видит данные в переменной.
<xsl:stylesheet xmlns:xsl = "http://www.w3.org/1999/XSL/Transform" version = "1.0"
xmlns:exsl = "http://exslt.org/common"
extension-element-prefixes = "exsl">
<xsl:output method = "xml" indent = "yes"/>
<!-- ++++++++++++++++++++++++++++++++++++++++++++++
Trying to let this be referenced by other templates.
-->
<xsl:variable name = "processedOrders" select = "/invoice/orders"/>
<!-- Handling of order items -->
<xsl:template match = "item">
<xsl:variable name = "subtotal" select = "@price * @quantity" />
<item price = "{@price}" quantity = "{@quantity}" subtotal = "{format-number($subtotal,'0.00')}" />
</xsl:template>
<!-- Root template. Calculates total based on subtotals -->
<xsl:template match = "orders">
<orders totalPrice = "{format-number(sum(exsl:node-set($processedOrders)//item/@subtotal),'0.00')}">
<xsl:copy-of select = "exsl:node-set($processedOrders)/*"/>
</orders>
</xsl:template>
<!-- Identity template -->
<xsl:template match = "@*|node()">
<xsl:copy>
<xsl:apply-templates select = "@*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Это выводит:
<?xml version=\"1.0\" encoding=\"UTF-8\"?><invoice>
<orders totalPrice=\"0.00\">
<order id=\"1\">
<lineitem id=\"1\">
<item quantity=\"2\" price=\"100\"/>
<item quantity=\"3\" price=\"50\"/>
</lineitem>
</order>
<order id=\"2\">
<lineitem id=\"1\">
<item quantity=\"5\" price=\"20\"/>
<item quantity=\"1\" price=\"100\"/>
</lineitem>
</order>
</orders>
</invoice>
Обновление: пятница, 14 июня 2024 г., 13:36:37.
Используя ответ @Martin Honnen, я смог пройти остаток пути. Мартин показал мне, что мне не нужно хранить результаты первого прохода в глобальной переменной, а вместо этого отправлять результаты первого прохода на второй проход в том же шаблоне, и что второй проход можно отправить на корневой уровень. шаблон — второй проход снова проходит по всему документу.
Недостающим элементом было то, как добавить элемент statistics и не дублировать его, создавая один на первом проходе, а другой на втором проходе. Я использую режимы, чтобы справиться с этим. Ниже приведен мой новый XSLT с комментариями, которые помогают мне понять, что происходит.
<xsl:stylesheet xmlns:xsl = "http://www.w3.org/1999/XSL/Transform" version = "1.0"
xmlns:exsl = "http://exslt.org/common"
extension-element-prefixes = "exsl">
<xsl:output method = "xml" indent = "yes"/>
<!-- Two passes.
The template matching / is the starting point for the transformation.
It's doing two passes over your input by placing the result of the first
pass inside a variable and then reprocessing that result in a second pass.
IMPORTANT: we are using modes to discriminate between logic we want to run
on the second pass only.
- The first pass (first use of <xsl:apply-templates/>
within this template) has no mode. It will run all other matching templates
that have no mode.
- The second pass (second use of <xsl:apply-templates/>) runs in a mode
called "add-statistics". This will run all matching templates without a
mode plus any templates matching this mode. This is done to ensure that
the statistics element is only added once - in the second pass only, and
not duplicated within the first pass.
-->
<xsl:template match = "/">
<xsl:variable name = "first-transformation">
<xsl:apply-templates/>
</xsl:variable>
<xsl:apply-templates select = "exsl:node-set($first-transformation)/node()" mode = "add-statistics"/>
</xsl:template>
<!-- Write out the statistics element.
Will write out the statistics element with the total price and total
quantity, but *only* when invoked by a calling template whose mode is
"add-statistics". This template will be skipped by the first pass, because
it has no mode. The second pass has the "add-statistics" mode so the
element will be added then, ensuring we do not get a duplicate.
-->
<xsl:template match = "invoice" mode = "add-statistics">
<invoice>
<statistics>
<totalPrice>
<xsl:value-of select = "format-number(sum(//item/@subtotal),'0.00')"/>
</totalPrice>
<totalQuantity>
<xsl:value-of select = "sum(//item/@quantity)"/>
</totalQuantity>
</statistics>
<xsl:apply-templates/>
</invoice>
</xsl:template>
<!-- Handling of order items.
The template matching item multiplies the price and quantity attributes of
each item element to calculate a subtotal, then produces a new item element
with the price, quantity and calculated subtotal as attributes.
-->
<xsl:template match = "item">
<xsl:variable name = "subtotal" select = "@price * @quantity"/>
<item price = "{@price}" quantity = "{@quantity}" subtotal = "{format-number($subtotal,'0.00')}"/>
</xsl:template>
<!-- Root template. Calculates total based on subtotals
The template matching orders calculates the total price by summing all the
subtotals, then produces a new orders element with that total as one attribute.-->
<xsl:template match = "orders">
<orders totalPrice = "{format-number(sum(//item/@subtotal),'0.00')}">
<xsl:apply-templates/>
</orders>
</xsl:template>
<!-- Identity template.
The identity template is a design pattern that is used in XSLT where one
wants to transform a source tree into a result tree, making changes to some
part of it but keeping the rest of the tree the same. This basically copies
over anything that doesn't match another template rule
-->
<xsl:template match = "@*|node()">
<xsl:copy>
<xsl:apply-templates select = "@*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Я использую Ксалан Дж. Только что понял. Посмотрю эту страницу, спасибо @michael.hor257k.





Вам нужно выполнить проходы по всему документу, и на втором проходе вы сможете подсчитать итоговые суммы; для простоты я не использовал два режима, а просто переменную и исключил неправильные/пустые итоги из вычислений первого прохода, не обрабатывая атрибут в заказах:
<xsl:stylesheet xmlns:xsl = "http://www.w3.org/1999/XSL/Transform" version = "1.0"
xmlns:exsl = "http://exslt.org/common"
extension-element-prefixes = "exsl">
<xsl:output method = "xml" indent = "yes"/>
<xsl:template match = "/">
<xsl:variable name = "first-transformation">
<xsl:apply-templates/>
</xsl:variable>
<xsl:apply-templates select = "exsl:node-set($first-transformation)/node()"/>
</xsl:template>
<!-- Handling of order items -->
<xsl:template match = "item">
<xsl:variable name = "subtotal" select = "@price * @quantity" />
<item price = "{@price}" quantity = "{@quantity}" subtotal = "{format-number($subtotal,'0.00')}" />
</xsl:template>
<!-- Root template. Calculates total based on subtotals -->
<xsl:template match = "orders">
<orders totalPrice = "{format-number(sum(//item/@subtotal),'0.00')}">
<xsl:apply-templates/>
</orders>
</xsl:template>
<!-- Identity template -->
<xsl:template match = "@*|node()">
<xsl:copy>
<xsl:apply-templates select = "@*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Это дало мне подсказку, необходимую для завершения решения. Теперь я лучше понимаю, как реализовать несколько проходов.
Если вы используете процессор Xalan-J, вы можете воспользоваться функцией расширения EXSLT dyn:sum(), чтобы завершить все преобразование за один проход:
XSLT 1.0 + EXSLT
<xsl:stylesheet version = "1.0"
xmlns:xsl = "http://www.w3.org/1999/XSL/Transform"
xmlns:dyn = "http://exslt.org/dynamic"
extension-element-prefixes = "dyn">
<xsl:output method = "xml" version = "1.0" encoding = "UTF-8" indent = "yes"/>
<!-- identity transform -->
<xsl:template match = "@*|node()">
<xsl:copy>
<xsl:apply-templates select = "@*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match = "/invoice">
<xsl:copy>
<!-- add grand totals -->
<xsl:variable name = "items" select = "orders/order/lineitem/item"/>
<statistics>
<totalPrice>
<xsl:value-of select = "format-number(dyn:sum($items, '@quantity * @price'), '0.00')"/>
</totalPrice>
<totalQuantity>
<xsl:value-of select = "sum($items/@quantity)"/>
</totalQuantity>
</statistics>
<!-- process orders -->
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match = "item">
<!-- add subtotal -->
<item quantity = "{@quantity}" price = "{@price}" subtotal = "{format-number(@quantity * @price, '0.00')}"/>
</xsl:template>
</xsl:stylesheet>
Хотя пурист отметил бы, что у этого метода есть недостаток: промежуточные итоги вычисляются дважды.
Необходимо настроить шаблон item следующим образом: <xsl:template match = "item"> <item quantity = "{@quantity}" price = "{@price}"> <xsl:attribute name = "subtotal"> <xsl:value-of select = "format-number(@quantity * @price, '0.00')"/> </xsl:attribute> </item> </xsl:template>
Это блестяще! Я не думал, что Xalan Java 2.7.2 поддерживает динамический тег. Меня не волнует необходимость подсчитывать эти суммы дважды, хотя обычно мне приходится это делать. Это пример, который я придумал, чтобы помочь мне решить мою реальную проблему. Нужно было придумать, как лучше использовать эту технологию. Спасибо.
Нет необходимости в регулировке; Я просто забыл фигурные скобки в исходном сообщении (вы, должно быть, пропустили редактирование). Мой ответ пуристу однозначен, но сравните общий объем обработки двух методов.
Какой процессор XSLT 1.0 вы используете? Некоторые могут сделать это за один проход, используя функции динамического расширения EXSLT: exslt.github.io/dyn/index.html