gbentley
gbentley

Reputation: 35

XSLT 1.0 - group by unknown parent and parent sibling

Following on from my query regarding XSLT 1.0 - Concatenate known child nodes, group by unknown parent , and in a similar predicament to Group/merge childs of same nodes in xml/xslt when repeating upper nodes , I want to further define my grouping, and transform

    <root>
    <object>
    <entry>
        <id>apples</id>
        <parent1>
            <object_id>1</object_id>
        </parent1>
        <parent1>
            <object_id>2</object_id>
        </parent1>
        <parent2>
            <object_id>3</object_id>
        </parent2>
        <parent2>
            <object_id>4</object_id>
        </parent2>
        <parent2>
            <object_id>5</object_id>
        </parent2>
    </entry>
    </object>
    <object>
    <entry>
        <id>pears</id>
        <parent1>
            <object_id>5</object_id>
        </parent1>
        <parent1>
            <object_id>4</object_id>
        </parent1>
        <parent2>
            <object_id>3</object_id>
        </parent2>
        <parent2>
            <object_id>2</object_id>
        </parent2>
        <parent2>
            <object_id>1</object_id>
        </parent2>
    </entry>
    </object>
    </root>

into

    <root>
    <object>
        <entry>
            <id>apples</id>
            <parent1>1-2</parent1>
            <parent2>3-4-5</parent2>
        </entry>
    </object>
    <object>
        <entry>
            <id>pears</id>
            <parent1>5-4</parent1>
            <parent2>3-2-1</parent2>
        </entry>
    </object>
    </root>

I'm trying something like this (although this entire example is simplified):

    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" />
    <xsl:key name="groupKey" match="/object/*/*/object_id" use="concat(../../id/text(),name(..))"/>
    <xsl:template match="/">
            <xsl:apply-templates select="./*[object_id]"/>
    </xsl:template>

    <xsl:template match="/object/*/*[generate-id(object_id)=generate-id(key('groupName',concat(../id/text(),name()))[1])]">
            <field>
                  <xsl:attribute name="name">
                    <xsl:value-of select="local-name()" />
                </xsl:attribute>
                 <xsl:for-each select="key('groupName',concat(../id/text(),name()))">
                    <xsl:if test="not(position()=1)">-</xsl:if>
                    <xsl:value-of select="."/>
                 </xsl:for-each>
       </field>
     </xsl:template>

    </xsl:stylesheet>

but my understanding of XPath is lacking, and this is collating ALL the object ids in the first of each parent grouping (ie, concatenated key isn't working).

If someone could help me tidy up my XPath syntax I'd be extremely grateful.

Thanks in advance!

Upvotes: 1

Views: 558

Answers (3)

Sean B. Durkin
Sean B. Durkin

Reputation: 12729

Strangely enough, my answer to your previous question, unaltered but for a small defect correction (identity template for group-head mode) works perfectly for this question too. Granted my answer was not accepted, but I find it ironic that I gave you the answer to this question (apparently untried), even before you posted it!

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

<xsl:key name="kParents" match="*[object_id]" use="local-name()" />

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

<xsl:template match="*[*/object_id]">
  <xsl:variable name="grandparent-id" select="generate-id()" /> 
 <xsl:copy>
   <xsl:apply-templates select="@* | node()[not(object_id)] |
    *[generate-id()=
      generate-id(
        key('kParents',local-name())[generate-id(..)=$grandparent-id][1])]"
      mode="group-head" />
 </xsl:copy>
</xsl:template>

<xsl:template match="*[object_id]" mode="group-head">
 <xsl:variable name="grandparent-id" select="generate-id(..)" /> 
 <xsl:copy>
   <xsl:apply-templates select="@* | node()[not(self::object_id)]" />
   <xsl:for-each select="key('kParents',local-name())[generate-id(..)=$grandparent-id]/object_id">
     <xsl:value-of select="." />
     <xsl:if test="position() != last()"> - </xsl:if>  
   </xsl:for-each>  
  </xsl:copy>
</xsl:template>

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

</xsl:stylesheet>

Upvotes: 1

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243579

This short transformation:

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

 <xsl:key name="kObjByParentAndId" match="object_id"
  use="concat(../../id,'+',name(..))"/>

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

 <xsl:template match="*[object_id]"/>

 <xsl:template  priority="2" match=
 "*[object_id and
    generate-id(object_id)
   =
    generate-id(key('kObjByParentAndId', concat(../id,'+',name()))[1])
    ]">
    <xsl:copy>
      <xsl:for-each select="key('kObjByParentAndId', concat(../id,'+',name()))">
        <xsl:if test="position()>1"> - </xsl:if>
        <xsl:value-of select="."/>
      </xsl:for-each>
    </xsl:copy>
 </xsl:template>
</xsl:stylesheet>

when applied on the provided XML document:

<root>
    <object>
        <entry>
            <id>apples</id>
            <parent1>
                <object_id>1</object_id>
            </parent1>
            <parent1>
                <object_id>2</object_id>
            </parent1>
            <parent2>
                <object_id>3</object_id>
            </parent2>
            <parent2>
                <object_id>4</object_id>
            </parent2>
            <parent2>
                <object_id>5</object_id>
            </parent2>
        </entry>
    </object>
    <object>
        <entry>
            <id>pears</id>
            <parent1>
                <object_id>5</object_id>
            </parent1>
            <parent1>
                <object_id>4</object_id>
            </parent1>
            <parent2>
                <object_id>3</object_id>
            </parent2>
            <parent2>
                <object_id>2</object_id>
            </parent2>
            <parent2>
                <object_id>1</object_id>
            </parent2>
        </entry>
    </object>
</root>

produces the wanted, correct result:

<root>
   <object>
      <entry>
         <id>apples</id>
         <parent1>1 - 2</parent1>
         <parent2>3 - 4 - 5</parent2>
      </entry>
   </object>
   <object>
      <entry>
         <id>pears</id>
         <parent1>5 - 4</parent1>
         <parent2>3 - 2 - 1</parent2>
      </entry>
   </object>
</root>

Explanation:

  1. The identity rule is used to recreate the upper hierarchy.

  2. There are two templates overriding the identity template -- one "deletes (has empty body) any matched element that has an object_id child. The other (with higher priority) is selected for any element that has an object_id child that is "the first in its group" -- using classic Muenchian grouping method.

  3. The Muenchian grouping uses a composite key, which is the concatenation (with a safety delimiter) of the id of the whole group and the name of any parent of an object_id in this group.

Upvotes: 1

Tim C
Tim C

Reputation: 70648

I think in this example you are gouping by the 'parent' elements, and the object_id elements will be in the group. Therefore you could define your key like so

<xsl:key name="groupKey" match="*[object_id]" use="concat(../id/text(), '|', name())"/>

Note there is no need to specify the complete path to the parent elements. You would only do this if you wanted to restrict it to a certain part of the hierarchy. Also note the use of the delimiter '|' here. Possibly not necessary in this case, it is often used in concatenated keys to prevent two different pairs of value being concatenated to the same value.

Then, when you are positioned on an entry element, you would get the unique 'parent' elements like so:

<xsl:apply-templates 
  select="*
     [object_id]
     [generate-id() = generate-id(key('groupKey', concat(../id/text(), '|', name()))[1])]" />

And then, when you are positioned on the first of each distinct 'parent' elements, you would get the object_id elements, like so

<xsl:apply-templates select="key('groupKey', concat(../id/text(), '|', name()))/object_id"/>

The template that matched this would just output the text, and a delimiter if required.

Here is the full XSLT

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
   <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
   <xsl:key name="groupKey" match="*[object_id]" use="concat(../id/text(), '|', name())"/>

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

   <xsl:template match="entry">
      <entry>
         <xsl:apply-templates select="id|*[object_id][generate-id() = generate-id(key('groupKey', concat(../id/text(), '|', name()))[1])]"/>
      </entry>
   </xsl:template>

   <xsl:template match="*[object_id]">
      <xsl:copy>
         <xsl:apply-templates select="key('groupKey', concat(../id/text(), '|', name()))/object_id"/>
      </xsl:copy>
   </xsl:template>

   <xsl:template match="object_id">
      <xsl:if test="not(position()=1)">-</xsl:if>
      <xsl:value-of select="."/>
   </xsl:template>
</xsl:stylesheet>

When applied to your sample XML, the following is output

<root>
   <object>
      <entry>
         <id>apples</id>
         <parent1>1-2</parent1>
         <parent2>3-4-5</parent2>
      </entry>
   </object>
   <object>
      <entry>
         <id>pears</id>
         <parent1>5-4</parent1>
         <parent2>3-2-1</parent2>
      </entry>
   </object>
</root>

Upvotes: 1

Related Questions