I have an XML that needs to be transformed into a better grouping of the items.
- The items need to be counted.
- I do not know how many dogs there will
be in the list.
- I may not know how many descriptive items about the
dogs there will be. In the example there are three, but it could be
any number. If this cannot be flexible, then a fixed number is ok.
Preferrably XSLT 1.0
Is this possible?
I need to go from this:
<table name = "dogs">
<fields>
<field name = "name" value = "dog1"></field>
<field name = "age" value = "2"></field>
<field name = "haircolor" value = "brown"></field>
<field name = "name" value = "dog2"></field>
<field name = "age" value = "10"></field>
<field name = "haircolor" value = "white"></field>
<field name = "name" value = "dog3"></field>
<field name = "age" value = "7"></field>
<field name = "haircolor" value = "black"></field>
<field name = "name" value = "dog4"></field>
<field name = "age" value = "4"></field>
<field name = "haircolor" value = "brown"></field>
</fields>
</table>
To this:
<dogs count = "4">
<dog>
<name>dog1</name>
<age>2</age>
<haircolor>brown</haircolor>
</dog>
<dog>
<name>dog2</name>
<age>10</age>
<haircolor>white</haircolor>
</dog>
<dog>
<name>dog3</name>
<age>7</age>
<haircolor>black</haircolor>
</dog>
<dog>
<name>dog4</name>
<age>4</age>
<haircolor>brown</haircolor>
</dog>
</dogs>
Assuming each group of fields starts with a name:
.
XSLT 1.0
<?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:key name="fields-by-lead" match="field[@name!='name']" use="generate-id(preceding-sibling::field[@name='name'][1])" />
<xsl:template match="/table">
<xsl:variable name="names" select="fields/field[@name='name']" />
<dogs count="{count($names)}">
<xsl:for-each select="$names">
<dog>
<name><xsl:value-of select="@value"/></name>
<xsl:for-each select="key('fields-by-lead', generate-id())">
<xsl:element name="{@name}">
<xsl:value-of select="@value"/>
</xsl:element>
</xsl:for-each>
</dog>
</xsl:for-each>
</dogs>
</xsl:template>
</xsl:stylesheet>
Edit:
The following modification starts a new group for every field whose name matches the name of the very first field.
XSLT 1.0
<?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:key name="fields-by-lead" match="field" use="generate-id(preceding-sibling::field[@name=/table/fields/field[1]/@name][1])" />
<xsl:template match="/table">
<xsl:variable name="lead-label" select="/table/fields/field[1]/@name" />
<xsl:variable name="leads" select="fields/field[@name=$lead-label]" />
<dogs count="{count($leads)}">
<xsl:for-each select="$leads">
<dog>
<xsl:for-each select=". | key('fields-by-lead', generate-id())[@name!=$lead-label]">
<xsl:element name="{@name}">
<xsl:value-of select="@value"/>
</xsl:element>
</xsl:for-each>
</dog>
</xsl:for-each>
</dogs>
</xsl:template>
</xsl:stylesheet>
Here is an XSLT 1.0 way of doing it:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="table">
<xsl:element name="{@name}">
<xsl:variable name="fields" select="fields/field[@name = current()/fields/field[1]/@name]"/>
<xsl:attribute name="count"><xsl:value-of select="count($fields)"/></xsl:attribute>
<xsl:apply-templates select="$fields"/>
</xsl:element>
</xsl:template>
<xsl:template match="field">
<xsl:variable name="this" select="."/>
<xsl:element name="{../../@name}">
<name><xsl:value-of select="@value"/></name>
<xsl:apply-templates select="following-sibling::field[1][not(@name = $this/@name)]" mode="trans">
<xsl:with-param name="head-name" select="$this/@name"/>
</xsl:apply-templates>
</xsl:element>
</xsl:template>
<xsl:template match="field" mode="trans">
<xsl:param name="head-name"/>
<xsl:element name="{@name}"><xsl:value-of select="@value"/></xsl:element>
<xsl:apply-templates select="following-sibling::field[1][not(@name = $head-name)]" mode="trans">
<xsl:with-param name="head-name" select="$head-name"/>
</xsl:apply-templates>
</xsl:template>
</xsl:stylesheet>
If you want to use a parameter then the code could use
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:param name="head-name" select="'name'"/>
<xsl:output method="xml" indent="yes"/>
<xsl:template match="table">
<xsl:element name="{@name}">
<xsl:variable name="fields" select="fields/field[@name = $head-name]"/>
<xsl:attribute name="count"><xsl:value-of select="count($fields)"/></xsl:attribute>
<xsl:apply-templates select="$fields"/>
</xsl:element>
</xsl:template>
<xsl:template match="field">
<xsl:variable name="this" select="."/>
<xsl:element name="{../../@name}">
<name><xsl:value-of select="@value"/></name>
<xsl:apply-templates select="following-sibling::field[1][not(@name = $head-name)]" mode="trans"/>
</xsl:element>
</xsl:template>
<xsl:template match="field" mode="trans">
<xsl:element name="{@name}"><xsl:value-of select="@value"/></xsl:element>
<xsl:apply-templates select="following-sibling::field[1][not(@name = $head-name)]" mode="trans">
<xsl:with-param name="head-name" select="$head-name"/>
</xsl:apply-templates>
</xsl:template>
</xsl:stylesheet>