Reputation: 3826
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.
I appreciate your help very much.
Upvotes: 1
Views: 5235
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
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
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