XSLT- trying to avoid for each looping - xslt-1.0

I have the source collection as below.
<SourceCollection>
<Row1>
<Col1>Store1</Col1>
<Col2>Med1</Col2>
<Col3>Val1</Col3>
</Row1>
<Row2>
<Col1>Store1</Col1>
<Col2>Med1</Col2>
<Col3>Val2</Col3>
</Row2>
<Row3>
<Col1>Store1</Col1>
<Col2>Med2</Col2>
<Col3>Val2</Col3>
</Row3>
<Row4>
<Col1>Store2</Col1>
<Col2>Med1</Col2>
<Col3>Val4</Col3>
</Row4>
</SourceCollection>
I want the target collection as below with minimal usage of for each.
<TargetCollection>
<Store value=Store1>
<Dim value=Med1>
<DimCode>Val1</DimCode>
<DimCode>Val2</DimCode>
</Dim>
<Dim value=Med2>
<DimCode>Val3</DimCode>
</Dim>
</Store>
<Store value=Store2>
<Dim value=Med1>
<DimCode>Val4</DimCode>
</Dim>
</Store>
</TargetCollection> `
In my initial design I used 3 for-each loops on the same source collection to form the target xml as required. But,the source collection contains millions of rows, which is making my transformation to traverse the whole list (Rows)to the power of 3 times, which is hitting the system performance.
Please let me know any ideas to avoid this.

Generally, using xsl:key can help speed up transforms. Try something like this:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output indent="yes"/>
<xsl:key name="Col1" match="Col1" use="."/>
<xsl:key name="Col2byCol1" match="Col2" use="concat(../Col1, '-', .)"/>
<xsl:key name="Col3byCol1andCol2"
match="Col3" use="concat(../Col1, '-', ../Col2, '-', .)"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="SourceCollection">
<TargetCollection>
<xsl:apply-templates
select="*/Col1[generate-id() =
generate-id(key(local-name(), .)[1])]"/>
</TargetCollection>
</xsl:template>
<xsl:template match="Col1">
<xsl:variable name="Column1" select="."/>
<Store value="{.}">
<xsl:apply-templates select="../../*/Col2[generate-id() =
generate-id(key('Col2byCol1', concat($Column1, '-' ,.))[1])]"/>
</Store>
</xsl:template>
<xsl:template match="Col2">
<xsl:variable name="Column1" select="../Col1"/>
<xsl:variable name="Column2" select="."/>
<Dim value="{.}">
<xsl:apply-templates
select="../../*[Col2=$Column2]/Col3[generate-id() =
generate-id(key('Col3byCol1andCol2',
concat($Column1, '-', $Column2, '-',.)))]"/>
</Dim>
</xsl:template>
<xsl:template match="Col3">
<DimCode value="{.}">
<xsl:apply-templates />
</DimCode>
</xsl:template>
</xsl:stylesheet>

Related

XSLT 1.0: How to determine the position of a node based on the value

thanks for taking the time!
I am trying to get the position of a node based on its value, so that I can then get corresponding entries. Please let me show you with an example, it will be easier to explain.
Here is the source XML:
<?xml version="1.0" encoding="UTF-8"?>
<sampleSourceXML>
<Table>
<Header>
<Column>Name</Column>
<Column>Surname</Column>
<Column>Title</Column>
</Header>
<Body>
<Row>
<Entry>James</Entry>
<Entry>Bond</Entry>
<Entry>Mr</Entry>
</Row>
<Row>
<Entry>Harry</Entry>
<Entry>Potter</Entry>
<Entry>Mr</Entry>
</Row>
</Body>
</Table>
</sampleSourceXML>
This is the resulting XML that I would like to achieve:
<?xml version="1.0" encoding="UTF-8"?>
<sampleResultXML>
<Person>
<FirstName>James</FirstName>
<LastName>Bond</LastName>
<Title>Mr</Title>
</Person>
<Person>
<FirstName>Harry</FirstName>
<LastName>Potter</LastName>
<Title>Mr</Title>
</Person>
</sampleResultXML>
I tried it with this XSLT:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/">
<xsl:element name="sampleResultXML">
<xsl:for-each select="sampleSourceXML/Table/Rows/Row">
<xsl:element name="Person">
<xsl:element name="FirstName">
<xsl:value-of select="./Entry[1]/text()"></xsl:value-of>
</xsl:element>
<xsl:element name="LastName">
<xsl:value-of select="./Entry[2]/text()"></xsl:value-of>
</xsl:element>
<xsl:element name="Title">
<xsl:value-of select="./Entry[3]/text()"></xsl:value-of>
</xsl:element>
</xsl:element>
</xsl:for-each>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
This works.
But! The source XML can have a different order. For example the Title element can be above the Name element. So the hardcoded position in my XSLT (e.g. Entry[3]) will not work any more.
The header order and the entry order are always corresponding. So if the Column Title is in position 3, so will be the corresponding row.
So I need a way to get the position of, let's say, the Title element dynamically. So I have to go through the Column elements until I reach the one with value "Title". This position I thought I'd save in a variable (e.g. Position_Title) so that I can fill the value later like so:
<xsl:element name="Title">
<xsl:value-of select="./Entry[$Position_Title]/text()"></xsl:value-of>
</xsl:element>
However I haven't been able to find a way to do this...
Do you have any ideas how I can achieve this?
Thanks!
Nick
EDIT: Thanks to Michael for the help! Here is the final XSLT I'm using:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"/>
<xsl:template match="/sampleSourceXML">
<xsl:variable name="cols" select="Table/Header/Column"/>
<sampleResultXML>
<xsl:for-each select="Table/Body/Row">
<Person>
<xsl:for-each select="Entry">
<xsl:variable name="i" select="position()"/>
<xsl:variable name="elementName">
<xsl:choose>
<xsl:when test="$cols[$i]='Name'">
<xsl:value-of select="'FirstName'"/>
</xsl:when>
<xsl:when test="$cols[$i]='Surname'">
<xsl:value-of select="'LastName'"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$cols[$i]"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:element name="{$elementName}">
<xsl:value-of select="."/>
</xsl:element>
</xsl:for-each>
</Person>
</xsl:for-each>
</sampleResultXML>
</xsl:template>
</xsl:stylesheet>
So I need a way to get the position of, let's say, the Title element dynamically. So I have to go through the Column elements until I reach the one with value "Title". This position I thought I'd save in a variable (e.g. Position_Title) so that I can fill the value later like so:
I think it could be much simpler if you do it from the opposite direction. Try:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="utf-8" indent="yes"/>
<xsl:template match="/sampleSourceXML">
<xsl:variable name="cols" select="Table/Header/Column" />
<sampleResultXML>
<xsl:for-each select="Table/Body/Row">
<Person>
<xsl:for-each select="Entry">
<xsl:variable name="i" select="position()" />
<xsl:element name="{$cols[$i]}">
<xsl:value-of select="."/>
</xsl:element>
</xsl:for-each>
</Person>
</xsl:for-each>
</sampleResultXML>
</xsl:template>
</xsl:stylesheet>

Apply templates that matchs two conditions

I need to list all the INST names but only if the "onlyTesters" node don´t exists in the "inst/idef" part of XML body above.
I know thats strange but I can´t change the XML I receive.
XML:
<river>
<station num="699">
<inst name="FLU(m)" num="1">
<idef></idef>
</inst>
<inst name="Battery(V)" num="18">
<idef>
<onlyTesters/>
</idef>
</inst>
</station>
<INST name="PLU(mm)" num="0" hasData="1" virtual="0"/>
<INST name="FLU(m)" num="1" hasData="1" virtual="0"/>
<INST name="Q(m3/s)" num="3" hasData="1" virtual="1"/>
<INST name="Battery(V)" num="18" hasData="1" virtual="0"/>
</river>
XSL:
<xsl:template match="/">
<xsl:apply-templates select="//INST[#hasData = 1 and not(//inst[#num=(current()/#num)]/idef/onlyTesters)]/#name"/>
</xsl:template>
<xsl:template match="//INST[#hasData = 1 and not(//inst[#num=(current()/#num)]/idef/onlyTesters)]/#name">
<xsl:value-of select="#name"/>,
</xsl:template>
I´m having no match.
This is the result I expect:
PLU(mm),FLU(m),Q(m3/s)
You can achieve this with only one template:
<xsl:template match="/">
<xsl:for-each select="//INST[#hasData='1' and not(#name=//inst[idef/onlyTesters]/#name)]">
<xsl:value-of select="#name"/>
<xsl:if test="position() != last()">, </xsl:if>
</xsl:for-each>
</xsl:template>
Output is:
PLU(mm), FLU(m), Q(m3/s)
Cross-references are best resolved using a key - for example:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="UTF-8" />
<xsl:key name="inst" match="inst" use="#name" />
<xsl:template match="/river">
<xsl:for-each select="INST[#hasData = 1 and not(key('inst', #name)/idef/onlyTesters)]">
<xsl:value-of select="#name"/>
<xsl:if test="position() != last()">,</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Or even simpler:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="UTF-8" />
<xsl:key name="exclude" match="onlyTesters" use="ancestor::inst/#name" />
<xsl:template match="/river">
<xsl:for-each select="INST[#hasData = 1 and not(key('exclude', #name))]">
<xsl:value-of select="#name"/>
<xsl:if test="position() != last()">, </xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

XSLT1.0 Remove repeat nodes

I am trying to apply Muenchian grouping to eliminate duplicate nodes, but I seem not to be able to make it work.
Input:
<GetUsersByRoleRspMsg>
<UserList>
<User>
<UserId>PPAUSER1</UserId>
<Email>xyz#gmail.com</Email>
</User>
<User>
<UserId>PPAUSER1</UserId>
<Email>xyz#gmail.com</Email>
</User>
</UserList>
</GetUsersByRoleRspMsg>
Required:
<GetUsersByRoleRspMsg>
<UserList>
<User>
<UserId>PPAUSER1</UserId>
<Email>xyz#gmail.com</Email>
</User>
</UserList>
</GetUsersByRoleRspMsg>
The critical part of my code is
<xsl:key name="userIdEmailPair"
match="User"
use="concat(UserId,' ',Email)"/>
<xsl:copy>
<xsl:for-each select="User[
count(. | key('userIdEmailPair',
concat(UserId,' ',Email))[1])
= 1]">
<xsl:copy-of select="."/>
</xsl:for-each>
</xsl:copy>
I don't see any problem with your grouping code. If you try:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="userIdEmailPair" match="User" use="concat(UserId,' ',Email)"/>
<!-- identity transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="UserList">
<xsl:copy>
<xsl:for-each select="User[count(. | key('userIdEmailPair',concat(UserId,' ',Email))[1]) = 1]">
<xsl:copy-of select="."/>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
you will get the result you're looking for.
Of course, you could shorten the last template to:
<xsl:template match="UserList">
<xsl:copy>
<xsl:copy-of select="User[count(. | key('userIdEmailPair',concat(UserId,' ',Email))[1]) = 1]"/>
</xsl:copy>
</xsl:template>

Remove Duplicates from a list XSLT 1.0 based on element value

I need help with writing a function in xslt 1.0 that I can pass a list and it'll return the list with duplicates removed. It needs to be a template that I can easily clone or modify for other lists since there are several lists that I want to be able to run this.
Here is one example:
Input list:
<Diagnoses>
<Code>444.4</Code>
<Code>959.99</Code>
<Code>524</Code>
<Code>444.4</Code>
</Diagnoses>
Desired Output, after the duplicate code value 444.4 is removed:
<Diagnoses>
<Code>444.4</Code>
<Code>959.99</Code>
<Code>524</Code>
</Diagnoses>
Here is what I have so far, but it doesn't seem to be working:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:math="http://exslt.org/math"
xmlns:exsl="http://exslt.org/common"
xmlns:data="http://example.com/data" version="1.0"
extension-element-prefixes="math exsl"
exclude-result-prefixes="math exsl data">
<xsl:output omit-xml-declaration="yes" />
<xsl:variable name="DiagnosesList" select="Diagnoses"/>
<Diagnoses>
<xsl:call-template name="DedupeLists">
<xsl:with-param name="Input" select = "exsl:node-set($DiagnosesList)" />
</xsl:call-template>
</Diagnoses>
<xsl:template name="DedupeLists">
<xsl:param name = "Input" />
<xsl:for-each select="$Input/*">
<xsl:if test="Code[not(preceding::Code)]">
<xsl:copy-of select="."/>
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
#lingamurthy,
Just
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:strip-space elements="*"/>
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Code[. = preceding-sibling::Code]"/>
</xsl:stylesheet>
This is one way:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:strip-space elements="*"/>
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Code[not(. = preceding-sibling::Code)]">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Code"/>
</xsl:stylesheet>

XSLT code for converting attributes to child elements and putting the value of current element into its another new child element

I am unaware of XSLT,Please help:
I have the following XML:
<OLifE>
<Holding id="1234">
<HoldingKey>1397650618090</HoldingKey>
<HoldingTypeCode tc="2">Policy</HoldingTypeCode>
<HoldingStatus tc="2">Inactive</HoldingStatus>
<CarrierAdminSystem>PAS</CarrierAdminSystem>
</Holding>
</OLifE>
I want the output like this:
<OLifE>
<Holding>
<id>1234</id>
<HoldingKey>1397650618090</HoldingKey>
<HoldingTypeCode>
<tc>2</tc>
<value>Policy</value>
</HoldingTypeCode>
<HoldingStatus>
<tc>2</tc>
<value>Inactive</value>
</HoldingStatus>
<CarrierAdminSystem>PAS</CarrierAdminSystem>
</Holding>
</OLifE>
please note that, all attributes are needed to be converted to child elements but, for elements that have a tc attribute specified:
<HoldingTypeCode tc="2">Policy</HoldingTypeCode>
need to be handled carefully.
I DO NOT want the output like:
<HoldingTypeCode>
<tc>2</tc>Policy</HoldingTypeCode>
I have the following XSLT code which needs the slight modification:
<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml"/>
<xsl:template match="*">
<xsl:element name="{name()}">
<xsl:for-each select="#*">
<xsl:element name="{name()}">
<xsl:value-of select="."/>
</xsl:element>
</xsl:for-each>
<xsl:apply-templates select="*|text()"/>
</xsl:element>
</xsl:template>
Please help.
Assuming you want a generic solution, how about:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<!-- modified identity transform -->
<xsl:template match="node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<!-- attributes to elements -->
<xsl:template match="#*">
<xsl:element name="{name()}">
<xsl:value-of select="."/>
</xsl:element>
</xsl:template>
<!-- avoid mixed content -->
<xsl:template match="text()[../#*]">
<value>
<xsl:value-of select="."/>
</value>
</xsl:template>
</xsl:stylesheet>
Edit
However, the root element in my original XML has got xmlns="some web
address" attribute.
In such case, change the first template to:
<!-- modified identity transform -->
<xsl:template match="*">
<xsl:element name="{name()}">
<xsl:apply-templates select="#* | node()"/>
</xsl:element>
</xsl:template>
This is assuming you want all the output nodes to be in no namespace.
I think you can define a template like this to be called where you want to output the attributes :
<xsl:template name="transform.attrs">
<xsl:for-each select="#*">
<xsl:element name="{name()}">
<xsl:value-of select="."/>
</xsl:element>
</xsl:for-each>
</xsl:template>
For the most general case you use almost the same template as before, just call the new template for attributes:
<xsl:template match="*">
<xsl:element name="{name()}">
<xsl:call-template name="transform.attrs">
<xsl:apply-templates select="*|text()"/>
</xsl:element>
</xsl:template>
And for the specific elements, bearing the #tc attribute:
<xsl:template match="*[#tc]">
<xsl:element name="{name()}">
<xsl:call-template name="transform.attrs" />
<value>
<xsl:apply-templates select="*|text()"/>
</value>
</xsl:element>
</xsl:template>