Produce context data for first and last occurrences of every value of an element - xslt

Given the following xml:
<container>
<val>2</val>
<id>1</id>
</container>
<container>
<val>2</val>
<id>2</id>
</container>
<container>
<val>2</val>
<id>3</id>
</container>
<container>
<val>4</val>
<id>1</id>
</container>
<container>
<val>4</val>
<id>2</id>
</container>
<container>
<val>4</val>
<id>3</id>
</container>
I'd like to return something like
2 - 1
2 - 3
4 - 1
4 - 3
Using a nodeset I've been able to get the last occurrence via:
exsl:node-set($list)/container[not(val = following::val)]
but I can't figure out how to get the first one.

To get the first and the last occurrence (document order) in each "<val>" group, you can use an <xsl:key> like this:
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:output method="text" />
<xsl:key name="ContainerGroupByVal" match="container" use="val" />
<xsl:variable name="ContainerGroupFirstLast" select="//container[
generate-id() = generate-id(key('ContainerGroupByVal', val)[1])
or
generate-id() = generate-id(key('ContainerGroupByVal', val)[last()])
]" />
<xsl:template match="/">
<xsl:for-each select="$ContainerGroupFirstLast">
<xsl:value-of select="val" />
<xsl:text> - </xsl:text>
<xsl:value-of select="id" />
<xsl:value-of select="'
'" /><!-- LF -->
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
EDIT #1: A bit of an explanation since this might not be obvious right away:
The <xsl:key> returns all <container> nodes having a given <val>. You use the key() function to query it.
The <xsl:variable> is where it all happens. It reads as:
for each of the <container> nodes in the document ("//container") check…
…if it has the same unique id (generate-id()) as the first node returned by key() or the last node returned by key()
where key('ContainerGroupByVal', val) returns the set of <container> nodes matching the current <val>
if the unique ids match, include the node in the selection
the <xsl:for-each> does the output. It could just as well be a <xsl:apply-templates>.
EDIT #2: As Dimitre Novatchev rightfully points out in the comments, you should be wary of using the "//" XPath shorthand. If you can avoid it, by all means, do so — partly because it potentially selects nodes you don't want, and mainly because it is slower than a more specific XPath expression. For example, if your document looks like:
<containers>
<container><!-- ... --></container>
<container><!-- ... --></container>
<container><!-- ... --></container>
</containers>
then you should use "/containers/container" or "/*/container" instead of "//container".
EDIT #3: An alternative syntax of the above would be:
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:output method="text" />
<xsl:key name="ContainerGroupByVal" match="container" use="val" />
<xsl:variable name="ContainerGroupFirstLast" select="//container[
count(
.
| key('ContainerGroupByVal', val)[1]
| key('ContainerGroupByVal', val)[last()]
) = 2
]" />
<xsl:template match="/">
<xsl:for-each select="$ContainerGroupFirstLast">
<xsl:value-of select="val" />
<xsl:text> - </xsl:text>
<xsl:value-of select="id" />
<xsl:value-of select="'
'" /><!-- LF -->
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Explanation: The XPath union operator "|" combines it's arguments into a node-set. By definition, a node-set cannot contain duplicate nodes — for example: ". | . | ." will create a node-set containing exactly one node (the current node).
This means, if we create a union node-set from the current node ("."), the "key(…)[1]" node and the "key(…)[last()]" node, it's node count will be 2 if (and only if) the current node equals one of the two other nodes, in all other cases the count will be 3.

Basic XPath:
//container[position() = 1] <- this is the first one
//container[position() = last()] <- this is the last one
Here's a set of XPath functions in more detail.

I. XSLT 1.0
Basically the same solution as the one by Tomalak, but more understandable Also it is complete, so you only need to copy and paste the XML document and the transformation and then just press the "Transform" button of your favourite XSLT IDE:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:key name="kContByVal" match="container"
use="val"/>
<xsl:template match="/*">
<xsl:for-each select=
"container[generate-id()
=
generate-id(key('kContByVal',val)[1])
]
">
<xsl:variable name="vthisvalGroup"
select="key('kContByVal', val)"/>
<xsl:value-of select=
"concat($vthisvalGroup[1]/val,
'-',
$vthisvalGroup[1]/id,
'
'
)
"/>
<xsl:value-of select=
"concat($vthisvalGroup[last()]/val,
'-',
$vthisvalGroup[last()]/id,
'
'
)
"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
when this transformation is applied on the originally-provided XML document (edited to be well-formed):
<t>
<container>
<val>2</val>
<id>1</id>
</container>
<container>
<val>2</val>
<id>2</id>
</container>
<container>
<val>2</val>
<id>3</id>
</container>
<container>
<val>4</val>
<id>1</id>
</container>
<container>
<val>4</val>
<id>2</id>
</container>
<container>
<val>4</val>
<id>3</id>
</container>
</t>
the wanted result is produced:
2-1
2-3
4-1
4-3
Do note:
We use the Muenchian method for grouping to find one container element for each set of such elements that have the same value for val.
From the whole node-list of container elements with the same val value, we output the required data for the first container element in the group and for the last container element in the group.
II. XSLT 2.0
This transformation:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xsl:output method="text"/>
<xsl:template match="/*">
<xsl:for-each-group select="container"
group-by="val">
<xsl:for-each select="current-group()[1], current-group()[last()]">
<xsl:value-of select=
"concat(val, '-', id, '
')"/>
</xsl:for-each>
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>
when applied on the same XML document as above, prodices the wanted result:
2-1
2-3
4-1
4-3
Do note:
The use of the <xsl:for-each-group> XSLT instruction.
The use of the current-group() function.

Related

XSLT 2 - find first missing element in source list

I have problem with XSLT and/or XPATH. Let's say I have XML Input:
<context>
<pdpid-set>
<list>
<item>1</item>
<item>2</item>
<item>4</item>
<item>6</item>
<item>7</item>
<item>8</item>
</list>
</pdpid-set>
</context>
Task is: find FIRST missing element in array pdpid-set/list. In example above answer is 3.
I tried to use <xsl:for-each to find missing element but there is no possibility to break such loop so my XSL produce more than one element in output:
<xsl:variable name="list" select="context/pdpid-set/list"/>
<xsl:variable name="length" select="count(context/pdpid-set/list/item)"/>
<xsl:for-each select="1 to ($length)">
<xsl:variable name="position" select="position()"/>
<xsl:if test="$list/item[$position] > $position">
<missing-value>
<xsl:value-of select="$position"/>
</missing-value>
</xsl:if>
</xsl:for-each>
in code above output will be:
<missing-value>3</missing-value><missing-value>4</missing-value><missing-value>5</missing-value>...
I don't want to have more than one missing-value. Any suggestion?
Even in XPath 1.0
/context
/pdpid-set
/list
/item[not(position()=.)][1]
Do note: this select the first item not aligned with the ascending order. I still think that position() is better than following-sibling axis performance wise and for code clarity. Also, it lets you easily change starting number and step like in:
/context
/pdpid-set
/list
/item[not((position() - 1) * $step + $start = .)][1]
Task is: find FIRST missing element in array pdpid-set/list. In
example above answer is 3
Here is a correct XPath 1.0 expression that when evaluates to the wanted result (3):
/*/*/*/item[not(. +1 = following-sibling::*[1])][1] + 1
The XPath expression in the currently selected answer, on the other side, selects this element:
<item>4</item>
And the complete correct XSLT 1.0 transformation is:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="/">
<missing-value>
<xsl:copy-of select="/*/*/*/item[not(. +1 = following-sibling::*[1])][1] + 1"/>
</missing-value>
</xsl:template>
</xsl:stylesheet>
When applied on the provided XML document, the wanted, correct result is produced:
<missing-value>3</missing-value>
Finally, if the task is to find all missing elements:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="xs">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match=
"item[following-sibling::* and not(number(.) +1 = following-sibling::*[1]/number())]">
<xsl:for-each select="xs:integer(.) + 1 to following-sibling::*[1]/xs:integer(.) -1">
<missing-value><xsl:copy-of select="."/></missing-value>
</xsl:for-each>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
when this XSLT 2.0 transformation is applied on the following XML document (missing 3, 5, and 6):
<context>
<pdpid-set>
<list>
<item>1</item>
<item>2</item>
<item>4</item>
<item>7</item>
<item>8</item>
</list>
</pdpid-set>
</context>
the wanted, correct result is produced:
<missing-value>3</missing-value>
<missing-value>5</missing-value>
<missing-value>6</missing-value>

xsl:for-each not working for child nodes?

XML:
<?xml version="1.0" encoding="UTF-8"?>
<Service>
<Author name="Raymond">
<Book>Master Mind</Book>
<Book>Big Bites</Book>
</Author>
<Author name="CLAYTON">
<Book>Beyond the RACK</Book>
</Author>
</Service>`
using this XSLT:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/">
<xsl:for-each select="//Author">
<xsl:value-of select="#name" />
<xsl:for-each select="//Book">
<xsl:value-of select="." />
</xsl:for-each>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>`
expected output:
Raymond Master Mind Big Bites CLAYTON Beyond the RACK
Use a relative path select="Book" for the inner for-each
<xsl:for-each select="//Book">
selects all Book nodes in the entire document, starting from the / root node. To select only Books that are children of the current Author, try:
<xsl:for-each select="Book">
--
Note: It's not clear to me on what basis do you expect white spaces to be inserted in-between the values written to the output.

Get the last last value of a node

I am trying to get the last value of a node that belongs to a group.
Given the following XML -
<books>
<author>
<name>R.R.</name>
<titles>
<titlename>North</titlename>
<titlename>King</titilename>
</titles>
</author>
<author>
<name>R.L.</name>
<titles>
<titlename>Dragon</titlename>
<titlename>Wild</titilename>
</titles>
</author>
</books>
I assume it would be something like -
<template match="/">
<for-each-group select="books/author" group-by="name">
<lastTitle>
<name><xsl:value-of select="name"/></name>
<title><xsl:value-of select="last()/name"/></title>
</lastTitle
</for-each-group>
<template>
Thus the result would be -
<lastTitle>
<name>R.R</name>
<title>King</title>
</lastTitle>
<lastTitle>
<name>R.L.</name>
<title>Wild</title>
</lastTitle>
How can I produce this result?
Instead of doing this
<xsl:value-of select="last()/name"/>
The expression you are looking for is this:
<xsl:value-of select="titles/titlename[last()]"/>
However, it might be worth pointing out, this isn't really a 'grouping' problem. (It would only be a grouping problems if you have two different author elements with the same name). You can actually just use a simple xsl:for-each here
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<xsl:for-each select="books/author">
<lastTitle>
<name>
<xsl:value-of select="name"/>
</name>
<title>
<xsl:value-of select="titles/titlename[last()]"/>
</title>
</lastTitle>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

XPath relative path in expression

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>

Highest sum of values per group in XSL

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))