Question

I have XML data like

<items>
<item color="b" value="10"/>
<item color="r" value="1"/>
<item color="r" value="3"/>
<item color="b" value="20"/>
<item color="b" value="25"/>
</items>

and want to compute this:

<item_value_change color="b" change="10"/>
<item_value_change color="b" change="5"/>
<item_value_change color="r" change="2"/>

In other words, for each class of item (color here), find some function (in this case, difference in value) of each item and the one preceding it.

When I have all items of only one class in a file, I simply use preceding::sibling to grab the previous item along with the current. So one solution is to create separate files for each class and process each individually.

However, I thought there should be a way to do this using for-each-group. The problem is that preceding::sibling will give you the last element in the document, not the previous element in the group. Perhaps there is a way to put a predicate on preceding::sibling that will also match the class to the current-grouping-key()?

Was it helpful?

Solution

If I understood you correctly, the "class" you mention in this case is the color attribute.

You said that it works when all the items have the same color. So I assume that you were doing something like this, on each node (calculate the difference between the current node's value and the preceding one):

@value - preceding-sibling::item[1]/@value

If you add a predicate to restrict that node-set to the item elements that have the same color the first preceding-sibling will now refer to the previous item with the same @color:

@value - preceding-sibling::item[@color = current()/@color][1]/@value"

You can then achieve your expected result XML fragment using any XSLT stylesheet (can be XSLT 1.0):

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

    <xsl:template match="item">
        <xsl:variable name="change" select="@value - preceding-sibling::item[@color = current()/@color][1]/@value"/>
        <xsl:if test="string($change)">
            <item_value_change color="{@color}" change="{$change}"/>
        </xsl:if>
    </xsl:template>
</xsl:stylesheet>

The test in the <xsl:if> requires the string() otherwise a case of 0 would not be printed.

OTHER TIPS

In XQuery:

let $items :=
  <items>
  <item color="b" value="10"/>
  <item color="r" value="1"/>
  <item color="r" value="3"/>
  <item color="b" value="20"/>
  <item color="b" value="25"/>
  </items>
for $i in $items/item
let $most-recent := $i/preceding-sibling::item[@color = $i/@color][1]
where ($most-recent)
return 
  element item_value_change {
    $i/@color,
    attribute change { $i/@value - $most-recent/@value }
  }

Another solution using XQuery 3.0 and the newly introduced features of group by and sliding windows.

let $items :=
  <items>
  <item color="b" value="10"/>
  <item color="r" value="1"/>
  <item color="r" value="3"/>
  <item color="b" value="20"/>
  <item color="b" value="25"/>
  </items>

for $s in (
  for $item in $items/item
  let $color := $item/@color
  group by $color
  return <t>{$item}</t>
)
let $seq := $s/item
  for sliding window $w in $seq
  start $s at $sp when true()
  only end $e at $ep when $ep - $sp eq 1
  return 
    element item_value_change {
      $s/@color,
      attribute change {$e/@value - $s/@value}
    }

This first part groups the items based on their color and then it slides over each pair of elements (i.e. a sequence that is two elements long) and outputs them accordingly.

Although it is longer and seemingly more complicated than the XQuery 1.0 compatible answer @wst provided, I think it is quite nice at is closer to the actual thinking process as a human: Most likely would also also first group by a color in our mind and then calculate the difference for each pair. Also, it provides the correct output order as in your sample output. This might not be relevant for you, but it could be, at least for others.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top