XSLT - Grouping same items - xslt

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.

Related

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

Index of the node selected not based on his parent

I have an xml like this one:
<?xml version="1.0" encoding="WINDOWS-1252" ?>
<?xml-stylesheet type="text/xsl" href="test.xsl"?>
<root>
<something>
<cd>a1</cd>
<cd>a2</cd>
</something>
<another>
<cd>b1</cd>
<cd>b2</cd>
<cd>b3</cd>
<cd>b1</cd>
<cd>b2</cd>
<cd>b3</cd>
<cd>b1</cd>
<cd>b2</cd>
</another>
</root>
I'm selecting all the cd nodes and trying to take the cds from 10 to 10 (1st, 11th, 21th...) this way:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<html><body>
<xsl:apply-templates select="//cd" />
</body></html>
</xsl:template>
<xsl:template match="cd"/>
<xsl:template match="cd[position() mod 10 = 1]">
<div>
pos:<xsl:value-of select="position()"/>;
value:<xsl:value-of select="."/>
</div>
</xsl:template>
</xsl:stylesheet>
The output:
pos:1; value:a1
pos:3; value:b3
And the expected output:
pos:1; value:a1
pos:11; value:b11
The problem is that position() in the match is the node number based on his parent. So the position() of "b1" is 1 and not 3. Is there a function to know the index of the current node?
Thanks in advance.
//EDIT to extend the example
You can get a more accurate count by using xsl:number:
<xsl:template match="cd">
<div>pos: <xsl:number level="any"/></div>
</xsl:template>
You could also count the preceding cd elements:
<xsl:template match="cd">
<div>pos: <xsl:value-of select="count(preceding::cd)+1"/></div>
</xsl:template>
I prefer xsl:number, but I suppose it depends on how you're using the value. If you give a better example of what you're trying to do with that number, I can update my answer.
Example based on question update:
Input
<root>
<something>
<cd>a1</cd>
<cd>a2</cd>
</something>
<another>
<cd>b1</cd>
<cd>b2</cd>
<cd>b3</cd>
<cd>b1</cd>
<cd>b2</cd>
<cd>b3</cd>
<cd>b1</cd>
<cd>b2</cd>
</another>
<another>
<cd>c1</cd>
<cd>c2</cd>
<cd>c3</cd>
<cd>c1</cd>
<cd>c2</cd>
<cd>c3</cd>
<cd>c1</cd>
<cd>c2</cd>
</another>
</root>
XSLT 1.0
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<html><body>
<xsl:apply-templates select="//cd" />
</body></html>
</xsl:template>
<xsl:template match="cd"/>
<xsl:template match="cd[(count(preceding::cd)+1) mod 10 = 1]">
<div>
pos:<xsl:value-of select="position()"/>;
value:<xsl:value-of select="."/>
</div>
</xsl:template>
</xsl:stylesheet>
Output
<html>
<body>
<div>
pos:1;
value:a1
</div>
<div>
pos:11;
value:c1
</div>
</body>
</html>
The position() in the match of template filters "cd"s based on position in its parent:
<xsl:template match="cd[position() mod (10) = 1]">
and the position() inside the template definition,
<div>pos:<xsl:value-of select="position()"/></div>
gives you the cd's sequence which the template has been applied for.
Thus, "position() mod (10) = 1" filters only those "cd"s which are 1st, 11th, 21st, etc. children of their parents.
Hence, you can remove the filter from xsl:template's match and put it inside like this:
<xsl:template match="cd">
<div>pos:<xsl:if test="position() mod 10 = 1">
<xsl:value-of select="position()"/>
</xsl:if>
</div>
</xsl:template>
This will save all the CDs in a variable. In the for-each, the context does not depend on the parent nodes so you will get the position among all CDs:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:variable name="all-cds" select="//cd" />
<xsl:template match="root">
<xsl:for-each select="$all-cds">
<div>pos: <xsl:value-of select="position()"/></div>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

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: how to test for the existence of at least one qualified child node/#date

I want to test for events within a group that qualify for both city and current-date() so that I can output a header.
To find the city ($place eq //event/#city) seems to work. But I can't figure out how to express "some eventTime/#date is less than $today". The error message is "A sequence of more than one item is not allowed as the second operand of 'eq'" Which is confusing because I've written test="($place eq //event/#city) and (xs:date($today) lt xs:date(//eventTime/#date)).
How should I be comparing $today to the #date in my eventTime? Here's the input.
<calendar>
<group month="2012-04-01">
<event city="paris">
<eventTime date="2012-04-02"/>
<eventText>Paris - expired April date</eventText>
</event>
<event city="london">
<eventTime date="2012-04-19"/>
<eventText>London - current April 19 date</eventText>
</event>
<event city="london">
<eventTime date="2012-04-24"/>
<eventText>London - current April date</eventText>
</event>
</group>
<group month="2012-05-01">
<event city="london">
<eventTime date="2012-05-02"/>
<eventText>London - current May date</eventText>
</event>
<event city="paris">
<eventTime date="2012-05-01"/>
<eventText>Paris - current May date</eventText>
</event>
<event city="london">
<eventTime date="2012-05-02"/>
<eventText>London - current May date</eventText>
</event>
</group>
</calendar>
Here's the XSLT:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:fn="http://johnadamturnbull.com/xslt"
exclude-result-prefixes="xs"
version="2.0" >
<xsl:output method="html" indent="yes" name="html"/>
<xsl:param name="place" as="xs:string" required="yes"></xsl:param>
<xsl:variable name="today" select="current-date()" as="xs:date"/>
<xsl:template match="/">
<html>
<body>
<xsl:apply-templates select="calendar/group"/>
</body>
</html>
</xsl:template>
<xsl:template match = "group">
<xsl:if test="($place eq //event/#city) and
(xs:date($today) ge xs:date(//eventTime/#date))">
<h4 class = "dateHeader">
<xsl:value-of select="format-date(./#month,'[MNn] [Y]')"/>
</h4>
<ul>
<xsl:apply-templates select="event"></xsl:apply-templates>
</ul>
</xsl:if>
</xsl:template>
<xsl:template match="event">
<xsl:variable name="eventTime" select="eventTime/#date" as="xs:date"/>
<xsl:choose>
<xsl:when test="($eventTime ge $today) and
(($place eq #city) or (#city eq ''))">
<li>
<xsl:apply-templates></xsl:apply-templates>
</li>
</xsl:when>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
There isn't an example of what the HTML output is supposed to be, but I'm pretty sure I can tell what you're trying to achieve.
I think your XSLT can be simplified by removing the xsl:if and xsl:choose and adding predicates to do your testing.
This XSLT 2.0 stylesheet:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:fn="http://johnadamturnbull.com/xslt" exclude-result-prefixes="xs fn" version="2.0">
<xsl:output method="html" indent="yes" name="html"/>
<xsl:strip-space elements="*"/>
<xsl:param name="place" as="xs:string" required="yes"/>
<xsl:variable name="today" select="current-date()" as="xs:date"/>
<xsl:template match="/">
<html>
<body>
<xsl:apply-templates select="calendar/group"/>
</body>
</html>
</xsl:template>
<!--Match group if #city matches $place or is empty and has an eventTime
with a #date that is greater than or equal to today's date.-->
<xsl:template match="group[event[#city=$place or #city=''][xs:date(eventTime/#date) >= $today]]">
<h4 class="dateHeader">
<xsl:value-of select="format-date(#month,'[MNn] [Y]')"/>
</h4>
<ul>
<!--Only apply-templates to events that have a #city that matches $place
or has a #city that is empty.-->
<xsl:apply-templates select="event[#city=$place or #city='']"/>
</ul>
</xsl:template>
<!--Only match events that have an eventTime with a #date that is greater than
or equal to today's date.-->
<xsl:template match="event[#city=$place or #city=''][xs:date(eventTime/#date) >= $today]">
<li>
<xsl:apply-templates/>
</li>
</xsl:template>
<xsl:template match="event"/>
</xsl:stylesheet>
applied to your example XML input produces this HTML output:
<html>
<body>
<h4 class="dateHeader">April 2012</h4>
<ul>
<li>London - current April 19 date</li>
<li>London - current April date</li>
</ul>
<h4 class="dateHeader">May 2012</h4>
<ul>
<li>London - current May date</li>
<li>London - current May date</li>
</ul>
</body>
</html>
If this isn't what you're looking for, please add an example of what the HTML output should look like.

create HTML from a list of XML nodes using XSLT

I am a noob on XSLT.
I have a XML where t nodes are followed by other nodes, and then another t node might appear again followed by nodes again, and so on
<t />
<n1 />
<n2 />
..
<t/>
<n3 />
<n4 />
...
What I need to turn this XML into is a HTML where t nodes wraps all nodes following it up to the next t node
<div class='t'>
<div class='n1'/>
<div class='n2'/>
...
</div>
<div class='t'>
<div class='n3'/>
<div class='n4'/>
...
</div>
I am having a hard time implementing this.
Any ideas \ hints?
Thanks!
This is grouping adjacents. There are many solutions:
Whit this wellformed input:
<root>
<t />
<n1 />
<n2 />
<t/>
<n3 />
<n4 />
</root>
XSLT 1.0: traversing with following axis
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:strip-space elements="*"/>
<xsl:template match="root">
<xsl:copy>
<xsl:apply-templates select="node()[1]" mode="group"/>
<xsl:apply-templates select="t"/>
</xsl:copy>
</xsl:template>
<xsl:template match="t">
<div class="t">
<xsl:apply-templates select="following-sibling::node()[1]"
mode="group"/>
</div>
</xsl:template>
<xsl:template match="t" mode="group"/>
<xsl:template match="node()" mode="group">
<xsl:apply-templates select="."/>
<xsl:apply-templates select="following-sibling::node()[1]"
mode="group"/>
</xsl:template>
<xsl:template match="*[starts-with(name(),'n')]">
<div class="{name()}"/>
</xsl:template>
</xsl:stylesheet>
XSLT 1.0: Keys
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:strip-space elements="*"/>
<xsl:key name="kNodeByMark"
match="node()[../t][not(self::t)]"
use="generate-id((..|preceding-sibling::t[1])[last()])"/>
<xsl:template match="root">
<xsl:copy>
<xsl:apply-templates select="key('kNodeByMark',generate-id())"/>
<xsl:for-each select="t">
<div class="t">
<xsl:apply-templates
select="key('kNodeByMark',generate-id())"/>
</div>
</xsl:for-each>
</xsl:copy>
</xsl:template>
<xsl:template match="*[starts-with(name(),'n')]">
<div class="{name()}"/>
</xsl:template>
</xsl:stylesheet>
XSLT 2.0: for-each-group instruction
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:strip-space elements="*"/>
<xsl:template match="root">
<xsl:copy>
<xsl:apply-templates select="node()[../t[1] >> .]"/>
<xsl:for-each-group select="node()" group-starting-with="t">
<div class="t">
<xsl:apply-templates
select="current-group()[position()>1]"/>
</div>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>
<xsl:template match="*[starts-with(name(),'n')]">
<div class="{name()}"/>
</xsl:template>
</xsl:stylesheet>
Output:
<root>
<div class="t">
<div class="n1" />
<div class="n2" />
</div>
<div class="t">
<div class="n3" />
<div class="n4" />
</div>
</root>
EDIT: Traversing following axis refactored to look like the others solutions. Stripping identity rules.
See my note on your question, regarding "which XSLT version?". If grouping is supported in your target version, see other answers here, as that is easier to understand and will almost certainly perform better on any XSLT processor. If you aren't certain, I recommend going with a 1.0 solution like this one.
You can do it with the "XML fragment" exactly like you posted with most XSLT processors, but I added a "root" element to your XML, to reduce certain unknowns in answering your question.
In this solution below, I've tried to keep a direct correlation between the shape of the XSLT and the shape of the output you desire. In my opinion that makes it easier to maintain/understand, at least for smaller stylesheets.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/root">
<xsl:for-each select="t">
<div class='t'>
<xsl:for-each select="following-sibling::*[count(preceding-sibling::t)=(count(current()/preceding-sibling::t) + 1) and not(self::t)]">
<div class='{name()}' />
</xsl:for-each>
</div>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
The right-hand side of "following-sibling::*[count(preceding-sibling::t)=(count(current()/preceding-sibling::t) + 1) and not(self::t)]" could be simplified, I'm sure, using something like "current()::position()" (which isn't valid, fyi), but I'm rusty and couldn't remember some of the alias syntax.
This basically says: 1) Evaluate every T. 2) Select elements with the same quantity of T preceding them, as the index of the T we are currently evaluating.
Note that you've probably tried iterating through procedurally, and found you can't store the last value found in XSLT. Or you've found that you can, but only with nested templates. This same type of pivot you are performing has many XSLT neophytes hitting roadblocks, so don't feel bad.