I've been looking at threads about sorting XML using <xsl:sort> and variables, and still can't get my sorting to work. Here's some XML structure for context:
<records>
<record>
<contributors>
<authors>
<author>Author 1</author>
<author>Author 2</author>
</authors>
</contributors>
<titles>
<title>I'm a Title!</title>
<secondary-title></secondary-title>
</titles>
<dates>
<year>1901</year>
</dates>
</record>
<record>...</record>
<record>...</record>
</records>
And here's the relevant XSL:
<xsl:variable name="sortby"
select="contributors/authors/author[1]" as="element()*"/>
<xsl:for-each select="//record">
<xsl:sort select="$sortby" order="ascending"/>
[a bunch of HTML to render the records as a bibliography]
</xsl:for-each>
If I copy the string in the variable's "select" attributes and paste it into sort, like this:
<xsl:sort select="contributors/authors/author[1]" order="ascending">
then it works. With the variable, it doesn't. I tried it both with and without as="element()*" -- Help?
It is not possible in general to perform dynamic evaluation of XPath expressions -- neither in XSLT/Xpath 1.0 or in XSLT/Xpath 2.0.
This said, one can always implement sorting guided by variables, if there are some limitations on their contents.
Here is an example that solves your specific problem and a class of similar problems:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:param name="pSortName" select="'authors'"/>
<xsl:param name="pSortPosition" select="1"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="records">
<records>
<xsl:apply-templates>
<xsl:sort select=
".//*[name()=$pSortName]/*
[position()=$pSortPosition]"/>
</xsl:apply-templates>
</records>
</xsl:template>
</xsl:stylesheet>
when this transformation is applied on this XML document:
<records>
<record>
<contributors>
<authors>
<author>X.Y.Z</author>
<author>A.B.C</author>
</authors>
</contributors>
<titles>
<title>Title B</title>
<secondary-title>Title AB</secondary-title>
</titles>
<dates>
<year>1901</year>
</dates>
</record>
<record>
<contributors>
<authors>
<author>T.U.V</author>
<author>D.E.F</author>
</authors>
</contributors>
<titles>
<title>Title A</title>
<secondary-title>Title BA</secondary-title>
</titles>
<dates>
<year>2001</year>
</dates>
</record>
</records>
the wanted, correct result (the records sorted by first author) is produced:
<records>
<record>
<contributors>
<authors>
<author>T.U.V</author>
<author>D.E.F</author>
</authors>
</contributors>
<titles>
<title>Title A</title>
<secondary-title>Title BA</secondary-title>
</titles>
<dates>
<year>2001</year>
</dates>
</record>
<record>
<contributors>
<authors>
<author>X.Y.Z</author>
<author>A.B.C</author>
</authors>
</contributors>
<titles>
<title>Title B</title>
<secondary-title>Title AB</secondary-title>
</titles>
<dates>
<year>1901</year>
</dates>
</record>
</records>
If we change the parameters to:
<xsl:param name="pSortName" select="'authors'"/>
<xsl:param name="pSortPosition" select="2"/>
then the transformation sorts using as sort-key the second author.
If we change the parameters to:
<xsl:param name="pSortName" select="'titles'"/>
<xsl:param name="pSortPosition" select="1"/>
then the transformation sorts using as sort-key the titles/title element.
If we change the parameters to:
<xsl:param name="pSortName" select="'titles'"/>
<xsl:param name="pSortPosition" select="2"/>
then the transformation sorts using as sort-key the titles/secondary-title element.
Do note: Here we assume that there will be a unique descendent of any element being sorted, whose name is equal to the value specified in pSortName. We also assume that this element has children elements and pSortPosition specifies the position of the child to be used as a sort key.
Two other solutions that haven't been mentioned:
(a) Many processors have an extension, called something like dyn:evaluate() that evaluates an XPath expression supplied in the form of a character string
(b) In some environments it's feasible to modify the stylesheet (using an XSLT transformation of course) before executing it. This allows you to insert whatever XPath expression you need. In XSLT 2.0 you can write the sort key as select="my:sort(.)", and then define my:sort() in a separate xsl:included stylesheet module.
Another related option which I've seen is to use an external entity: select="&sortkey;", where the entity reference can be redirected programmatically to a different XPath expression using an EntityResolver registered with the XML parser.
You can't use a variable in the select= portion of an xsl:sort element. The XSLT specification states:
xsl:sort has a select attribute whose value is an expression. For each node to be processed, the expression is evaluated with that node as the current node and with the complete list of nodes being processed in unsorted order as the current node list.
The expression is evaluated only once, not twice as you seem to expect. Your expression $sortby is evaluated once to result in something that is the same every time (the actual value depends on what was the current node at the time the xsl:variable assignment ran). Therefore, the sort does not change the order of the selected elements.
You must use a specific expression as the sort criteria, as you have discovered.
Related
Info
Why is there different behaviour of Recursive Descent operator between
● template's match attribute where it is ignored and only children are selected ignoring their descendants
● for-each's select attribute where it works properly
Two examples of test.xsl are given which both operate on the following test.xml.
test.xml
<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="test.xsl"?>
<people>
<person id="(1)">
<name>Lucy</name>
</person>
<class>
<person id="(2)">
<name>David</name>
<person id="(21)">
<name>David</name>
</person>
</person>
</class>
</people>
match="//person"
In this example we are trying to use match="//person" to select ALL
person elements from document which doesn't work. Instead of
selecting ALL root descendants person elements, person elements which
are inside other person elements (like id="(21)") are not
included.
test.xsl
<?xml version="1.0" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="text()"/>
<xsl:template match="//person">
<xsl:value-of select="#id"/>
</xsl:template>
</xsl:stylesheet>
output.xml
(1)(2)
select="//person"
In this example we are using select="//person" to select ALL person
elements from the document. This will properly select ALL root
descendants person elements including id="(21)". Value of
match="class" is irrelevant since select="//person" uses absolute
path.
test.xsl
<?xml version="1.0" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="text()"/>
<xsl:template match="class">
<xsl:for-each select="//person">
<xsl:value-of select="#id"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
output.xml
(1)(2)(21)
There is a difference between a match pattern and a select expression. A match pattern does not select anything - it is only used to see if the current node matches the pattern. For this reason, the match pattern match="//person" is the same as match="person".
If a node never gets to be the current node (i.e. templates are not applied to it), then it doesn't matter if it matches any template's match pattern or not. This is shown in your first example: the built-in template rules are applied recursively until the <person id="(2)"> node is encountered; this matches a pattern of a more specific template - but that template has no instructions to apply templates to any of its descendants, so the chain breaks at this point, and the node <person id="(21)"> is never examined to see if there is a template matching it.
Well a template with a match on its own does not cause any processing and your template with
<xsl:template match="//person">
<xsl:value-of select="#id"/>
</xsl:template>
outputs the id attribute and does no further processing. So you will need to make sure you have an apply-templates e.g.
<xsl:template match="//person">
<xsl:value-of select="#id"/>
<xsl:apply-templates/>
</xsl:template>
or you will need a template like
<xsl:template match="/">
<xsl:apply-templates select="//person"/>
</xsl:template>
that makes sure all person elements are processed.
Also note that a match="//person" is the same as match="person" so all you need is
<xsl:template match="/">
<xsl:apply-templates select="//person"/>
</xsl:template>
and
<xsl:template match="person">
<xsl:value-of select="#id"/>
</xsl:template>
Using XSLT 1.0
Is it possible to filter many to many attributes, i mean as below example:
"../../../../fieldmap/field[#name" i.e. more then 1 elements as fieldmap containing "field/#name" attribute are exists and it is comparing with definition/#title and there too more then one definition element exists containing #title.
EXAMPLE:
<xsl:for-each select="../../../../fieldmaps/field[#name=../destination/#title]">
Can you please suggest me how it could possible to achieve -- if field containing #name existed in any of defination/#title then only those records should be process within for-each loop?
(as now, it looks, it will just compare with first #title attribute and consider all fieldmaps/field/#name attributes)
Thanks
You can achieve that using a variable:
<xsl:variable name="titles" select="../destination/#title"/>
<!--now "titles" contains a nodeset with all the titles -->
<xsl:for-each select="../../../../fieldmaps/field[#name=$titles]">
<!-- you process each field with a name contained inside the titles nodeset -->
</xsl:for-each>
Here you have a simplified example:
INPUT:
<parent>
<fieldmaps>
<field name="One"/>
<field name="Two"/>
<field name="Three"/>
</fieldmaps>
<destinations>
<destination title="One"/>
<destination title="Two"/>
</destinations>
</parent>
TEMPLATE:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<!-- ++++++++++++++++++++++++++++++++ -->
<xsl:template match="parent">
<Results>
<xsl:variable name="titles" select="destinations/destination/#title"/>
<xsl:for-each select="fieldmaps/field[#name=$titles]">
<Result title="{#name}"/>
</xsl:for-each>
</Results>
</xsl:template>
<!-- ++++++++++++++++++++++++++++++++ -->
</xsl:stylesheet>
OUTPUT:
<Results>
<Result title="One"/>
<Result title="Two"/>
</Results>
I hope this helps!
I am in 'group' node. From it, I want to find such 'item' node, that has 'id' attribute equals to current's 'group' node 'ref_item_id' attribute value. So in my case, by being in 'group' node B, I want 'item' node A as output. This works:
<xsl:value-of select="preceding-sibling::item[#id='1']/#description"/>
But this doesn't (gives nothing):
<xsl:value-of select="preceding-sibling::item[#id=#ref_item_id]/#description"/>
When I type:
<xsl:value-of select="#ref_item_id"/>
I have '1' as result. So this attribute is for sure accessible, but I can't find path to it from XPath expression above. I tried many '../' combinations, but couldn't get it work.
Code to test: http://www.xmlplayground.com/7l42fo
Full XML:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<item description="A" id="1"/>
<item description="C" id="2"/>
<group description="B" ref_item_id="1"/>
</root>
Full XSLT:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="no"/>
<xsl:template match="root">
<xsl:for-each select="group">
<xsl:value-of select="preceding-sibling::item[#id=#ref_item_id]/#description"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
This has to do with context. As soon as you enter a predicate, the context becomes the node currently being filtered by the predicate, and no longer the node matched by the template.
You have two options - use a variable to cache the outer scope data and reference that variable in your predicate
<xsl:variable name='ref_item_id' select='#ref_item_id' />
<xsl:value-of select="preceding-sibling::item[#id=$ref_item_id]/#description"/>
or make use of the current() function
<xsl:value-of select="preceding-sibling::item[#id=current()/#ref_item_id]/#description"/>
Your expression searches for an item whose id attribute matches its own ref_item_id. You need to capture the current ref_item_id in an xsl:variable and refer to that xsl:variable in the expression.
One more possible solution using xsl:key
<xsl:key name="kItemId" match="item" use="#id" />
<xsl:template match="root">
<xsl:for-each select="group">
<xsl:value-of select="key('kItemId', #ref_item_id)[1]/#description"/>
</xsl:for-each>
</xsl:template>
Looking at the XML, if I assume that you have <item> and <group> as siblings and in any order.
Then a sample input XML would look like the following.
<?xml version="1.0" encoding="UTF-8"?>
<root>
<item description="A" id="1"/>
<item description="C" id="2"/>
<group description="B" ref_item_id="1"/>
<item description="D" id="1"/>
<group description="E" ref_item_id="2"/>
</root>
Now, if the goal is to extract the description of all the <item> nodes whose id is matching with corresponding <group> *nodes ref_item_id*. Then we can simply loop over only such <item> nodes and get their description.
<xsl:output method="text" indent="no"/>
<xsl:template match="root">
<xsl:for-each select="//item[(./#id=following-sibling::group/#ref_item_id) or (./#id=preceding-sibling::group/#ref_item_id)]">
<xsl:value-of select="./#description"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Since you say that nodes are having unique id and all nodes are placed before nodes.
I would recommend you to use the following XSL and loop over specific nodes instead of nodes.
<xsl:output method="text" indent="no"/>
<xsl:template match="root">
<xsl:for-each select="//item[./#id=following-sibling::group/#ref_item_id]">
<xsl:value-of select="./#description"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
I was hoping I could get a little assistance with an XSLT transform. I can't seem to get it right.
Here is a sample of the source xml document:
<?xml version="1.0" encoding="UTF-8"?>
<Locations>
<header>
<location>Location Field</location>
<job_function>Job Function Field</job_function>
<count>Count</count>
</header>
<data>
<location>2177</location>
<job_function>ADM</job_function>
<count>1</count>
</data>
<data>
<location>2177</location>
<job_function>OPS</job_function>
<count>1</count>
</data>
<data>
<location>2177</location>
<job_function>SLS</job_function>
<count>5</count>
</data>
<data>
<location>2179</location>
<job_function>ADM</job_function>
<count>1</count>
</data>
<data>
<location>2179</location>
<job_function>SEC</job_function>
<count>1</count>
</data>
</Locations>
I want to transform it into the following format:
<Locations>
<data>
<PeopleSoftID>2177</PeopleSoftID>
<ADM>1</ADM>
<OPS>1</OPS>
<SLS>5</SLS>
<TotalCount>7</TotalCount>
</data>
<data>
<PeopleSoftID>2179</PeopleSoftID>
<ADM>1</ADM>
<SEC>1</SEC>
<TotalCount>2</TotalCount>
</data>
</Locations>
So basically, as you can see in the sample source document there are multiple elements that have the same value. In the destination document, there should now only be one record (<PeopleSoftID> element) per <location> element value in the source document. Since there were 3 <location> elements with the value of 2177, the destination document now has just 1 <PeopleSoftID> element that contains that value. The value of the <job_function> element in the source document becomes an element in the destination document. The value of that new element ends up being the sibling value of the <count> element from the source document. The <TotalCount> element in the destination document is the SUM of the values of all the new elements that are generated from the source <job_function> element.
I hope that explanation did not confuse anybody =).
I am a little new to XSLTs still so I am having trouble getting the logic right on this.
I can only use XSLT 1.0 too.
If I did not provide enough information let me know, and I will try to provide more as soon as I am able.
Thanks guys!
Read up on xsl:key and grouping with the Muenchian Method
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes" />
<!--Group the data elements by their location values -->
<xsl:key name="data-by-location" match="data" use="location" />
<xsl:template match="Locations">
<xsl:copy>
<!--Get a distinct list of location values,
using the Muenchian Method -->
<xsl:for-each
select="data[generate-id() =
generate-id(key('data-by-location', location)[1])]">
<xsl:copy>
<PeopleSoftID>
<xsl:value-of select="location"/>
</PeopleSoftID>
<!--For every data element matching this location... -->
<xsl:for-each select="key('data-by-location',location)">
<!--Create an element using the job_function
as the element name -->
<xsl:element name="{job_function}">
<!--The value of the count element
as the value of the generated element-->
<xsl:value-of select="count"/>
</xsl:element>
</xsl:for-each>
<TotalCount>
<!--calculate the sum of all the count element values
for this location -->
<xsl:value-of select="sum(key('data-by-location',
location)/count)"/>
</TotalCount>
</xsl:copy>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
There is probably a very easy solution for this problem. I could easily do this in C#-LINQ. Unfortunately, I'm not so experienced with XPath and XSL.
I have an input XML file that contains the following structure:
<group>
<val>1</val>
<val>3</val>
<val>1</val>
</group>
<group>
<val>3</val>
<val>2</val>
<val>2</val>
</group>
Now in my XSL transform I want to define 1 variable "highestsum", which contains the highest sum of 'values'. So for the example, it would return 7, the sum of all values in the second group.
After some searching, this is the closest solution I found:
http://w3schools.invisionzone.com/index.php?showtopic=24265
But I have a feeling that there's a better way than using sorting in a template to achieve this result. Any takers?
I. A good XSLT 1.0 solution (brief, efficient and understandable):
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/*">
<xsl:for-each select="group">
<xsl:sort select="sum(val)" data-type="number"
order="descending"/>
<xsl:if test="position()=1">
<xsl:value-of select="sum(val)"/>
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
when this transformation is applied on the following XML document:
<t>
<group>
<val>1</val>
<val>3</val>
<val>1</val>
</group>
<group>
<val>3</val>
<val>2</val>
<val>2</val>
</group>
</t>
the wanted, correct result is produced:
7
To get the desired variable definition, simply put the <xsl:for-each> instruvtion from the above code in the body of the variable.
II. An even better XSLT 2.0 (and actually XPath 2.0 one-liner) solution:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/*">
<xsl:sequence select="max(group/sum(val))"/>
</xsl:template>
</xsl:stylesheet>
when this transformation is applied on the same XML document, the same correct answer is produced:
7
And the wanted variable definition is simply:
<xsl:variable name="vHighestSum"
select="max(group/sum(val))"/>
Finally, the same Xpath expression can be used in XQuery to define the required variable:
let $vHighestSum := max(/*/group/sum(val))