How to check the sibling to output correct data after grouping? - xslt-2.0

I want to group by Reference below. XML input file
<root>
<row>
<Company>HHH</Company>
<Posting_Key>40</Posting_Key>
<Reference>12345</Reference>
<Assignment>54321</Assignment>
</row>
<row>
<Company>HHH</Company>
<Posting_Key>15</Posting_Key>
<Reference>12345</Reference>
<Assignment>321</Assignment>
</row>
<row>
<Company>HHH</Company>
<Posting_Key>15</Posting_Key>
<Reference>12345</Reference>
<Assignment>311</Assignment>
</row>
<root>
<row>
<Company>SSS</Company>
<Posting_Key>40</Posting_Key>
<Reference>67890</Reference>
<Assignment>78968</Assignment>
</row>
<row>
<Company>SSS</Company>
<Posting_Key>15</Posting_Key>
<Reference>67890</Reference>
<Assignment>98769</Assignment>
</row>
Desired Output where Posting Key 40 is inside the row, and posting key 15 are inside the details within that row.
<root>
<row>
<Company>HHH</Company>
<Posting_Key>40</Posting_Key>
<Reference>12345</Reference>
<Assignment>54321</Assignment>
<details>
<Company>HHH</Company>
<Posting_Key>15</Posting_Key>
<Reference>12345</Reference>
<Assignment>54321</Assignment>
</details>
<details>
<Company>HHH</Company>
<Posting_Key>15</Posting_Key>
<Reference>12345</Reference>
<Assignment>311</Assignment>
</details>
</row>
<row>
<Company>SSS</Company>
<Posting_Key>40</Posting_Key>
<Reference>67890</Reference>
<Assignment>78968</Assignment>
<details>
<Company>SSS</Company>
<Posting_Key>15</Posting_Key>
<Reference>67890</Reference>
<Assignment>98769</Assignment>
</details>
</row>
</root>
Current XSL code,
this only gets the data on the row where posting key is 40. How to check the sibling Posting Key 40 or 15 to output the correct value on the details section?
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="xs" version="2.0">
<xsl:template match="root">
<root>
<xsl:for-each-group select="row[Posting_Key='40']" group-by="Reference">
<row>
<xsl:value-of select="Company_Code"/>
<xsl:value-of select="Posting_Key"/>
<xsl:value-of select="Reference"/>
<xsl:value-of select="Assignment"/>
<xsl:for-each select="current-group()">
<details>
<xsl:value-of select="Company_Code"/>
<xsl:value-of select="Posting_Key"/>
<xsl:value-of select="Reference"/>
<xsl:value-of select="Assignment"/>
</details>
</xsl:for-each>
</row>
</xsl:for-each-group>
</root>
</xsl:template>
</xsl:stylesheet>
this only gets the data on the row where posting key is 40. How to check the sibling Posting Key 40 or 15 to output the correct value on the details section?

It looks like you rather want to use group-starting-with than group-by:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
version="3.0">
<xsl:mode on-no-match="shallow-copy"/>
<xsl:output indent="yes"/>
<xsl:template match="root">
<xsl:copy>
<xsl:for-each-group select="row" group-starting-with="row[Posting_Key = 40]">
<xsl:copy>
<xsl:apply-templates/>
<xsl:apply-templates select="tail(current-group())"/>
</xsl:copy>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>
<xsl:template match="row">
<details>
<xsl:apply-templates/>
</details>
</xsl:template>
</xsl:stylesheet>
https://xsltfiddle.liberty-development.net/gVhDDyL

Related

Keeping track of an iterator through a nested xsl:for-each

I want to take the following input:
<test>
<a>
<b />
<b />
</a>
<a>
<b />
<b />
</a>
</test>
And create the following output using XSLT 2.0:
<items>
<item num="1">
<item num="2"/>
<item num="3"/>
</item>
<item num="4">
<item num="5"/>
<item num="6"/>
</item>
</items>
I know this is wrong, but for a starting point, here's my current XSLT:
<xsl:template match="test">
<items>
<xsl:for-each select="a">
<item num="{position()}">
<xsl:for-each select="b">
<item num="{position()}"/>
</xsl:for-each>
</item>
</xsl:for-each>
</items>
</xsl:template>
This is clearly not the way to do it, because position() only considers elements in the same level. But how would I do this?
This transformation:
<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:template match="/*">
<items><xsl:apply-templates/></items>
</xsl:template>
<xsl:template match="a|b">
<xsl:variable name="vNum">
<xsl:number level="any" count="a|b"/>
</xsl:variable>
<item num="{$vNum}">
<xsl:apply-templates/>
</item>
</xsl:template>
</xsl:stylesheet>
when applied on the provided XML document:
<test>
<a>
<b />
<b />
</a>
<a>
<b />
<b />
</a>
</test>
produces the wanted, correct result:
<items>
<item num="1">
<item num="2"/>
<item num="3"/>
</item>
<item num="4">
<item num="5"/>
<item num="6"/>
</item>
</items>
In XSLT 2, using xsl:number is the right way, if your XSLT processor also supports XSLT 3, then using an accumulator https://www.w3.org/TR/xslt-30/#element-accumulator is an alternative:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
version="3.0">
<xsl:mode on-no-match="shallow-copy" use-accumulators="item-count"/>
<xsl:accumulator name="item-count" as="xs:integer" initial-value="0">
<xsl:accumulator-rule match="test" select="0"/>
<xsl:accumulator-rule match="test/a | test/a/b" select="$value + 1"/>
</xsl:accumulator>
<xsl:template match="test">
<items>
<xsl:apply-templates/>
</items>
</xsl:template>
<xsl:template match="a | b">
<item num="{accumulator-before('item-count')}">
<xsl:apply-templates/>
</item>
</xsl:template>
</xsl:stylesheet>
https://xsltfiddle.liberty-development.net/3NSSEvn
That would even work with streaming in Saxon EE where xsl:number is not supported:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
version="3.0">
<xsl:mode on-no-match="shallow-copy" use-accumulators="item-count" streamable="yes"/>
<xsl:accumulator name="item-count" as="xs:integer" initial-value="0" streamable="yes">
<xsl:accumulator-rule match="test" select="0"/>
<xsl:accumulator-rule match="test/a | test/a/b" select="$value + 1"/>
</xsl:accumulator>
<xsl:template match="test">
<items>
<xsl:apply-templates/>
</items>
</xsl:template>
<xsl:template match="a | b">
<item num="{accumulator-before('item-count')}">
<xsl:apply-templates/>
</item>
</xsl:template>
</xsl:stylesheet>
I figured out one solution: use xsl:number with any level and counting both a and b:
<xsl:template match="test">
<items>
<xsl:for-each select="a">
<item>
<xsl:attribute name="num">
<xsl:number level="any" count="a|b"/>
</xsl:attribute>
<xsl:for-each select="b">
<item>
<xsl:attribute name="num">
<xsl:number level="any" count="a|b"/>
</xsl:attribute>
</item>
</xsl:for-each>
</item>
</xsl:for-each>
</items>
</xsl:template>

update more than one result in same uri

I want to update the same result document to accumulate all the meta elements. I have tried to recreate the multiple dita-ot templates to explain the problem here. My question is, is it possible to update keyword.xml in the
<xsl:template match="html" mode="pages">
template itself? maybe using xsl:stream or xsl:accumulator? XSLT 3 and Saxon-HE-9.8.0-12
Input XML
<root>
<article>
<html name="firsthtm">
<head>Head1</head>
<meta>keyword;firsthtm</meta>
</html>
<html name="secondhtm">
<head>Head2</head>
<meta>keyword;secondhtm</meta>
</html>
<html name="thirdhtm">
<head>Head3</head>
<meta>keyword;thirdhtm</meta>
</html>
</article>
XSLT
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="xs" version="3.0">
<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="article">
<xsl:apply-templates mode="pages"/>
</xsl:template>
<xsl:template match="html" mode="pages">
<xsl:result-document href="{#name}.html">
<html>
<title>
<xsl:value-of select="#name"/>
</title>
</html>
</xsl:result-document>
<!-- update keyword.xml for each html -->
<xsl:result-document href="keyword.xml">
<root>
<xsl:copy-of select="meta"/>
</root>
</xsl:result-document>
</xsl:template>
firsthtm.htm
<html>
<title>firsthtm</title>
</html>
secondhtm.htm
<html>
<title>secondhtm</title>
</html>
thirdhtm.htm
<html>
<title>thirdhtm</title>
</html>
keyword.xml
<root>
<meta>keyword;secondhtm</meta>
<meta>keyword;secondhtm</meta>
<meta>keyword;thridhtm</meta>
</root>
Just create the result document in the template matching article:
<xsl:template match="article">
<xsl:apply-templates mode="pages"/>
<xsl:result-document href="keyword.xml">
<root>
<xsl:copy-of select="html/meta"/>
</root>
</xsl:result-document>
</xsl:template>
If you want to use the match="html" mode="pages" then you have to decide on which match you want to construct that result e.g on the first
<xsl:template match="html" mode="pages">
<xsl:result-document href="{#name}.html">
<html>
<title>
<xsl:value-of select="#name"/>
</title>
</html>
</xsl:result-document>
<!-- update keyword.xml for first html -->
<xsl:variable name="html-index" as="xs:integer">
<xsl:number/>
</xsl:variable>
<xsl:if test="$html-index = 1">
<xsl:result-document href="keyword.xml">
<root>
<xsl:copy-of select="ancestor::article/html/meta"/>
</root>
</xsl:result-document>
</xsl:if>
</xsl:template>
In simple cases (there are only those html element children for the article and you have used xsl:strip-space) it might suffice to simply test <xsl:if test="position() = 1">.
A simple solution would be moving the xsl:result-document to the article template and copy all html/meta elements from there:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="xs" version="3.0">
<xsl:template match="/">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="article">
<xsl:apply-templates mode="pages"/>
<xsl:result-document href="keyword.xml">
<root>
<xsl:copy-of select="html/meta"/>
</root>
</xsl:result-document>
</xsl:template>
<xsl:template match="html" mode="pages">
<xsl:result-document href="{#name}.html">
<html>
<title>
<xsl:value-of select="#name"/>
</title>
</html>
</xsl:result-document>
<!-- update keyword.xml for each html -->
</xsl:template>
</xsl:stylesheet>

Applying second grouping for all single emenets in a group

I used for-each-group, if want to group items based on key and then apply second grouping if they are not part of any group.
My sample xml is
<items>
<item id="123" name="Java">
<price></price>
<description></description>
</item>
<item id="123" name="Java and XML">
<price></price>
<description></description>
</item>
<item id="234" name="python">
<price></price>
<description></description>
</item>
<item id="456" name="scala">
<price></price>
<description></description>
</item>
<item id="" name="python">
<price></price>
<description></description>
</item>
<item id="768" name="scala">
<price></price>
<description></description>
</item>
<item id="891" name="angular">
<price></price>
<description></description>
</item>
</items>
First i want to group by id and if there are multiple elements in group then i will form a group, otherwise i will apply another grouping with name then form the group, finally if it is not any grouping then make its own group.
Output should be some thing like this
<items>
<group>
<item id="123" name="Java">
<price></price>
<description></description>
</item>
<item id="123" name="Java and XML">
<price></price>
<description></description>
</item>
</group>
<group>
<item id="234" name="python">
<price></price>
<description></description>
</item>
<item id="" name="python">
<price></price>
<description></description>
</item>
</group>
<group>
<item id="456" name="scala">
<price></price>
<description></description>
</item>
<item id="768" name="scala">
<price></price>
<description></description>
</item>
</group>
<group>
<item id="891" name="angular">
<price></price>
<description></description>
</item>
</group>
</items>
How to apply grouping for the ones which are not in any group ? is for-each-group the right one for this.
Update
I tried this approach
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="nonMatched" as="element()*">
<xsl:for-each-group select="./items/item" group-by="#id">
<xsl:if test="count(current-group()) lt 2">
<xsl:sequence select="current-group()"/>
</xsl:if>
</xsl:for-each-group>
</xsl:variable>
<xsl:template match="/">
<items>
<xsl:for-each-group select="/items/item" group-by="#id">
<xsl:if test="count(current-group()) gt 1">
<group>
<xsl:copy-of select="current-group()"/>
</group>
</xsl:if>
</xsl:for-each-group>
<xsl:for-each-group select="$nonMatched" group-by="#name">
<group>
<xsl:copy-of select="current-group()"/>
</group>
</xsl:for-each-group>
</items>
</xsl:template>
One way would be to push the items through different templates where you use keys to identify the groups and the conditions you have for the groups and then form the group for the first item in each group and use an empty template for the other items in each group; note that the following exploits the priority imposed by order https://www.w3.org/TR/xslt-30/#conflict so the order used below for the templates is important, although you could as well use priority attributes to impose your rules to favour #id based grouping, then #name based grouping, then group any item (i.e. match="item") not covered by the other grouping conditions:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
version="3.0">
<xsl:mode on-no-match="shallow-copy"/>
<xsl:output method="xml" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="id" match="item[normalize-space(#id)]" use="#id"/>
<xsl:key name="name" match="item" use="#name"/>
<xsl:template match="item">
<group>
<xsl:copy-of select="."/>
</group>
</xsl:template>
<xsl:template match="item[key('name', #name)[2] and . is key('name', #name)[1]]">
<group>
<xsl:copy-of select="key('name', #name)"/>
</group>
</xsl:template>
<xsl:template match="item[key('name', #name)[2] and not(. is key('name', #name)[1])]"/>
<xsl:template match="item[key('id', #id)[2] and . is key('id', #id)[1]]">
<group>
<xsl:copy-of select="key('id', #id)"/>
</group>
</xsl:template>
<xsl:template match="item[key('id', #id)[2] and not(. is key('id', #id)[1])]"/>
</xsl:stylesheet>
Online at https://xsltfiddle.liberty-development.net/bdxtqt, uses XSLT 3 but you could replace the xsl:mode declaration used at the beginning with the identity template
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
to have it work with an XSLT 2 processor. Only caveat there is an XSLT 2 processor could resort to reporting an error if there are multiple matches instead of taking the last matching template, I don't remember which XSLT 2 processor does that, but as said explicitly using priorities would solve that.
Your posted approach should also work:
<xsl:template match="items">
<xsl:copy>
<xsl:variable name="nonMatched" as="element()*">
<xsl:for-each-group select="item" group-by="#id">
<xsl:sequence
select="if (not(current-group()[2]))
then .
else ()"/>
</xsl:for-each-group>
</xsl:variable>
<xsl:for-each-group select="item except $nonMatched" group-by="#id">
<group>
<xsl:apply-templates select="current-group()"/>
</group>
</xsl:for-each-group>
<xsl:for-each-group select="$nonMatched" group-by="#name">
<group>
<xsl:apply-templates select="current-group()"/>
</group>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>
https://xsltfiddle.liberty-development.net/3NzcBtv

Create a list of element from nested elements in xslt?

I have a document with nested elements.Now I want to list them one element after another element using XSLT 2.0
Here is the input:
<?xml version="1.0" encoding="UTF-8"?>
<a>
<b>
test text
<b>
this text is in b
</b>
</b>
<b>
this text is
<b>
this text is test
</b>
</b>
</a>
This is what I expect:
<a>
<b>test text</b>
<b>this text is in b</b>
<b>this text is </b>
<b>this text is test</b>
</a>
I have no any idea to do this..I tried grouping concept..but it was not succeeded.Please help me to resolve this.
try this
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:strip-space elements="*"/>
<xsl:output indent="yes" omit-xml-declaration="yes"/>
<xsl:template match="*|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="b[b]">
<xsl:copy>
<xsl:apply-templates select="text()"/>
</xsl:copy>
<xsl:apply-templates select="*"/>
</xsl:template>
<xsl:template match="text()">
<xsl:value-of select="normalize-space(.)"/>
</xsl:template>
</xsl:stylesheet>

Getting the value of sibling nodes in XSLT

I have the following XML
<CN>12<CN>
<CT>XYXY</CT>
I need the result AS
<DIV>12 XYXY</DIV>
I'm USING the floowing XSLT but it's not working
<xsl:variable name="x"><xsl:value-of select="CN"/></xsl:variable>
<xsl:template match="CT">
<div class="chap-title"><span><xsl:value-of select="$x"/></span></div>
</xsl:template>
Input:
<input>
<CN>12</CN>
<CT>XYXY</CT>
</input>
XSLT:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0">
<xsl:output indent="yes"
method="xml"
encoding="UTF-8" />
<xsl:template match="/input">
<DIV>
<xsl:value-of select="CN"/>
<xsl:text> </xsl:text>
<xsl:value-of select="CT"/>
</DIV>
</xsl:template>
</xsl:stylesheet>

Resources