开发者

Using XSLT, how do I separate nodes based on their value?

I have a pretty flat XML structure that I need to reorder into categorised sections and, for the life of me, I can't figure out how to do it in XSLT (not that I'm by any means an expert.)

Basically, the original XML looks kinda like:

<things>
  <thing>
    <value>one</value>
    <type>a</type>
  </thing>
  <thing>
    <value>two</value>
    <type>b</type>
  </thing>
  <thing>
    <value>thee</value>
    <type>b</type>
  </thing>
  <thing>
    <value>four</value>
    <type>a</type>
  </thing>
  <thing>
    <value>five</value>
    <type>d</type>
  </thing>
</things>

And I need to output something like:

<data>
  <a-things>
    <a>one</a>
    <a>four</a>
  </a-things>
  <b-things>
    <b>two</b>
    <b>three</b>
  </b-things>
  <d-things>
    <d>five</d>
  </d-things>
</data>

Note that I can't output <c-things> if there aren't any <c> elements, but I do know ahead of time what the complete list of types is, and it's fairly short so handcoding templates for each type is definitely possible. It feels like I could probably hack something together using <xsl:if> and <xsl:for-each&g开发者_C百科t; but it also feels like there must be a more ... 'templatey' way to do it. Can anyone help?

Cheers.


As you are using Saxon, use the native XSLT 2.0 grouping.

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

    <xsl:output method="xml" indent="yes" />

    <xsl:template match="things">
        <data>
            <xsl:for-each-group select="thing" group-by="type">
                <xsl:element name="{concat(current-grouping-key(),'-things')}">
                    <xsl:for-each select="current-group()">
                        <xsl:element name="{current-grouping-key()}">
                            <xsl:value-of select="value" />
                        </xsl:element>
                    </xsl:for-each>
                </xsl:element>
            </xsl:for-each-group>
        </data>
    </xsl:template>

</xsl:stylesheet>

In XSLT 1.0 you can group with keys. This approach is called Muenchian Grouping.

The xsl:key defines an index containing thing elements, grouped by the string value of their type element. Function key() returns all nodes from the key with the specified value.

The outer xsl:for-each selects the thing elements that are the first returned by key() for their value.

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

    <xsl:output method="xml" indent="yes" />

    <xsl:key name="thing" match="thing" use="type" />

    <xsl:template match="things">
        <data>
            <xsl:for-each select="thing[generate-id(.)=generate-id(key('thing',type)[1])]">
                <xsl:element name="{concat(type,'-things')}">
                    <xsl:for-each select="key('thing',type)">
                        <xsl:element name="{type}">
                            <xsl:value-of select="value" />
                        </xsl:element>
                    </xsl:for-each>
                </xsl:element>
            </xsl:for-each>
        </data>
    </xsl:template>

</xsl:stylesheet>


The generic solution is to use an XSL key:

<xsl:key name="kThingByType" match="thing" use="type" />

<xsl:template match="things">
  <xsl:copy>
    <xsl:apply-templates select="thing" mode="group">
      <xsl:sort select="type" />
    </xsl:apply-templates>
  </xsl:copy>
</xsl:template>

<xsl:template match="thing" mode="group">
  <xsl:variable name="wholeGroup" select="key('kThingByType', type)" />
  <xsl:if test="generate-id() = generate-id($wholeGroup[1])">
    <xsl:element name="{type}-thing">
      <xsl:copy-of select="$wholeGroup/value" />
    </xsl:element>
  </xsl:if>
</xsl:template>

The above yields:

<things>
  <a-thing>
    <value>one</value>
    <value>four</value>
  </a-thing>
  <b-thing>
    <value>two</value>
    <value>thee</value>
  </b-thing>
  <d-thing>
    <value>five</value>
  </d-thing>
</things>


In XSLT 2, you can do this very elegantly. Say you have a template for formatting each thing before it is wrapped in an <a> element:

<xsl:template match="thing" mode="format-thing">
    <xsl:value-of select="value/text()"/>
</xsl:template>

Then you can apply that to each thing of some $type to build the <a-things> elements via a function:

<xsl:function name="my:things-group" as="element()">
    <xsl:param name="type" as="xs:string"/>
    <xsl:param name="things" as="element(thing)*"/>

    <xsl:element name="{ concat($type, '-things') }">
        <xsl:for-each select="$things[type/text() eq $type]">
            <xsl:element name="{ $type }">
                <xsl:apply-templates select="." mode="format-thing"/>
            </xsl:element>
        </xsl:for-each>
    </xsl:element>
</xsl:function>

Then you can call that function for each unique type (a, b, d in your sample input) to build the entire output and you're done:

<xsl:template match="/">
    <data>
        <xsl:sequence select="
            for $type in distinct-values(things/thing/type/text())
            return my:things-group($type, /things/thing)
            "/>
    </data>
</xsl:template>


Of course, asking the question made it obvious...

My solution does use an <xsl:if>, but I can't see how it couldn't now I think about it. My solution looks basically like:

<xsl:if test="/things/thing/type = 'a'">
  <a-things>
    <xsl:apply-templates select="/things/thing[type='a']" mode="a" />
  </a-things>
</if>

<xsl:template match="/things/thing[type='a']" mode="a">
    <a><xsl:value-of select="value"/>
</xsl:template>

And repeat for the other types. I've coded it up, and it seems to work just fine.


<a-things>
    <xsl:for-each select="thing[type = 'a']">
        <a><xsl:value-of select="./value" /></a>
    </xsl:for-each>
</a-things>

If you want to get really snazzy, replace the <a-things> and the predicate with parameters and use attribute value templates:

<xsl:param name="type" />
<xsl:element name="{$type}-things">
    <xsl:for-each select="thing[type = $type]">
        <xsl:element name="{$type}"><xsl:value-of select="./value" /></xsl:element>
    </xsl:for-each>
</xsl:element>


And using grouping, you can do it without the if:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="things">
    <data>
      <xsl:for-each select="thing[not(type=preceding-sibling::thing/type)]">
        <xsl:variable name="type"><xsl:value-of select="type" /></xsl:variable>
          <xsl:element name="concat($type, '-things')">
            <xsl:for-each select="../thing[type=$type]">
              <xsl:element name="$type">
                <xsl:value-of select="value" />
              </xsl:element>
            </xsl:for-each>
          </xsl:element>
      </xsl:for-each>
    </data>
  </xsl:template>
</xsl:stylesheet>
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜