Markus
Markus

Reputation: 3397

Selecting the first elements of element groups using XSLT 1.0

Given the following XML document:

<foo>
  <bar>
    <type>1</type>
    <id>1</id>
    <a1>0</a1>
    <other_stuff/>
  </bar>
  <bar>
    <type>1</type>
    <id>1</id>
    <a1>0</a1>
    <other_stuff/>
  </bar>
  <bar>
    <type>1</type>
    <id>2</id>
    <a1>0</a1>
    <other_stuff/>
  </bar>
  <bar>
    <type>1</type>
    <id>2</id>
    <a1>0</a1>
    <other_stuff/>
  </bar>
</foo>

The XML elements bar are list elements, logically grouped by type and id. The sample document therefore contains two ordered lists (type=1 and id=1, and type=1 and id=2) with two elements each. In the real document there are many more lists (using different types and ids) of various length.

Now I need to update the first element a1 of each list with a different value, resulting e.g. in the following document:

<foo>
  <bar>
    <type>1</type>
    <id>1</id>
    <a1>-100</a1>
    <other_stuff/>
  </bar>
  <bar>
    <type>1</type>
    <id>1</id>
    <a1>0</a1>
    <other_stuff/>
  </bar>
  <bar>
    <type>1</type>
    <id>2</id>
    <a1>-100</a1>
    <other_stuff/>
  </bar>
  <bar>
    <type>1</type>
    <id>2</id>
    <a1>0</a1>
    <other_stuff/>
  </bar>
</foo>

In pseudo SQL this would likely look like this:

update bar set a1 = -100 where position() = 1 group by type, id

Is this possible with XSLT 1.0? I think it boils down to being able to write an XPath expression giving the same result as my pseudo SQL statement.

Upvotes: 0

Views: 765

Answers (1)

Martin Honnen
Martin Honnen

Reputation: 167716

Use Muenchian grouping to identify the first item in each group and change the child, copy the rest with the identity transformation:

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

    <xsl:param name="new-value" select="-100"/>

    <xsl:key name="group" match="bar" use="concat(type, '|', id)"/>

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

    <xsl:template match="bar[generate-id() = generate-id(key('group', concat(type, '|', id))[1])]/a1">
        <xsl:copy>
            <xsl:value-of select="$new-value"/>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>

As for further explanation, the key definition <xsl:key name="group" match="bar" use="concat(type, '|', id)"/> lets the XSLT processor index the bar elements on the concat(type, '|', id) so that we can use the key function with e.g. key('group', '1|1') to find all the bar elements with that value for concat(type, '|', id).

You only want to change the first bar element in each group so we need to have a way to identify that first item inside of a predicate to bar[...] where we need to find a pattern that expresses the condition

bar[. is the first item in its group]

The group we get inside the pattern with key('group', concat(type, '|', id)), so we want to write a condition

bar[. is the first item in key('group', concat(type, '|', id))]

which we can express nearly literally in XSLT 2.0 with the is operator and the pattern

bar[. is key('group', concat(type, '|', id))[1]]

With XSLT 1.0 we can express it using generate-id, as done in

bar[generate-id() = generate-id(key('group', concat(type, '|', id))[1])]

which selects/matches those bar element for which the generated id is equal to the generated id of the first ([1]) item in the group computed by key('group', concat(type, '|', id)).

Upvotes: 2

Related Questions