Search code examples
xmlxsltxslt-2.0

XSLT discard duplicates on multiple criteria


I have the following data structure and need to output the ids of every node with every combination of v1 and v2 exactly once, where v equals A.

The nodes with ids 2,3,4,6,7 should be printed.

<root>
    <node>
        <v>A</v>
        <id>2</id>
        <v1>S</v1>
        <v2>S</v2>
    </node>
    <node>
        <v>A</v>
        <id>3</id>
        <v1>S</v1>
        <v2>S1</v2>
    </node>
    <node>
        <v>A</v>
        <id>4</id>
        <v1>S2</v1>
        <v2>S1</v2>
    </node>
    <node>
        <v>B</v>
        <id>5</id>
        <v1>S2</v1>
        <v2>S3</v2>
    </node>
    <node>
        <v>A</v>
        <id>6</id>
        <v1>S2</v1>
        <v2>S3</v2>
    </node>
    <node>
        <v>A</v>
        <id>7</id>
        <v1>S</v1>
        <v2>S3</v2>
    </node>
    <node>
        <v>A</v>
        <id>8</id>
        <v1>S</v1>
        <v2>S</v2>
    </node>
</root>

I tried using an xsl:key, however unfortunately only unique elements are printed (id=2 is missing)

Using preceeding as shown in the following does neither generated the desired result.

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

    <!-- pos 1 -->
    <xsl:key name="keys" match="node" use="concat(v1, '|', v2, '|', v)"/>
    <!-- /pos 1 -->

    <xsl:template match="root">
        <xsl:for-each select="node[v='A']">

            <!-- pos 1 -->
            <xsl:variable name="vDups" select="key('keys', concat(v1, '|', v2, '|', v))[not(generate-id() = generate-id(current()))]" />
            <xsl:if test="not($vDups)">
                <node>
                    <xsl:value-of select="current()/id"/>
                </node>
            </xsl:if>
            <!-- /pos 1 -->

            <!-- pos 2 -->
            <xsl:if test="not(preceding::node/v1=current()/v1 and preceding::node/v2 = current()/v2)">
                <node>
                    <xsl:value-of select="id" />
                </node>
            </xsl:if>
            <!-- /pos 2 -->

        </xsl:for-each>
    </xsl:template>
</xsl:stylesheet>

How can I achieve the desired result?


Solution

  • You've tagged this XSLT 2.0, and have version="2.0" in your stylesheet, in which case you can make use of xsl:for-each-group to simplify your XSLT

    <xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
        <xsl:output indent="yes"/>
    
        <xsl:template match="root">
            <xsl:for-each-group select="node[v = 'A']" group-by="concat(v1, '|', v2)">
                    <node>
                        <xsl:value-of select="id"/>
                    </node>
            </xsl:for-each-group>
        </xsl:template>
    </xsl:stylesheet>