I have a strange transformation that I'm trying to do.
XML looks like this:
<?xml version="1.0" standalone="yes"?>
<Parent>
<RecordCount>4</RecordCount>
<Record name="1">
<Child1>Value 1</Child1>
<Child2>Value 2</Child2>
</Record>
</Parent>
And this is what it needs to look like:
<?xml version="1.0" standalone="yes"?>
<Parent>
<RecordCount>4</RecordCount>
<Record name="1">
<Child1>Value 1</Child1>
<Child2>Value 2</Child2>
</Record>
<Record name="2">
<Child1>Value 1</Child1>
<Child2>Value 2</Child2>
</Record>
<Record name="3">
<Child1>Value 1</Child1>
<Child2>Value 2</Child2>
</Record>
<Record name="4">
<Child1>Value 1</Child1>
<Child2>Value 2</Child2>
</Record>
</Parent>
Is something like this even possible with XSLT or should I rather just handle this in code?
Here's another way using XSLT 2.0...
XML Input
<Parent>
<RecordCount>4</RecordCount>
<Record name="1">
<Child1>Value 1</Child1>
<Child2>Value 2</Child2>
</Record>
</Parent>
XSLT 2.0
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="RecordCount">
<xsl:variable name="record" select="../Record"/>
<xsl:copy-of select="."/>
<xsl:for-each select="1 to .">
<xsl:apply-templates select="$record" mode="replicate">
<xsl:with-param name="cnt" select="."/>
</xsl:apply-templates>
</xsl:for-each>
</xsl:template>
<xsl:template match="Record" mode="replicate">
<xsl:param name="cnt"/>
<Record name="{$cnt}">
<xsl:apply-templates select="#* except #name|node()"/>
</Record>
</xsl:template>
<xsl:template match="Record"/>
</xsl:stylesheet>
Output
<Parent>
<RecordCount>4</RecordCount>
<Record name="1">
<Child1>Value 1</Child1>
<Child2>Value 2</Child2>
</Record>
<Record name="2">
<Child1>Value 1</Child1>
<Child2>Value 2</Child2>
</Record>
<Record name="3">
<Child1>Value 1</Child1>
<Child2>Value 2</Child2>
</Record>
<Record name="4">
<Child1>Value 1</Child1>
<Child2>Value 2</Child2>
</Record>
</Parent>
Try following xlst
<?xml version="1.0" encoding="UTF-8"?>
<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="/Parent">
<Parent>
<xsl:variable name="count" select="RecordCount" />
<xsl:call-template name="multiply">
<xsl:with-param name="maxCount" select="$count" />
<xsl:with-param name="nodeToCopy" select="Record" />
</xsl:call-template>
</Parent>
</xsl:template>
<xsl:template name="multiply">
<xsl:param name="maxCount" />
<xsl:param name="i" select="1" />
<xsl:param name="nodeToCopy" />
<xsl:choose>
<xsl:when test="$i <= $maxCount">
<xsl:element name="{name($nodeToCopy)}">
<xsl:attribute name="name">
<xsl:value-of select="$i" />
</xsl:attribute>
<xsl:copy-of select="$nodeToCopy/child::*" />
</xsl:element>
<xsl:call-template name="multiply">
<xsl:with-param name="maxCount" select="$maxCount" />
<xsl:with-param name="nodeToCopy" select="$nodeToCopy" />
<xsl:with-param name="i" select="$i+1" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise />
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
It is based on recursive calling of named template with increasing of "iterating" value. If would be something unclear just write a comment.
Related
I am trying to re-structure xml data into groups and sub-group. I was able to get it to work, but my code has to include something, that looks (at least to me) like a workaround. Here are my sample files:
Data.xml:
<data>
<record Group="g1" SubGroup="sg1">Record 1</record>
<record Group="g2" SubGroup="sg1">Record 2</record>
<record Group="g1" SubGroup="sg1">Record 3</record>
<record Group="g2" SubGroup="sg1">Record 4</record>
<record Group="g2" SubGroup="sg2">Record 5</record>
<record Group="g1" SubGroup="sg2">Record 6</record>
</data>
Stylesheet.xsl:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" version="1.0" indent="yes" encoding="UTF-8"/>
<xsl:key name="Group" match="record" use="#Group" />
<xsl:key name="SubGroup" match="record" use="#SubGroup" />
<xsl:template match="/data">
<xsl:variable name="Records" select="record"/>
<data>
<xsl:for-each select="$Records[generate-id(.)=generate-id(key('Group',#Group)[1])]">
<xsl:sort select="#Group"/>
<xsl:variable name="Group" select="#Group"/>
<xsl:call-template name="Group">
<xsl:with-param name="Records" select="$Records[#Group = $Group]"/>
<xsl:with-param name="Group" select="$Group"/>
</xsl:call-template>
</xsl:for-each>
</data>
</xsl:template>
<xsl:template name="Group">
<xsl:param name="Records"/>
<xsl:param name="Group"/>
<group name="{$Group}">
<xsl:for-each select="$Records[generate-id(.)=generate-id(key('SubGroup',#SubGroup)[1])]">
<!-- this works: <xsl:for-each select="$Records[generate-id(.)=generate-id(key('SubGroup',#SubGroup)[#Group = $Group][1])]"> -->
<xsl:sort select="#SubGroup"/>
<xsl:variable name="SubGroup" select="#SubGroup"/>
<xsl:call-template name="SubGroup">
<xsl:with-param name="Records" select="$Records[#SubGroup = $SubGroup]"/>
<xsl:with-param name="Group" select="$Group"/>
<xsl:with-param name="SubGroup" select="$SubGroup"/>
</xsl:call-template>
</xsl:for-each>
</group>
</xsl:template>
<xsl:template name="SubGroup">
<xsl:param name="Records"/>
<xsl:param name="Group"/>
<xsl:param name="SubGroup"/>
<subgroup name="{$SubGroup}">
<xsl:for-each select="$Records">
<xsl:copy-of select="."/>
</xsl:for-each>
</subgroup>
</xsl:template>
</xsl:stylesheet>
This is the output generated:
<?xml version="1.0" encoding="UTF-8"?>
<data>
<group name="g1">
<subgroup name="sg1">
<record Group="g1" SubGroup="sg1">Record 1</record>
<record Group="g1" SubGroup="sg1">Record 3</record>
</subgroup>
</group>
<group name="g2">
<subgroup name="sg2">
<record Group="g2" SubGroup="sg2">Record 5</record>
</subgroup>
</group>
</data>
but this is the output, I want to have:
<?xml version="1.0" encoding="UTF-8"?>
<data>
<group name="g1">
<subgroup name="sg1">
<record Group="g1" SubGroup="sg1">Record 1</record>
<record Group="g1" SubGroup="sg1">Record 3</record>
</subgroup>
<subgroup name="sg2">
<record Group="g1" SubGroup="sg2">Record 6</record>
</subgroup>
</group>
<group name="g2">
<subgroup name="sg1">
<record Group="g2" SubGroup="sg1">Record 2</record>
<record Group="g2" SubGroup="sg1">Record 4</record>
</subgroup>
<subgroup name="sg2">
<record Group="g2" SubGroup="sg2">Record 5</record>
</subgroup>
</group>
</data>
The problem is the for-each loop in the tempalte named "Group". It seems, that the key()-function is not working on the nodes contained in $Records but on the entire input XML file.
I get identical results with xsltproc and with saxon, so I do not think, it is a bug in my xslt processor. It seems, that I did not completely understand, how key() works.
If I add an additional selector [#Group = $Group] to key()'s output, I get the expected result.
Can somebody explain what is going on and why the additional selector [#Group = $Group] is needed.
Mario
When you want to do sub-grouping, you need to use a concatenated key of both the main group and the sub-group
<xsl:key name="SubGroup" match="record" use="concat(#Group,'|', #SubGroup)" />
Then, just use it in the same way as before, with the concatenation
<xsl:for-each select="$Records[generate-id(.)=generate-id(key('SubGroup',concat(#Group,'|', #SubGroup))[1])]">
Try this XSLT (which I have also simplified to utilise the key when calling your named templates with the records)
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" version="1.0" indent="yes" encoding="UTF-8"/>
<xsl:key name="Group" match="record" use="#Group" />
<xsl:key name="SubGroup" match="record" use="concat(#Group,'|', #SubGroup)" />
<xsl:template match="/data">
<data>
<xsl:for-each select="record[generate-id(.)=generate-id(key('Group',#Group)[1])]">
<xsl:sort select="#Group"/>
<xsl:call-template name="Group">
<xsl:with-param name="Records" select="key('Group',#Group)"/>
</xsl:call-template>
</xsl:for-each>
</data>
</xsl:template>
<xsl:template name="Group">
<xsl:param name="Records"/>
<group name="{#Group}">
<xsl:for-each select="$Records[generate-id(.)=generate-id(key('SubGroup',concat(#Group,'|', #SubGroup))[1])]">
<xsl:sort select="#SubGroup"/>
<xsl:call-template name="SubGroup">
<xsl:with-param name="Records" select="key('SubGroup',concat(#Group,'|', #SubGroup))"/>
</xsl:call-template>
</xsl:for-each>
</group>
</xsl:template>
<xsl:template name="SubGroup">
<xsl:param name="Records"/>
<subgroup name="{#SubGroup}">
<xsl:for-each select="$Records">
<xsl:copy-of select="."/>
</xsl:for-each>
</subgroup>
</xsl:template>
</xsl:stylesheet>
I'm trying to transform a XML file with XSLT 1.0 but I'm having troubles with this.
Input:
<task_order>
<Q>
<record id="1">
<column name="task_externalId">SPLIT4_0</column>
</record>
<record id="2">
<column name="task_externalId">SPLIT4_1</column>
</record>
</Q>
<task>
<id>SPLIT4</id>
<name>test</name>
</task>
</task_order>
Wanted result:
For each task_order element: When there is more than 1 record-element (SPLIT4 and SPLIT4_1) I need to duplicate the task element and change the orginal task-id with the id from record elements.
<task_order>
<Q>
<record id="1">
<column name="task_externalId">SPLIT4_0</column>
</record>
<record id="2">
<column name="task_externalId">SPLIT4_1</column>
</record>
</Q>
<task>
<id>SPLIT4_0</id>
<name>test</name>
</task>
<task>
<id>SPLIT4_1</id>
<name>test</name>
</task>
</task_order>
Any suggestions?
Thank you
First start off with the Identity Template which will handle copying across all existing nodes
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
Next, to make looking up the columns slightly easier, consider using an xsl:key
<xsl:key name="column" match="column" use="substring-before(., '_')" />
Then, you have a template matching task where you can look up all matching column elements using the key, and create a new task element for each
<xsl:template match="task">
<xsl:variable name="task" select="." />
<xsl:for-each select="key('column', id)">
<!-- Create new task -->
</xsl:for-each>
</xsl:template>
Try this XSTL
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" indent="yes" />
<xsl:key name="column" match="column" use="substring-before(., '_')" />
<xsl:template match="task">
<xsl:variable name="task" select="." />
<xsl:for-each select="key('column', id)">
<task>
<id><xsl:value-of select="." /></id>
<xsl:apply-templates select="$task/*[not(self::id)]" />
</task>
</xsl:for-each>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Please help with this xml:
<document>
<sheet id="0" name="Sheet1">
<line id="0">
<field id="0"><![CDATA[Calculate]]></field>
</line>
<line id="1">
<field id="0"><![CDATA[Quantity]]></field>
<field id="1"><![CDATA[Value]]></field>
</line>
<line id="2">
<field id="0"><![CDATA[3]]></field>
<field id="1"><![CDATA[2]]></field>
</line>
<line id="3">
<field id="0"><![CDATA[2]]></field>
<field id="1"><![CDATA[7]]></field>
</line>
<line id="4">
<field id="0"></field>
<field id="1"></field>
</line>
</sheet>
</document>
I need to get Sum of field[#id=1] before multiply this field with field[#id=0]. Line[#id=4] is empty so there should be condition to eliminate this line.
correct result should be:
<document>
<id>Calculate</id>
<line>
<sum>20</sum>
</line>
</document>
Here is an XSLT 2.0 solution that can be run with XSLT 2.0 processors like Saxon 9, XQSharp, AltovaXML:
<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:output indent="yes"/>
<xsl:template match="document">
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="sheet">
<id><xsl:value-of select="line[#id = 0]/field"/></id>
<line>
<sum>
<xsl:value-of select="sum(line[field[#id = 1] castable as xs:double and field[#id = 0] castable as xs:double]/(field[#id = 1] * field[#id = 0]))"/>
</sum>
</line>
</xsl:template>
</xsl:stylesheet>
[edit] I am adding an XSLT 1.0 stylesheet with a named recursive template below:
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output indent="yes"/>
<xsl:template match="document">
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template name="sum">
<xsl:param name="lines"/>
<xsl:param name="total" select="0"/>
<xsl:choose>
<xsl:when test="not($lines)">
<xsl:value-of select="$total"/>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="sum">
<xsl:with-param name="lines" select="$lines[position() > 1]"/>
<xsl:with-param name="total" select="$total + $lines[1]/field[#id = 1] * $lines[1]/field[#id = 0]"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="sheet">
<id><xsl:value-of select="line[#id = 0]/field"/></id>
<line>
<sum>
<xsl:call-template name="sum">
<xsl:with-param name="lines"
select="line[number(field[#id = 1]) = number(field[#id = 1])
and number(field[#id = 0]) = number(field[#id = 0])]"/>
</xsl:call-template>
</sum>
</line>
</xsl:template>
</xsl:stylesheet>
How can I eliminate duplicate nodes based on values of multiple (more than 1) attributes? Also the attribute names are passed as parameters to the stylesheet. Now I am aware of the Muenchian method of grouping that uses a <xsl:key> element. But I came to know that XSLT 1.0 does not allow paramters/variables in <xsl:key>.
Is there another method(s) to achieve duplicate nodes removal? It is fine if it not as efficient as the Munechian method.
Update from previus question:
XML:
<data id = "root">
<record id="1" operator1='xxx' operator2='yyy' operator3='zzz'/>
<record id="2" operator1='abc' operator2='yyy' operator3='zzz'/>
<record id="3" operator1='abc' operator2='yyy' operator3='zzz'/>
<record id="4" operator1='xxx' operator2='yyy' operator3='zzz'/>
<record id="5" operator1='xxx' operator2='lkj' operator3='tyu'/>
<record id="6" operator1='xxx' operator2='yyy' operator3='zzz'/>
<record id="7" operator1='abc' operator2='yyy' operator3='zzz'/>
<record id="8" operator1='abc' operator2='yyy' operator3='zzz'/>
<record id="9" operator1='xxx' operator2='yyy' operator3='zzz'/>
<record id="10" operator1='rrr' operator2='yyy' operator3='zzz'/>
</data>
Other approach for a single transformation in two steps:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
exclude-result-prefixes="msxsl">
<xsl:key name="kItemByLocal" match="record[#local-key]" use="#local-key"/>
<xsl:param name="pAttNames" select="'operator1 operator2 operator3'"/>
<xsl:template match="/">
<xsl:variable name="vFirstRTF">
<xsl:apply-templates/>
</xsl:variable>
<xsl:apply-templates select="msxsl:node-set($vFirstRTF)/node()"/>
</xsl:template>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="record[not(#local-key)]">
<xsl:copy>
<xsl:attribute name="local-key">
<xsl:call-template name="local-key"/>
</xsl:attribute>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="record[#local-key]
[count(.|key('kItemByLocal',#local-key)[1])
!= 1]|#local-key"/>
<xsl:template name="local-key">
<xsl:param name="pAttributes" select="concat($pAttNames,' ')"/>
<xsl:if test="normalize-space($pAttributes)">
<xsl:variable name="vName"
select="substring-before($pAttributes,' ')"/>
<xsl:variable name="vAttribute" select="#*[name()=$vName]"/>
<xsl:value-of select="concat($vName,'+',$vAttribute,'+')"/>
<xsl:call-template name="local-key">
<xsl:with-param name="pAttributes"
select="substring-after($pAttributes,' ')"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Output:
<data id="root">
<record id="1" operator1="xxx" operator2="yyy" operator3="zzz"></record>
<record id="2" operator1="abc" operator2="yyy" operator3="zzz"></record>
<record id="5" operator1="xxx" operator2="lkj" operator3="tyu"></record>
<record id="10" operator1="rrr" operator2="yyy" operator3="zzz"></record>
</data>
Edit: Also without named template for #local-key generation
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
exclude-result-prefixes="msxsl">
<xsl:key name="kItemByLocal" match="record[#local-key]" use="#local-key"/>
<xsl:param name="pAttNames" select="'operator1 operator2 operator3'"/>
<xsl:template match="/">
<xsl:variable name="vFirstRTF">
<xsl:apply-templates/>
</xsl:variable>
<xsl:apply-templates select="msxsl:node-set($vFirstRTF)/node()"/>
</xsl:template>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="record[not(#local-key)]">
<xsl:variable name="vAttNames"
select="concat(' ',$pAttNames,' ')"/>
<xsl:copy>
<xsl:attribute name="local-key">
<xsl:for-each select="#*[contains(
$vAttNames,
concat(' ',name(),' ')
)]">
<xsl:sort select="substring-before(
$vAttNames,
concat(' ',name(),' ')
)"/>
<xsl:value-of select="concat(name(),'++',.,'++')"/>
</xsl:for-each>
</xsl:attribute>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="record[#local-key]
[count(.|key('kItemByLocal',#local-key)[1])
!= 1]|#local-key"/>
</xsl:stylesheet>
Note: If you are positive sure that attributes order is the same for all elements, then you could remove the sorting.
Use this transformation (simple and no need to generate a new stylesheet):
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ext="http://exslt.org/common">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:param name="pAttribs">
<name>operator1</name>
<name>operator2</name>
<name>operator3</name>
</xsl:param>
<xsl:variable name="vAttribs" select=
"document('')/*/xsl:param[#name='pAttribs']"/>
<xsl:key name="kRecByAtts" match="record"
use="#___g_key"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="/">
<xsl:variable name="vrtdPass1">
<xsl:apply-templates/>
</xsl:variable>
<xsl:variable name="vPass1" select=
"ext:node-set($vrtdPass1)/*"/>
<xsl:apply-templates select="$vPass1"/>
</xsl:template>
<xsl:template match="record[not(#___g_key)]">
<xsl:copy>
<xsl:copy-of select="#*"/>
<xsl:attribute name="___g_key">
<xsl:for-each select="#*[name()=$vAttribs/name]">
<xsl:sort select="name()"/>
<xsl:value-of select=
"concat('___Attrib___',name(),'___Value___',.,'+++')"/>
</xsl:for-each>
</xsl:attribute>
</xsl:copy>
</xsl:template>
<xsl:template match=
"record[#___g_key]
[not(generate-id()
=
generate-id(key('kRecByAtts', #___g_key)[1])
)
]
"/>
<xsl:template match="#___g_key"/>
</xsl:stylesheet>
When applied to the XML document of your previous question:
<data id = "root">
<record id="1" operator1='xxx' operator2='yyy' operator3='zzz'/>
<record id="2" operator1='abc' operator2='yyy' operator3='zzz'/>
<record id="3" operator1='abc' operator2='yyy' operator3='zzz'/>
<record id="4" operator1='xxx' operator2='yyy' operator3='zzz'/>
<record id="5" operator1='xxx' operator2='lkj' operator3='tyu'/>
<record id="6" operator1='xxx' operator2='yyy' operator3='zzz'/>
<record id="7" operator1='abc' operator2='yyy' operator3='zzz'/>
<record id="8" operator1='abc' operator2='yyy' operator3='zzz'/>
<record id="9" operator1='xxx' operator2='yyy' operator3='zzz'/>
<record id="10" operator1='rrr' operator2='yyy' operator3='zzz'/>
</data>
The wanted, correct result is produced:
<data id="root">
<record id="1" operator1="xxx" operator2="yyy" operator3="zzz"/>
<record id="2" operator1="abc" operator2="yyy" operator3="zzz"/>
<record id="5" operator1="xxx" operator2="lkj" operator3="tyu"/>
<record id="10" operator1="rrr" operator2="yyy" operator3="zzz"/>
</data>
If you want to pass in the attribute names as a parameter then one approach could be a two step transformation where the first step takes any XML input and simply the attribute names and the element names as parameters to generate a second stylesheet that then eliminates the duplicates.
Here is an example first stylesheet:
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
xmlns:axsl="http://www.w3.org/1999/XSL/TransformAlias"
exclude-result-prefixes="axsl exsl"
version="1.0">
<xsl:param name="parent-name" select="'items'"/>
<xsl:param name="element-name" select="'item'"/>
<xsl:param name="att-names" select="'att1,att2'"/>
<xsl:param name="sep" select="'|'"/>
<xsl:namespace-alias stylesheet-prefix="axsl" result-prefix="xsl"/>
<xsl:output method="xml" indent="yes"/>
<xsl:variable name="key-value">
<xsl:text>concat(</xsl:text>
<xsl:call-template name="define-values">
<xsl:with-param name="att-names" select="$att-names"/>
</xsl:call-template>
<xsl:text>)</xsl:text>
</xsl:variable>
<xsl:template name="define-values">
<xsl:param name="att-names"/>
<xsl:choose>
<xsl:when test="contains($att-names, ',')">
<xsl:value-of select="concat('#', substring-before($att-names, ','), ',"', $sep, '",')"/>
<xsl:call-template name="define-values">
<xsl:with-param name="att-names" select="substring-after($att-names, ',')"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="concat('#', $att-names)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="/">
<axsl:stylesheet version="1.0">
<axsl:output indent="yes"/>
<axsl:key name="k1" match="{$parent-name}/{$element-name}" use="{$key-value}"/>
<axsl:template match="#* | node()">
<axsl:copy>
<axsl:apply-templates select="#* | node()"/>
</axsl:copy>
</axsl:template>
<axsl:template match="{$parent-name}">
<axsl:copy>
<axsl:apply-templates select="#*"/>
<axsl:apply-templates select="{$element-name}[generate-id() = generate-id(key('k1', {$key-value})[1])]"/>
</axsl:copy>
</axsl:template>
</axsl:stylesheet>
</xsl:template>
</xsl:stylesheet>
It takes four parameters:
parent-name: the name of the element containing those elements of which you want to eliminate duplicates
element-name: the name of those elements of which you want to eliminate duplicates
att-names: a comma separated list of attribute names
sep: a separator character that should not occur in attribute values in the input XML
The stylesheet then generates a second stylesheet that applies Muenchian grouping to eliminate duplicates. For instance with the default parameters given in the stylesheet Saxon 6.5.5 generates the following stylesheet:
<axsl:stylesheet xmlns:axsl="http://www.w3.org/1999/XSL/Transform" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<axsl:output indent="yes"/>
<axsl:key name="k1" match="items/item" use="concat(#att1,"|",#att2)"/>
<axsl:template match="#* | node()">
<axsl:copy>
<axsl:apply-templates select="#* | node()"/>
</axsl:copy>
</axsl:template>
<axsl:template match="items">
<axsl:copy>
<axsl:apply-templates select="#*"/>
<axsl:apply-templates select="item[generate-id() = generate-id(key('k1', concat(#att1,"|",#att2))[1])]"/>
</axsl:copy>
</axsl:template>
</axsl:stylesheet>
This can the be applied to an XML document like
<items>
<item att1="a" att2="1" att3="A"/>
<item att1="b" att2="1" att3="A"/>
<item att1="a" att2="1" att3="B"/>
<item att1="c" att2="2" att3="A"/>
<item att1="d" att2="3" att3="C"/>
</items>
and the output is
<items>
<item att1="a" att2="1" att3="A"/>
<item att1="b" att2="1" att3="A"/>
<item att1="c" att2="2" att3="A"/>
<item att1="d" att2="3" att3="C"/>
</items>
Using the XSL:
<?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:output method="xml"/>
<xsl:template match="/">
<records>
<record>
<!-- Group record by bigID, for further processing -->
<xsl:for-each-group select="records/record" group-by="bigID">
<xsl:sort select="bigID"/>
<xsl:for-each select="current-group()">
<!-- Create new combined record -->
<bigID>
<!-- <xsl:value-of select="."/> -->
<xsl:for-each select=".">
<xsl:value-of select="bigID"/>
</xsl:for-each>
</bigID>
<text>
<xsl:value-of select="text"/>
</text>
</xsl:for-each>
</xsl:for-each-group>
</record>
</records>
</xsl:template>
</xsl:stylesheet>
I'm trying to change:
<?xml version="1.0" encoding="UTF-8"?>
<records>
<record>
<bigID>123</bigID>
<text>Contains text for 123</text>
<bigID>456</bigID>
<text>Some 456 text</text>
<bigID>123</bigID>
<text>More 123 text</text>
<bigID>123</bigID>
<text>Yet more 123 text</text>
</record>
</records>
into:
<?xml version="1.0" encoding="UTF-8"?>
<records>
<record>
<bigID>123
<text>Contains text for 123</text>
<text>More 123 text</text>
<text>Yet more 123 text</text>
</bigID>
<bigID>456
<text>Some 456 text</text>
</bigID>
</record>
</records>
Right now, I'm just listing the grouped <bigID>s, individually. I'm missing the step after grouping, where I combine the grouped <bigID> nodes. My suspicion is that I need to use the "key" function somehow, but I'm not sure.
Thanks for any help.
Here is the wanted XSLT 2.0 transformation:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:key name="kTextforId" match="text"
use="preceding-sibling::bigID[1]"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="record">
<record>
<xsl:for-each-group select="bigID" group-by=".">
<bigID>
<xsl:sequence select="current-grouping-key()"/>
<xsl:copy-of select=
"key('kTextforId', current-grouping-key())"/>
</bigID>
</xsl:for-each-group>
</record>
</xsl:template>
</xsl:stylesheet>
When performed on the provided XML document, the wanted result is produced.
In XSLT 2.0 you don't need keys for grouping.
Since you are just copying the text elements in the group, the inner for-each can be removed.
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" />
<xsl:template match="/">
<records>
<record>
<xsl:for-each-group select="records/record/bigID" group-by=".">
<xsl:sort select="." data-type="number" />
<bigID>
<xsl:value-of select="." />
<xsl:copy-of select="current-group()/following-sibling::text[1]" />
</bigID>
</xsl:for-each-group>
</record>
</records>
</xsl:template>
</xsl:stylesheet>
If you instead wanted to output the bigID elements followed by their text elements, then my loop would be replaced by the following.
<xsl:for-each-group select="records/record/bigID" group-by=".">
<xsl:sort select="." data-type="number" />
<xsl:copy-of select="." />
<xsl:copy-of select="current-group()/following-sibling::text[1]" />
</xsl:for-each-group>