Charles
Charles

Reputation: 11796

XPATH select distinct values

I would like to select pages when one of two attributes (width or height) are distinct.

Here is my XML:

<pages>
    <page id="page0001" height="1000" width="960"></page>
    <page id="page4931" height="1000" width="960"></page>
    <page id="page6342" height="1000" width="961"></page>
    <page id="page7995" height="1001" width="961"></page>
    <page id="page4461" height="1001" width="960"></page>
    <page id="page4690" height="1001" width="960"></page>
    <page id="page9001" height="1001" width="961"></page>
</pages>

Here my select

<xsl:for-each select="/prototype/pages/page[@width != (preceding-sibling::*/@width) or @height != (preceding-sibling::*/@height) or position() = 1]">
    <div class="{@width}x{@height} {@id}"></div>
</xsl:for-each>

Here the result:

<div class="960x1000 page0001"></div>
<div class="961x1000 page6342"></div>
<div class="961x1001 page7995"></div>
<div class="960x1001 page4461"></div>
<div class="960x1001 page4690"></div>
<div class="961x1001 page9001"></div>

What I expect to have

<div class="960x1000 page0001"></div>
<div class="961x1000 page6342"></div>
<div class="961x1001 page7995"></div>
<div class="960x1001 page4461"></div>

I know that my select is incorrect and I understand why. Is there any possibility to modify it in order to get the right result?

Upvotes: 0

Views: 907

Answers (1)

Ian Roberts
Ian Roberts

Reputation: 122424

The issue is that comparison operators such as = and != in XPath are implicitly existentially quantified, in other words

@width != preceding-sibling::*/@width

checks whether there is any preceding sibling whose width is different from mine. What you actually want in this case is that the combination of width and height for this node is different from the combination of width and height for any of its predecessors. You could do this with a for-each over all pages and a test within the body of the for-each

<xsl:for-each select="/prototype/pages/page">
    <xsl:variable name="myWidth" select="@width" />
    <xsl:variable name="myHeight" select="@height" />
    <xsl:if test="not(preceding-sibling::*[@height=$myHeight][@width=$myWidth])">
        <div class="{@width}x{@height} {@id}"></div>
    </xsl:if>
</xsl:for-each>

Alternatively you can treat it as a grouping problem - group the page elements by their width and height, and output one div per group. In XSLT 2.0 this could be

<xsl:for-each-group select="/prototype/pages/page"
      group-by="concat(@width, 'x', @height)">
    <div class="{current-grouping-key()} {@id}" />
</xsl:for-each-group>

In 1.0 you can get the same effect using the Muenchian grouping method (there are many other questions you can find with the search function that explain it well so I won't repeat it again here).

Upvotes: 1

Related Questions