Mathias Bader
Mathias Bader

Reputation: 3826

XSLT-XPath: For-each path specification for re-grouping of elements

I have XML documents which I want to transfer into another structure. An example of a document looks as follows:

<application>
    <contactPerson>
        <name>Dominik</name>
        <countryCode>DE</countryCode>
    </contactPerson>
    <contactPerson>
        <name>Dorothea</name>
        <countryCode>DE</countryCode>
    </contactPerson>
    <contactPerson>
        <name>Fiona</name>
        <countryCode>FR</countryCode>
    </contactPerson>
    <contactPerson>
        <name>Fabian</name>
        <countryCode>FR</countryCode>
    </contactPerson>
    <contactPerson>
        <name>Florian</name>
        <countryCode>FR</countryCode>
    </contactPerson>
    <contactPerson>
        <name>Gabi</name>
        <countryCode>GB</countryCode>
    </contactPerson>
    <contactPerson>
        <name>Gert</name>
        <countryCode>GB</countryCode>
    </contactPerson>
</application>

Now what I want to do is group the elements by country, meaning the result should look like this:

<application>
    <memberState>
        <countryCode>De</countryCode>
        <contactPerson>
            <name>Dominik</name>
        </contactPerson>
        <contactPerson>
            <name>Dorothea</name>
        </contactPerson>
    </memberState>
    <memberState>
        <countryCode>FR</countryCode>
        <contactPerson>
            <name>Fiona</name>
        </contactPerson>
        <contactPerson>
            <name>Fabian</name>
        </contactPerson>
        <contactPerson>
            <name>Florian<name>
        </contactPerson>
    </memberState>
    <memberState>
        <countryCode>GB</countryCode>
        <contactPerson>
            <name>Gabi</name>
        </contactPerson>
        <contactPerson>
            <name>Gert</name>
        </contactPerson>
    </memberState>
</application>

I am using an XPath for-each pattern to select all countries, but it doesn't do what it is supposed to do. My pattern looks as follows:

  <xsl:template match="/">
    <application>
      <xsl:for-each select="/application/contactPerson/countryCode[not(.=preceding-sibling::*/application/contactPerson/countryCode)]">
        <memberState>
          <countryCode>
            <xsl:value-of select="."/>
          </countryCode>
          <contactPerson>
            <name>
              <xsl:value-of select="../name"/>
            </name>
          </contactPerson>
        </memberState>
      </xsl:for-each>
    </application>
  </xsl:template>

The error is probably somewhere in the XPath expression which does not compile. I changed it to the following

<xsl:for-each select="/application/contactPerson/countryCode[not(.=preceding-sibling::*)]">

because I think I am already at the right position of my tree. This solution compiles, but it doesn't give me a unique list of countries as I intended by using "preceding-sibling" but the complete list instead.

Besides that I need the solution to my problem, I would be especially thankful for some help on understanding what is actually happening here.

  1. Is it possible to give a relative path as in my second solution or do I have to give the complete path each time?
  2. Am I on the right track for making a unique list of country codes, or would that generally be implemented differently?
  3. Is the solution I am looking for actually implementable with XSLT?

I appreciate your help very much.

Upvotes: 1

Views: 5235

Answers (3)

JLRishe
JLRishe

Reputation: 101728

Your XPath expression was almost correct:

<xsl:for-each 
    select="/application/contactPerson/countryCode[not(.=preceding-sibling::*)]"> 

The reason it didn't work is that countryCode doesn't have any siblings. To find the previous countryCodes, you need to move up one level and then back down:

<xsl:for-each 
    select="/application/contactPerson/countryCode[not(. = ../preceding-sibling::*/countryCode)]"> 

I believe that should successfully iterate through the distinct countryCodes.

This modification to your template should work. You needed another loop to iterate through all the people for each country:

<xsl:template match="/">
    <application>
      <xsl:for-each select="/application/contactPerson/countryCode[not(.=../preceding-sibling::*/countryCode)]">
        <memberState>
          <countryCode>
            <xsl:value-of select="."/>
          </countryCode>
          <xsl:for-each select="/application/contactPerson[countryCode = current()]">
          <contactPerson>
            <name>
              <xsl:value-of select="name"/>
            </name>
          </contactPerson>
          </xsl:for-each>
        </memberState>
      </xsl:for-each>
    </application>
  </xsl:template>

Upvotes: 1

Martin Honnen
Martin Honnen

Reputation: 167696

Here is an XSLT 1.0 solution using Muenchian grouping:

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

<xsl:output indent="yes"/>

<xsl:key name="country" match="contactPerson" use="countryCode"/>

<xsl:template match="application">
  <xsl:copy>
    <xsl:apply-templates select="contactPerson[generate-id() = generate-id(key('country', countryCode)[1])]" mode="group"/>
  </xsl:copy>
</xsl:template>

<xsl:template match="contactPerson" mode="group">
  <memberState>
    <xsl:copy-of select="countryCode"/>
    <xsl:apply-templates select="key('country', countryCode)"/>
  </memberState>
</xsl:template>

<xsl:template match="contactPerson">
  <xsl:copy>
    <xsl:copy-of select="*[not(self::countryCode)]"/>
  </xsl:copy>
</xsl:template>

</xsl:stylesheet>

Upvotes: 2

Chris
Chris

Reputation: 7853

You could use the key tag to get access to each element by the countryCode like this:

<xsl:key name="groups" match="/application/contactPerson" use="countryCode" />

Then you can iterate the contents of that key. See Walk/loop through an XSL key: how?

So if you're stuck to XSLT 1.0 that would be a good point to start at. If you can use XSLT 2.0 you should have a look at group-by.

Upvotes: 2

Related Questions