1

I have an xml attribute that contains elements with id, some of those ids may occure more than once. In that case, I'd like to append _copy to all but the first element with that id.

So my xml file looks like this:

<elems>
<elem id="123"/>
<elem id="2832"/>
<elem id="2272"/>
<elem id="123"/>
<elem id="123"/>
</elems>

Desired output:

<elems>
<elem id="123"/>
<elem id="2832"/>
<elem id="2272"/>
<elem id="123_copy"/>
<elem id="123_copy"/>
</elems>

What would be the best way to do that? I was thinking something along the lines of reading the document into a variable and then checking if the id occurs more than once...

Thanks for help and tips!

4
  • 1
    Is it important to preserve the original order of the elems? If not, you could group them by id and append "_copy" to all members of a group except the first one. Commented Mar 13, 2015 at 14:56
  • no, the order doesnt matter. Ive just tried it with for-each-group. How would I append the "_copy"? Commented Mar 13, 2015 at 16:16
  • @user3813234 Please start accepting answers if they solved your problem or were helpful by ticking them right. Commented Mar 16, 2015 at 1:41
  • Sorry, was busy for a while! Commented Mar 19, 2015 at 9:37

3 Answers 3

3

Would this XSLT solve your problem:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
    <xsl:output method="xml" indent="yes"/>
    <xsl:strip-space elements="*"/>

    <xsl:key name="element" match="elem" use="@id"/>

    <xsl:template match="@* | node()">
        <xsl:copy>
            <xsl:apply-templates select="@*, node()"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="elem[count(key('element', @id)[1] | .) = 2]">
        <elem id="{concat(@id, '_copy')}"/>
    </xsl:template>
</xsl:stylesheet>
Sign up to request clarification or add additional context in comments.

1 Comment

A simpler predicate that doesn't require the key would be match="elem[preceding-sibling::elem/@id = @id]"
2

Ive just tried it with for-each-group. How would I append the "_copy"?

How about:

XSLT 2.0

<xsl:stylesheet version="2.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="/elems">
    <xsl:copy>
        <xsl:for-each-group select="elem" group-by="@id">
            <xsl:for-each select="current-group()">
                <elem id="{@id}{if(position() gt 1) then '_copy' else ''}"/>
            </xsl:for-each>
        </xsl:for-each-group>
    </xsl:copy>
</xsl:template>

</xsl:stylesheet>

1 Comment

I used this version slightly adapted. Thanks a lot!!
2

I would key the attribute itself and write the condition with the XPath 2.0 is operator:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">

<xsl:param name="marker" select="'_copy'"/>

<xsl:key name="id" match="@id" use="."/>

<xsl:template match="@* | node()">
  <xsl:copy>
    <xsl:apply-templates select="@* , node()"/>
  </xsl:copy>
</xsl:template>

<xsl:template match="@id[not(. is key('id', .)[1])]">
  <xsl:attribute name="{name()}" select="concat(., $marker)"/>
</xsl:template>

</xsl:stylesheet>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.