XSL v.1, Muenchian Grouping, Summing Line Items per Invoice, Call-Template - sum

I'm trying to sum the aggregate of line item amounts in a group with multiple groups. That is, there are many Invoices with many Line Items per Invoice. I need to sum the Amounts of Line Items across each Invoice.
I've searched across various posts here on Stack, as well as various forums, and haven't been able to decipher the code to be able to sum values with a Muenchian Grouping approach. If there is already a solution for this, please point me in the right direction.
The grouping occurs on the recordId attribute.
XML:
<?xml version="1.0" encoding="UTF-8"?>
<query>
<results total="6">
<result recordId="15918960" associatedRecordId="null" boId="10002385">
<columns>
<column>
<field>AmountNU</field>
<LI_Amount_display><![CDATA[$20.74]]></LI_Amount_display>
</column>
</columns>
</result>
<result recordId="15918960" associatedRecordId="null" boId="10002385">
<columns>
<column>
<field>AmountNU</field>
<LI_Amount_display><![CDATA[$30.74]]></LI_Amount_display>
</column>
</columns>
</result>
<result recordId="15918960" associatedRecordId="null" boId="10002385">
<columns>
<column>
<field>AmountNU</field>
<LI_Amount_display><![CDATA[$40.74]]></LI_Amount_display>
</column>
</columns>
</result>
<result recordId="15918961" associatedRecordId="null" boId="10002385">
<columns>
<column>
<field>AmountNU</field>
<LI_Amount_display><![CDATA[$20.74]]></LI_Amount_display>
</column>
</columns>
</result>
<result recordId="15918961" associatedRecordId="null" boId="10002385">
<columns>
<column>
<field>AmountNU</field>
<LI_Amount_display><![CDATA[$30.74]]></LI_Amount_display>
</column>
</columns>
</result>
<result recordId="15918962" associatedRecordId="null" boId="10002385">
<columns>
<column>
<field>AmountNU</field>
<LI_Amount_display><![CDATA[$29.74]]></LI_Amount_display>
</column>
</columns>
</result>
</results>
</query>
XSL:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
xmlns:datetime="http://exslt.org/dates-and-times"
xmlns:exsl="http://exslt.org/common"
xmlns:fn="http://www.w3.org/2013/xpath-functions"
exclude-result-prefixes="datetime">
<xsl:output method="text" encoding="UTF-8" indent="no"/>
<xsl:key name="recordID" match="result" use="#recordId"/><!-- Define a key to use for grouping the results -->
<!-- Might be needed for nulls
<xsl:template match="/results/result/columns/column/LI_Amount_display[not(text()[normalize-space()])]">
<xsl:element name='LI_Amount_display' value="0.00"></xsl:element>
</xsl:template> -->
<xsl:template match="/">
<xsl:call-template name="fixTheWidth" >
<!-- This parameter is a Id for each group of records based on the result/#recordId attribute. This groups all records to the record ID-->
<xsl:with-param name="resultIndex" select="//results/result[generate-id(.) = generate-id(key('recordID', #recordId)[1])]" />
</xsl:call-template>
</xsl:template>
<xsl:template name="fixTheWidth" match="/results">
<xsl:param name="resultIndex" /> <!-- A unique index based on grouping the records on the recordID -->
<!-- MORE CODE HERE USING $resultIndex has been redacted for simplicity-->
<xsl:for-each select="$resultIndex" >
<xsl:text> BEGIN | </xsl:text>
<xsl:value-of select="number(translate(substring(key('recordID',#recordId)/columns/column/LI_Amount_display,2),',',''))"></xsl:value-of>
<!-- <xsl:value-of select="sum(number(translate(substring(key('recordID',#recordId)/columns/column/LI_Amount_display,2),',','')))"></xsl:value-of>-->
</xsl:for-each>
<xsl:text> | END </xsl:text>
<!-- MORE CODE HERE USING $resultIndex has been redacted for simplicity-->
</xsl:template>
</xsl:stylesheet>
I realize that using the "call-template" strategy is not necessarily the "best" approach, but its what I've come to such that I can create a fixed width flat file and not have to refactor the entire script. If there's a way to accomplish this otherwise, I'm all ears.

You cannot sum nodes that aren't numbers. A value of $20.74 is a string, not a number. You must first convert the values to numbers by removing the currency symbol (and any other non-numeric characters, if they are allowed in the input), then proceed to group the resulting nodes and sum the groups.
Here's an example:
XSLT 1.0
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
xmlns:exsl="http://exslt.org/common"
exclude-result-prefixes="exsl">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:key name="amt" match="amount" use="#id"/>
<xsl:template match="/query">
<xsl:variable name="amounts">
<xsl:for-each select="results/result">
<amount id="{#recordId}"><xsl:value-of select="translate(columns/column/LI_Amount_display, '$', '')"/></amount>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="amount-set" select="exsl:node-set($amounts)/amount" />
<xsl:for-each select="$amount-set[generate-id() = generate-id(key('amt', #id)[1])]" >
<xsl:text> BEGIN | </xsl:text>
<xsl:value-of select="sum(key('amt', #id))"/>
</xsl:for-each>
<xsl:text> | END </xsl:text>
</xsl:template>
</xsl:stylesheet>
Applied to your example input, the result is:
BEGIN | 92.22 BEGIN | 51.48 BEGIN | 29.74 | END
I couldn't understand the strategy of calling a template and "create a fixed width flat file". In any case, it seems unrelated to the question at hand. You might post a separate question, if necessary.

Related

How do I use XSLT to merge XML elements and then structure elements/attributes of the same name/type?

Main issue
I have an XML file which has multiple similar elements with slightly different information. So, lets say I have two elements with the same X attirbute, but different Y attributes. Here is an example:
<FILE>
<ELEMENT>
<ATTRIBUTEX>1</ATTRIBUTEX>
<ATTRIBUTEY>A</ATTRIBUTEY>
</ELEMENT>
<ELEMENT>
<ATTRIBUTEX>1</ATTRIBUTEX>
<ATTRIBUTEY>B</ATTRIBUTEY>
</ELEMENT>
</FILE>
What I want to do is--based on the fact that both elements have the same X attribute--merge them into a single element with one attribute X and multiple child elements (of the same type)--each of which contain each of the different attributes Y. So, for example, I want my file to end up like this:
<FILE>
<ELEMENT>
<ATTRIBUTEX>1</ATTRIBUTEX>
<NEWELEMENT>
<ATTRIBUTEY>A</ATTRIBUTEY>
</NEWELEMENT>
<NEWELEMENT>
<ATTRIBUTEY>B</ATTRIBUTEY>
</NEWELEMENT>
</ELEMENT>
</FILE>
Possible solution?
One possible solution I can think of, but lack the knowledge to execute, is made up of two steps:
Firstly, I could merge all elements which contain the X attribute so that the information is, at the least, in one place. I don't know quite how to do this.
Secdonly, once this is done, this opens me up--in theory--to use my knowledge of XSLT to re-structure the element.
Problem with the possible solution
However, once all elements that containin the X attribute are merged together (first step), I will have a file in which there will be attributes with the same name (specifically, the Y attribute). Here is an example:
<FILE>
<ELEMENT>
<ATTRIBUTEX>1</ATTRIBUTEX>
<ATTRIBUTEY>A</ATTRIBUTEY>
<ATTRIBUTEY>B</ATTRIBUTEY>
</ELEMENT>
</FILE>
What this means is that--at least with my knowledge--when I execute an XSLT file on the above XML (second step), it cannot distinguish between the two Y elements when sorting them into the two new child elements (NEWELEMENT).
So, let's say I execute the following XSLT on the above XML:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="FILE">
<Record>
<xsl:for-each select = "ELEMENT">
<ELEMENT>
<xsl:copy-of select="ATTRIBUTEX"/>
<NEWELEMENT>
<xsl:copy-of select="ATTRIBUTEY"/>
</NEWELEMENT>
<NEWELEMENT>
<xsl:copy-of select="ATTRIBUTEY"/>
</NEWELEMENT>
</ELEMENT>
</xsl:for-each>
</Record>
</xsl:template>
</xsl:stylesheet>
The output is this:
<Record>
<ELEMENT>
<ATTRIBUTEX>1</ATTRIBUTEX>
<NEWELEMENT>
<ATTRIBUTEY>A</ATTRIBUTEY>
<ATTRIBUTEY>B</ATTRIBUTEY>
</NEWELEMENT>
<NEWELEMENT>
<ATTRIBUTEY>A</ATTRIBUTEY>
<ATTRIBUTEY>B</ATTRIBUTEY>
</NEWELEMENT>
</ELEMENT>
</Record>
As you can see, the XSLT has taken all and any ATTRIBUTEY elements and put them into each NEWELEMENT rather than distinguishing between them and placing one ATTRIBUTEY in the first NEWELEMENT, and the second ATTRIBUTEY in the second NEWELEMENT. I am in need of an XSLT file which does just that, and, as stated further up, produces an XML that looks like this:
<FILE>
<ELEMENT>
<ATTRIBUTEX>1</ATTRIBUTEX>
<NEWELEMENT>
<ATTRIBUTEY>A</ATTRIBUTEY>
</NEWELEMENT>
<NEWELEMENT>
<ATTRIBUTEY>B</ATTRIBUTEY>
</NEWELEMENT>
</ELEMENT>
</FILE>
Would anyone be able to help me with this? Any solutions which generate the desired output (above) from the very first example file would be much appreciated! It's a bonus if that solution follows steps one and two. Thanks!
I'm getting your requested output from the XSLT 3.0 below.
Just a couple of notes beforehand:
You mention ATTRIBUTEs but, in fact, everything is an element in your XML document. There are no attributes in the XML sense.
I really don't see the need to create the additional subelements .. you can already have a list of elements of the same name.
Nonetheless, as an exercise in style(sheets), here is a stylesheet that produces your output from your given input:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="3.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all">
<xsl:mode on-no-match="shallow-copy"/>
<xsl:output method="xml" indent="yes" />
<xsl:template match="/FILE" >
<FILE>
<xsl:for-each-group select="*" group-by="name(.)" >
<xsl:variable name="parentName" as="xs:string" select="current-grouping-key()" />
<xsl:variable name="childNamesSameValues" as="xs:string*" >
<xsl:for-each-group select="current-group()/*" group-by="name(.)" >
<xsl:if test="count(distinct-values(current-group()/text())) eq 1">
<xsl:sequence select="current-grouping-key()" />
</xsl:if>
</xsl:for-each-group>
</xsl:variable>
<xsl:element name="{$parentName}" >
<xsl:for-each-group select="current-group()/*" group-by="name(.)" >
<xsl:choose>
<xsl:when test="current-grouping-key() = $childNamesSameValues">
<xsl:copy-of select="current-group()[1]" />
</xsl:when>
<xsl:otherwise >
<xsl:for-each select="current-group()" >
<xsl:element name="NEW{$parentName}" >
<xsl:copy-of select="." />
</xsl:element>
</xsl:for-each>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</xsl:element>
</xsl:for-each-group>
</FILE>
</xsl:template>
</xsl:stylesheet>

Sort sequence from distinct values

What is the cleanest way to sort a sequence I get from distinct-values()? I use XPath 2.0 and my files are the following:
This is my XML file
<root>
<value>3</value>
<value>1</value>
<value>7</value>
</root>
and this is my transformation:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:template match="root">
<output>
<xsl:variable name="availableValues" select="distinct-values(value)"/>
<!-- now the availableValues contains the sequence (3,1,7) -->
<!-- My goal is to get this into a sorted sequence (1,3,7) -->
<!-- This is just for testing the $availableValues -->
<xsl:attribute name="count" select="count($availableValues)"/>
<xsl:for-each select="$availableValues">
<val>
<xsl:value-of select="."/>
</val>
</xsl:for-each>
</output>
</xsl:template>
</xsl:stylesheet>
I have tried to use
<xsl:variable name="availableValues">
<xsl:for-each select="distinct-values(value)">
<xsl:sort/>
<xsl:value-of select="."/>
</xsl:for-each>
</xsl:variable>
but this (of course) doesn't give me a sequence, just the concatenation of the values (although sorted).
EDIT:
This is what I came up so far, and it looks like a proper solution. Is there a better solution?
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
>
<xsl:output indent="yes"/>
<xsl:template match="root">
<output>
<xsl:variable name="availableValues" as="xs:integer *">
<xsl:for-each select="distinct-values(value)">
<xsl:sort/>
<xsl:sequence select="."/>
</xsl:for-each>
</xsl:variable>
<!-- This is just for testing the $availableValues -->
<xsl:attribute name="count" select="count($availableValues)"/>
<xsl:for-each select="$availableValues">
<val>
<xsl:value-of select="."/>
</val>
</xsl:for-each>
</output>
</xsl:template>
</xsl:stylesheet>
Use xsl:perform-sort.
Here is an example from the link:
<xsl:perform-sort select="//BOOK">
<xsl:sort select="author/last-name"/>
<xsl:sort select="author/first-name"/>
</xsl:perform-sort>

XSLT: How to select multiple sub nodes into a single list

How can I extract sub nodes to a single list for processing in a template?
Consider the following XML.
<document>
<menu></menu>
<feature>
<header>Header 1</header>
<article>article 1</article>
<article>article 2</article>
</feature>
<feature>
<header>Heading 2</header>
<article>article 1a</article>
<article>article 2a</article>
</feature>
</document>
I'd like to extract all the article nodes to a single list for processing in a template.
I need article nodes to be available at once, because I need to do calculations based on the number of articles there are.
you can try the following stylesheet:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ext="http://exslt.org/common"
exclude-result-prefixes="ext">
<xsl:output indent="yes" omit-xml-declaration="yes"/>
<xsl:template match="/">
<xsl:variable name="list">
<articles>
<xsl:copy-of select="descendant::article"/>
</articles>
</xsl:variable>
<xsl:variable name="vPass1" select="ext:node-set($list)"/>
<xsl:apply-templates select="$vPass1/*"/>
</xsl:template>
<xsl:template match="articles">
<xsl:copy>
<xsl:text>Number of articles: </xsl:text><xsl:value-of select="count(article)"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
when applied your input, it produces:
<articles>Number of articles: 4</articles>

Concat distinct values in a for-each

I've run into a problem and i'm looking for a quick fix. I have an xml from which i take a some values:
<root>
<item>
<property1>value</property1>
<property2>value</property2>
<property3>value</property3>
</item>
<item>...</item>
<item>...</item>
<item>...</item>
</root>
I'm making a variable to use after using:
<xsl:for-each select="root/item"><xsl:value-of select="concat(property1,';')"/></xsl:for-each>
But i've ran into a problem when too many items, the variable gets too big (over 255 characters). So i was thinking of taking only the unique values (unique property values).
Any simple way to do it ?
Thanks
Please test the stylesheet below:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:key name="group" match="property1" use="."/>
<xsl:variable name="unique_properties">
<xsl:for-each select="//property1[count(. | key('group', .)[1]) = 1]"><!-- this selects unique values -->
<xsl:value-of select="concat(.,';')"/>
</xsl:for-each>
</xsl:variable>
<xsl:template match="root">
<xsl:value-of select="$unique_properties"/>
</xsl:template>
</xsl:stylesheet>

how to display content that appears once outside the repeating nodes in the expression of every node in xslt

I've got data in XML that has some header information then a series of items. I'm using XSLT to translate that into a different format, also with a header area and a series of items.
However in the post translation result, I want one piece of data only found in the header to be included in every instance of the items, even though it will simply be repeating the same value. (this value may change so I cannot hard code it)
Sample data (significantly simplified)
<rss>
<channel>
<title>Playlist One</title>
<items>
<item>
<title>Video One</title>
</item>
<item>
<title>Video Two</title>
</item>
<item>
<title>Video Three</title>
</item>
</items>
</channel>
</rss>
My desired result is something like this:
playlist_header_title=Playlist One
playlist_title=Playlist One
video_title=Video One
playlist_title=Playlist One
video_title=Video Two
playlist_title=Playlist One
video_title=Video Three
My XSLT is very complicated (and unfortunately I inherited it from someone else so I'm not sure what everything does, I've self-taught myself online but am in a bit over my head)
Roughly the key pieces look like this:
<xsl:template name="rss" match="/">
<xsl:variable name="playlist_title">
<xsl:value-of select="string(/rss/channel/title)"/>
</xsl:variable>
<xsl:for-each select="rss">
<xsl:apply-templates name="item" select="channel/items/item"/>
</xsl:for-each>
</xsl:template>
Then there's a massive template called "item" that I won't include here, but basically it outputs all the item data as desired, I just can't figure out how to access the "playlist_title".
When I try to call (from inside the template "item")
<xsl:value-of select="string($playlist_title)"/>
it returns a blank. I assume this is because that variable was created outside the for-each loop and so it not available. (it will display the data correctly when I output it before the for-each loop in the result's version of the header, but that doesn't get me far enough)
I've tried using with-param in the apply-templates, and also tried changing it to call-template inside another loop also using with-param, but they also display a blank.
I've also tried sending in a string rather than pulling the playlist_title from the XML just to confirm I am able to pass any value into the template, but they too come out blank.
For instance:
<xsl:for-each select="channel/items/item">
<xsl:call-template name="item">
<xsl:with-param name="playlist_title">blah</xsl:with-param>
</xsl:call-template>
</xsl:for-each>
<xsl:template name="item">
<xsl:param name="playlist_title" />
playlist_title=<xsl:value-of select="string($playlist_title)"/>
video_title=...
...
</xsl:template>
This did not return the value "blah" but just a blank. (my hope was to then replace "blah" with the playlist_title value pulled from the XML, but none of it is getting through)
I'm stumped! Thanks for any help.
In your first example, attempting to reference the $playlist_title inside of your template for item is failing because the variable does not exist inside of that template. It was created inside of the template match for rss and is locally scoped.
If you want to be able to use the $playlist_title in other templates, you need to declare it outside of the rss template in the "global scope".
For instance:
<xsl:variable name="playlist_title">
<xsl:value-of select="string(/rss/channel/title)"/>
</xsl:variable>
<xsl:template name="rss" match="/">
<xsl:for-each select="rss">
<xsl:apply-templates name="item" select="channel/items/item"/>
</xsl:for-each>
</xsl:template>
As for your second attempt, referencing the xsl:param playlist_title, it worked for me. Double-check to ensure that you don't have a type-o or other difference between the name of the parameter that was declared and the name of the parameter that you are referencing.
You might try something like this to achieve your desired output, using an xsl:variable:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="yes"/>
<xsl:variable name="playlist_title" select="/rss/channel/title" />
<xsl:template match="/">
<xsl:apply-templates select="rss/channel"/>
</xsl:template>
<xsl:template match="channel">
<xsl:text>playlist_header_title=</xsl:text>
<xsl:value-of select="title"/>
<xsl:text>
</xsl:text>
<xsl:apply-templates select="items/item"/>
</xsl:template>
<xsl:template match="item">
<xsl:text>playlist_title=</xsl:text>
<xsl:value-of select="$playlist_title"/>
<xsl:text>
</xsl:text>
<xsl:text>video_title=</xsl:text>
<xsl:value-of select="title"/>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
Or you can eliminate the xsl:variable and use an xsl:param:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="yes"/>
<xsl:template match="/">
<xsl:apply-templates select="rss/channel"/>
</xsl:template>
<xsl:template match="channel">
<xsl:text>playlist_header_title=</xsl:text>
<xsl:value-of select="title"/>
<xsl:text>
</xsl:text>
<xsl:apply-templates select="items/item">
<xsl:with-param name="playlist_title" select="title"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="item">
<xsl:param name="playlist_title" />
<xsl:text>playlist_title=</xsl:text>
<xsl:value-of select="$playlist_title"/>
<xsl:text>
</xsl:text>
<xsl:text>video_title=</xsl:text>
<xsl:value-of select="title"/>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>