XSLT 1.0 variant for distinct-values

16,296

Solution 1

I don't think you can use directly an XPath - but you should be able to check for only the valid values like this:

<xsl:for-each select="//CodePart/value">
  <xsl:variable name="CodePart" select="."/>
  <xsl:if test="$variableX/ValidCode[@CodePart=$CodePart]">
    . . .
  </xsl:if>
</xsl:for-each> 

or more simply (thanks to the comments):

<xsl:for-each select="//CodePart/value">
  <xsl:if test="$variableX/ValidCode[@CodePart=current()]">
    . . .
  </xsl:if>
</xsl:for-each> 

If $variableX is not a node set but an XML fragment it needs to be converted to a node set - this is implementation-dependent, using Microsoft processors:

<xsl:for-each select="//CodePart/value" xmlns:msxsl="urn:schemas-microsoft-com:xslt">
  <xsl:if test="msxsl:node-set($variableX)/ValidCode[@CodePart=current()]">
    . . .
  </xsl:if>
</xsl:for-each> 

Solution 2

Note that I can't use a key, as it's a dynamically determined variable and not part of the input file.

This simply isn't true.

Here is a short, simple and most efficient solution using keys (btw, not using any conditional instructions or xsl:for-each at all :) ):

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:ext="http://exslt.org/common">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:key name="kCodeByPart" match="ValidCode" use="@CodePart"/>

    <xsl:variable name="variableX">
        <ValidCode CodePart="CP1" Code="C1"/>
        <ValidCode CodePart="CP2" Code="C2"/>
        <ValidCode CodePart="CP1" Code="C3"/>
        <ValidCode CodePart="CP2" Code="C4"/>
        <ValidCode CodePart="CP2" Code="C5"/>
    </xsl:variable>

    <xsl:variable name="vCodes" select="ext:node-set($variableX)/*"/>


 <xsl:template match="/">
  <xsl:apply-templates select=
  "$vCodes[generate-id()
          =
           generate-id(key('kCodeByPart', @CodePart)[1])
           ]"/>
 </xsl:template>

 <xsl:template match="ValidCode">

  Code Part: <xsl:value-of select="@CodePart"/>
    Codes: <xsl:apply-templates select=
      "key('kCodeByPart', @CodePart)/@Code"/>
 </xsl:template>

 <xsl:template match="@Code">
   <xsl:value-of select="concat(., ' ')"/>
 </xsl:template>
</xsl:stylesheet>

When this transformation is applied on any XML document (not used), the wanted, correct result is produced:

  Code Part: CP1
    Codes: C1 C3 

  Code Part: CP2
    Codes: C2 C4 C5 

Explanation:

As specified in the W3C XSLT 1.0 Recommendation:

"The key can be used to retrieve a key from a document other than the document containing the context node."

In the provided solution we don't even use this possibility -- both the context node and the retrieved key are from the same document -- the temporary tree contained in $vCodes.

Again there, it is specified that a key can be used for more than one document:

"A stylesheet declares a set of keys for each document using the xsl:key element."

To put it in simple words: the indexing is performed on the current document (the document that contains the context (current) node).

Solution 3

In XPath 1.0, you can emulate the distinct-values function by looking for preceding siblings with the same value.

I have simplified your first document to this so it can be easier reproduced (without even using XSLT):

<?xml version="1.0"?>
<root>
    <ValidCode CodePart="CP1" Code="C1"/>
    <ValidCode CodePart="CP2" Code="C2"/>
    <ValidCode CodePart="CP1" Code="C3"/>
    <ValidCode CodePart="CP2" Code="C4"/>
    <ValidCode CodePart="CP2" Code="C5"/>
    <ValidCode CodePart="CP3" Code="C5"/>
</root>

Then, the following XPath expression will select only one CodePart attribute with each of the values that occur in the document:

//ValidCode/@CodePart[not(../preceding-sibling::ValidCode/@CodePart = .)]

So, the condition for the CodePart attributes to select is that there is no preceding sibling of the <ValidCode> element whose CodePart attribute has the same value as the currently examined (selected) CodePart attribute.

Solution 4

One way to solve this (without a key) would be to sort and test against preceding-sibling

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt">

    <xsl:variable name="variableX">
        <ValidCode CodePart="CP1" Code="C1"/>
        <ValidCode CodePart="CP2" Code="C2"/>
        <ValidCode CodePart="CP1" Code="C3"/>
        <ValidCode CodePart="CP2" Code="C4"/>
        <ValidCode CodePart="CP2" Code="C5"/>
     </xsl:variable>

     <xsl:variable name="refvarX" select="msxsl:node-set($variableX)" />

     <xsl:template match="/">
         <root>
             <xsl:for-each select="$refvarX/ValidCode">
                 <xsl:sort select="./@CodePart" data-type="text" />
                 <xsl:if test="not(./@CodePart = preceding-sibling::ValidCode/@CodePart)">
                     <cp>
                         <xsl:value-of select="./@CodePart" />
                     </cp>
                 </xsl:if>
             </xsl:for-each>
         </root>
    </xsl:template>

</xsl:stylesheet>
Share:
16,296
Maestro13
Author by

Maestro13

Independent IT professional. Keywords: Swift, integration, message transformation, interfacing, XML, XSD, XSLT, SOAP, PHP. Branch: banking, in particular Custody and safekeeping

Updated on June 17, 2022

Comments

  • Maestro13
    Maestro13 almost 2 years

    I have an XSLT in which I create (from the input data) intermediary variables like the following (hard-coded example, but dynamic in nature):

    <xsl:variable name="variableX">
        <ValidCode CodePart="CP1" Code="C1"/>
        <ValidCode CodePart="CP2" Code="C2"/>
        <ValidCode CodePart="CP1" Code="C3"/>
        <ValidCode CodePart="CP2" Code="C4"/>
        <ValidCode CodePart="CP2" Code="C5"/>
    </xsl:variable>
    

    I wish to loop over the distinct occurrences of CodePart values. In XSLT 2.0 it's easy:

    <xsl:for-each select="distinct-values($variableX/ValidCode/@CodePart)">...</xsl:for-each>
    

    But how best do this in XSLT 1.0?
    Note that I can't use a key, as it's a dynamically determined variable and not part of the input file.

    My input file does contain a list of all possible code parts as follows:

    <root>
        <CodePart><value>CP1</value></CodePart>
        <CodePart><value>CP2</value></CodePart>
        <CodePart><value>CP3</value></CodePart>
    </root>
    

    So I thought of looping over //CodePart/value instead, ensuring uniqueness for starters. But then I need some Xpath expression that includes the condition

    "value occurs in the node-set of all $variableX/ValidCode/@CodePart values"

    and use something like

    <xsl:for-each select="//CodePart[..condition..]/value">...</xsl:for-each>
    

    Is there a simple form of the Xpath expression I am looking for?
    Or is another approach preferable?