This is the mail archive of the xsl-list@mulberrytech.com mailing list .


Index Nav: [Date Index] [Subject Index] [Author Index] [Thread Index]
Message Nav: [Date Prev] [Date Next] [Thread Prev] [Thread Next]
Other format: [Raw text]

Re: Seeking help on Grouping distingt sub-elements


Hi Jim,

> Below is the input XML that I'm starting with. The goal is for all
> of the chassis in a shipment (in a company, in an order), where the
> chassis have the same <commonid> value, combine those chassis into
> one chassis element, where that chassis element has the 'common'
> elements of each of the matching chassis (<commonid> and
> <commonelement>) and to then add a new tag that enumerates each of
> the specific <specificelement> elements from the group. Note that
> one twist on this is that the <commonelement> element can have a
> different value in each <chassis> group member and that the last
> <commonelement> encountered for a group of <chassis> elements (in
> the same <shipment>) with the same <commonid> is the one that 'wins'
> - ie, is written to the output document - also note that the values
> don't determine which one 'wins' - I just used 1's and 2's for
> illustrative purposes - its the position (ie, last encountered in
> the group) that matters.

Let's start with the XSLT 2.0 method, because that makes it easier to
see what's going on. Within each shipment...

<xsl:template match="shipment">
  <shipment>
    <xsl:copy-of select="shiptoid" />
    ...
  </shipment>
</xsl:template>

You want to group together all the chassis elements by their
commonid...

<xsl:template match="shipment">
  <shipment>
    <xsl:copy-of select="shiptoid" />
    <xsl:for-each-group select="chassis" group-by="commonid">
      <chassis>
        <xsl:copy-of select="commonid" />
        ...
      </chassis>
    </xsl:for-each-group>
  </shipment>
</xsl:template>

You want the commonelement to be copied from the *last* chassis in the
group of chassis elements that you're looking at...

<xsl:template match="shipment">
  <shipment>
    <xsl:copy-of select="shiptoid" />
    <xsl:for-each-group select="chassis" group-by="commonid">
      <chassis>
        <xsl:copy-of select="commonid" />
        <xsl:copy-of select="current-group()[last()]/commonelement" />
        ...
      </chassis>
    </xsl:for-each-group>
  </shipment>
</xsl:template>

And then you want to provide a newtag wrapper element, inside which
you copy all the specificelement children of the chassis elements in
the current group:

<xsl:template match="shipment">
  <shipment>
    <xsl:copy-of select="shiptoid" />
    <xsl:for-each-group select="chassis" group-by="commonid">
      <chassis>
        <xsl:copy-of select="commonid" />
        <xsl:copy-of select="current-group()[last()]/commonelement" />
        <newtag>
          <xsl:copy-of select="current-group()/specificelement" />
        </newtag>
      </chassis>
    </xsl:for-each-group>
  </shipment>
</xsl:template>

You can create the rest of the tree with a simple identity template:

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

The XSLT 1.0 way is basically the same, except that there's no
xsl:for-each-group instruction, so we'll use the Muenchian Method
instead. We need to group together the chassis elements by both their
commonid *and* (I think you might have missed this bit out) an id for
the shipment. We'll generate that id through generate-id(), just
because it's a bit shorter than using a combination of the company
name and the shiptoid. So the key needs to look like:

<xsl:key name="chassis" match="chassis"
  use="concat(generate-id(parent::shipment), '+', commonid)" />

or just:

<xsl:key name="chassis" match="chassis"
         use="concat(generate-id(..), '+', commonid)" />

Given that we're processing a chassis element, we can then get the
'current group' using the key function as follows:

  <xsl:variable name="current-group"
    select="key('chassis', concat(generate-id(..), '+', commonid))" />

To get the *unique* chassis elements within the current shipment, we
need to go through them one by one and see if they're the first one
with the particular commonid in that shipment, using the familiar
expression:

  chassis[generate-id() =
          generate-id(key('chassis',
                          concat(generate-id(..), '+', commonid))[1])]

or:

  chassis[count(.|key('chassis',
                      concat(generate-id(..), '+', commonid))[1]) = 1]

Putting these into the XSLT 2.0 template we put together about, we get
the following:

<xsl:template match="shipment">
  <shipment>
    <xsl:copy-of select="shiptoid" />
    <xsl:for-each
      select="chassis[count(.|key('chassis',
                                  concat(generate-id(..), '+',
                                         commonid))[1]) = 1]">
      <xsl:variable name="current-group"
        select="key('chassis', concat(generate-id(..), '+', commonid))" />
      <chassis>
        <xsl:copy-of select="commonid" />
        <xsl:copy-of select="$current-group[last()]/commonelement" />
        <newtag>
          <xsl:copy-of select="$current-group/specificelement" />
        </newtag>
      </chassis>
    </xsl:for-each-group>
  </shipment>
</xsl:template>

In fact you could select the *last* of the nodes returned by the key,
rather than the first, if you wanted, while extracting the unique
chassis. That way, you wouldn't have to use $current-group[last()] to
get the correct commonelement element:

<xsl:template match="shipment">
  <shipment>
    <xsl:copy-of select="shiptoid" />
    <xsl:for-each
      select="chassis[count(.|key('chassis',
                                  concat(generate-id(..), '+',
                                         commonid))[last()]) = 1]">
      <xsl:variable name="current-group"
        select="key('chassis', concat(generate-id(..), '+', commonid))" />
      <chassis>
        <xsl:copy-of select="commonid" />
        <xsl:copy-of select="commonelement" />
        <newtag>
          <xsl:copy-of select="$current-group/specificelement" />
        </newtag>
      </chassis>
    </xsl:for-each-group>
  </shipment>
</xsl:template>

> Any help is VERY much appreciated!! In the end, I think its coming
> down to a lack of sufficient understanding of the <xsl:key> tag on
> my part.

The xsl:key element tells the processor to build up a hashtable (or
something) of nodes indexed by values. When you use the key()
function, you retrieve all the nodes that have a particular value from
that hashtable. The point with the Muenchian method is that you only
process the *first* (or last, if you like) node that's returned for
each value stored by the key in order to create the group-level output
(like the commonid/commonelement elements). But there's no way to get
at the values that are stored in the key aside from by going through
all the nodes that might be stored in the key and trying out their
value, which is why you have to use one of those horrible expressions
in the select attribute. I don't know if that explains things
sufficiently; do ask if you have any questions.

Cheers,

Jeni

---
Jeni Tennison
http://www.jenitennison.com/


 XSL-List info and archive:  http://www.mulberrytech.com/xsl/xsl-list


Index Nav: [Date Index] [Subject Index] [Author Index] [Thread Index]
Message Nav: [Date Prev] [Date Next] [Thread Prev] [Thread Next]