Reputation: 3
How would I transform the following in order to combine all the conditions and measurements for each Dish? I'm struggling with combining repeating child elements.
<?xml version="1.0" encoding="UTF-8"?>
<Test>
<Experiment id='1'>
<Dish1>
<Conditions pressure='x' temp='y'/>
<Measurement timeStamp='8am' reading='y'/>
</Dish1>
<Dish2>
<Conditions pressure='x' temp='y'/>
<Measurement timeStamp='8am' reading='y'/>
</Dish2>
<Dish1>
<Conditions pressure='x' temp='y'/>
<Measurement timeStamp='2pm' reading='y'/>
</Dish1>
<Dish2>
<Conditions pressure='x' temp='y'/>
<Measurement timeStamp='2pm' reading='y'/>
</Dish2>
</Experiment>
<Experiment id='2'>
<Dish1>
<Conditions pressure='x' temp='y'/>
<Measurement timeStamp='9am' reading='y'/>
</Dish1>
</Experiment>
<Experiment id='2'>
<Dish1>
...
</Test>
Desired outcome:
<?xml version="1.0" encoding="UTF-8"?>
<Test>
<Experiment id='1'>
<Dish1>
<Observation pressure='x' temp='y' timeStamp='8am' reading='y'/>
<Observation pressure='x' temp='y' timeStamp='2pm' reading='y'/>
</Dish1>
<Dish2>
<Observation pressure='x' temp='y' timeStamp='8am' reading='y'/>
<Observation pressure='x' temp='y' timeStamp='2pm' reading='y'/>
</Dish2>
</Experiment>
<Experiment id='2'>
<Dish1>
<Observation pressure='x' temp='y' timeStamp='9am' reading='y'/>
...
Please and thank you!
Here's what I've tried so far:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="Experiment">
<xsl:copy>
<xsl:for-each select="Dish1">
<xsl:element name="Observation">
<xsl:attribute name="pressure"><xsl:value-of select="Conditions/@pressure"/></xsl:attribute>
<xsl:attribute name="temp"><xsl:value-of select="Conditions/@temp"/></xsl:attribute>
<xsl:attribute name="TimeStamp"><xsl:value-of select="Measurement/@TimeStamp"/></xsl:attribute>
<xsl:attribute name="reading"><xsl:value-of select="Measurement/@reading"/></xsl:attribute>
</xsl:element>
</xsl:for-each>
</xsl:copy>
</xsl:template>
<!-- copy everthing not covered above-->
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
While writing up my failed attempts I came up with the above transformation. It seems to be working but I need to validate the output. Any suggestions/improvements would be appreciated. Thank you.
...my transformation works only on the first Experiment. When I add:
<xsl:template match="Experiment">
<xsl:copy>
<xsl:for-each select="Dish2">
<xsl:element name="Observation">
<xsl:attribute name="pressure"><xsl:value-of select="Conditions/@pressure"/></xsl:attribute>
<xsl:attribute name="temp"><xsl:value-of select="Conditions/@temp"/></xsl:attribute>
<xsl:attribute name="TimeStamp"><xsl:value-of select="Measurement/@TimeStamp"/></xsl:attribute>
<xsl:attribute name="reading"><xsl:value-of select="Measurement/@reading"/></xsl:attribute>
</xsl:element>
</xsl:for-each>
</xsl:copy>
</xsl:template>
...it can't find any Dish2 elements.
Thanks Ian Roberts, having me write up my failed attempts triggered an idea, which led to the working solution below.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="Experiment">
<xsl:copy>
<xsl:for-each select="Dish1">
<xsl:element name="Observation">
<xsl:attribute name="pressure"><xsl:value-of select="Conditions/@pressure"/></xsl:attribute>
<xsl:attribute name="temp"><xsl:value-of select="Conditions/@temp"/></xsl:attribute>
<xsl:attribute name="TimeStamp"><xsl:value-of select="Measurement/@TimeStamp"/></xsl:attribute>
<xsl:attribute name="reading"><xsl:value-of select="Measurement/@reading"/></xsl:attribute>
</xsl:element>
</xsl:for-each>
<xsl:for-each select="Dish2">
<xsl:element name="Observation">
<xsl:attribute name="pressure"><xsl:value-of select="Conditions/@pressure"/></xsl:attribute>
<xsl:attribute name="temp"><xsl:value-of select="Conditions/@temp"/></xsl:attribute>
<xsl:attribute name="TimeStamp"><xsl:value-of select="Measurement/@TimeStamp"/></xsl:attribute>
<xsl:attribute name="reading"><xsl:value-of select="Measurement/@reading"/></xsl:attribute>
</xsl:element>
</xsl:for-each>
</xsl:copy>
</xsl:template>
<!-- copy everthing not covered above-->
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Upvotes: 0
Views: 6793
Reputation: 70598
To be fully flexible, you probably want to avoid hard-coding Dish1 and Dish2 and allow any number of dishes. Effectively you want to group the 'dish' elements by their experiment ID and the dish name.
In XSLT 1.0 you would use a technique called Muenchian Grouping for this. First define a key to group your dish elements
<xsl:key name="Dish" match="Experiment/*" use="concat(../@id, '-', local-name())" />
Then, to get the unique 'dish' elements for the experiment, you have to select the dish elements that occur first in the group for their given name
<xsl:apply-templates
select="*[generate-id() = generate-id(key('Dish', concat(../@id, '-', local-name()))[1])]" />
Then, to get all the dishes within this group, you select them like so:
<xsl:apply-templates select="key('Dish', concat(../@id, '-', local-name()))" mode="group" />
And to combine the child elements of the 'Dish' elements, you just need a single template
<xsl:template match="Experiment/*" mode="group">
<Observation>
<xsl:apply-templates select="*/@*" />
</Observation>
</xsl:template>
Here is the full XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:key name="Dish" match="Experiment/*" use="concat(../@id, '-', local-name())" />
<xsl:template match="Experiment">
<xsl:copy>
<xsl:apply-templates select="@*" />
<xsl:apply-templates select="*[generate-id() = generate-id(key('Dish', concat(../@id, '-', local-name()))[1])]" />
</xsl:copy>
</xsl:template>
<xsl:template match="Experiment/*">
<xsl:copy>
<xsl:apply-templates select="key('Dish', concat(../@id, '-', local-name()))" mode="group" />
</xsl:copy>
</xsl:template>
<xsl:template match="Experiment/*" mode="group">
<Observation>
<xsl:apply-templates select="*/@*" />
</Observation>
</xsl:template>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Note the use of mode here because the XSLT contains two templates matching the child elements of Experiment so the XSLT needs to distinguish between them. (The first is to output the distinct 'Dish' names, the second is used to match and process all the elements with that name and combine the children).
When applied to your (truncated) XML sample, the following is output
<Test>
<Experiment id="1">
<Dish1>
<Observation pressure="x" temp="y" timeStamp="8am" reading="y"/>
<Observation pressure="x" temp="y" timeStamp="2pm" reading="y"/>
</Dish1>
<Dish2>
<Observation pressure="x" temp="y" timeStamp="8am" reading="y"/>
<Observation pressure="x" temp="y" timeStamp="2pm" reading="y"/>
</Dish2>
</Experiment>
<Experiment id="2">
<Dish1>
<Observation pressure="x" temp="y" timeStamp="9am" reading="y"/>
</Dish1>
</Experiment>
</Test>
If you can use XSLT 2.0, then xsl:for-each-group is your new best friend. Try this XSLT for XSLT 2.0
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="Experiment">
<xsl:copy>
<xsl:apply-templates select="@*" />
<xsl:for-each-group select="*" group-by="local-name()">
<xsl:copy>
<xsl:apply-templates select="current-group()" />
</xsl:copy>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>
<xsl:template match="Experiment/*">
<Observation>
<xsl:apply-templates select="*/@*" />
</Observation>
</xsl:template>
<xsl:template match="@*|node()">
<xsl:copy>
<xsl:apply-templates select="@*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Notice in both cases there is no mention of Dish1 and Dish anywhere, so you can easily have Dish3 or more without having to change the XSLT.
Upvotes: 2