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

Я преобразую 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, чтобы сделать его глобальной переменной, которую я могу использовать в двух местах:

  1. Шаблон заказов (<xsl:template match = "orders">), в котором exsl:node-set используется для суммирования промежуточных итогов и получения totalPrice.
  2. Новый шаблон 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>

Какой процессор XSLT 1.0 вы используете? Некоторые могут сделать это за один проход, используя функции динамического расширения EXSLT: exslt.github.io/dyn/index.html

michael.hor257k 13.06.2024 14:40

Я использую Ксалан Дж. Только что понял. Посмотрю эту страницу, спасибо @michael.hor257k.

Robert Mark Bram 15.06.2024 02:52
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
2
64
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

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

<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>

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

Robert Mark Bram 14.06.2024 05:42

Если вы используете процессор 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>

Robert Mark Bram 15.06.2024 04:51

Это блестяще! Я не думал, что Xalan Java 2.7.2 поддерживает динамический тег. Меня не волнует необходимость подсчитывать эти суммы дважды, хотя обычно мне приходится это делать. Это пример, который я придумал, чтобы помочь мне решить мою реальную проблему. Нужно было придумать, как лучше использовать эту технологию. Спасибо.

Robert Mark Bram 15.06.2024 04:52

Нет необходимости в регулировке; Я просто забыл фигурные скобки в исходном сообщении (вы, должно быть, пропустили редактирование). Мой ответ пуристу однозначен, но сравните общий объем обработки двух методов.

michael.hor257k 15.06.2024 05:03

Другие вопросы по теме