How to get XSLT key-function working with my scenario? - xslt

Here is my in-data:
<Results>
<Result>
<Id>1</Id>
</Result>
<Result>
<Id>2</Id>
</Result>
</Results>
<Results>
<RefId>1</RefId>
<Text>One</Text>
</Results>
<Results>
<RefId>2</RefId>
<Text>Two</Text>
</Results>
How the output should be:
<OBR></OBR>
<OBX>One</OBX>
<OBR></OBR>
<OBX>Two</OBX>
My xslt-code
<xsl:key name="test" match="Results/Result" use="Id"/>
<xsl:template match="Results/Result">
<OBR></OBR>
<xsl:for-each select="Results[key('test', RefId)/RefId]">
<OBX><xsl:value-of select="Text" /></OBX>
</xsl:for-each>
</xsl:template>
It does not work. My result is:
<OBR></OBR>
<OBX>One</OBX>
<OBX>Two</OBX>
<OBR></OBR>
<OBX>One</OBX>
<OBX>Two</OBX>
I assume that the problem is with the for-each in my template.. It´s looping twice every time the template runs. Any suggestions?

I complicated it unnecessarily much with they key-function. I solved with just creating a variable named ID and it´s based on the Id-field. Then in the for-each I just tested if the variable and the RefId-element matched and it works perfectly.
<xsl:template match="Results/Result">
<xsl:variable name="ID" select="Id"></xsl:variable>
<OBR></OBR>
<xsl:for-each select="Results[(RefId = $ID)]">
<OBX><xsl:value-of select="Text" /></OBX>
</xsl:for-each>
</xsl:template>

Related

Using XSL:sort within xsl:if

I'm just figuring out how XSL works... mostly. I have 3 nodes (STUX, KbnApp, KbnStorge) that I want to transform in the same way except for how their list of "Servers" children are sorted. I wanted to do this with a single template that uses an xsl:if to choose an alternate sorting method for the "STUX". But I couldn't get it to work. The STUX Servers should end up in descending order, the others should stay in whatever order they are currently in.
Here is my XML
<?xml version="1.0" encoding="UTF-8"?>
<Categories>
<STUX>
<Servers>stuxsh01</Servers>
<Servers>stuxsh03</Servers>
<Servers>stuxsh02</Servers>
<UnitTest>Pass</UnitTest>
</STUX>
<KbnApp>
<Servers>stks01</Servers>
<Servers>stks03</Servers>
<Servers>stks02</Servers>
<UnitTest>Pass</UnitTest>
</KbnApp>
<KbnStorage>
<Servers>stksnfs01</Servers>
<Servers>stksnfs02</Servers>
<UnitTest>Fail</UnitTest>
</KbnStorage>
</Categories>
This transform gives me the result that I want, but I've had to make two templates that are almost identical, wasting lines. I think there should be a way to do this with one template and an xsl:if with an xsl:sort inside it. Can anyone tell me how I would format XSL to do that? Every time I've tried putting an xsl:if the XSL can't be parsed/won't apply and I don't know why. PS I'm SURE there is things I've done here that aren't proper, but by golly I got it to work :) I'm definitely open to learning how to do it better!
EDIT: Corrected a typo
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<categories>
<countCategories><xsl:value-of select="count(child::*)" /></countCategories>
<xsl:apply-templates />
</categories>
</xsl:template>
<xsl:template match="KbnApp|KbnStorage">
<Category>
<Name><xsl:value-of select="name()" /></Name>
<countServers><xsl:value-of select="count(Servers)" /></countServers>
<xsl:copy-of select="UnitTest" />
<xsl:for-each select="Servers">
<Server><xsl:value-of select="."/></Server>
</xsl:for-each>
</Category>
</xsl:template>
<xsl:template match="STUX">
<Category>
<Name><xsl:value-of select="name()" /></Name>
<countServers><xsl:value-of select="count(Servers)" /></countServers>
<xsl:copy-of select="UnitTest" />
<xsl:for-each select="Servers">
<xsl:sort order="descending"/>
<Server><xsl:value-of select="."/></Server>
</xsl:for-each>
</Category>
</xsl:template>
</xsl:stylesheet>
For reference...The desired result
<categories>
<countCategories>1</countCategories>
<Category>
<Name>STUX</Name>
<countServers>3</countServers>
<UnitTest>Pass</UnitTest>
<Server>stuxsh03</Server>
<Server>stuxsh02</Server>
<Server>stuxsh01</Server>
</Category>
<Category>
<Name>KbnApp</Name>
<countServers>3</countServers>
<UnitTest>Pass</UnitTest>
<Server>stks01</Server>
<Server>stks03</Server>
<Server>stks02</Server>
</Category>
<Category>
<Name>KbnStorage</Name>
<countServers>2</countServers>
<UnitTest>Fail</UnitTest>
<Server>stksnfs01</Server>
<Server>stksnfs02</Server>
</Category>
</categories>
Try changing:
<xsl:sort order="descending"/>
to:
<xsl:sort select="self::*[parent::STUX]" order="descending"/>
Then you can use this in a template that matches all your categories, and have only the STUX category sorted.

grouping with complex "selection"

This is the source XML:
<root>
<!-- a and b have the same date entries, c is different -->
<variant name="a">
<booking>
<date from="2017-01-01" to="2017-01-02" />
<date from="2017-01-04" to="2017-01-06" />
</booking>
</variant>
<variant name="b">
<booking>
<date from="2017-01-01" to="2017-01-02" />
<date from="2017-01-04" to="2017-01-06" />
</booking>
</variant>
<variant name="c">
<booking>
<date from="2017-04-06" to="2017-04-07" />
<date from="2017-04-07" to="2017-04-09" />
</booking>
</variant>
</root>
I'd like to group the three variants so that each variants with same #from and #to in each date should be grouped together.
My attempt is:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"></xsl:output>
<xsl:template match="root">
<variants>
<xsl:for-each-group select="for $i in variant return $i" group-by="booking/date/#from">
<group>
<xsl:attribute name="cgk" select="current-grouping-key()"/>
<xsl:copy-of select="current-group()"></xsl:copy-of>
</group>
</xsl:for-each-group>
</variants>
</xsl:template>
</xsl:stylesheet>
But this gives too many groups. (How) is this possible to achieve?
Using a composite key and XSLT 3.0 you could use
<xsl:template match="root">
<variants>
<xsl:for-each-group select="variant" group-by="booking/date/(#from, #to)" composite="yes">
<group key="{current-grouping-key()}">
<xsl:copy-of select="current-group()"/>
</group>
</xsl:for-each-group>
</variants>
</xsl:template>
which should group any variant elements together which have the same descendant date element sequence.
XSLT 3.0 is supported by Saxon 9.8 (any edition) or 9.7 (PE and EE) or a 2017 release of Altova XMLSpy/Raptor.
Using XSLT 2.0 you could concatenate all those date values with string-join():
<xsl:template match="root">
<variants>
<xsl:for-each-group select="variant" group-by="string-join(booking/date/(#from, #to), '|')">
<group key="{current-grouping-key()}">
<xsl:copy-of select="current-group()"/>
</group>
</xsl:for-each-group>
</variants>
</xsl:template>
Like the XSLT 3.0 solution, it only groups variant with the same sequence of date descendants, I am not sure whether that suffices or whether you might want to sort any date descendants first before computing the grouping key. In the XSLT 3 case you could do that easily with
<xsl:for-each-group select="variant" group-by="sort(booking/date, (), function($d) { xs:date($d/#from), xs:date($d/#to) })!(#from, #to)" composite="yes">
inline (although that leaves 9.8 HE behind as it does not support function expressions/higher order functions, so there you would need to move the sorting to your own user-defined xsl:function and in there use xsl:perform-sort).

Change template so XSLT Outputs a sum instead of a list of values

I have an XSLT template that is working fine.
<xsl:template match="Row[contains(BenefitType, 'MyBenefit')]">
<value>
<xsl:value-of select="BenefitList/Row/Premium* 12" />
</value>
</xsl:template>
The output is
<value>100</value>
<value>110</value>
What I would prefer is if it would just output 220. So, basically in the template I would need to use some sort of variable or looping to do this and then output the final summed value?
XSLT 1 compliance is required.
The template is being used as follows:
<xsl:apply-templates select="Root/Row[contains(BenefitType, 'MyBenefit')]" />
For some reason, when I use the contains here it only sums the first structure that matches and not all of them. If The XML values parent wasn't dependent on having a sibling element that matched a specific value then a'sum' approach would work.
The direct solution to the problem was already mentioned in the comments, but assuming you really want to do the same with some variables, this might be interesting for you:
XML:
<Root>
<Row>
<BenefitType>MyBenefit</BenefitType>
<BenefitList>
<Premium>100</Premium>
</BenefitList>
</Row>
<Row>
<BenefitType>MyBenefit, OtherBenefit</BenefitType>
<BenefitList>
<Premium>100</Premium>
</BenefitList>
</Row>
<Row>
<BenefitType>OtherBenefit</BenefitType>
<BenefitList>
<Premium>1000</Premium>
</BenefitList>
</Row>
<Row>
<BenefitType>OtherBenefit</BenefitType>
<BenefitList>
<Premium>1000</Premium>
</BenefitList>
</Row>
</Root>
XSLT:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
exclude-result-prefixes="exsl">
<xsl:template match="/">
<total>
<xsl:variable name="valuesXml">
<values>
<xsl:apply-templates select="Root/Row[contains(BenefitType, 'MyBenefit')]" />
</values>
</xsl:variable>
<xsl:variable name="values" select="exsl:node-set($valuesXml)/values/value" />
<xsl:value-of select="sum($values)" />
</total>
</xsl:template>
<xsl:template match="Row[contains(BenefitType, 'MyBenefit')]">
<value>
<xsl:value-of select="BenefitList/Premium * 12" />
</value>
</xsl:template>
</xsl:stylesheet>
Here the same result set generated in your question is saved in another variable, which can then again be processed.

how to call my template and my function sequentially in xslt 2.0?

I am using xslt2.0 for convert one xml format to another xml format. This is my sample xml document.
<w:document>
<w:body>
<w:p>Para1</w:p>
<w:p>Para2</w:p>
<w:p>Para3</w:p>
<w:p>Para4</w:p>
</w:body>
</w:document>
Initially this is my xml format.so, i handled each and every <w:p> elements through my function in xslt given below...
<xsl:template match="document">
<Document>
<xsl:sequence select="mf:group(body/p, 1,count(//w:body//w:p)-1)"/>
</Document>
</xsl:template>
So,In that xslt function, i have coded how to reformat those elements.It's working fine...
But now,Xml format is restructured like given below...
<w:document>
<w:body>
<w:tbl><!--some text with children elements--></w:tbl>
<w:tbl><!--some text with children elements--></w:tbl>
<w:p>Para1</w:p>
<w:p>Para2</w:p>
<w:p>Para3</w:p>
<w:p>Para4</w:p>
</w:body>
</w:document>
So, As of now i have to handle both and elements in a same sequence.....
What i want to do is,
If i encounter elemtents then i have to call my template given below...
<xsl:template match="document">
<Document>
<xsl:for-each select="w:tbl">
<xsl:apply-templates select="w:tbl">
</xsl:apply-templates>
</xsl:for-each>
<xsl:sequence select="mf:group(body/p, 1,count(//w:body//w:p)-1)"/>
</Document>
</xsl:template>
<xsl:template match="w:tbl">
<!--xslt code here -->
</xsl:template>
But the for-each statement is not executed when I trying transformation...
So, Please guide me to get out of this issue...
I think instead of
<xsl:template match="document">
<Document>
<xsl:for-each select="w:tbl">
<xsl:apply-templates select="w:tbl">
</xsl:apply-templates>
</xsl:for-each>
<xsl:sequence select="mf:group(body/p, 1,count(//w:body//w:p)-1)"/>
</Document>
</xsl:template>
you simply want
<xsl:template match="document">
<Document>
<xsl:apply-templates select="w:body/w:tbl"/>
<xsl:sequence select="mf:group(body/p, 1,count(//w:body//w:p)-1)"/>
</Document>
</xsl:template>
If that does not do what you want then please show the result you want.

xslt wrapping duplicate lines case insensitive inside a for-each

I am trying to write a loop using XSLT so that it automatically groups all items with the same ID but in a case insensitive way. Unfortunately the data that I am trying to parse through is client driven so I cannot change it prior to load.
regardless here is a XML structure...
<Document>
<Row>
<Cell>ID</Cell>
</Row>
<Row>
<Cell>hi</Cell>
</Row>
<Row>
<Cell>Hi</Cell>
</Row>
<Row>
<Cell>Hello</Cell>
</Row>
<Row>
<Cell>Hello</Cell>
</Row>
<Row>
<Cell>Hola</Cell>
</Row>
</Document>
This is the XSLT I am currently using...
<xsl:template match="Document">
<NewDocument xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<xsl:for-each select="//Row[position() > 1]/Cell[1][not(.=preceding::Row/Cell[1])]">
<xsl:variable name="currentOrderID" select="." />
<xsl:variable name="currentOrderGroup" select="//Row[Cell[1] = $currentOrderID]" />
<MainID>
<xsl:value-of select="$currentOrderGroup[1]/Cell[1]"/>
</MainID>
<IDs>
<xsl:for-each select="$currentOrderGroup">
<id>
<xsl:value-of select="Cell[1]"/>
</id>
</xsl:for-each>
</IDs>
</xsl:for-each>
</NewDocument>
</xsl:template>
This is just wrapping up things as expected in a CaSe SeNSiTiVe way...
I've been trying to use a translate in there in order to make everything uppercase, however I can't seem to get the syntax just right.
The result I am trying to achieve here is this:
<NewDocument>
<MainID>hi</MainID>
<IDs>
<id>hi</id>
<id>Hi</id>
</IDs>
<MainID>Hello</MainID>
<IDs>
<id>Hello</id>
<id>Hello</id>
</IDs>
<MainID>Hola</MainID>
<IDs>
<id>Hola</id>
</IDs>
</NewDocument>
Can't seem to find anything specifically for what I need.
Thanks!
In XSLT1.0, to convert strings to lower case you need to use the rather cumbersome translate function in xpath.
translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')
Furthermore, your problem is one of grouping, and in XSLT1.0 that usually means a technique known as Meunchian Grouping. To do, this you first define a key to look up items in the groups you require
<xsl:key
name="Cell"
match="Cell"
use="translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')"/>
Here we are looking up cells based on their (lower-case) text content.
To find the first element in each group, you look for Cell elements in the XML which also happen to be the first element occurring in your look-up key
<xsl:apply-templates
select="Row/Cell
[generate-id()
= generate-id(
key('Cell',
translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'))[1])]"/>
Then, when you match the first element, you can then match all elements within the group by looking at the key.
Here is the full XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:key name="Cell" match="Cell" use="translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')"/>
<xsl:template match="Document">
<NewDocument>
<xsl:apply-templates select="Row/Cell[generate-id() = generate-id(key('Cell', translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'))[1])]"/>
</NewDocument>
</xsl:template>
<xsl:template match="Cell">
<MainID>
<xsl:value-of select="."/>
</MainID>
<IDs>
<xsl:apply-templates select="key('Cell', translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'))" mode="group"/>
</IDs>
</xsl:template>
<xsl:template match="Cell" mode="group">
<id>
<xsl:value-of select="."/>
</id>
</xsl:template>
</xsl:stylesheet>
Note the use of the mode attribute, to distinguish between the two templates matching Cell elements.
When applied to your XML, the following is output:
<NewDocument>
<MainID>ID</MainID>
<IDs>
<id>ID</id>
</IDs>
<MainID>hi</MainID>
<IDs>
<id>hi</id>
<id>Hi</id>
</IDs>
<MainID>Hello</MainID>
<IDs>
<id>Hello</id>
<id>Hello</id>
</IDs>
<MainID>Hola</MainID>
<IDs>
<id>Hola</id>
</IDs>
</NewDocument>
Note, I wasn't sure what to do with the Cell with ID as a value, so I left that it in. If you do want to exclude it, just add this line to the XSLT
<xsl:template match="Cell[. = 'ID']" />