pagid
pagid

Reputation: 13867

How to remove specific subelements in XSLT?

I've a XML structure like this:

<root>
    <element>
        <name>Foo</name>
        <subelement>
             <key>1.1</key>
             <value>Lorem ipsum</value>
        </subelement>
        <subelement>
             <key>1.2</key>
             <value>Lorem ipsum dolor</value>
        </subelement>
    </element>
    <element>
        <name>Bar</name>
        <subelement>
             <key>7.3.4</key>
             <value>Seven three four</value>
        </subelement>
        <subelement>
             <key>7.3.8</key>
             <value>Seven three eight</value>
        </subelement>
        <subelement>
             <key>7.1</key>
             <value>Seven one</value>
        </subelement>
    </element>
</root>

What I try to achieve is to remove all <subelement>s except the one with the "highest" key. I can't seem to find any way to compare the <key>s of the <subelements>s within a certain <element>.

The resulting XML would look like:

<root>
    <element>
        <name>Foo</name>
        <subelement>
             <key>1.2</key>
             <value>Lorem ipsum dolor</value>
        </subelement>
    </element>
    <element>
        <name>Bar</name>
        <subelement>
             <key>7.3.8</key>
             <value>Seven three eight</value>
        </subelement>
    </element>
</root>

Any hints are very welcome.

Upvotes: 3

Views: 361

Answers (2)

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243479

This XSLT 2.0 transformation works for any number of "key components" in a key and for any possible positive integer value of any key component:

<xsl:stylesheet version="2.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:xs="http://www.w3.org/2001/XMLSchema"
 xmlns:my="my:my">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <xsl:template match="node()|@*">
  <xsl:copy>
   <xsl:apply-templates select="node()|@*"/>
  </xsl:copy>
 </xsl:template>

 <xsl:template match=
 "subelement
   [not(key eq my:max(../subelement/key))
   or
    key = preceding-sibling::subelement/key
   ]"/>

 <xsl:function name="my:max" as="xs:string">
   <xsl:param name="pValues" as="xs:string+"/>

   <xsl:sequence select=
    "if(not(distinct-values($pValues)[2]))
       then $pValues[1]
     else
      for $vMax1 in
             max(for $s in $pValues
                  return
                    xs:integer(substring-before(concat($s,'.'),'.'))
                 ),

          $vcntMax1Values in
             count($pValues[starts-with(., string($vMax1))])

        return
          if($vcntMax1Values eq 1)
            then $pValues[starts-with(., string($vMax1))]
                          [1]
            else
             for $submax in
                     (my:max(for $val in
                                    $pValues[starts-with(., string($vMax1))]
                                                [contains(., '.')],
                                    $subval in substring-after($val, '.')
                                return
                                    $subval
                             )
                      )
                 return
                   concat($vMax1, '.', $submax)

    "/>
 </xsl:function>
</xsl:stylesheet>

When this transformation is applied on the following XML document (the provided one, extended to be made more interesting):

<root>
    <element>
        <name>Foo</name>
        <subelement>
            <key>1.1</key>
            <value>Lorem ipsum</value>
        </subelement>
        <subelement>
            <key>1.2</key>
            <value>Lorem ipsum dolor</value>
        </subelement>
    </element>
    <element>
        <name>Bar</name>
        <subelement>
            <key>7.3.4</key>
            <value>Seven three four</value>
        </subelement>
        <subelement>
            <key>7.3.8</key>
            <value>Seven three eight</value>
        </subelement>
        <subelement>
            <key>7.3.8.1</key>
            <value>Seven three eight one</value>
        </subelement>
        <subelement>
            <key>7.3.8.1</key>
            <value>Seven three eight one</value>
        </subelement>
        <subelement>
            <key>7.1</key>
            <value>Seven one</value>
        </subelement>
        <subelement>
            <key>10.1</key>
            <value>Ten one</value>
        </subelement>
        <subelement>
            <key>10.1</key>
            <value>Ten one</value>
        </subelement>
    </element>
</root>

the wanted, correct result is produced:

<root>
   <element>
      <name>Foo</name>
      <subelement>
         <key>1.2</key>
         <value>Lorem ipsum dolor</value>
      </subelement>
   </element>
   <element>
      <name>Bar</name>
      <subelement>
         <key>10.1</key>
         <value>Ten one</value>
      </subelement>
   </element>
</root>

Explanation:

At the core of this generic solution is the function my:max() which given a non-empty sequence of "structured values" (a structured value is a sequence of positive integers, joined with the '.' character), produces one (of the many possible) maximum value.

This function is recursive. It does the following:

  1. If all the passed values are identical, then the first of them is returned.

  2. Else, finds the maximum value of the first components.

  3. If only one of the values has a first component with the found maximum value, return this value.

  4. Else, find the maximum (recursively) of the "tails" of all values that have the maximum first component.

  5. Finally, join together the maximum first component with the maximum of the "tails", found in the previous step -- and return this value.

Upvotes: 2

Kevan
Kevan

Reputation: 891

The following version 2.0 XSLT

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:template match="@* | node()">
        <xsl:copy>
            <xsl:apply-templates select="@* | node()"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="element">
        <xsl:variable name="sorted">
            <xsl:for-each select="subelement">
                <xsl:sort select="key"/>
                <xsl:copy-of select="."/>
            </xsl:for-each>
        </xsl:variable>
        <xsl:variable name="highest" select="$sorted/subelement[count($sorted/subelement)]/key"/>
        <element>
            <xsl:copy-of select="name"/>
            <xsl:copy-of select="subelement[key=$highest]"/>
        </element>
    </xsl:template>

</xsl:stylesheet>

When applied to the sample input XML produces the required output XML

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <element>
        <name>Foo</name>
        <subelement>
            <key>1.2</key>
            <value>Lorem ipsum dolor</value>
        </subelement>
    </element>
    <element>
        <name>Bar</name>
        <subelement>
            <key>7.3.8</key>
            <value>Seven three eight</value>
        </subelement>
    </element>
</root>

Upvotes: -1

Related Questions