XSLT: Grouping and sorting....how? - xslt

I have an XML file that looks like the following...
<states>
<state>
<name>North Carolina</name>
<city>Charlotte</city>
</state>
<state>
<name>Alaska</name>
<city>Fairbanks</city>
</state>
<state>
<name>Virginia</name>
<city>Leesburg</city>
</state>
<state>
<name>Alaska</name>
<city>Coldfoot</city>
</state>
<state>
<name>North Carolina</name>
<city>Harrisburg</city>
</state>
<state>
<name>Virginia</name>
<city>Ashburn</city>
</state>
</states>
I need to produce a report that lists each state, is alphabetical order with each city following.... such as ..
Alaska - Fairbanks, Coldfoot
North Carolina - Charlotte, Harrisburg
Virginia - Leesburg, Ashburn
(the cities do not have to be in alpha order, just the states)
I tried to solve this by doing a for-each on states/state, sorting it by name and processing it. Like this....
<xsl:for-each select="states/state">
<xsl:sort select="name" data-type="text" order="ascending"/>
<xsl:value-of select="name"/>-<xsl:value-of select="city"/>
</xsl:for-each>
This gave me....
Alaska - Fairbanks
Alaska - Coldfoot
North Carolina - Charlotte
North Carolina - Harrisburg
Virginia - Leesburg
Virginia - Ashburn
The sorting worked, now I want to group. The only thing I could think to do was to compare to the previous state, since it is sorted, it should recognize if the state value has not changed. Like this...
<xsl:for-each select="states/state">
<xsl:sort select="name" data-type="text" order="ascending"/>
<xsl:variable name="name"><xsl:value-of select="name">
<xsl:variable name="previous-name"><xsl:value-of select="(preceding-sibling::state)/name">
<xsl:if test="$name != $previous-name">
<br/><xsl:value-of select="name"/>-
</xsl:if>
<xsl:value-of select="city"/>
</xsl:for-each>
Sadly, it appears that the preceding-sibling feature does not work well with the sort, so, the first time through (on the first Alaska) it saw the first North Carolina as a preceding sibling. This causes some weird results, which were not at all to my liking.
So, I am using XSLT1.0... Any thoughts/suggestions?
Thanks

This stylesheet:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:key name="kStateByName" match="state" use="name"/>
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:apply-templates
select="/*/state[count(.|key('kStateByName',name)[1])=1]">
<xsl:sort select="name"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="state">
<xsl:value-of select="concat(name,' - ')"/>
<xsl:apply-templates select="key('kStateByName',name)/city"/>
</xsl:template>
<xsl:template match="city">
<xsl:value-of select="concat(.,substring(', ',
1 div (position()!=last())),
substring('
',
1 div (position()=last())))"/>
</xsl:template>
</xsl:stylesheet>
Output:
Alaska - Fairbanks, Coldfoot
North Carolina - Charlotte, Harrisburg
Virginia - Leesburg, Ashburn
Note: Grouping by State's name. Separator substring expression only works with a pull style (applying templates to city)
An XSLT 2.0 solution:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="states">
<xsl:for-each-group select="state" group-by="name">
<xsl:sort select="name"/>
<xsl:value-of select="concat(name,
' - ',
string-join(current-group()/city,', '),
'
')"/>
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>
Just for fun, this XPath 2.0 expression:
string-join(for $state in distinct-values(/*/*/name)
return concat($state,
' - ',
string-join(/*/*[name=$state]/city,
', ')),
'
')

For grouping in XSLT 1.0 you are probably going to have to use the Muenchian Method. It can be hard to understand but once you get it working you should be good to go.

This will return a distinct list of states:
<xsl:for-each select="states/state">
<xsl:sort select="name" />
<xsl:if test="not(name = preceding-sibling::state/name)" >
<xsl:value-of select="name" />
</xsl:if>
</xsl:for-each>
I used your example XML, built a little style sheet with the above, ran it through Xalan-j, and it returns:
Alaska
North Carolina
Virginia
So from there you should be able to apply a template or another for-each loop to pull the list of cities for each distinct state.
Chris

Related

How get first name initial and last name in XSLT

I want yo get the first name initial and last name.
Input :
<root>
<ele name="Samp Huwani"/>
<ele name="Gong Gitry"/>
<ele name="Dery Wertnu"/>
</root>
Output
<names>S Huwani</name>
<names>G Gitry</name>
<names>D Wertnu</name>
Tried Code:
<xsl:template match="root/name">
<names>
<xsl:value-of select="#name" />
</name>
</xsl:template>
I am using XSLT 2.0 . Thank you
With the given example, you could use:
<xsl:template match="/root">
<xsl:copy>
<xsl:for-each select="ele">
<name>
<xsl:value-of select="substring(#name, 1, 1)"/>
<xsl:text> </xsl:text>
<xsl:value-of select="substring-after(#name, ' ')"/>
</name>
</xsl:for-each>
</xsl:copy>
</xsl:template>
However, names often do not conform to the same pattern.
In XSLT 2.0, you could simplify(?) this by using regex, e.g.:
<xsl:value-of select="replace(#name, '^(.{1}).* (.*)', '$1 $2')"/>

Better way to cycle xsl:for-each letter of the alphabet?

I have a long XML file from which I ned to pull out book titles and other information, then sort it alphabetically, with a separator for each letter. I also need a section for items that don't begin with a letter, say a number or symbol. Something like:
#
1494 - hardcover, $9.99
A
After the Sands - paperback, $24.95
Arctic Spirit - hardcover, $65.00
B
Back to the Front - paperback, $18.95
…
I also need to create a separate list of authors, created from the same data but showing different kinds of information.
How I'm currently doing it
This is simplified, but I basically have this same code twice, once for titles and once for authors. The author version of the template works with different elements and does different things with the data, so I can't use the same template.
<xsl:call-template name="BIP-letter">
<xsl:with-param name="letter" select="'#'" />
</xsl:call-template>
<xsl:call-template name="BIP-letter">
<xsl:with-param name="letter" select="'A'" />
</xsl:call-template>
…
<xsl:call-template name="BIP-letter">
<xsl:with-param name="letter" select="'Z'" />
</xsl:call-template>
<xsl:template name="BIP-letter">
<xsl:param name="letter" />
<xsl:choose>
<xsl:when test="$letter = '#'">
<xsl:text>#</xsl:text>
<xsl:for-each select="//Book[
not(substring(Title,1,1) = 'A') and
not(substring(Title,1,1) = 'B') and
…
not(substring(Title/,1,1) = 'Z')
]">
<xsl:sort select="Title" />
<xsl:appy-templates select="Title" />
<!-- Add other relevant data here -->
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$letter" />
<xsl:for-each select="//Book[substring(Title,1,1) = $letter]">
<xsl:sort select="Title" />
<xsl:appy-templates select="Title" />
<!-- Add other relevant data here -->
</xsl:for-each>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
My questions
The code above works just fine, but:
Manually cycling through each letter gets very long, especially having to do it twice. Is there a way to simplify that? Something like a <xsl:for-each select="[A-Z]"> that I could use to set the parameter when calling the template?
Is there a simpler way to select all titles that don't begin with a letter? Something like //Book[not(substring(Title,1,1) = [A-Z])?
There may be cases where the title or author name starts with a lowercase letter. In the code above, they would get grouped with under the # heading, rather than with the actual letter. The only way I can think to accommodate that—doing it manually—would significantly bloat up the code.
This solution answers all questions asked:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="vLowercase" select="'abcdefghijklmnopqrstuvuxyz'"/>
<xsl:variable name="vUppercase" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<xsl:variable name="vDigits" select="'0123456789'"/>
<xsl:key name="kBookBy1stChar" match="Book"
use="translate(substring(Title, 1, 1),
'abcdefghijklmnopqrstuvuxyz0123456789',
'ABCDEFGHIJKLMNOPQRSTUVWXYZ##########'
)"/>
<xsl:template match="/*">
<xsl:apply-templates mode="firstInGroup" select=
"Book[generate-id()
= generate-id(key('kBookBy1stChar',
translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)
)[1]
)
]">
<xsl:sort select="translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="Book" mode="firstInGroup">
<xsl:value-of select="'
'"/>
<xsl:value-of select="translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)"/>
<xsl:apply-templates select=
"key('kBookBy1stChar',
translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)
)">
<xsl:sort select="Title"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="Book">
<xsl:value-of select="'
'"/>
<xsl:value-of select="concat(Title, ' - ', Binding, ', $', price)"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the following xml document (none provided in the question!):
<Books>
<Book>
<Title>After the Sands</Title>
<Binding>paperback</Binding>
<price>24.95</price>
</Book>
<Book>
<Title>Cats Galore: A Compendium of Cultured Cats</Title>
<Binding>hardcover</Binding>
<price>5.00</price>
</Book>
<Book>
<Title>Arctic Spirit</Title>
<Binding>hardcover</Binding>
<price>65.00</price>
</Book>
<Book>
<Title>1494</Title>
<Binding>hardcover</Binding>
<price>9.99</price>
</Book>
<Book>
<Title>Back to the Front</Title>
<Binding>paperback</Binding>
<price>18.95</price>
</Book>
</Books>
the wanted, correct result is produced:
#
1494 - hardcover, $9.99
A
After the Sands - paperback, $24.95
Arctic Spirit - hardcover, $65.00
B
Back to the Front - paperback, $18.95
C
Cats Galore: A Compendium of Cultured Cats - hardcover, $5.00
Explanation:
Use of the Muenchian method for grouping
Use of the standard XPath translate() function
Using mode to process the first book in a group of books starting with the same (case-insensitive) character
Using <xsl:sort> to sort the books in alphabetical orser
The most problematic part is this:
I also need a section for items that don't begin with a letter, say a number or symbol.
If you have a list of all possible symbols that an item can begin with, then you can simply use translate() to convert them all to the # character. Otherwise it gets more complicated. I would try something like:
XSLT 1.0 (+ EXSLT node-set())
<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:output method="text" encoding="UTF-8"/>
<xsl:key name="book" match="Book" use="index" />
<xsl:template match="/Books">
<!-- first-pass: add index char -->
<xsl:variable name="books-rtf">
<xsl:for-each select="Book">
<xsl:copy>
<xsl:copy-of select="*"/>
<index>
<xsl:variable name="index" select="translate(substring(Title, 1, 1), 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')" />
<xsl:choose>
<xsl:when test="contains('ABCDEFGHIJKLMNOPQRSTUVWXYZ', $index)">
<xsl:value-of select="$index"/>
</xsl:when>
<xsl:otherwise>#</xsl:otherwise>
</xsl:choose>
</index>
</xsl:copy>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="books" select="exsl:node-set($books-rtf)/Book" />
<!-- group by index char -->
<xsl:for-each select="$books[count(. | key('book', index)[1]) = 1]">
<xsl:sort select="index"/>
<xsl:value-of select="index"/>
<xsl:text>
</xsl:text>
<!-- list books -->
<xsl:for-each select="key('book', index)">
<xsl:sort select="Title"/>
<xsl:value-of select="Title"/>
<xsl:text> - </xsl:text>
<xsl:value-of select="Binding"/>
<xsl:text>, </xsl:text>
<xsl:value-of select="Price"/>
<xsl:text>
</xsl:text>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
However, this still leaves the problem of items that begin with a diacritic, e.g. "Österreich" or say a Greek letter. Under this method they too will be clumped under #.
Unfortunately, the only good solution for this is to move to XSLT 2.0.
Demo: https://xsltfiddle.liberty-development.net/jyRYYjj/2

I want to sort parent node CommlCoverage , based on child's name of From Tag

we want to xsl-sort of from element in
<?xml version="1.0"?>
<General><CommlCoverage>
<Audit>
<AuditMtc>
<Term>
<From>2013-07-05</From>
<To>2014-06-02</To>
</Term>
</AuditMtc>
</Audit>
<Supplement>
<Audit>
<AuditMtc>
<Term>
<From>2013-07-02</From>
<To>2014-06-02</To>
</Term>
</AuditMtc>
</Audit>
</Supplement>
<Supplement>
<Audit>
<AuditMtc>
<Term>
<From>2013-01-02</From>
<To>2014-06-02</To>
</Term>
</AuditMtc>
</Audit>
</Supplement>
</CommlCoverage>
<CommlCoverage>
<Audit>
<AuditMtc><Term><From>2013-07-05</From><To>2014-06-02</To></Term></AuditMtc></Audit>
<Supplement><Audit><AuditMtc><Term><From>2013-07-02</From><To>2014-06-02</To></Term></AuditMtc></Audit></Supplement>
</CommlCoverage></General>
need to sorting based on date first tag Audit having different structure with date second Supplement tag having different structure with different date
My code:
<xsl:for-each select="CommlCoverage">
<xsl:sort select="From"/>
<xsl:for-each select="Audit">
<xsl:value-of select="AuditMtc/Term/From"/>
</xsl:for-each>
<xsl:for-each select="Supplement/Audit">
<xsl:value-of select="AuditMtc/Term/From"/>
</xsl:for-each>
</xsl:for-each>
We have update the desired output:
[2013-01-02 To 2014-06-02]
[2013-01-02 To 2014-06-02]
[2013-07-02 To 2014-06-02] [2013-07-02 To 2014-06-02] [2013-07-05 To 2014-06-02] [2013-07-05 To 2014-06-02]
You would probably start off by having a template to match the root element
<xsl:template match="/*">
Within this, you then just need to select the Term children in order of their From date, which could be at different levels. (This assumes the From element is always a direct child of Term
<xsl:apply-templates select=".//Term">
<xsl:sort select="From" />
</xsl:apply-templates>
Note that the syntax .//Term will search for Term elements at any level in the hierarchy below the current node.
Then, you just need a template to match the Term elements, and output the From and To values, like so
<xsl:template match="Term">
[<xsl:value-of select="From"/> to <xsl:value-of select="To" />]
</xsl:template>
Try this XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" />
<xsl:template match="/*">
<xsl:apply-templates select=".//Term">
<xsl:sort select="From" />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="Term">
[<xsl:value-of select="From"/> to <xsl:value-of select="To" />]
</xsl:template>
</xsl:stylesheet>
This outputs the following:
[2013-01-02 to 2014-06-02]
[2013-07-02 to 2014-06-02]
[2013-07-02 to 2014-06-02]
[2013-07-05 to 2014-06-02]
[2013-07-05 to 2014-06-02]
(This doesn't quite match your expected output, because your expected output shows six lots of From and To times, but only five are in your input).
Not sure if I'm right about the requirement, but, this is what the following code does:
Sorts "CommlCoverage" based on any //Term/From and then writes all //Term/From in sorted order.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >
<xsl:output method="text" indent="yes"/>
<xsl:template match="/General">
<xsl:for-each select="CommlCoverage">
<xsl:sort>
<xsl:for-each select="current()//Term/From">
<xsl:sort select="."/>
<xsl:if test="position() = 1">
<xsl:value-of select="."/>
</xsl:if>
</xsl:for-each>
</xsl:sort>
<xsl:apply-templates select="Audit | Supplement/Audit">
<xsl:sort select="AuditMtc/Term/From"/>
</xsl:apply-templates>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
<xsl:template match="Audit">
<xsl:value-of select="concat('[', AuditMtc/Term/From, ' To ', AuditMtc/Term/To, ']')"/>
</xsl:template>
</xsl:stylesheet>

simple loop in xslt

Having trouble figuring out a simple XSLT loop that counts and returns the name of the actor.
<stars>
<star ID="001">Leonardo DiCaprio</star>
<star ID="002">Matt Damon</star>
<star ID="003">Jack Nicholson</star>
</stars>
This is what I made to give the result I wanted but if there was a fourth or fifth actor I would need to add to the code.
<xsl:value-of select="stars/star[#ID='001']"/>
<xsl:text>, </xsl:text>
<xsl:value-of select="stars/star[#ID='002']"/>
<xsl:text>, </xsl:text>
<xsl:value-of select="stars/star[#ID='003']"/>
Basically I need the loop to display the name of the star separated by a comma. Any help is appreciated.
Use a template instead of looping. XSLT processors are optimized for template matching.
<xsl:template match="star">
<xsl:value-of select="." />
<xsl:if test="position() != last()">
<xsl:text>, </xsl:text>
</xsl:if>
</xsl:template>
You can use repetition instruction (without any worry about performance):
<xsl:template match="stars">
<xsl:value-of select="star[1]"/>
<xsl:for-each select="star[position()>1]">
<xsl:value-of select="concat(', ',.)"/>
</xsl:for-each>
</xsl:template>
gets:
Leonardo DiCaprio, Matt Damon, Jack Nicholson
This is probably one of the simplest transformations -- note that there is neither need for xsl:for-each nor for any explicit XSLT conditional instruction:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:template match="star[position() >1]">
<xsl:text>, </xsl:text><xsl:apply-templates/>
</xsl:template>
</xsl:stylesheet>
when applied on the provided source XML document:
<stars>
<star ID="001">Leonardo DiCaprio</star>
<star ID="002">Matt Damon</star>
<star ID="003">Jack Nicholson</star>
</stars>
the wanted, correct output is produced:
Leonardo DiCaprio, Matt Damon, Jack Nicholson

Building list with xslt

I am trying to build a list that parses my entire xml document. I need to list the numeric names then the alpha names. The list should look something like this.
6
6600 Training
6500 Training
A
Accelerated Training
T
Training
This is a snippet of the xml.
<courses>
<course>
<name>Accelerated Training</name>
</course>
<course>
<name>6600 Training</name>
</course>
<course>
<name>Training</name>
</course>
<course>
<name>6500 Training</name>
</course>
</courses>
This is the code I am currently using. I found this in another question on the site and have customized it somewhat. Currently it doesn't take into account my need for parsing by number and it also returns out of alphabetical order.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:variable name="vLower" select= "'abcdefghijklmnopqrstuvwxyz'"/>
<xsl:variable name="vUpper" select= "'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<xsl:key name="kTitleBy1stLetter" match="courses/course" use="substring(name,1,1)"/>
<xsl:template match="/*">
<xsl:for-each select="course [generate-id() = generate-id(key('kTitleBy1stLetter', substring(name,1,1)) [1] ) ]">
<xsl:variable name="v1st" select="substring(name,1,1)"/>
<h2><xsl:value-of select="$v1st"/></h2>
<div class="{translate($v1st, $vUpper, $vLower)}-content">
<ul>
<xsl:for-each select="key('kTitleBy1stLetter',$v1st)">
<li><xsl:value-of select="name"/></li>
</xsl:for-each>
</ul>
</div>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Basically you need to group by first letter and sort by <name>. You are on a good way with your Muenchian grouping approach already.
I would suggest an alternative that's a bit easier on the eye:
<xsl:key name="kInitial" match="course" use="substring(name, 1, 1)" />
<xsl:template match="courses">
<xsl:apply-templates select="course" mode="initial">
<xsl:sort select="name" />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="course" mode="initial">
<xsl:variable name="initial" select="substring(name, 1, 1)" />
<xsl:variable name="courses" select="key('kInitial', $initial)" />
<xsl:if test="generate-id() = generate-id($courses[1])">
<h2><xsl:value-of select="$initial"/></h2>
<ul>
<xsl:apply-templates select="$courses">
<xsl:sort select="name" />
</xsl:apply-templates>
</ul>
</xsl:if>
</xsl:template>
<xsl:template match="course">
<li>
<xsl:value-of select="name"/>
</li>
</xsl:template>
outputs:
<h2>6</h2>
<ul>
<li>6500 Training</li>
<li>6600 Training</li>
</ul>
<h2>A</h2>
<ul>
<li>Accelerated Training</li>
</ul>
<h2>T</h2>
<ul>
<li>Training</li>
</ul>
EDIT: For the sake of legibility I left out the upper-casing of the first letter. The correct key would be this (you can't use a variable in a key, hence the literal alphabet strings):
<xsl:key name="kInitial" match="course" use="
translate(
substring(name, 1, 1),
'abcdefghijklmnopqrstuvwxyz',
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
)
" />
The same goes of course for the $initial variable in the second template, but here you can in fact use variables again.
EDIT #2: Since sorting is case-sensitive as well, you can use the same expression:
<xsl:sort select="translate(substring(name, 1, 1), $vLower, $vUpper)" />
An XSLT 2.0 solution:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/*">
<xsl:for-each-group select="course"
group-by="upper-case(substring(name,1,1))">
<xsl:sort select="current-grouping-key()"/>
<xsl:sequence select=
"concat('
', current-grouping-key())"/>
<xsl:for-each select="current-group()">
<xsl:sort select="upper-case(name)"/>
<xsl:sequence select="concat('
', name)"/>
</xsl:for-each>
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>
When the above transformation is applied on the originally-provided XML document:
<courses>
<course>
<name>Accelerated Training</name>
</course>
<course>
<name>6600 Training</name>
</course>
<course>
<name>Training</name>
</course>
<course>
<name>6500 Training</name>
</course>
</courses>
the wanted result is produced (in text format for simplicity -- producing the Html is left as an exercise for the reader :)
6
6500 Training
6600 Training
A
Accelerated Training
T
Training
Do note:
The use of the <xsl:for-each-group> XSLT 2.0 instruction
The use of the current-grouping-key() and current-group() XSLT 2.0 functions.
The use of the upper-case() XPath 2.0 function
Well the numbers part is tricky if you want anything complex, but based on your ideal output all you're missing is a simple sort on your for-each:
<xsl:sort select="key('kTitleBy1stLetter', substring(name,1,1))" />
caveat: I make no claims about this being the best or only or otherwise method, merely that this works full stop, and uses what you already have.