XSL Grouping and sub grouping - xslt

I found this post on Muenchian grouping XSLT 1.0 Group By, and got part way with what I want to do, but I can't work out how to group my sub-nodes.
My XML looks a little like this:
<NewDataSet>
<Vehicle>
<ManufacturerId>53</ManufacturerId>
<ManufacturerName>VAUXHALL</ManufacturerName>
<Model>Corsa</Model>
</vehicle>
<Vehicle>
<ManufacturerId>53</ManufacturerId>
<ManufacturerName>VAUXHALL</ManufacturerName>
<Model>Astra</Model>
</vehicle>
<Vehicle>
<ManufacturerId>53</ManufacturerId>
<ManufacturerName>VAUXHALL</ManufacturerName>
<Model>Corsa</Model>
</vehicle>
<Vehicle>
<ManufacturerId>54</ManufacturerId>
<ManufacturerName>FORD</ManufacturerName>
<Model>KA</Model>
</vehicle>
<Vehicle>
<ManufacturerId>54</ManufacturerId>
<ManufacturerName>FORD</ManufacturerName>
<Model>Focus</Model>
</vehicle>
<Vehicle>
<ManufacturerId>54</ManufacturerId>
<ManufacturerName>FORD</ManufacturerName>
<Model>KA</Model>
</vehicle>
<Vehicle>
<ManufacturerId>55</ManufacturerId>
<ManufacturerName>CITROEN</ManufacturerName>
<Model>C4</Model>
</vehicle>
<NewDataSet>
This is the code I have thus far
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" omit-xml-declaration="yes" />
<xsl:key name="groups" match="/NewDataSet/Vehicle" use="ManufacturerName" />
<xsl:template match="/NewDataSet">
<xsl:apply-templates select="Vehicle[generate-id() = generate-id(key('groups', ManufacturerName)[1])]"/>
</xsl:template>
<xsl:template match="Vehicle">
<div class="makeLnk">
<xsl:value-of select="ManufacturerName"/>
<xsl:for-each select="key('groups', ManufacturerName)">
<div class="modelLnk"><xsl:value-of select="Model"/></div>
</xsl:for-each>
</div>
</xsl:template>
</xsl:stylesheet>
This groups together the ManufacturerNames and lists all the models under. But I would now like to group the models as well, thus removing duplicate names. But can't work out the syntax.
Would appreciate any help.
Thanks.

Use a second key composed of the ManufacturerName of the Vehicle and the value of the Model:
<xsl:key name="model" match="Vehicle/Model" use="concat(../ManufacturerName, '|', .)"/>
then replace
<xsl:template match="Vehicle">
<div class="makeLnk">
<xsl:value-of select="ManufacturerName"/>
<xsl:for-each select="key('groups', ManufacturerName)">
<div class="modelLnk"><xsl:value-of select="Model"/></div>
</xsl:for-each>
</div>
</xsl:template>
with
<xsl:template match="Vehicle">
<div class="makeLnk">
<xsl:value-of select="ManufacturerName"/>
<xsl:for-each select="key('groups', ManufacturerName)/Model[generate-id() =
generate-id(key('model', concat(../ManufacturerName, '|', .))[1])]">
<div class="modelLnk"><xsl:value-of select="."/></div>
</xsl:for-each>
</div>
</xsl:template>

Related

XSLT - Remove duplicates from mapped results

This isn't quite the threads on removing duplicates I've found on this forum.
I have a key/value map and I want to remove duplicates from the final results of the mapping.
Source Document:
<article>
<subject code="T020-060"/>
<subject code="T020-010"/>
<subject code="T090"/>
</article>
Mapping:
<xsl:variable name="topicalMap">
<topic MapCode="T020-060">Value 1</topic>
<topic MapCode="T020-010">Value 1</topic>
<topic MapCode="T090">Value 3</topic>
</xsl:variable>
Desired Result:
<article>
<topic>Value 1</topic>
<topic>Value 3</topic>
</article>
XSLT I'm working with (note, it has a testing tags and code to make sure the mapping works):
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output method="xml" encoding="utf8" indent="yes" exclude-result-prefixes="#all"/>
<xsl:template match="article">
<article>
<xsl:for-each-group select="subject" group-by="$topicalMap/topic[#MapCode = #code]">
<test-group>
<code>Current code: <xsl:value-of select="#code"/></code>
<topic>Current keyword: <xsl:value-of
select="$topicalMap/topic[#MapCode = #code]"/></topic>
</test-group>
</xsl:for-each-group>
<simple-mapping><xsl:apply-templates/></simple-mapping>
</article>
</xsl:template>
<!-- Simple Mapping Topics -->
<xsl:template match="subject">
<xsl:variable name="ArticleCode" select="#code"/>
<topic>
<xsl:value-of select="$topicalMap/topic[#MapCode = $ArticleCode]"/>
</topic>
</xsl:template>
<!-- Keyword Map -->
<xsl:variable name="topicalMap">
<topic MapCode="T020-060">Value 1</topic>
<topic MapCode="T020-010">Value 1</topic>
<topic MapCode="T090">Value 3</topic>
</xsl:variable>
</xsl:stylesheet>
Doing the group-by that way produces nothing. If I duplicate the topics in the source document and do group-by="#code" that works to remove before applying the mapping. But I want to remove resultant duplicate values not duplicate keys.
Simple-mapping stuff is just to show working code.
Use
<xsl:for-each-group select="subject" group-by="$topicalMap/topic[#MapCode = current()/#code]">
<topic>
<xsl:value-of select="current-grouping-key()"/>
</topic>
</xsl:for-each-group>
or better yet
<xsl:key name="map" match="topic" use="#MapCode"/>
<xsl:template match="article">
<article>
<xsl:for-each-group select="subject" group-by="key('map', #code, $topicalMap)">
<topic>
<xsl:value-of select="current-grouping-key()"/>
</topic>
</xsl:for-each-group>
</article>
</xsl:template>

XSLT: find first element above selected element

I want to get the first heading (h1) before a table in a docx.
I can get all headings with:
<xsl:template match="w:p[w:pPr/w:pStyle[#w:val='berschrift1']]">
<p>
<context>
<xsl:value-of select="." />
</context>
</p>
</xsl:template>
and I can also get all tables
<xsl:template match="w:tbl">
<p>
<table>
<xsl:value-of select="." />
</table>
</p>
</xsl:template>
But unfortunetly the processor does not accept
<xsl:template match="w:tbl/preceding-sibling::w:p[w:pPr/w:pStyle[#w:val='berschrift1']]">
<p>
<table>
<xsl:value-of select="." />
</table>
</p>
</xsl:template>
Here is a reduced XML file extracted from a docx: http://pastebin.com/KbUyzRVv
I want something like that as a result:
<context>Let’s get it on</context> <- my heading
<table>data</table>
<context>Let’s get it on</context> <- my heading
<table>data</table>
<context>We’re in the middle of something</context> <- my heading
<table>data</table>
Thanks to Daniel Haley I was able to find a solution for that problem. I'll post it here, so it is independend of the pastebin I postet below.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:v="urn:schemas-microsoft-com:vml" exclude-result-prefixes="xsl w v">
<xsl:output method="xml" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="w:tbl">
<context>
<xsl:value-of select="(preceding-sibling::w:p[w:pPr/w:pStyle[#w:val = 'berschrift1']])[last()]"/>
</context>
<table>
<xsl:value-of select="."/>
</table>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
Hard to answer without a Minimal, Complete, and Verifiable example, but try this:
<xsl:template match="w:tbl">
<p>
<table>
<xsl:value-of select="(preceding::w:p[w:pPr/w:pStyle[#w:val='berschrift1']])[last()]"/>
</table>
</p>
</xsl:template>
Assuming you can use XSLT 2.0 (and most people can, nowadays), I find a useful technique here is to have a global variable that selects all the relevant nodes:
<xsl:variable name="special"
select="//w:tbl/preceding-sibling::w:p[w:pPr/w:pStyle[#w:val='berschrift1']][1]"/>
and then use this variable in a template rule:
<xsl:template match="w:p[. intersect $special]"/>
In XSLT 3.0 you can reduce this to
<xsl:template match="$special"/>

Sorting grouped items using XSLT 1.0

I'be got a rather large XML file that contains a list of vehicle models, their price and a monthly payment price. There is actually loads of other information in there, but those are the salient bits of data I'm interested in.
There are loads of duplicate models in there at different prices/monthly payments. My task, which I have done so far with the help of this forum, is to construct a distinct list of the models (IE. no duplicates), showing the lowest priced vehicle in that model.
The bit I'm stuck on, is I then need to sort this list displaying the lowest monthly payment to the highest monthly payment. The complication being the lowest priced vehicle don't always equal the lowest monthly payment.
My XML looks a bit like this:
<?xml version="1.0" encoding="utf-8"?>
<Dealer>
<Vehicle>
<Model>KA</Model>
<DealerPriceNoFormat>8700.00</DealerPriceNoFormat>
<OptionsFinanceMonthlyPayment>300.50</OptionsFinanceMonthlyPayment>
</Vehicle>
<Vehicle>
<Model>KA</Model>
<DealerPriceNoFormat>10000.50</DealerPriceNoFormat>
<OptionsFinanceMonthlyPayment>270.50</OptionsFinanceMonthlyPayment>
</Vehicle>
<Vehicle>
<Model>Focus</Model>
<DealerPriceNoFormat>12000.00</DealerPriceNoFormat>
<OptionsFinanceMonthlyPayment>340.00</OptionsFinanceMonthlyPayment>
</Vehicle>
<Vehicle>
<Model>KA</Model>
<DealerPriceNoFormat>9910.00</DealerPriceNoFormat>
<OptionsFinanceMonthlyPayment>430.75</OptionsFinanceMonthlyPayment>
</Vehicle>
<Vehicle>
<Model>KUGA</Model>
<DealerPriceNoFormat>23010.00</DealerPriceNoFormat>
<OptionsFinanceMonthlyPayment>550.20</OptionsFinanceMonthlyPayment>
</Vehicle>
<Vehicle>
<Model>Focus</Model>
<DealerPriceNoFormat>15900.00</DealerPriceNoFormat>
<OptionsFinanceMonthlyPayment>430.00</OptionsFinanceMonthlyPayment>
</Vehicle>
</Dealer>
As I said, there is loads of other data in there, but that's the basic structure.
And my XSLT looks like this:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >
<xsl:output method="html" omit-xml-declaration="yes" indent="yes" version="4.0" encoding="iso-8859-1" />
<xsl:key name="by-id" match="Dealer/Vehicle" use="Model"/>
<xsl:template match="Dealer">
<xsl:copy>
<xsl:for-each select="Vehicle[generate-id() = generate-id(key('by-id', Model)[1])]">
<xsl:for-each select="key('by-id', Model)">
<xsl:sort select="DealerPriceNoFormat" data-type="number" order="ascending" />
<xsl:if test="position()=1">
<p>
<xsl:value-of select="Model" /><br />
<xsl:value-of select="DealerPriceNoFormat" /><br />
<xsl:value-of select="OptionsFinanceMonthlyPayment" />
</p>
</xsl:if>
</xsl:for-each>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Like I say, I'm almost there, just can't figure out how to then sort the output list by OptionsFinanceMonthlyPayment.
So the in the case above output would look something like this, showing the cheapest car in each model, but sorted by the monthly payment on the output list:
KA
8700.00
300.50
Focus
12000.00
340.00
KUGA
23010.00
550.20
Thanks in advance.
I would do this in two passes - something like:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:key name="vehicle-by-model" match="Vehicle" use="Model"/>
<xsl:template match="/Dealer">
<!-- first-pass -->
<xsl:variable name="groups">
<xsl:for-each select="Vehicle[generate-id() = generate-id(key('vehicle-by-model', Model)[1])]">
<group>
<xsl:for-each select="key('vehicle-by-model', Model)">
<xsl:sort select="DealerPriceNoFormat" data-type="number" order="ascending" />
<xsl:if test="position()=1">
<model><xsl:value-of select="Model" /></model>
<price><xsl:value-of select="DealerPriceNoFormat" /></price>
<pmt><xsl:value-of select="OptionsFinanceMonthlyPayment" /></pmt>
</xsl:if>
</xsl:for-each>
</group>
</xsl:for-each>
</xsl:variable>
<!-- output -->
<html>
<xsl:for-each select="exsl:node-set($groups)/group">
<xsl:sort select="pmt" data-type="number" order="ascending" />
<p>
<xsl:value-of select="model" /><br />
<xsl:value-of select="price" /><br />
<xsl:value-of select="pmt" />
</p>
</xsl:for-each>
</html>
</xsl:template>
</xsl:stylesheet>
Note that this requires a processor that supports a node-set() extension function.

XSL cross reference

I am stuck with a XSLT 1.0 problem. I tried to find info on StackOverflow but I couldn't apply the examples.
Here is the structure of my XML:
<XML>
<PR>
<AS>
<ID_AS>AS-001</ID_AS>
<FIRST>
<ID_CATALOG>Id-001</ID_CATALOG>
<STATUS>NOK</STATUS>
</FIRST>
<SECOND>
<ID_CATALOG>Id-002</ID_CATALOG>
<STATUS>OK</STATUS>
</SECOND>
</AS>
<AS>
<ID_AS>AS-002</ID_AS>
<FIRST>
<ID_CATALOG>Id-003</ID_CATALOG>
<STATUS>OK</STATUS>
</FIRST>
<SECOND>
<ID_CATALOG>Id-004</ID_CATALOG>
<STATUS>OK</STATUS>
</SECOND>
</AS>
</PR>
<METADATA>
<ID_CATALOG>Id-001</ID_CATALOG>
<ANGLES>32.25</ANGLES>
</METADATA>
<METADATA>
<ID_CATALOG>Id-002</ID_CATALOG>
<ANGLES>18.75</ANGLES>
</METADATA>
<METADATA>
<ID_CATALOG>Id-003</ID_CATALOG>
<ANGLES>5.23</ANGLES>
</METADATA>
<METADATA>
<ID_CATALOG>Id-004</ID_CATALOG>
<ANGLES>12.41</ANGLES>
</METADATA>
</XML>
I want to display for each AS, the FIRST/ID_CATALOG, FIRST/STATUS and ANGLES corresponding to the ID_CATALOG, then SECOND/etc.
The output would be similar to:
AS-001
Id-001 NOK 32.25
Id-002 OK 18.75
AS-002
Id-003 OK 5.23
Id-004 OK 12.41
I tried the following XSL but I only get the ANGLES for the first item
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0" xmlns="http://earth.google.com/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:hma="http://earth.esa.int/hma" xmlns:gml="http://www.opengis.net/gml" xmlns:xlink="http://www.w3.org/1999/xlink">
<xsl:output method="xml" indent="yes" encoding="ISO-8859-1"/>
<!--==================MAIN==================-->
<xsl:template match="/">
<html>
<body>
AS List:
<br/><br/>
<xsl:call-template name="ASandCo"/>
</body>
</html>
</xsl:template>
<!--==================TEMPLATES==================-->
<xsl:template name="ASandCo">
<AS>
<xsl:for-each select="XML/PR/AS">
<xsl:value-of select="ID_AS"/>
<br/>
<xsl:value-of select="FIRST/ID_CATALOG"/> - <xsl:value-of select="FIRST/STATUS"/> -
<xsl:if test="contains(/XML/METADATA/ID_CATALOG, FIRST/ID_CATALOG)">
<xsl:value-of select="/XML/METADATA/ANGLES"/>
</xsl:if>
<br/>
<xsl:value-of select="SECOND/ID_CATALOG"/> - <xsl:value-of select="SECOND/STATUS"/> -
<xsl:if test="contains(/XML/METADATA/ID_CATALOG, SECOND/ID_CATALOG)">
<xsl:value-of select="/XML/METADATA/ANGLES"/>
</xsl:if>
<br/><br/>
</xsl:for-each>
</AS>
</xsl:template>
</xsl:stylesheet>
This XSLT will be applied to very large XML files, so I am trying to find the most efficient way.
Thank you very much in advance!
It seems like you want to look up some metadata metadata based on the ID_CATALOG value.
An efficient way to do this is by using a key. You can define a key on the top level:
<xsl:key name="metadata-by-id_catalog" match="METADATA" use="ID_CATALOG"/>
And then you can look up the ANGLES value using the key for a given ID_CATALOG value like this:
<xsl:value-of select="key('metadata-by-id_catalog', FIRST/ID_CATALOG)/ANGLES"/>
and this:
<xsl:value-of select="key('metadata-by-id_catalog', SECOND/ID_CATALOG)/ANGLES"/>

XSLT - Grouping same items

I have the following XML:
<Info>
<Name>Dan</Name>
<Age>24</Age>
</Info>
<Info>
<Name>Tom</Name>
<Age>15</Age>
</Info>
<Info>
<Name>Dan</Name>
<Age>24</Age>
</Info>
<Info>
<Name>James</Name>
<Age>18</Age>
</Info>
And I need to produce the following HTML:
<ul class="data">
<li>Dan</li>
<li>Dan</li>
</ul>
<ul class="data">
<li>James</li>
</ul>
<ul class="data">
<li>Tom</li>
<li>Tom</li>
</ul>
As well as producing the output it needs to sort based on the Name also. Any help appreciated, started by looking at group-by but couldnt work out how to get it finished:
Pretty sure its wrong?
<xsl:for-each-group select="Info" group-by="#Name">??????
<xsl:for-each-group select="current-group()" sort-by="#Name">
I don't have an XSLT 2.0 parser, but to do it in XSLT 1.0 at least you could use Muenchian Grouping to do this...
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output encoding="utf-8" method="html" version="1.0"/>
<xsl:key name="Names" match="Name" use="."/>
<xsl:template match="/">
<xsl:for-each select="//Info[generate-id(Name) = generate-id(key('Names', Name)[1])]">
<xsl:sort select="Name" />
<ul class="data">
<xsl:for-each select="key('Names', Name)">
<li><xsl:value-of select="." /></li>
</xsl:for-each>
</ul>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
I'm not sure why your result has two 'Tom' elements, I assume you have an extra node in the XML that you didn't provide in your sample.
Anyway, the XSLT would look something like this:
<xsl:for-each-group select="Info" group-by="Name">
<xsl:sort select="current-grouping-key()"/>
<ul class="data">
<xsl:for-each select="current-group()/Name">
<li><xsl:value-of select="." /></li>
</xsl:for-each>
</ul>
</xsl:for-each-group>
I don't have an XSLT 2.0 parser handy to test it, but I think that should work.
I don't have an xslt 2.0 parser to test this, but at the very least you will need to change "#Name" to just "Name", since it is a subelement not an attribute.