Count with conditions of a 2nd list - xslt

XML I: (e.g. shopping-cart, image-gallery)
<list1>
<entry>
...
<items>
<item id="1"></item>
<item id="2"></item>
<item id="3"></item>
</items>
</entry>
</list1>
XML II: (=associated items)
<list2>
<entry id="1">
...
<visibility>
<item value="public">Public</item>
</visibility>
</entry>
<entry id="3">
...
<visibility>
<item value="private">Private</item>
</visibility>
</entry>
<entry id="5">
...
<visibility>
<item value="public">Public</item>
</visibility>
</entry>
</list2>
Notice: The ID's in list2 might match or not...
How to get the count of items of list1 which have an ID in list2 and /visibility/item/#value = 'public' ?

With XSLT 1.0 you can use count(/list1/entry/item[#id = document('xml2.xml')/list2/entry[visibility/item/#value = 'public']/#id]).
With XSLT 2.0 a key
<xsl:template name="k1" match="list2/entry[visibility/item/#value = 'public']" use="#id"/>
and then
count(/list1/entry/item[key('k1', #id, document('xml2.xml'))])
makes the lookup more efficient.

Related

What does the expression $data[not(key('myKey',#myRef))] in XSLT mean?

I read expressions like this
<xsl:variable name="myVar" select="$data[not(key('myKey',#myRef))]"/>
in legacy code. Most likeley it is code from experts ;-). I'm wondering what it does, how it works and how i could reeng it in order to make it more readable. Thank you.
Keys are an important aspect of XSLT. Instead of re-engineering them, it's better to learn the concept.
Keys can be understood as tables with nodes stored under specific keys. They are defined like this:
<xsl:key name="addressByStreet" match="address" use="street"/>
The name attribute is just a QName (similar to a variable name). The match attribute holds an XPath expression that works similarly to the match attribute of <xsl:template>. When the processor finds a node that matches the expression, it evaluates the XPath expression of the use attribute in the context of the matched element. If this expression returns values, they will be used to create new entries in the "key table" for the matched element.
To illustrate that: The above key creates a table with all the <address> elements in the processed document, keyed by the value of their <street> child. This means, if you have these elements:
<address>
<street>Main Street</street>
<number>123</number>
</address>
<address>
<street>Main Street</street>
<number>456</number>
</address>
<address>
<street>Country Road</street>
<street>Country Rd.</street>
<number>789</number>
</address>
… you could then use key('addressByStreet', 'Main Street') to retrieve all the listed addresses in Main Street.
You can use both key('addressByStreet', 'Country Road') and key('addressByStreet', 'Country Rd.') to retrieve the last address.
Why use keys here? The above expression could be re-implemented like //address[street='Main Street'], but now every time this expression is called, the XSLT processor likely goes through the entire document again. That's a problem if a template or loop is called often. Keys can have huge performance benefits (e.g. reduce complexity from O(n²) to O(n)) because the results are "cached".
There are many applications and patterns in which keys are used. For example if you have this XML:
<street-list>
<street>Main Street</street>
<street>Bumpy Road</street>
</street-list>
The expression street-list/street[not(key('addressByStreet', .))] will filter the list of streets and only return streets for which there is no address in the above list – i.e. only "Bumpy Road" in this case because for "Main Street", a key entry exists.
A typical application of keys in XSLT 1 is Muenchian grouping.
I've got the use case now. No, it is not legacy code. This is clear from context and definition of key and data.
If we have data like this:
<xsl:variable name="dict">
<ITEMS>
<ITEM id="1" content="it1">
<ITEM-REF ref="3"/>
</ITEM>
<ITEM id="2" content="it2">
<ITEM-REF ref="1"/>
</ITEM>
<ITEM id="3" content="it3">
<ITEM-REF ref="6"/>
</ITEM>
<ITEM id="4" content="it4">
<ITEM-REF ref="3"/>
</ITEM>
<ITEM id="5" content="it5">
<ITEM-REF ref="5"/>
</ITEM>
<ITEM id="6" content="it6">
<ITEM-REF ref="8"/>
</ITEM>
<ITEM id="7" content="it7">
<ITEM-REF ref="9"/>
</ITEM>
</ITEMS>
</xsl:variable>
And we want to get all ITEM-REF elements with #ref values where there is no ITEM with the same #id value (broken links) the expression can help out:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
<xsl:output method="xml" encoding="utf-8" indent="yes"/>
<xsl:variable name="dict">
<ITEMS>
<ITEM id="1" content="it1">
<ITEM-REF ref="3"/>
</ITEM>
<ITEM id="2" content="it2">
<ITEM-REF ref="1"/>
</ITEM>
<ITEM id="3" content="it3">
<ITEM-REF ref="6"/>
</ITEM>
<ITEM id="4" content="it4">
<ITEM-REF ref="3"/>
</ITEM>
<ITEM id="5" content="it5">
<ITEM-REF ref="5"/>
</ITEM>
<ITEM id="6" content="it6">
<ITEM-REF ref="8"/>
</ITEM>
<ITEM id="7" content="it7">
<ITEM-REF ref="9"/>
</ITEM>
</ITEMS>
</xsl:variable>
<xsl:key name="itemkey" match="ITEM" use="#id"/>
<xsl:template match="START">
<xsl:variable name="allItems" select="msxsl:node-set($dict)//ITEM"/>
<xsl:variable name="allItemRefs" select="msxsl:node-set($dict)//ITEM-REF"/>
<xsl:variable name="itemRefsNotReferencingOtherItems" select="$allItemRefs[not(key('itemkey',#ref))]"/>
<REFERENCED-NOT-EXISTING>
<xsl:for-each select="msxsl:node-set($itemRefsNotReferencingOtherItems)">
<ITEM>
<xsl:attribute name="id">
<xsl:value-of select="#ref"/>
</xsl:attribute>
</ITEM>
</xsl:for-each>
</REFERENCED-NOT-EXISTING>
</xsl:template>
</xsl:stylesheet>
Output:
<?xml version="1.0" encoding="utf-8"?>
<REFERENCED-NOT-EXISTING>
<ITEM id="8" />
<ITEM id="9" />
</REFERENCED-NOT-EXISTING>
Input file:
<?xml version="1.0" encoding="utf-8"?>
<START/>

Setting XSLT variables within for-each select

Good Day,
I have an XSLT template I'm assembling that look like:
<xsl:for-each select="CarParts/Items">
<div class="columns"><xsl:value-of select="Quantity"/></div>
<div class="columns"><xsl:value-of select="Amount"/></div>
<div class="columns">[SUBTOTAL]</div><br />
</xsl:for-each>
I know that I can define an XSLT variable like this:
<xsl:variable name="totalAmount" select="sum(CarParts/Items/Amount)" />
But I want my XSLT variable to be [SUBTOTAL] which is equal to Quantity * Amount within the for-each select loop. Is this possible? If this was SQL, this would be the equivalent of a computed column.
Any suggestions?
TIA,
coson
What you want to do is cast the value to a number, then you can multiply it as desired:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/">
<results>
<xsl:for-each select="CarParts/Items">
<Item id="{position()}">
<q><xsl:value-of select="Quantity"/></q>
<a><xsl:value-of select="Amount"/></a>
<st><xsl:value-of select="number(Quantity)*number(Amount)"/></st>
</Item>
</xsl:for-each>
</results>
</xsl:template>
</xsl:stylesheet>
I changed the formatting a little since there was no provided input/CSS, but you should see what I was going for. Running it on my sample input of
<CarParts>
<Items>
<Quantity>1</Quantity>
<Amount>100.00</Amount>
</Items>
<Items>
<Quantity>2</Quantity>
<Amount>25.00</Amount>
</Items>
<Items>
<Quantity>3</Quantity>
<Amount>6</Amount>
</Items>
</CarParts>
I get the result of
<?xml version="1.0" encoding="utf-8"?>
<results>
<Item id="1">
<q>1</q>
<a>100.00</a>
<st>100</st>
</Item>
<Item id="2">
<q>2</q>
<a>25.00</a>
<st>50</st>
</Item>
<Item id="3">
<q>3</q>
<a>6</a>
<st>18</st>
</Item>
</results>

Grouping XML elements using XSLT's for-each-group and group-starting-with attribute when the node set is stored in a variable

I'm processing a source HTML file that holds tabular data in an unstructured way. Basically it's a bunch of absolutely positioned divs. My goal is to rebuild some sort of structured XML data. So far, using XSLT 2.0 I was able to produce an XML looking like this:
<data>
<line top="44">
<item left="294">Some heading text</item>
</line>
<line top="47">
<item left="718">A</item> <!-- this item is a section-start -->
<item left="764">Section heading</item>
</line>
<line top="78">
<item left="92">Data</item>
<item left="144">Data</item>
<item left="540">Data</item>
<item left="588">Data</item>
</line>
<line top="101">
<item left="61">B</item> <!-- this item is a section-start -->
<item left="144">Section heading</item>
</line>
<line top="123">
<item left="92">Data</item>
<item left="144">Data</item>
</line>
</data>
However, what I need to do next is group lines into sections. Each section starts with a line whose first item's value consists of a single letter A – Z. My approach is to hold all the <line> elements in a $lines variable and then use xsl:for-each-group with group-starting-with attribute to identify the element starting a new section.
The respective XSLT fragment looks like this:
<xsl:for-each-group select="$lines/line" group-starting-with="...pattern here...">
<section>
<xsl:copy-of select="current-group()"/>
</section>
</xsl:for-each-group>
The problem is I can't figure out a working pattern to identify section starts. The best I could do was ensuring that //line/item[1]/text()[matches(., '^[A-Z]$')] works when used separately in an XPath evaluator. However, I can't seem to derive a working version to be used with group-starting-with.
Update Hence the wanted result should look like this:
<data>
<section> <!-- this section started automatically because of being at the beginning -->
<line top="44">
<item left="294">Some heading text</item>
</line>
</section>
<section>
<line top="47">
<item left="718">A</item> <!-- this item is a section-start -->
<item left="764">Section heading</item>
</line>
<line top="78">
<item left="92">Data</item>
<item left="144">Data</item>
<item left="540">Data</item>
<item left="588">Data</item>
</line>
</section>
<section>
<line top="101">
<item left="61">B</item> <!-- this item is a section-start -->
<item left="144">Section heading</item>
</line>
<line top="123">
<item left="92">Data</item>
<item left="144">Data</item>
</line>
</section>
</data>
The solution:
<xsl:for-each-group select="$lines/line" group-starting-with="line[matches(child::item[1], '^[A-Z]$')]">
<section name="{current-group()[1]/item[1]}">
<xsl:copy-of select="current-group()"/>
</section>
</xsl:for-each-group>
The trick is really understanding that group-starting-with shall be a pattern not a condition.

Why is my XSLT key not working as I expect?

I feel like I'm missing something obvious, but I can't work out why my XSLT 1.0 key is not working for me.
My desired output is "Sample Brand" (see comment in XSLT below), but nothing is output at all.
The testing I've done seems to indicate that the key isn't being generated, as when I do a for-each using the key() function with some dummy output, nothing is output then either (it seems like there are 0 key items). But I'm not sure of this.
XML:
<data>
<products-by-instances>
<entry id="1975">
<name>Sample Name</name>
<brand>
<item id="1970">Sample Brand</item>
</brand>
<instances>
<item id="1972">MILT501</item>
<item id="1974">MILT502</item>
</instances>
</entry>
</products-by-instances>
<shopping-cart items="2" total="35">
<item id="1972" num="1" sum="5" />
<item id="1974" num="3" sum="30" />
</shopping-cart>
</data>
XSLT:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:key name="products-by-instance-id" match="/data/products-by-instances/entry" use="instances/item/#id"/>
<!-- other templates redacted for brevity; the below template is being applied -->
<xsl:template match="/data/shopping-cart/item">
<xsl:value-of select="key(products-by-instance-id, #id)/brand/item"/>
<!-- desired output is "Sample Brand" -->
</xsl:template>
It's now been pointed out to me that I neglected to put the key name in quotes:
<xsl:value-of select="key('products-by-instance-id', #id)/brand/item"/>
Key now working as expected.

What this XPath expression match in an XSLT template rule? *|/

From my understanding of XSLT I can't figure out what does it match.
<xsl:template match="*|/">
XPath matching is relative to a 'current node' (i.e. current position) in the XML being matched.
* matches all child elements of the current node, whereas / matches the root - and only the root - regardless of what the current node is.
Note also that the root is NOT the root element of the XML, it is actually a level above it - the root element is a child of the root.
If you have this XSLT:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/|*">
<item name="{name()}">
<xsl:apply-templates/>
</item>
</xsl:template>
</xsl:stylesheet>
and you apply it to this XML:
<rootElement>
<subElement1/>
<subElement2/>
</rootElement>
you get this:
<item name="">
<item name="rootElement">
<item name="subElement1" />
<item name="subElement2" />
</item>
</item>
the first <item name=""> correspond to the match of / (that is not an element, and so does not have a name), the others are the matches of *.