XSLT - Sorting multiple values - xslt

I've already created my XSLT but id like to be able to sort the data, also add some kind of index so i can group the items together, the difficulty Im having is the the node i want to sort by contains multiple values - values id like to sort by.
For example here is my XML:
<item>
<title>Item 1</title>
<subjects>English,Maths,Science,</subjects>
<description>Blah Blah Bah...</description>
</item>
<item>
<title>Item 2</title>
<subjects>Geography,Physical Education</subjects>
<description>Blah Blah Bah...</description>
</item>
<item>
<title>Item 3</title>
<subjects>History, Technology</subjects>
<description>Blah Blah Bah...</description>
</item>
<item>
<title>Item 4</title>
<subjects>Maths</subjects>
<description>Blah Blah Bah...</description>
</item>
So if i sort by <subjects> I get this order:
English,Maths,Science,
Geography,Physical Education
History, Technology
Maths
But I would like this kind of output:
English
Geography
History
Maths
Maths
Physical Education
Science
Technology
Outputting the XML for each subject contained in <subjects>, so Item1 contains subjects Maths, English & Science so I want to output that Title and Description 3 times because its relevant to all 3 subjects.
Whats the best way in XSLT to do this?

I think one way to do this would be by using the node-set extenstion function to do multi-pass processing. Firstly you would loop through the existing subject nodes, splitting them by commas, to create a new set of item nodes; one per subject.
Next, you would loop through this new node set in subject order.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exsl="urn:schemas-microsoft-com:xslt" extension-element-prefixes="exsl" version="1.0">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:variable name="newitems">
<xsl:for-each select="items/item">
<xsl:call-template name="splititems">
<xsl:with-param name="itemtext" select="subjects"/>
</xsl:call-template>
</xsl:for-each>
</xsl:variable>
<xsl:for-each select="exsl:node-set($newitems)/item">
<xsl:sort select="text()"/>
<xsl:value-of select="text()"/>
<xsl:text> </xsl:text>
</xsl:for-each>
</xsl:template>
<xsl:template name="splititems">
<xsl:param name="itemtext"/>
<xsl:choose>
<xsl:when test="contains($itemtext, ',')">
<item>
<xsl:value-of select="substring-before($itemtext, ',')"/>
</item>
<xsl:call-template name="splititems">
<xsl:with-param name="itemtext" select="substring-after($itemtext, ',')"/>
</xsl:call-template>
</xsl:when>
<xsl:when test="string-length($itemtext) > 0">
<item>
<xsl:value-of select="$itemtext"/>
</item>
</xsl:when>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Note that the above example uses Microsoft's Extension functions. Depending on what XSLT processor you are using, you may have to specify another namespace for the processor.
You may also need to do some 'trimming' of the subjects, because in your XML sample above, there is a space before one of the subjects (Technology) in the comma-delimited list.

Well, processing the contents of text nodes isn't really the mandate of XSLT. If you can, you should probably change the representation to add some more XML structure into the subjects elements. Otherwise you'll have to write some really clever string processing code using XPath string functions, or perhaps use a Java-based XSLT processor and hand off the string processing to a Java method. It's not straightforward.

Related

How to find unmatched rows with XSLT

I have two large xml files, one of which has the following format:
<Persons>
<Person>
<ID>1</ID>
<LAST_NAME>London</LAST_NAME>
</Person>
<Person>
<ID>2</ID>
<LAST_NAME>Twain</LAST_NAME>
</Person>
<Person>
<ID>3</ID>
<LAST_NAME>Dikkens</LAST_NAME>
</Person>
</Persons>
The second file has the following format:
<SalesPersons>
<SalesPerson>
<ID>2</ID>
<LAST_NAME>London</LAST_NAME>
</SalesPerson>
<SalesPerson>
<ID>3</ID>
<LAST_NAME>Dikkens</LAST_NAME>
</SalesPerson>
</SalesPersons>
I need to find those records from file 1, which does not exist in file 2. Although I have it done using for-each loop, such an approach is taking a substantial amount of time. Is it possible to somehow make it run faster using a different approach?
Using a key can help to improve performance on lookups:
<xsl:key name="sales-person" match="SalesPerson" use="concat(ID, '|', LAST_NAME)"/>
<xsl:template match="/">
<xsl:for-each select="Persons/Person">
<xsl:variable name="person" select="."/>
<!-- need to change context document for key function use -->
<xsl:for-each select="$doc2">
<xsl:if test="not(key('sales-person', concat($person/ID, '|', $person/LAST_NAME)))">
<xsl:copy-of select="$person"/>
</xsl:if>
</xsl:for-each>
</xsl:for-each>
</xsl:template>
That assumes you have bound doc2 as a variable or parameter with e.g. <xsl:param name="doc2" select="document('sales-persons.xml')"/>.

How can I loop and generate keys for maps with XSLT 3.0?

I tried to construct a new map. In my source xml I've got many products (product data and IDs). How can I generate so many keys like products?
The goal is a transformation from XML to XML with XSLT. The idea was to create a map and in a next step call the keys for adressing the specifics product datas I need. So I need to know if this is possible with using maps or is there another solution?
Example for the source XML
<?xml version="1.0" encoding="UTF-8"?>
<root>
<row>
<id>102</id>
<product>Lenovo 1234</product>
<productfamily>laptop</productfamily>
</row>
<row>
.....
XSLT
<xsl:variable name="val" as="map(xs:integer, xs:integer)">
<xsl:map>
<xsl:for-each select="//id">
<xsl:map-entry key="" select="."/>
</xsl:map>
</xsl:variable>
<xsl:template match="/">
<xsl:value-of select="map:get($val , 102)"/>
</xsl:template>
To create a map based on a simple functional relationship in the data you can do
<xsl:variable name="index" as="map(*)">
<xsl:map>
<xsl:for-each select="//x">
<xsl:map-entry key=".//#id" select="."/>
</xsl:for-each>
</xsl:map>
</xsl:variable>
or if you prefer
<xsl:variable name="index" as="map(*)"
select="map:merge(//x ! map:entry(.//#id, .))"/>

hyphenation character in xslt attribute (xsl-fo)

I think it's a very simple question. But although I build very fancy xslt transformation, this simple one cannot be solved by me.
The problem is:
I want to add attributes to xsl-fo nodes, depending on xml data. These attributes have often a hyphen in it. How can I add these with an xslt transformation where xsl:attributes doesn't like the hyphenation character.
In a xml node I have got two attributes (name and value)
Example: name="font_size", value="7pt"
<Report>
<text content="I am a text">
<blockFormat name="font_size" value="7pt" />
</text>
</Report>
(I understand this is not wanted because you want to work with styles etceters. It's just an example with a simplified problem)
Now I want to make a xsl-fo block, and I want to place that attributes in the block element by using the xsl-function xsl:attribute
<fo:block>
<attribute name="{replace(#name,'_','-')}" select="#value" />
....
</fo:block>
goal to achieve after transformation
<fo:block font-size="7pt">
....
</fo:block
It doesn't function and I think this is because in xslt I can't put an hyphen in the attribute name, but in the fo-attribute it is needed.
Is there a way to use the xsl:attribute function for this?
And when not, what kind of working around do you suggest.
Thank you for helping!!!!
There are 1000 ways to do it, here is one (I didn't do anything with your Report element):
Input:
<Report>
<text content="I am a text">
<blockFormat name="font_size" value="7pt" />
</text>
</Report>
XSL:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fo="http://www.w3.org/1999/XSL/Format"
version="1.0">
<xsl:template match="Report">
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="text">
<fo:block>
<xsl:apply-templates select="blockFormat/#*"/>
<xsl:value-of select="#content"/>
</fo:block>
</xsl:template>
<xsl:template match="#name">
<xsl:attribute name="{translate(.,'_','-')}">
<xsl:value-of select="ancestor::blockFormat/#value"/>
</xsl:attribute>
</xsl:template>
<xsl:template match="#value"/>
</xsl:stylesheet>
Output:
<Report>
<fo:block xmlns:fo="http://www.w3.org/1999/XSL/Format" font-size="7pt">I am a text</fo:block>
</Report>
Use #select instead of #value:
<fo:block>
<attribute name="{replace(#name,'_','-')}" select="#value" />
....
</fo:block>
See https://www.w3.org/TR/xslt20/#creating-attributes
Also, you need to be using XSLT 2.0 or 3.0 to use #select. If you're using XSLT 1.0, you'd have to do it as xsl:attribute/xsl:value-of/#select.
(It would also have helped understanding of your problem if you'd also shown the wrong result that you were getting.)

How to compare (partial) subtrees of an XML document using XSLT, returning a boolean value based on the comparison as a single tag?

Assume the existence of a software system that enables the use of XSLTs to specify a certain predicate on an XML message. Specifically: transform an input document to an output document of the following form: <predicate>true</predicate> (or <predicate>false</predicate>).
For some simple cases (like message contains XPath) this is rather trivial, but I now need write an XSLT for something like the following:
<change>
<!-- state before change -->
<item>
<name>
<first>...</first>
<last>...</last>
</name>
<something>
...
</something>
</item>
<!-- state after change -->
<item>
<name>
<first>...</first>
<last>...</last>
</name>
<something>
...
</something>
</item>
</change>
And I would like to return <predicate>true</predicate> for a definition of a mutation if:
The before or after state (or both) actually contain a subtree of something data (as this part is optional), so basically change/item[1]/something | change/item[2]/something, and
The before and the after state with both having any something data removed are not identical to each other.
The second part could be something like the following pseudocode: $before variable is change/item[1]/something with any existing something subtree removed from it, $after variable is change/item[2]/something with any existing something subtree removed from it and then perhaps something like not(deep-equal($before,$after))...?
Anyone here have any idea how I could do this using XSLT 2.0, as I suspect this to be totally impossible in XSLT 1.0?
Try along the lines of
<xsl:template match="change">
<xsl:choose>
<xsl:when test="item[1]/something | item[2]/something">
<xsl:variable name="before" as="element(item)">
<xsl:apply-templates select="item[1]" mode="rs"/>
</xsl:variable>
<xsl:variable name="after" as="element(item)">
<xsl:apply-templates select="item[2]" mode="rs"/>
</xsl:variable>
<predicate><xsl:value-of select="not(deep-equal($before,$after))"/></predicate>
</xsl:when>
<xsl:otherwise>
<predicate>false</predicate>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="#* | node()" mode="rs">
<xsl:copy>
<xsl:apply-templates select="#* , node()" mode="rs"/>
</xsl:copy>
</xsl:template>
<xsl:template match="something" mode="rs"/>
Untested but should give you an idea. It basically runs a transformation in mode="rs" (remove-something) on the two item elements to strip the something element, then it does the comparison you posted (not(deep-equal($before,$after))).

XSLT Transformation (help)

I newby to XSLT and having some trouble to solve this problem.
The input is coming from an XML Excel document and has this format :
<Row>
<Cell><Data ss:Type="String">ToE.3</Data></Cell>
<Cell ss:Index="15"><Data ss:Type="String">Maintain</Data></Cell>
<Cell><Data ss:Type="Number">3</Data></Cell>
<Cell><Data ss:Type="String">Other</Data></Cell>
<Cell ss:Index="131"><Data ss:Type="String">Windows 2003</Data></Cell>
<Cell><Data >Microsoft SQL Server 2005</Data></Cell>
</Row>
..more rows (note the excel sheet has 132 columns)
I need to convert this to a standard text file, something like (with the right column) separator :
Col1 Col2 Col3 ..To.. Col15 Col16 ..To.. Col131
ToE.3 Maintain 3 Windows 2003
The problem is how to insert the empty row values that are skipt with the Index attribute.
The transformation without the empty, index handling looks like :
<xsl:for-each select="Row">
<xsl:for-each select="Cell/Data">
<xsl:value-of select="current()"/>
<xsl:text>\</xsl:text>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:for-each>
Some help would be warmly appreciated
step1: you need to declare output format, ie, "text" and not "xml"..
step2: you need to get rid of additional whitespace. use Strip-space with element='*', that means 'all'!
step3: you need to write header row first ie, col1, col2 etc..
so using template match select an element row that is first in your XML.. assuming that all the rows have same number of columns, you need to write "COL+ NUMBER" .. column numbers = no of cells you have in first row.
step4: if the cell is last then insert 'enter character'..
step5: call the generic function
step6: explaining generic function:
this function copies data under each cells separated by \. Only for the first row, we would be calling it manually, otherwise template match will take care of it.
Here is the code:
<?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"/>
<xsl:strip-space elements="*"/>
<xsl:template name="Header" match="Row[not(preceding-sibling::Row)]">
<xsl:for-each select="Cell">
<xsl:value-of select="'Col'"/>
<xsl:value-of select="position()"/>
<xsl:if test="position()!=last()">
<xsl:value-of select="'\'"/>
</xsl:if>
</xsl:for-each>
<xsl:text>
</xsl:text>
<xsl:call-template name="CopyData"/>
</xsl:template>
<xsl:template name="CopyData" match="Row">
<xsl:for-each select="Cell">
<xsl:for-each select="Data">
<xsl:apply-templates select="."/>
</xsl:for-each>
<xsl:if test="position()!=last()">
<xsl:value-of select="'\'"/>
</xsl:if>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
corresponding sample output:
Col1\Col2\Col3\Col4\Col5\Col6
ToE.3\Maintain\3\Other\Windows 2003\Microsoft SQL Server 2005
ToE.3\Maintain\3\Other\Windows 2003\Microsoft SQL Server 2005
This is tricky because as you are seeing Excel skips columns in which no data appears, then provides an ss:Index attribute for the subsequent non-blank column. You have to reconstruct the "missing" cell positions on your own. That is, if you wish to retain the original column position like "15" or "131" in your example, with intervening blanks.
Agreeing with InfantProgrammer above, but suggest you'd add some logic to the "CopyData" template above to (a) determine the number of missing cells, then (b) call a recursive named template to write 'em to output.
<xsl:template name="WriteBlanks">
<xsl:param name="Count" select="0"/>
<xsl:if test="Count > 0">
<xsl:value-of select="'\'"/>
<xsl:call-template name="WriteBlanks">
<xsl:with-param name="Count" select="$Count - 1"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
You could do something similar to generate the first row of column headers.
Given the simplicity of your need to just write backslash characters as column separator, a more succinct approach of just creating a long string of them, then lopping off however many are needed with XPath substring() could be in reach. However a recursive template may be suitable for more complex outputs.