XSLT - Sum based on attribute values - xslt

I have following source XML
Source XML
<?xml version="1.0" encoding="UTF-16"?>
<PropertySet><SiebelMessage><ListOfXRX_spcUSCO_spcCOL_spcInvoice_spcAR_spcSummary>
<FS_spcInvoice
Type_spcCode="20"
XCS_spcINQ_spcPO_spcNumber="7500020052"
XCS_spcINQ_spcSerial_spcNumber="VDR551061"
XCS_spcINQ_spcCustomer_spcNumber="712246305"
XCS_spcINQ_spcInvoice_spcNumber="060853967"
Gross_spcAmount="747.06"
Invoice_spcDate="04/01/2012"></FS_spcInvoice>
<FS_spcInvoice
Type_spcCode="20"
XCS_spcINQ_spcPO_spcNumber="7500020052"
XCS_spcINQ_spcSerial_spcNumber="VDR551061"
XCS_spcINQ_spcCustomer_spcNumber="712346305"
XCS_spcINQ_spcInvoice_spcNumber="063853967"
Gross_spcAmount="947.06"
Invoice_spcDate="04/01/2013"></FS_spcInvoice>
</ListOfXRX_spcUSCO_spcCOL_spcInvoice_spcAR_spcSummary></SiebelMessage></PropertySet>
I need to generate sorted list of invoices in HTML format. I am able to do that with help following XSLT
XSLT
<?xml version="1.0" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >
<xsl:template match="/" >
<html><head></head><body>
<H3>Summary</H3>
<table>
<thead>
<tr><th>Invoice Number</th><th>Customer Number</th><th>Serial Number</th><th>PO Number</th><th>Invoice Date</th><th>Invoice Amount</th></tr>
</thead><tbody>
<xsl:apply-templates select="PropertySet/SiebelMessage/ListOfXRX_spcUSCO_spcCOL_spcInvoice_spcAR_spcSummary/FS_spcInvoice">
<xsl:sort select="#Gross_spcAmount" order="descending" />
</xsl:apply-templates>
<tr><td colspan="5">Total Amount</td><td></td></tr>
</tbody></table></body></html>
</xsl:template>
<xsl:template match='FS_spcInvoice'>
<tr>
<td><xsl:value-of select="#XCS_spcINQ_spcInvoice_spcNumber" /></td>
<td><xsl:value-of select="#XCS_spcINQ_spcCustomer_spcNumber" /></td>
<td><xsl:value-of select="#XCS_spcINQ_spcSerial_spcNumber" /></td>
<td><xsl:value-of select="#XCS_spcINQ_spcPO_spcNumber" /></td>
<td><xsl:value-of select="#Invoice_spcDate" /></td>
<td><xsl:value-of select="#Gross_spcAmount" /></td>
</tr>
</xsl:template>
</xsl:stylesheet>
I have two questions
1. How to SUM?
I need to display sum of all invoices in the last row of the table. I have an idea that I need to use nodeset but I am not able to figure out how?
2. Dynamic Sort
Is it possible to provide element name or attribute name dynamically to xsl:sort.
For example the attribute name on which to sort is provided as different element value.
<sortby>Gross_spcAmount</sortby>

(1) How to sum: use the built-in XPath library.
sum(/PropertySet/*/FS_spcInvoice)
There is a nuance if there is a risk that any of the numbers being addressed are not actually numbers, in which case the sum would be spoiled by including the bad value. This is prevented by:
sum(/PropertySet/*/FS_spcInvoice[number(.)=number(.)])
... which relies on the property of NaN that NaN != NaN.
(2) Dynamic sort: it is possible, though inelegant ... you have to express the calculation of the sort string using an address that composes the node you are looking for from whatever variable value or address you need. No real way around this other than exposing the name of the attribute node and checking it.
<xsl:sort select="#*[name(.)=/*/sortby]"/>

Related

XSLT display data in repeated columns

I need to create XSLT to show results in repeated columns to avoid scrolling in the page.
While checking for a solution i found an example from W3schools -
http://www.w3schools.com/xsl/tryxslt.asp?xmlfile=cdcatalog&xsltfile=cdcatalog
The above example shows how to add multiple columns using XSLT. But my requirement is different. If we take the same example, i want data to be displayed as below
Title Author Title Author
A-Title Gregory D-Title Ford
B-Title Dr.John E-Title Sean
C-Title Bellucci F-Title Steven
To avoid scrolling for the Users I need to split the data in two columns. Also the results need to be sorted vertically in alphabetical order.
Any suggestion would be greatly appreciated.
Thanks
John
You may want to try the following:
<?xml version="1.0" encoding="ISO-8859-1"?>
<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:template match="/">
<html>
<body>
<h1>Collection</h1>
<table border="1" style="display:inline-block">
<tr>
<th>Title</th>
<th>Artist</th>
<th>Title</th>
<th>Artist</th>
</tr>
<xsl:variable name="sorted-cds">
<xsl:for-each select="catalog/cd">
<xsl:sort select="title" order="ascending" data-type="text" />
<xsl:copy-of select="."/>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="n" select="ceiling(count(catalog/cd) div 2)"/>
<xsl:for-each select="exsl:node-set($sorted-cds)/cd[position() <= $n]">
<tr>
<td><xsl:value-of select="title"/></td>
<td><xsl:value-of select="artist"/></td>
<td><xsl:value-of select="following-sibling::cd[$n]/title"/></td>
<td><xsl:value-of select="following-sibling::cd[$n]/artist"/></td>
</tr>
</xsl:for-each>
</table>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
The basic idea is to sort the CDs as node set (sorted-cds)
calculate the half of number of rows (n) and and then iterate over
the first half of the collection (position() <= $n).
In order to get the corresponding second CD in the same row, just
skip (following-sibling::) $n CDs in the collection.
(Note that you need to include the common EXSL extensions xmlns:exsl...
because the non-standard exsl:node-set function is used.)
Another note: If you want to try out the above in the page
you linked in your question, you may need to replace the <=
by &lt;=; they seem to have a quoting bug there(?).

XML to HTML table using XSL

I have an XML file that I export from my DB which is structured as below,
'
<output>
<row>
<Month>October</Month>
<Location>kansas</Location>
<bus_name>bus1</bus_name>
<bus_type>volvo</bus_type>
<bus_colour>red</bus_colour>
<bus_count>10</bus_count>
</row>
<row>
<Month>October</Month>
<Location>kansas</Location>
<bus_name>bus1</bus_name>
<bus_type>Volvo</bus_type>
<bus_colour>green</bus_colour>
<bus_count>11</bus_count>
</row>
<Month>October</Month>
<Location>kansas</Location>
<bus_name>bus1</bus_name>
<bus_type>Merc</bus_type>
<bus_colour>blue</bus_colour>
<bus_count>5</bus_count>
</row>
So on...
</output>
I need the table to look like the image attached below. The XSL and XML file will be refreshed periodically.The cells will have similar color's based on bus type.
I'm new to XSL thus having a really hard time coming up with a solution. Any help would be appreciated.
Just as a different approach and also handling the colours for the same bus_types.
Demo
<?xml version="1.0"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html"/>
<xsl:template match="/">
<table>
<tr>
<td colspan="9">ACME BUS SERVICE</td>
</tr>
<tr>
<td colspan="9">
Month: <xsl:value-of select="//Month"/>
</td>
</tr>
<tr>
<td>Season</td>
<td>Location</td>
<td>Bus Name</td>
<td colspan="2">RED</td>
<td colspan="2">GREEN</td>
<td colspan="2">BLUE</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td>Bus Type</td>
<td>Bus Count</td>
<td>Bus Type</td>
<td>Bus Count</td>
<td>Bus Type</td>
<td>Bus Count</td>
</tr>
<xsl:for-each select="//row[Location[not(preceding::Location/. = .)]]" >
<xsl:variable name="currentLocation" select="./Location"/>
<tr>
<xsl:attribute name="class">
<xsl:value-of select="$currentLocation"/>
</xsl:attribute>
<td>
<xsl:if test="position()=1">Winter</xsl:if>
</td>
<td>
<xsl:value-of select="$currentLocation"/>
</td>
<td>
<xsl:value-of select="./bus_name"/>
</td>
<td>
<xsl:if test="count(//row[Location= $currentLocation]
[bus_type = //row[Location= $currentLocation]
[bus_colour = 'red']/bus_type]) > 1">
<xsl:attribute name="class">color</xsl:attribute>
</xsl:if>
<xsl:value-of select="//row[Location= $currentLocation]
[bus_colour = 'red']/bus_type"/>
</td>
<td>
<xsl:value-of select="//row[Location= $currentLocation]
[bus_colour = 'red']/bus_count"/>
</td>
<td>
<xsl:if test="count(//row[Location= $currentLocation]
[bus_type = //row[Location=$currentLocation]
[bus_colour = 'green']/bus_type]) > 1">
<xsl:attribute name="class">color</xsl:attribute>
</xsl:if>
<xsl:value-of select="//row[Location= $currentLocation]
[bus_colour = 'green']/bus_type"/>
</td>
<td>
<xsl:value-of select="//row[Location= $currentLocation]
[bus_colour = 'green']/bus_count"/>
</td>
<td>
<xsl:if test="count(//row[Location= $currentLocation]
[bus_type = //row[Location=$currentLocation]
[bus_colour = 'blue']/bus_type]) > 1">
<xsl:attribute name="class">color</xsl:attribute>
</xsl:if>
<xsl:value-of select="//row[Location= $currentLocation]
[bus_colour = 'blue']/bus_type"/>
</td>
<td>
<xsl:value-of select="//row[Location= $currentLocation]
[bus_colour = 'blue']/bus_count"/>
</td>
</tr>
</xsl:for-each>
</table>
</xsl:template>
</xsl:stylesheet>
For every location, the location is set as classname to the <tr>, e.g. <tr class="kansas">. Every td with a bus type that is used more than once at a location gets the class="color". So to display the table with different colors, you can just add CSS like e.g. .kansas .color { background-color: blue; }. In case you want to display the same color based on the bus_type, just adjust the classname "color" in the xslt to the current bus_type.
Note: In the linked example I've only added one row for Texas to show that the XSLT displays multiple locations, only sets the season for the first one, and will also work in case not all colors are provided for a location. And the output is not valid HTML (no html-, head-, body-tags etc provided). As you mentioned you'd like to get HTML ouput, you probably already have an XSLT generating valid HTML where you can adjust/include the part you need for the table.
Update for the question in the comments: To set the class name for a <tr> to the name of the bus_type (in case a bus_type is used more than once at a location) instead of the location:
Change this in above XSLT:
<tr>
<xsl:attribute name="class">
<xsl:value-of select="$currentLocation"/>
</xsl:attribute>
into
<tr>
<xsl:if test="count(//row[Location=$currentLocation]) >
count(//row[Location=$currentLocation]/
bus_type[not(. = preceding::bus_type)])">
<xsl:attribute name="class">
<xsl:value-of select="//row[Location=$currentLocation]/
bus_type[ . = preceding::bus_type]"/>
</xsl:attribute>
</xsl:if>
Updated Demo 2 for this.
Additional notes and questions: One adjustment to the OP XML was to change the lowercase "volvo" to "Volvo". In case the original export from DB really mixes upper- and lowercase names, this can be handled in the XSLT to lowercase all bus_names (to get the unique values) and uppercase the first letter for the value in the <td>. Also it would be good to know if you use XSLT 2.0 or XSLT 1.0 as XSLT 2.0 provides functionality to simplify some tasks - e.g. 2.0 provides a lower-case() function where in 1.0 the same can be achieved using translate() - as reference for this as you mentioned you're new to XSL: How can I convert a string to upper- or lower-case with XSLT?
Further question is - as the XML example is only a part of the DB export - if there will be only one row for each location or if it is possible that there are various rows, e.g. kansas bus1, kansas bus2 etc.
Update 2 for the second question in the comments: I can add an (almost) line by line explanation and will drop a comment when done. I assume it's not necessary to cover the HTML part but only the XSLT. In the meantime, as you mentioned you're new to XSLT, maybe the following can be of use:
for <xsl:template match="/"> - https://stackoverflow.com/questions/3127108/xsl-xsltemplate-match
for XPath axes - http://www.xmlplease.com/axis
for some basics, e.g. - In what order do templates in an XSLT document execute, and do they match on the source XML or the buffered output?
Note that it should be avoided at SO to have extended comments - when there are too many comments below a post, an automated message will be displayed that suggests to move to chat. Because you need a reputation of 20 to chat (https://stackoverflow.com/help/privileges), this won't be possible at the moment.
The first three node has to be mapped with the first row
Now that is a specific question. Assuming your input is arranged so that each group of 3 consecutive <row> elements maps to a single table row (with internal group positions matching the column positions), try it this way:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" omit-xml-declaration="yes" version="1.0" encoding="utf-8" indent="yes"/>
<xsl:template match="/output">
<table border="1">
<tr>
<!-- build your header here -->
</tr>
<xsl:for-each select="row[position() mod 3 = 1]" >
<tr>
<td><xsl:value-of select="Location"/></td>
<td><xsl:value-of select="bus_name"/></td>
<xsl:for-each select=". | following-sibling::row[position() < 3]">
<td><xsl:value-of select="bus_type"/></td>
<td><xsl:value-of select="bus_colour"/></td>
<td><xsl:value-of select="bus_count"/></td>
</xsl:for-each>
</tr>
</xsl:for-each>
</table>
</xsl:template>
</xsl:stylesheet>
I suggest you ask a separate question regarding the coloring. Make sure we understand exactly what is known beforehand (e.g. a list of known bus types?) and what is the required output (post it as code).

position() and last() inside nested for-each xslt

I have a stylesheet I'm using with a perl module that only works with XSLT 1.0. I want to create a JSON array inside a JSON dictionary so I need proper comma seperation for the elements. I'm parsing an XHTML table where there are 1 or more spans in the second cell. So for-each select="./tr" and then for-each select="./td[1]/span" or something like that.
After changing it a little it behaves as expected as Ian said it would.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" version="1.0" doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd" doctype-public="-//W3C//DTD HTML 4.01//EN" encoding="UTF-8" />
<xsl:template match="text()">
</xsl:template>
<xsl:template match="table/tbody">
<xsl:text>[</xsl:text>
<xsl:for-each select="./tr[not(#class='no-results')]">
<xsl:text>{"</xsl:text>
<xsl:value-of select="normalize-space(.//strong)" />
<xsl:text>":{"ingredients":{</xsl:text>
<xsl:for-each select=".//div[#class='reagent-list']//a[#class='item-link reagent']">
<xsl:value-of select="substring(./#href, 14)" />:<xsl:value-of select="normalize-space(./span[1])" />
<xsl:if test="position() != last()">
<xsl:text>,</xsl:text>
</xsl:if>
</xsl:for-each>
<xsl:text>}</xsl:text>
<xsl:if test="position() != last()">
<xsl:text>,</xsl:text>
</xsl:if>
<xsl:text>
</xsl:text>
</xsl:for-each>
<xsl:text>]</xsl:text>
</xsl:template>
</xsl:stylesheet>
I realize that the stylesheet does not match the xml below. The actual document is huge. I hope you understand what I mean, though. I just made this up:
<table>
<thead>
<tr>
<th>a</th>
<th>b</th>
<th>c</th>
</tr>
</thead>
<tbody>
<tr>
<td>foo</td>
<td><span>an element</span></td>
<td>bar</td>
</tr>
<tr>
<td>foo</td>
<td><span>an element</span><span>an element</span></td>
<td>bar</td>
</tr>
<tr>
<td>foo</td>
<td><span>an element</span><span>an element</span><span>an element</span><span>an element</span></td>
<td>bar</td>
</tr>
</tbody>
</table>
=>
{
"Row one":["an element"],
"Row two":["an element", "an element"],
"Row three":["an element", "an element", "an element", "an element"]
}
Instead I get this:
{
"Row one":["an element",],
"Row two":["an element", "an element",],
"Row three":["an element" "an element" "an element" "an element"]
}
I've been using position() and last() in a test tag to print a comma and it seems to work correctly for the outer loop, but how do I tell my test tag to use the inner for-each scope when printing the commas that seperate the array?
As mentioned by #IanRoberts, it is difficult to give targeted assistance without seeing what your existing XSLT looks like.
That said, here is a solution that is push-oriented (i.e., no <xsl:for-each>) and does not require last().
When this XSLT:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="my"
exclude-result-prefixes="my"
version="1.0">
<xsl:output omit-xml-declaration="no" indent="yes" method="text" />
<xsl:strip-space elements="*" />
<my:ones>
<num>one</num>
<num>two</num>
<num>three</num>
<num>four</num>
<num>five</num>
<num>six</num>
<num>seven</num>
<num>eight</num>
<num>nine</num>
</my:ones>
<xsl:template match="/*">
<xsl:text>{
</xsl:text>
<xsl:apply-templates select="tbody/tr" />
<xsl:text>
}</xsl:text>
</xsl:template>
<xsl:template match="tr">
<xsl:variable name="vPos" select="position()" />
<xsl:if test="$vPos > 1">,
</xsl:if>
<xsl:text> "Row </xsl:text>
<xsl:value-of select="document('')/*/my:ones/*[$vPos]" />
<xsl:text>":[</xsl:text>
<xsl:apply-templates select="td[2]/span" />
<xsl:text>]</xsl:text>
</xsl:template>
<xsl:template match="span">
<xsl:if test="position() > 1">, </xsl:if>
<xsl:value-of select="concat('"', ., '"')" />
</xsl:template>
</xsl:stylesheet>
...is run against the provided XML:
<table>
<thead>
<tr>
<th>a</th>
<th>b</th>
<th>c</th>
</tr>
</thead>
<tbody>
<tr>
<td>foo</td>
<td>
<span>an element</span></td>
<td>bar</td>
</tr>
<tr>
<td>foo</td>
<td>
<span>an element</span><span>an element</span></td>
<td>bar</td>
</tr>
<tr>
<td>foo</td>
<td>
<span>an element</span><span>an element</span><span>an element</span><span>an element</span></td>
<td>bar</td>
</tr>
</tbody>
</table>
...the wanted result is produced:
{
"Row one":["an element"],
"Row two":["an element", "an element"],
"Row three":["an element", "an element", "an element", "an element"]
}
Explanation:
The first template matches the root element. It's purpose is to apply templates to that element's <tr> grandchildren and sandwich those results between { and } (adding newlines as appropriate).
The second template matches <tr> elements. It outputs row information and is instructed to apply templates to all <span> children of the second <td> element (again, sandwiching the results between braces and other text as necessary).
NOTE: instead of using last(), you'll see that the first element outputs a comma, followed by a newline, if the position of this <tr> in the current context is greater than 1. This has the same effect of applying commas correctly; it's merely a different way of looking at the same problem (and is what I use because it seems more efficient to me ;) ).
NOTE: to make this solution more extensible, you'll see that I'm not statically outputting the words "one", "two", etc. in each row. Instead, at the top of the XSLT, I've defined a <my:ones> element to hold onto the text values of each "ones" number. When processing each <tr>, I use the position of that <tr> in the current context to retrieve the correct <num> element's value. I've left it as an exercise to the reader, but it would indeed be possible to define <my:tens>, <my:hundreds>, etc. to scale this solution up to potentially large numbers of rows.
The final template matches <span> elements. Again, it uses an <xsl:if> element to test whether the position of this <span> in the current context is greater than 1; if so, a comma (followed by a space) is output. After that, we merely concatenate two " symbols with the value of the span sandwiched in-between.

Parse dynamic XML into html table

I have a problem when parsing dynamic xml data into a html table. The XML is as follow.
<results>
<result id="1" desc="Voltage and current">
<measure desc="VOLT" value="1.0" />
<measure desc="AMPERE" value="2.0" />
</result>
<result id="2" desc="Current-1">
<measure desc="AMPERE" value="5.0" />
</result>
</results>
from which I would like a html table like:
ID DESC VOLT AMPERE
1 Voltage and current 1.0 2.0
2 Current-1 5.0
Notice the empty cell at second voltage column. ID and DESC is taken from result/#id and result/#desc and the rest of the column names should come from measure/#desc
No column name should be duplicate, I managed to code that far, but when I start adding my measures I need to match each measure/#desc to correct column in the table. I tried double nested loops to first match all unique column names, and then loop all measures again to match the column header. But the xslt parser threw a NPE on me!
Sorry that I can't show any code as it is on a non-connected computer.
I've browsed so many Q/A here on SO but to no help for my specific problem.
Thanks in advance
Note: I am able to change the XML format in any way to make parsing easier if anyone come up with a neater format.
If you are using XSLT1.0, you can use a technique called 'Muenchian' grouping to get the distinct measure descriptions, which will form the basis of your head row, and also be used to output the values of each row.
Firstly, you define a key to look up measure elements by their #desc attribute
<xsl:key name="measures" match="measure" use="#desc" />
Then, to get the distinct measure descriptions you can iterate over the measure elements that appear first in the group for their given #desc attribute
<xsl:apply-templates
select="result/measure[generate-id() = generate-id(key('measures', #desc)[1])]"
mode="header" />
Then, for your header, you would simply have a template to output the description.
<xsl:template match="measure" mode="header">
<th>
<xsl:value-of select="#desc" />
</th>
</xsl:template>
For each result row, would do a similar thing, and iterate over all distinct measure values, but the only difference is you would have to pass in the current result element as a parameter, for later use.
<xsl:apply-templates
select="/results/result/measure[generate-id() = generate-id(key('measures', #desc)[1])]"
mode="data">
<xsl:with-param name="result" select="." />
</xsl:apply-templates>
Then, in the template that matched the measure this time, you could access the measure within the result element with a matching #desc attribute (and id there is no such attribute, nothing is output for the cell)
<xsl:template match="measure" mode="data">
<xsl:param name="result" />
<td>
<xsl:value-of select="$result/measure[#desc = current()/#desc]/#value" />
</td>
</xsl:template>
Here is the full XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:key name="measures" match="measure" use="#desc" />
<xsl:template match="/results">
<table>
<tr>
<th>ID</th>
<th>DESC</th>
<xsl:apply-templates select="result/measure[generate-id() = generate-id(key('measures', #desc)[1])]" mode="header" />
</tr>
<xsl:apply-templates select="result" />
</table>
</xsl:template>
<xsl:template match="result">
<tr>
<td><xsl:value-of select="#id" /></td>
<td><xsl:value-of select="#desc" /></td>
<xsl:apply-templates select="/results/result/measure[generate-id() = generate-id(key('measures', #desc)[1])]" mode="data">
<xsl:with-param name="result" select="." />
</xsl:apply-templates>
</tr>
</xsl:template>
<xsl:template match="measure" mode="header">
<th>
<xsl:value-of select="#desc" />
</th>
</xsl:template>
<xsl:template match="measure" mode="data">
<xsl:param name="result" />
<td>
<xsl:value-of select="$result/measure[#desc = current()/#desc]/#value" />
</td>
</xsl:template>
</xsl:stylesheet>
Note the use of the mode attributes because you have two templates matching the measure element, which function in different ways.
When applied to your input XML, the following is output
<table>
<tr>
<th>ID</th>
<th>DESC</th>
<th>VOLT</th>
<th>AMPERE</th>
</tr>
<tr>
<td>1</td>
<td>Voltage and current</td>
<td>1.0</td>
<td>2.0</td>
</tr>
<tr>
<td>2</td>
<td>Current-1</td>
<td/>
<td>5.0</td>
</tr>
</table>

How to Iterate Over Unknown Columns in XSLT

The Title pretty much says it all. I have an XML document that I am processing with XSLT .... but I don't know how many columns (fields) that are in the XML document (therefore, obviously, I don't know their names). How can I determine the number of "unknown" fields there are? Also, how can I read the attributes, if any, for the unknown fields?
Example data ....
<Dataset>
<Row>
<UnknownCol1 Msg="HowDoIGetThisMsgAttribute?"/>
<UnknownCol2 />
<UnknownCol3 />
</Row>
</Dataset>
How can I determine the number of
"unknown" fields there are?
This XPath expression:
count(/*/*/*)
evalutes to the count of the elements, that a re children of the elements that are children of the top node of the XML document -- exactly what is wanted in this case.
If the "Row" element can have children whose name does not start with "UnknownCol",
then this XPath expression provides the count of elements, whose name starts with "UnknownCol", and that are children of elements that are children of the top element:
count(/*/*/*[starts-with(name(), "UnknownCol")])
In case the top element may have other children than "Row", then an XPath expression giving the required count is:
count(/*/Row/*[starts-with(name(), "UnknownCol")])
Also, how can I read the attributes,
if any, for the unknown fields?
By knowing XPath :)
/*/Row/*[starts-with(name(), "UnknownCol")]/#*
selects all the attributes of all "UnknownCol"{String} elements
This XPath expression gives us the number of these attributes:
count(/*/Row/*[starts-with(name(), "UnknownCol")]/#*)
This Xpath expression gives us the name of the k-th such attribute ($ind must be set to the number k):
name( (/*/Row/*[starts-with(name(), "UnknownCol")]/#*)[$ind] )
And finally, this XPath expression produces the value of the k-th such attribute:
string( (/*/Row/*[starts-with(name(), "UnknownCol")]/#*)[$ind] )
Edit: The OP commented that he completely doesn't lnow the names of the children element.
The fix is easy: simply remove the predicate from all expressions:
count(/*/*/*)
count(/*/Row/*)
/*/Row/*/#*
count(/*/Row/*/#*)
name( (/*/Row/*/#*)[$ind] )
string( (/*/Row/*/#*)[$ind] )
There's easy ways around this problem.
For the example you provided you could use the following XPath:
select='/Dataset/Row/*/#Msg'
If you wanted a more specific example of this, you may want to make your question clearer on exactly what you'd like to do with the data and the unknown columns. Are you copying it exactly? What specifically do you want to transform it into? Do you have any keys you'd like to match?
That kind of thing.
Here is a small sample of XSLT 1.0 code that transforms your input into a HTML table.
<xsl:template match="Dataset">
<table>
<thead>
<tr>
<xsl:apply-templates select="Row[1]/*" mode="th" />
</tr>
</thead>
<tbody>
<xsl:apply-templates select="Row" />
</tbody>
</table>
</xsl:template>
<xsl:template match="Row">
<tr>
<xsl:apply-templates select="*" mode="td" />
</tr>
</xsl:template>
<xsl:template match="Row/*" mode="th">
<th>
<xsl:value-of select="local-name()" />
</th>
</xsl:template>
<xsl:template match="Row/*" mode="td">
<td>
<xsl:value-of select="#Msg" />
</td>
</xsl:template>
When applied to this sample input:
<Dataset>
<Row>
<UnknownCol1 Msg="Data_1_1" />
<UnknownCol2 Msg="Data_1_2" />
<UnknownCol3 Msg="Data_1_3" />
</Row>
<Row>
<UnknownCol1 />
<UnknownCol2 Msg="Data_2_2" />
<UnknownCol3 Msg="" />
</Row>
</Dataset>
this output is returned:
<table>
<thead>
<tr>
<th>UnknownCol1</th>
<th>UnknownCol2</th>
<th>UnknownCol3</th>
</tr>
</thead>
<tbody>
<tr>
<td>Data_1_1</td>
<td>Data_1_2</td>
<td>Data_1_3</td>
</tr>
<tr>
<td></td>
<td>Data_2_2</td>
<td></td>
</tr>
</tbody>
</table>
This can also help someone:
Going through unkown nodes that start-with() AMT_ and check if any of them are empty
XML:
<INCOME_FARMING_OPERATIONS>
<PERSONAL_FARMING>
<IND_SCHEDULE>Y</IND_SCHEDULE>
<DESCRIPTION>FARMING OPERATIONS</DESCRIPTION>
<IDENTIFIER>111111111111</IDENTIFIER>
<AMT_FARM_INC_GROSS>22222222</AMT_FARM_INC_GROSS>
<AMT_FARM_INC_PARTNER></AMT_FARM_INC_PARTNER>
<AMT_BAL_LIVESTOCK>99999999</AMT_BAL_LIVESTOCK>
</PERSONAL_FARMING>
</INCOME_FARMING_OPERATIONS>
XSLT
<xsl:variable name ="NodeValueIsEmpty">
<xsl:for-each select ="//INCOME_FARMING_OPERATIONS/PERSONAL_FARMING/*[starts-with(name(),'AMT_')]">
<xsl:if test ="string-length(.)=0">
empty found
</xsl:if>
</xsl:for-each>
</xsl:variable>
<xsl:value-of select="$IncomeFieldValues"/>
output:
empty found because of the empty
you can take it further:
<xsl:if test=string-length($NodeValueIsEmpty)>0>
//todo if any of the AMT_???? fields is not set
</xsl:if>