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.
Related
I just need some help on basic feature of XSL.
I would like to display a sum of amounts previously computed. But I do not know how to do it.
For information the XSL must work with XSLT 1.0, technical limitation on my side.
For instance here is my xml.
<A>
<amount>10</amount>
<rate>4</rate>
</A>
<A>
<amount>-21</amount>
<rate>2</rate>
</A>
<B>
<amount>8</amount>
<rate>1</rate>
</B>
<C>
<amount>7</amount>
<rate>32</rate>
</C>
and I would like to display the sum of each amount multiplied by each associated rate within a Total element.
<Total value="230">
<PositiveTotal>
272
</PositiveTotal>
<NegativeTotal>
-42
</NegativeTotal>
</Total>
I have no idea how to do it.
Thanks in advance
Regards,
One of possible multiple solutions. It will give you an idea, how to solve this.
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="root">
<xsl:variable name="positiveTotal">
<xsl:call-template name="sum">
<xsl:with-param name="items" select="*[not(starts-with(amount,'-') or starts-with(rate, '-'))]"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="negativTotal">
<xsl:call-template name="sum">
<xsl:with-param name="items" select="*[starts-with(amount,'-') or starts-with(rate, '-')]"/>
</xsl:call-template>
</xsl:variable>
<Total value="{$positiveTotal + $negativTotal}">
<PositivTotal>
<xsl:value-of select="format-number($positiveTotal, '0')"/>
</PositivTotal>
<NegativeTotal>
<xsl:value-of select="format-number($negativTotal, '0')"/>
</NegativeTotal>
</Total>
</xsl:template>
<xsl:template name="sum">
<xsl:param name="items" />
<xsl:param name="total" select="0" />
<xsl:choose>
<xsl:when test="not($items)">
<xsl:value-of select="$total"/>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="sum">
<xsl:with-param name="items" select="$items[position() > 1]" />
<xsl:with-param name="total"
select="$total + ($items[1]/amount * $items[1]/rate)" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
!! Change match="root" to your root-node! Given source-xml is not valid.
There are already many sum-questions! See the Related Box on your right side of screen.
This question has been asked many times and following the links to similar searches on SO should give you lots of ideas.
Computing the sum of computed values in XSLT 2.0 is trivial, but in XSLT 1.0 it isn't easy because there's no such data type in its data model as a set of numbers (sum() only works over a set of nodes). Possible solutions include:
(a) a recursive template call which supplies the running total as a parameter to the template, adds the next value, then calls the template again to process the rest of the list with the new running total
(b) a multiphase transformation where the computed values are placed in XML nodes during one phase, and summed using the sum() function in a second phase
(c) use of the FXSL library which uses xsl:apply-templates to simulate higher-order functions and then provides a fold mechanism which can be specialised to implement summation.
(d) calling out to extension functions in a procedural programming language
(e) upgrading to XSLT 2.0.
I would suggest you do it this way:
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:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/root">
<!-- first pass -->
<xsl:variable name="summands-rtf">
<xsl:for-each select="*">
<value>
<xsl:value-of select="amount * rate" />
</value>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="summands" select="exsl:node-set($summands-rtf)/value" />
<!-- output -->
<Total value="{sum($summands)}">
<PositiveTotal>
<xsl:value-of select="sum($summands[. > 0])" />
</PositiveTotal>
<NegativeTotal>
<xsl:value-of select="sum($summands[. < 0])" />
</NegativeTotal>
</Total>
</xsl:template>
</xsl:stylesheet>
When applied to a well-formed XML input (with a single root element):
XML
<root>
<A>
<amount>10</amount>
<rate>4</rate>
</A>
<A>
<amount>-21</amount>
<rate>2</rate>
</A>
<B>
<amount>8</amount>
<rate>1</rate>
</B>
<C>
<amount>7</amount>
<rate>32</rate>
</C>
</root>
the result will be:
<?xml version="1.0" encoding="UTF-8"?>
<Total value="230">
<PositiveTotal>272</PositiveTotal>
<NegativeTotal>-42</NegativeTotal>
</Total>
I need to get a value which is coming from two different nodes in the same XML file. For instance, my xml:
<asset>
<curr_wanted>EUR</curr_wanted>
<curr>USD</curr>
<value>50</value>
</asset>
<exchangeRates>
<USD>
<USD>1</USD>
<EUR>0.73</EUR>
</USD>
</exchangeRates>
and I want to get equivalent of 50 Dollars in Euro.
I tried :
<xsl:value-of select="(Asset/value * /exchangeRates[node() = curr]/curr_wanted)"/>
But it didn't work. Also I have to use XSLT 1.0. How can I get that value in Euro?
I did not test it very much but for input like
<?xml version="1.0" encoding="UTF-8"?>
<root>
<asset>
<curr_wanted>EUR</curr_wanted>
<curr>USD</curr>
<value>50</value>
</asset>
<asset>
<curr_wanted>EUR</curr_wanted>
<curr>USD</curr>
<value>25</value>
</asset>
<exchangeRates>
<USD>
<USD>1</USD>
<EUR>0.73</EUR>
</USD>
</exchangeRates>
</root>
something like following could work
for $asset in /root/asset, $rate in /root/exchangeRates
return $asset/value*$rate/*[name() = $asset/curr]/*[name() = $asset/curr_wanted]
But it will work only in xpath 2.0 and it also depends on the whole input xml (like if there might exist more asset elements, more exchangeRates elements, etc.).
Edit: In xslt 1.0 you could use xsl:variable to store some information and prevent them from context changes during xpath evaluation. Look for example at following template
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format">
<xsl:output method="text" />
<!-- Store "exchangeRates" in a global variable-->
<xsl:variable name="rates" select="/root/exchangeRates" />
<xsl:template match="/root">
<xsl:apply-templates select="asset" />
</xsl:template>
<xsl:template match="asset">
<!-- Store necessary values into local variables -->
<xsl:variable name="currentValue" select="value" />
<xsl:variable name="currentCurrency" select="curr" />
<xsl:variable name="wantedCurrency" select="curr_wanted" />
<xsl:variable name="rate" select="$rates/*[name() = $currentCurrency]/*[name() = $wantedCurrency]" />
<!-- Some text to visualize results -->
<xsl:value-of select="$currentValue" />
<xsl:text> </xsl:text>
<xsl:value-of select="$currentCurrency" />
<xsl:text> = </xsl:text>
<!-- using variable to prevent context changes during xpath evaluation -->
<xsl:value-of select="$currentValue * $rate" />
<!-- Some text to visualize results -->
<xsl:text> </xsl:text>
<xsl:value-of select="$wantedCurrency" />
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
which produces following output for input xml above.
50 USD = 36.5 EUR
25 USD = 18.25 EUR
In an input XML file, along with Static Columns, columns expecting data from other files (reference)is also available.
But for each reference, the input xml has separate row with same ID or UID.
The output file has to have all references and relations in one row (based on the ID or UID)
I wrote the XSLT for this transformation also. This XSLT is faster when the row count is less (< 100 or < 200). But, as the count grows, the output xml generation taking long time (for count of 1000 rows, around 30 mins).
I am using
<xsl:for-each select="z:row/#ID[generate-id() = generate-id(key('UniqueID',.))]">
in the XSLT. Because for the same ID in each row of input xml, it has to check for multiple references (like section) and relations (like Child) and populate the same as columns
Input Raw XML File.
<xml xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" xmlns:rs="urn:schemas-microsoft-com:rowset" xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" xmlns:z="#RowsetSchema">
<rs:data>
<z:row UID="PARENT_001_1221AD_A878" GroupID="" GroupRel="" ID="37" Name="Outer Asset Details" RelProduct="Line1" RelUID="CHILD1_101_9899_9POOU99" RelName="CHILD1" RelType="Child" Size="22"/>
<z:row UID="PARENT_001_1221AD_A878" GroupID="" GroupRel="" ID="37" Name="Outer Asset Details" RelProduct="Line1" RelUID="CHILD2_201_5646546_9890PBS" RelName="CHILD1" RelType="Child" Size="22"/>
<z:row UID="PARENT_001_1221AD_A878" GroupID="" GroupRel="" ID="37" Name="Outer Asset Details" RelProduct="Line1" RelUID="SEC_999_99565_998AFSD" RelName="Hydraulic Section" RelType="Section" Size="22"/>
</rs:data>
Child.xml
<Child xsi:noNamespaceSchemaLocation="../XSD/Child.xsd" FILE="Child" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Row UID="CHILD1_101_9899_9POOU99">
<Name>CHILD1</Name>
<Description>This has details about the Hydraulic sections of the automobile</Description>
</Row>
<Row UID="CHILD2_201_5646546_9890PBS">
<Name>CHILD2</Name>
<Description>This has details about the manual sections of the automobile</Description>
</Row>
Section.xml
<Section xsi:noNamespaceSchemaLocation="../XSD/Section.xsd" FILE="Section" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Row UID="SEC_999_99565_998AFSD">
<Name>Hydraulic Section</Name>
<Description>This has details about the Sections in which the Hydraulic Systems are used.</Description>
</Row>
XSLT File
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:s="uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882" xmlns:dt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882" xmlns:rs="urn:schemas-microsoft-com:rowset" xmlns:z="#RowsetSchema" exclude-result-prefixes="s dt z rs msxsl" xmlns:msxsl="urn:schemas-microsoft-com:xslt">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" omit-xml-declaration="yes"/>
<xsl:key name="UniqueID" match="z:row/#ID" use="."/>
<xsl:template match="/">
<Parent xsi:noNamespaceSchemaLocation="../XSD/Parent.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" FILE="Parent">
<xsl:for-each select="xml">
<xsl:apply-templates select="rs:data"/>
</xsl:for-each>
</Parent>
</xsl:template>
<xsl:template match="rs:data">
<xsl:for-each select="z:row/#ID[generate-id() = generate-id(key('UniqueID',.))]">
<xsl:variable name="FRId">
<xsl:value-of select="current()"/>
</xsl:variable>
<xsl:variable name="curNSet" select="//z:row[#ID=$FRId]"/>
<xsl:copy-of select="current()"/>
<Record>
<xsl:attribute name="UID"><xsl:value-of select="$curNSet/#UID"/></xsl:attribute>
<xsl:element name="Size">
<xsl:value-of select="$curNSet/#Size"/>
</xsl:element>
<xsl:element name="Child">
<xsl:apply-templates select="$curNSet[#RelType='Child']" mode="Relations">
<xsl:with-param name="RelType" select="'Child'"/>
<xsl:with-param name="DstFileName" select="'../Files/Child.xml'"/>
</xsl:apply-templates>
</xsl:element>
<xsl:element name="Section">
<xsl:apply-templates select="$curNSet[#RelType='Section']" mode="References">
<xsl:with-param name="RelType" select="'Section'"/>
<xsl:with-param name="DstFileName" select="'../Files/Section.xml'"/>
</xsl:apply-templates>
</xsl:element>
</Record>
</xsl:for-each>
</xsl:template>
<xsl:template match="z:row" mode="Relations">
<xsl:param name="RelType"/>
<xsl:param name="DstFileName"/>
<xsl:element name="{$RelType}">
<xsl:attribute name="DestinationKey"><xsl:value-of select="#RelUID"/></xsl:attribute>
<xsl:attribute name="RelFilePath"><xsl:value-of select="$DstFileName"/></xsl:attribute>
<xsl:attribute name="SequenceNumber"><xsl:value-of select="position()"/></xsl:attribute>
<xsl:value-of select="#RelName"/>
</xsl:element>
</xsl:template>
<xsl:template match="z:row" mode="References">
<xsl:param name="DstFileName"/>
<xsl:attribute name="DestinationKey"><xsl:value-of select="#RelUID"/></xsl:attribute>
<xsl:attribute name="RelFilePath"><xsl:value-of select="$DstFileName"/></xsl:attribute>
<xsl:attribute name="SequenceNumber"><xsl:value-of select="position()"/></xsl:attribute>
<xsl:value-of select="#RelName"/>
</xsl:template>
Output.xml
<Parent xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../XSD/Parent.xsd" FILE="Parent" ID="37">
<Record UID="PARENT_001_1221AD_A878">
<Size>22</Size>
<Child>
<Child DestinationKey="CHILD1_101_9899_9POOU99" RelFilePath="../Files/Child.xml" SequenceNumber="1">CHILD1</Child>
<Child DestinationKey="CHILD2_201_5646546_9890PBS" RelFilePath="../Files/Child.xml" SequenceNumber="2">CHILD1</Child>
</Child>
<Section DestinationKey="SEC_999_99565_998AFSD" RelFilePath="../Files/Section.xml" SequenceNumber="1">Hydraulic Section</Section>
</Record>
Please help me in optimizing the XSLT, so that the output file is generated faster
Consider to use
<xsl:key name="UniqueID" match="z:row" use="#ID"/>
then
<xsl:for-each select="z:row/#ID[generate-id() = generate-id(key('UniqueID',.))]">
<xsl:variable name="FRId">
<xsl:value-of select="current()"/>
</xsl:variable>
<xsl:variable name="curNSet" select="//z:row[#ID=$FRId]"/>
can be replaced with
<xsl:for-each select="z:row[generate-id() = generate-id(key('UniqueID', #ID))]">
<xsl:variable name="FRId" select="#ID"/>
<xsl:variable name="curNSet" select="key('UniqueID', #ID"/>
I am not sure you need the variable FRId at all but defining it with a select attribute instead of a nested value-of is certainly consuming less resources.
To make
<xsl:apply-templates select="$curNSet[#RelType='Child']" mode="Relations">
more efficient define a key
<xsl:key name="rel" match="z:row" use="concat(#ID, '|', #RelType)"/>
then use
<xsl:apply-templates select="key('rel', concat(#ID, '|', 'Child')" mode="Relations">
Then use the same approach for the other apply-templates.
All of the above is untested but should give you an idea.
I would like to know how to replace the string with the abbreviations.
My XML looks like below
<concept reltype="CONTAINS" name="Left Ventricular Major Axis Diastolic Dimension, 4-chamber view" type="NUM">
<code meaning="Left Ventricular Major Axis Diastolic Dimension, 4-chamber view" value="18074-5" schema="LN" />
<measurement value="5.7585187646">
<code value="cm" schema="UCUM" />
</measurement>
<content>
<concept reltype="HAS ACQ CONTEXT" name="Image Mode" type="CODE">
<code meaning="Image Mode" value="G-0373" schema="SRT" />
<code meaning="2D mode" value="G-03A2" schema="SRT" />
</concept>
</content>
</concept>
and I am selecting some value from the xml like,
<xsl:value-of select="concept/measurement/code/#value"/>
Now what I want is, I have to replace cm with centimeter. I have so many words like this. I would like to have a xml for abbreviations and replace from them.
I saw one similar example here.
Using a Map in XSL for expanding abbreviations
But it replaces node text, but I have text as attribute. Also, it would be better for me If I can find and replace when I select text using xsl:valueof select instead of having a separate xsl:template. Please help. I am new to xslt.
I have created XSLT v "1.1". For abbreviations I have created XML file as you have mentioned:
Abbreviation.xml:
<Abbreviations>
<Abbreviation>
<Short>cm</Short>
<Full>centimeter</Full>
</Abbreviation>
<Abbreviation>
<Short>m</Short>
<Full>meter</Full>
</Abbreviation>
</Abbreviations>
XSLT:
<xsl:stylesheet version="1.1" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes" method="xml" />
<xsl:param name="AbbreviationDoc" select="document('Abbreviation.xml')"/>
<xsl:template match="/">
<xsl:call-template name="Convert">
<xsl:with-param name="present" select="concept/measurement/code/#value"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="Convert">
<xsl:param name="present"/>
<xsl:choose>
<xsl:when test="$AbbreviationDoc/Abbreviations/Abbreviation[Short = $present]">
<xsl:value-of select="$AbbreviationDoc/Abbreviations/Abbreviation[Short = $present]/Full"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$present"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
INPUT:
as you have given <xsl:value-of select="concept/measurement/code/#value"/>
OUTPUT:
centimeter
You just need to enhance this Abbreviation.xml to keep short and full value of abbreviation and call 'Convert' template with passing current value to get desired output.
Here a little shorter version:
- with abbreviations in xslt file
- make use of apply-templates with mode to make usage shorter.
But with xslt 1.0 node-set extension is required.
<?xml version="1.0" encoding="utf-8"?>
<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="xml" indent="yes"/>
<xsl:variable name="abbreviations_txt">
<abbreviation abbrev="cm" >centimeter</abbreviation>
<abbreviation abbrev="m" >meter</abbreviation>
</xsl:variable>
<xsl:variable name="abbreviations" select="exsl:node-set($abbreviations_txt)" />
<xsl:template match="/">
<xsl:apply-templates select="concept/measurement/code/#value" mode="abbrev_to_text"/>
</xsl:template>
<xsl:template match="* | #*" mode="abbrev_to_text">
<xsl:variable name="abbrev" select="." />
<xsl:variable name="long_text" select="$abbreviations//abbreviation[#abbrev = $abbrev]/text()" />
<xsl:value-of select="$long_text"/>
<xsl:if test="not ($long_text)">
<xsl:value-of select="$abbrev"/>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
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.