Group Adjacent Functionality in XSLT - xslt

I have a question about the use of group-adjacent.
I have seen two patterns being used:
Pattern 1:
<xsl:for-each-group select="*" group-adjacent="boolean(self::p[#class = 'code'])">
Pattern 2:
<xsl:for-each-group select="*" group-adjacent="#class">
Based on what is used, I noticed that current-grouping-key() returns a false.
What is the purpose of using a boolean function in group-adjancent ?

With the form <xsl:for-each-group select="*" group-adjacent="boolean(self::p[#class = 'code'])"> the grouping key is a boolean value that is true for adjacent p elements having the class attribute with value code while with the second form <xsl:for-each-group select="*" group-adjacent="#class"> the grouping value is a string and groups all adjacent elements with the same class attribute values.
So it depends on your needs, if you have e.g.
<items>
<item class="c1">...</item>
<item class="c1">...</item>
<item class="c2">...</item>
</items>
you can use the second approach to group on the class value.
On the other hand, if you want to identify adjacent p elements with a certain class attribute, as e.g. in
<body>
<h1>...</h1>
<p class="code">...</p>
<p class="code">...</p>
<h2>...</h2>
<p class="code">...</p>
</body>
then the first approach allows that.

Based on what is used, I noticed that current-grouping-key() returns a
false.
current-grouping-key() returns either true or false, depending on the current group. In your first example, current-grouping-key() will be true for any group of adjacent p elements of class "code", false for the other groups.
What is the purpose of using a boolean function in group-adjancent ?
Without this, the grouping key would be the result of evaluating the expression self::p[#class = 'code'] which returns an empty sequence, which in turn causes an error.

Related

Comparing attribute values in XPath

I want to get the value of a specific node, specified by its id attribute. But the behaviour of my XSL parser, Saxon, is not how I expected it to work.
This is my XSL code:
<xsl:template match="synonyme">
<xsl:element name="corr">
<xsl:value-of select="#connecteur" />
<xsl:value-of select="/liste/connecteur[#id=#connecteur]/forme[1]" />
</xsl:element>
</xsl:template>
I just matched a tag named synonyme which has a connecteur attribute. My code outputs the value of this attribute.
I also want to output the value of another node which has an id attribute matching the connecteur attribute of my currently matched synonyme tag. But no results are ever found for this query, the second value-of always has empty output.
If I write, e.g. [#id='c160'], where c160 is the exact same thing that is output by the first value-of, it works! But not when comparing to the #attribute of my matched node. How can I fix this?
The XML is basically
<liste><connecteur id="c160"><forme>foo</forme></connecteur>
<connecteur id="c161"><synonyme connecteur="c160" /></connecteur>
</liste>
and the expected output in place of the synonyme is <corr>c160 foo</corr>.
The predicate you use:
[#id=#connecteur]
is looking for an element with two attributes - id and connecteur- with equal values. To look for an element with an id attribute whose value matches the value of the current element's connecteur value, you need to use:
[#id=current()/#connecteur]
See: https://www.w3.org/TR/xslt/#function-current
A better solution would be to define a key as:
<xsl:key name="ref" match="connecteur" use="#id" />
then use:
<xsl:value-of select="key('ref', #connecteur)/forme" />
to resolve the cross-reference.
See: https://www.w3.org/TR/xslt/#key

How to apply-templates to all templates except a spefic one

In the code bellow I'm applying all the templates to the element chapter (in the end of the code) but I would like to know how it is possible to apply all the templates to this element except a spefic one. In this case it's the element title because I'm already selecting it in the line before and it appears repeated in the html file. Someone?
<xsl:template match="chapter">
<h3>
<a name="{#id}"><xsl:value-of select="title"/></a>
</h3>
<xsl:apply-templates/>
</xsl:template>
Output:
<h3>Title</h3>
Title<br>
Text.
A plain <xsl:apply-templates/> is equivalent to <xsl:apply-templates select="node()" />, i.e. all child nodes. You can exclude certain nodes by using the XPath 2.0 except operator, e.g.
<xsl:apply-templates select="node() except title" />
This would select all child nodes except those that are elements with the name title. If you are only interested in child elements (not text nodes etc.) then you could use * except title instead.
The except operator essentially implements set difference - you're not limited to simple element names on the right, you can use any expression that returns a sequence of nodes, e.g.
node() except (title | div[#class = 'heading'])
X except Y selects all nodes that are in the sequence selected by X and not also in the sequence selected by Y

Manage flags in xslt?

All,
i am searching in a list of fields those who has the type clob and i am writing it separed by a comma like this [field1, field2, ... fieldn]
my problem is how to identify the first matched field to write it without comma ( i can't use position() because the first field matched can be the first of the list or the last of the list)
I want to make this algorithm in xslt,
variable is_first = TRUE;
if(is_first) {
do smthng;
isfirst = False;
}
Actually it is not possible to make something like this in xslt since variable are immutable. There probably could be workarounds but you have to specify your need in more details.
edit:
If your input is string with values separated by commas...
<xsl:variable name="inputString" select="'field1,field2,field3a,field4,field3b'" />
... you could use tokenize() functions...
<xsl:variable name="tokenized" select="tokenize($inputString, ',')" />
... and then select items corresponding to your condition
<!-- Select item corresponding to condition (e.g. it contains 3). Take first one if there are several such items -->
<xsl:value-of select="$tokenized[contains(., '3')][1]" />
Edit2:
You can use separator attribute of xsl:value-of (xslt 2.0) for output of delimited values.
Assuming following variable
<xsl:variable name="list">
<item>first</item>
<item>second</item>
<item>third</item>
</xsl:variable>
this <xsl:value-of select="$list/item" separator="," /> makes desired output first,second,third
You need to write this using functional code rather than procedural code. It's not possible to do the conversion without seeing the context (it's much easier to work from the problem rather than from the solution in a lower-level language).
But the most common equivalent in XSLT would take the form
<xsl:for-each select=".....">
<xsl:if test="position() = 1"><!-- first time code --></xsl:if>
....
</xsl:for-each>

XSLT Get First Element Node

<SMRCRLT_XML>
<AREA>
<DETAILS>
<DETAIL_REQUIREMENT>
<RULE_REQUIREMENT>
<DETAIL_REQUIREMENT>
<COURSE_ROWSET>
<COURSE_SET>
<COURSE_AREA>TESTSELECT</COURSE_AREA>
<COURSE_KEY_RULE>1200</COURSE_KEY_RULE>
<COURSE_SET>A</COURSE_SET>
<COURSE_SUBSET>1</COURSE_SUBSET>
<COURSE_SUBJ_CODE>CHEM</COURSE_SUBJ_CODE>
<COURSE_CRSE_NUMB_LOW>345A</COURSE_CRSE_NUMB_LOW>
</COURSE_SET>
</COURSE_ROWSET>
</DETAIL_REQUIREMENT>
<DETAIL_REQUIREMENT>
<COURSE_ROWSET>
<COURSE_SET>
<COURSE_KEY_RULE>1200</COURSE_KEY_RULE>
<COURSE_SET>A</COURSE_SET>
<COURSE_SUBSET>2</COURSE_SUBSET>
<COURSE_SUBJ_CODE>CHEM</COURSE_SUBJ_CODE>
<COURSE_CRSE_NUMB_LOW>476A</COURSE_CRSE_NUMB_LOW>
</COURSE_SET>
</COURSE_ROWSET>
</DETAIL_REQUIREMENT>
<DETAIL_REQUIREMENT>
<COURSE_ROWSET>
<COURSE_SET>
<COURSE_AREA>TESTSELECT</COURSE_AREA>
<COURSE_KEY_RULE>1200</COURSE_KEY_RULE>
<COURSE_SET>A</COURSE_SET>
<COURSE_SUBSET>3</COURSE_SUBSET>
<COURSE_SUBJ_CODE>PHIL</COURSE_SUBJ_CODE>
<COURSE_CRSE_NUMB_LOW>432</COURSE_CRSE_NUMB_LOW>
</COURSE_SET>
</COURSE_ROWSET>
</DETAIL_REQUIREMENT>
<DETAIL_REQUIREMENT>
<COURSE_ROWSET>
<COURSE_SET>
<COURSE_AREA>TESTSELECT</COURSE_AREA>
<COURSE_KEY_RULE>1200</COURSE_KEY_RULE>
<COURSE_SET>B</COURSE_SET>
<COURSE_SUBSET>4</COURSE_SUBSET>
<COURSE_SUBJ_CODE>PHIL</COURSE_SUBJ_CODE>
<COURSE_SUBJ_DESC>Philosophy</COURSE_SUBJ_DESC>
<COURSE_CRSE_NUMB_LOW>433</COURSE_CRSE_NUMB_LOW>
</COURSE_SET>
</COURSE_ROWSET>
</DETAIL_REQUIREMENT>
<DETAIL_REQUIREMENT>
<COURSE_ROWSET>
<COURSE_SET>
<COURSE_AREA>TESTSELECT</COURSE_AREA>
<COURSE_KEY_RULE>1200</COURSE_KEY_RULE>
<COURSE_SET>B</COURSE_SET>
<COURSE_SUBSET>5</COURSE_SUBSET>
<COURSE_SUBJ_CODE>ZOOL</COURSE_SUBJ_CODE>
<COURSE_CRSE_NUMB_LOW>321</COURSE_CRSE_NUMB_LOW>
</COURSE_SET>
</COURSE_ROWSET>
</DETAIL_REQUIREMENT>
<DETAIL_REQUIREMENT>
<COURSE_ROWSET>
<COURSE_SET>
<COURSE_AREA>TESTSELECT</COURSE_AREA>
<COURSE_KEY_RULE>1200</COURSE_KEY_RULE>
<COURSE_SET>B</COURSE_SET>
<COURSE_SUBSET>6</COURSE_SUBSET>
<COURSE_SUBJ_CODE>BIOC</COURSE_SUBJ_CODE>
<COURSE_CRSE_NUMB_LOW>456</COURSE_CRSE_NUMB_LOW>
</COURSE_SET>
</COURSE_ROWSET>
</DETAIL_REQUIREMENT>
</RULE_REQUIREMENT>
</DETAIL_REQUIREMENT>
</DETAILS>
</AREA>
</SMRCRLT_XML>
I am trying to get the first element from the XML for each COURSE_SET, but it returns all the values. Can someone please help. This is my template that I applied:
<xsl:apply-templates select="//SMRCRLT_XML/AREA/DETAILS/DETAIL_REQUIREMENT/RULE_REQUIREMENT/DETAIL_REQUIREMENT/COURSE_ROWSET/COURSE_SET[COURSE_AREA='TESTSELECT' and COURSE_KEY_RULE='1200'][1]"/>
The results I am getting are:
CHEM345A
PHIL432
PHIL433
ZOOL321
BIOC456
The result I am looking for is CHEM 345A and then PHIL433
You have several problems here.
First, the [1] in your XPath expression is filtering the XPath value by requiring that the COURSE_SET elements selected be the first child of their parent. Without that [1], your XPath expression reads:
//SMRCRLT_XML
/AREA
/DETAILS
/DETAIL_REQUIREMENT
/RULE_REQUIREMENT
/DETAIL_REQUIREMENT
/COURSE_ROWSET
/COURSE_SET
[COURSE_AREA='TESTSELECT' and COURSE_KEY_RULE='1200']
But every COURSE_SET that matches that path expression is the first child of its parent. (The only COURSE_SET elements which are not first children are children of COURSE_SET, not children of COURSE_ROWSET.)
The second problem is that it appears, judging by your question and your attempt at formulating the XPath expression you want, that you would like the courses to be grouped somehow (at first I thought you might want them grouped by department but now I expect you want them grouped by the value of the nested COURSE_SET element, which in your example has values A or B), so that by selecting the first COURSE_SET in some suitable context you can get the first course listed for each group. But the XML you show doesn't in fact group the courses by department or by course set; it provides a flat list of courses with no groupings at all. There are no elements here for which CHEM 345A and PHIL 433 are the first courses.
If your design calls for the courses to be grouped by department or course set, then your data source is not providing the data you want, and you will want to fix it.
If on the other hand you're stuck with this XML and want to use XPath to try to provide the structure that your data source is not capable of providing, then you don't want "the first element for each COURSE_SET", you want "each COURSE_SET which is in a department (or a COURSE_SET) different from the immediately preceding COURSE_SET". And your XPath expression can be something like
//COURSE_ROWSET/COURSE_SET
[not(COURSE_SET eq preceding::COURSE_SET[1])]
Your third problem is that your XML seems to be too fond of using the same name for different constructs (one set of COURSE_SET elements each of which contains a description of a course, with department and course number and so on, and a second set of COURSE_SET elements which contain the strings 'A' and 'B', two sets of DETAIL_REQUIREMENT with different content, and so on. It's confusing for people not familiar with the data, and it will make every single discussion of detail an opportunity for miscommunication and error.
The efficient way to handle a task like this in XSLT 1.0 is to use Muenchian grouping, like this:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>
<xsl:key name="kSet" match="COURSE_ROWSET/COURSE_SET" use="COURSE_SET" />
<xsl:template match="/">
<root>
<xsl:apply-templates
select="//COURSE_ROWSET/COURSE_SET[generate-id() =
generate-id(key('kSet', COURSE_SET)[1])]" />
</root>
</xsl:template>
<xsl:template match="COURSE_ROWSET/COURSE_SET">
<item>
<xsl:value-of select="concat(COURSE_SUBJ_CODE, COURSE_CRSE_NUMB_LOW)"/>
</item>
</xsl:template>
</xsl:stylesheet>
When this XSLT is applied to your sample input, the result is:
<root>
<item>CHEM345A</item>
<item>PHIL433</item>
</root>

Walk/loop through an XSL key: how?

Is there a way to walk-through a key and output all the values it contains?
<xsl:key name="kElement" match="Element/Element[#idref]" use="#idref" />
I though of it this way:
<xsl:for-each select="key('kElement', '.')">
<li><xsl:value-of select="." /></li>
</xsl:for-each>
However, this does not work. I simply want to list all the values in a key for testing purposes.
The question is simply: how can this be done?
You can't. That's not what keys are for.
You can loop through every element in a key using a single call to key() if and only if the key of each element is the same.
If you need to loop over everything the key is defined over, you can use the expression in the match="..." attribute of your <key> element.
So if you had a file like this:
<root>
<element name="Bill"/>
<element name="Francis"/>
<element name="Louis"/>
<element name="Zoey"/>
</root>
And a key defined like this:
<xsl:key name="survivors" match="element" use="#name"/>
You can loop through what the key uses by using the contents of its match attribute:
<xsl:for-each select="element">
<!-- stuff -->
</xsl:for-each>
Alternatively, if each element had something in common:
<root>
<element name="Bill" class="survivor"/>
<element name="Francis" class="survivor"/>
<element name="Louis" class="survivor"/>
<element name="Zoey" class="survivor"/>
</root>
Then you could define your key like this:
<xsl:key name="survivors" match="element" use="#class"/>
And iterate over all elements like this:
<xsl:for-each select="key('survivors', 'survivor')">
<!-- stuff -->
</xsl:for-each>
Because each element shares the value "survivor" for the class attribute.
In your case, your key is
<xsl:key name="kElement" match="Element/Element[#idref]" use="#idref" />
So you can loop through everything it has like this:
<xsl:for-each select="Element/Element[#idref]">
<!-- stuff -->
</xsl:for-each>
You CAN create a key to use for looping - if you simply specify a constant in the use attribute of the key element:
<xsl:key name="survivors" match="element" use="'all'"/>
Then you can loop over all elements in the following way:
<xsl:for-each select="key('survivors','all')">
...
</xsl:for-each>
Or count them:
<xsl:value-of select="count(key('survivors','all'))"/>
Note that the constant can be any string or even a number - but 'all' reads well.
However, you cannot use this key to lookup information about the individual entries (because they all have the same key).
In other words there are two types of possible keys:
"lookup keys" = standard keys with varying indexes in the use attribute
"looping keys" = keys with a constant in the use attribute
I do not know how efficient this method is to execute, it does however make the maintenance of the XSL more efficient by avoiding repetition of the same (potentially very complex) XPath expression throughout the XSL code.
Rather than think of the XSL keys in programming language terms, think of them as record sets of SQL. That will give a better understanding. For a given key index created as
<xsl:key name="paths" match="path" use="keygenerator()">
it can be "iterated"/"walk-through" as below
<xsl:for-each select="//path[generate-id()=generate-id(key('paths',keygenerator())[1])]">
To understand this magic number [1], let s go through the below example :
Consider this XML snippet
<root>
<Person>
<name>Johny</name>
<date>Jan10</date>
<cost itemID="1">34</cost>
<cost itemID="1">35</cost>
<cost itemID="2">12</cost>
<cost itemID="3">09</cost>
</Person>
<Person>
<name>Johny</name>
<date>Jan09</date>
<cost itemID="1">21</cost>
<cost itemID="1">41</cost>
<cost itemID="2">11</cost>
<cost itemID="2">14</cost>
</Person>
</root>
transformed using this XSL.
<xsl:for-each select="*/Person">
<personrecords>
<xsl:value-of select="generate-id(.)" />--
<xsl:value-of select="name"/>--
<xsl:value-of select="date"/>--
</personrecords>
</xsl:for-each>
<xsl:for-each select="*/*/cost">
<costrecords>
<xsl:value-of select="generate-id(.)" />--
<xsl:value-of select="../name"/>--
<xsl:value-of select="../date"/>--
<xsl:value-of select="#itemID"/>--
<xsl:value-of select="text()"/>
</costrecords>
</xsl:for-each>
The above XSL transformation lists the unique id of the Person nodes and the cost nodes in the form of idpxxxxxxx as the result below shows.
1. <personrecords>idp2661952--Johny--Jan10-- </personrecords>
2. <personrecords>idp4012736--Johny--Jan09-- </personrecords>
3. <costrecords>idp2805696--Johny-- Jan10-- 1-- 34</costrecords>
4. <costrecords>idp4013568--Johny-- Jan10-- 1-- 35</costrecords>
5. <costrecords>idp2808192--Johny-- Jan10-- 2-- 12</costrecords>
6. <costrecords>idp2808640--Johny-- Jan10-- 3-- 09</costrecords>
7. <costrecords>idp2609728--Johny-- Jan09-- 1-- 21</costrecords>
8. <costrecords>idp4011648--Johny-- Jan09-- 1-- 41</costrecords>
9. <costrecords>idp2612224--Johny-- Jan09-- 2-- 11</costrecords>
10.<costrecords>idp2610432--Johny-- Jan09-- 2-- 14</costrecords>
Let us create a key on the cost records using a combination of name and itemID values.
<xsl:key name="keyByNameItem" match="cost" use="concat(../name, '+', #itemID)"/>
Manually looking at the XML, the number of unique keys for the above would be three : Johny+1, Johny+2 and Johny+3.
Now lets test out this key by using the snippet below.
<xsl:for-each select="*/*/cost">
<costkeygroup>
<xsl:value-of select="generate-id(.)" />--
(1)<xsl:value-of select="generate-id(key('keyByNameItem',concat(../name, '+', #itemID) )[1] ) " />--
(2)<xsl:value-of select="generate-id(key('keyByNameItem',concat(../name, '+', #itemID) )[2] ) " />--
(3)<xsl:value-of select="generate-id(key('keyByNameItem',concat(../name, '+', #itemID) )[3] ) " />--
(4)<xsl:value-of select="generate-id(key('keyByNameItem',concat(../name, '+', #itemID) )[4] ) " />
</costkeygroup>
</xsl:for-each>
And here is the result:
1. <costkeygroup>idp2805696-- (1)idp2805696-- (2)idp4013568-- (3)idp2609728-- (4)idp4011648</costkeygroup>
2. <costkeygroup>idp4013568-- (1)idp2805696-- (2)idp4013568-- (3)idp2609728-- (4)idp4011648</costkeygroup>
3. <costkeygroup>idp2808192-- (1)idp2808192-- (2)idp2612224-- (3)idp2610432-- (4)</costkeygroup>
4. <costkeygroup>idp2808640-- (1)idp2808640-- (2)-- (3)-- (4)</costkeygroup>
5. <costkeygroup>idp2609728-- (1)idp2805696-- (2)idp4013568-- (3)idp2609728-- (4)idp4011648</costkeygroup>
6. <costkeygroup>idp4011648-- (1)idp2805696-- (2)idp4013568-- (3)idp2609728-- (4)idp4011648</costkeygroup>
7. <costkeygroup>idp2612224-- (1)idp2808192-- (2)idp2612224-- (3)idp2610432-- (4)</costkeygroup>
8. <costkeygroup>idp2610432-- (1)idp2808192-- (2)idp2612224-- (3)idp2610432-- (4)</costkeygroup>
Our interest is in trying to understand the importance of [1],[2], [3],[4]. In our case, the keygenerator is concat(../name, '+', #itemID).
For a given key, [1] refers to the first occurence of a node that satisfies the keygenerator. Similarly [2] refers to the second occurence of a node that satisfies the keygenerator. Thus [2], [3],[4], etc. are all nodes that satisfy the same key, and thus can be considered duplicates for the given key. The number of duplicates depends on the input XML. Thus:
Key Johny+1 satisfies 4 nodes (1)idp2805696-- (2)idp4013568-- (3)idp2609728-- (4)idp4011648
Key Johny+2 satisfies 3 nodes (1)idp2808192-- (2)idp2612224-- (3)idp2610432-- (4)
Key Johny+3 satisfies 1 node (1)idp2808640-- (2)-- (3)-- (4)
Thus we see that ALL 8 cost nodes of the XML can be accessed through the key.
Here is a image that combines the transformation results to help better understand.
The red squares indicate the matching nodes for Johny+1. The green squares indicate the matching nodes for Johny+3. Match the idpxxxxxxx values in <costkeygroup> to the values in <costrecords>. The <costrecords> help map the idpxxxxxxx values to the source XML.
The takeaway is that,
an XSL key does not filter or eliminate nodes. All nodes including duplicates can be accessed through the key. Thus when we say "walk through" of the key, there is no concept of a resultant subset of nodes from the original set of nodes made available to the key for processing.
To "walk through" only unique nodes of the key in the above example, use
<xsl:for-each select="*/*/workTime[generate-id()=generate-id(key('keyByNameItem', concat(../name, '+', #itemID) )[1] ) ] ">
[1] signifies that the first record for a given key value is denoted as the unique record. [1] is almost always used because there will exist at least one node that satisfies a given key value. If we are sure that there will be a minimum of 2 records to satisfy each key value in the key, we can go ahead and use [2] to identify the second record in the record set as the unique record.
P.S The words nodes / records / elements are used interchangeably.
There is no way to walk-through the keys, although we can output all the values it contains. In XSLT2 it is quite easier than in XSLT1 (e.g., using fn:generate-id according to the previous answer).
Using fn:distinct-values
<xsl:variable name="e" select="."/>
<xsl:for-each select="distinct-values(Element/Element[#idref]/#idref)">
<li key="{.}"><xsl:value-of select="key('kElement', ., $e )" /></li>
</xsl:for-each>
Using xsl:for-each-group
<xsl:for-each-group select="Element/Element[#idref]" group-by="#idref">
<li key="{current-grouping-key()}"><xsl:value-of select="current-group()" /></li>
</xsl:for-each-group>