1

I need to ignore duplicate values while iterating in ForEach loop with Custom Function. I have below examples and share xslt which am trying to do. I can't apply foreach group in xslt 2.0, as it will break existing Code Functionality. Am expecting to resolve this issue with Custom Function itself.

Custom Function:

<xsl:function name="OriginalBook.Genre">
    <xsl:param name="book"/>
    <xsl:for-each select="$book[price &lt 10]">
       <xsl:element name="OriginalGenre">
          <xsl:if test="book[not(preceding::genre)]">
            <xsl:value-of select="current()/genre"/>
          </xsl:if>
      </xsl:element>
    </xsl:for-each>
</xsl:function>

<xsl:template match="/">
    <OriginalBook>  
        <xsl:copy-of select="OriginalBook.Genre(/catalog/book)"/>
    </OriginalBook>
</xsl:template>

Input:

<?xml version="1.0"?>
<catalog>
   <book>
      <author>Gambardella, Matthew</author>
      <title>XML Developer's Guide</title>
      <genre>Computer</genre>
      <price>9.95</price>
      <publish_date>2000-10-01</publish_date>
   </book>
   <book>
      <author>Ralls, Kim</author>
      <title>XML Developer's Guide</title>
      <genre>Computer</genre>
      <price>5.95</price>
      <publish_date>2000-12-16</publish_date>
   </book>
   <book>
      <author>Corets, Eva</author>
      <title>Maeve Ascendant</title>
      <genre>Fantasy</genre>
      <price>5.95</price>
      <publish_date>2000-11-17</publish_date>
   </book>
   <book>
      <author>Galos, Mike</author>
      <title>Visual Studio 7: A Comprehensive Guide</title>
      <genre>Computer</genre>
      <price>49.95</price>
      <publish_date>2001-04-16</publish_date>
   </book>
</catalog>

Desired output:

<OriginalBook>
    <OriginalGenre>Computer</OriginalGenre>
    <OriginalGenre>Fantasy</OriginalGenre>
</OriginalBook>
  • 2
    XSLT 2 and later for distinct values has the function `distinct-values`, for grouping it has `for-each-group`. It is not clear from your question or your code sample why you tag the question as xslt-3.0 but claim that `for-each-group` would break something. – Martin Honnen Aug 11 '20 at 20:22
  • Hi @MartinHonnen, My expectation I cannot add for-each-group, since it will break other codes too, Am expecting something like distinct-values function to include under for-each select or in if condition. – Mohammed Abdullah Aug 12 '20 at 05:24
  • I fail to see why you can't use `xsl:for-each-group`. Your explanation doesn't make sense to me. – Michael Kay Aug 12 '20 at 07:11
  • We are not using xslt directly, we are generating xslt from java and doing xml transformation. Changing to for-each-group requires lots of code changes in Java, am hoping to resolve with custom function with for-each block itself. – Mohammed Abdullah Aug 12 '20 at 07:13

2 Answers2

1

For what its worth, the classic approaches assuming XSLT 3 to eliminate duplicates are for-each-group group-by, distinct-values or map:merge:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:map="http://www.w3.org/2005/xpath-functions/map"
    exclude-result-prefixes="#all"
    xmlns:mf="http://example.com/mf"
    expand-text="yes"
    version="3.0">
    
  <xsl:function name="mf:grouping-example" as="element(OriginalGenre)*">
      <xsl:param name="books" as="element(book)*"/>
      <xsl:for-each-group select="$books[price &lt; 10]" group-by="genre">
          <OriginalGenre>{current-grouping-key()}</OriginalGenre>
      </xsl:for-each-group>
  </xsl:function>
  
  <xsl:function name="mf:distinct-values-example" as="element(OriginalGenre)*">
      <xsl:param name="books" as="element(book)*"/>
      <xsl:for-each select="distinct-values($books[price &lt; 10]/genre)">
          <OriginalGenre>{.}</OriginalGenre>
      </xsl:for-each>
  </xsl:function>
  
  <xsl:function name="mf:map-merge-example" as="element(OriginalGenre)*">
      <xsl:param name="books" as="element(book)*"/>
      <xsl:for-each 
          select="$books[price &lt; 10]/genre!map { data() : . } 
                  => map:merge() => map:keys()">
          <OriginalGenre>{.}</OriginalGenre>
      </xsl:for-each>
  </xsl:function>
  
  <xsl:output indent="yes"/>
  
  <xsl:template match="/">
      <Results>
          <Result-Grouping>
              <xsl:sequence select="mf:grouping-example(catalog/book)"/>
          </Result-Grouping>
          <Result-distinct-values>
              <xsl:sequence select="mf:distinct-values-example(catalog/book)"/>
          </Result-distinct-values>
          <Result-map-merge-example>
              <xsl:sequence select="mf:map-merge-example(catalog/book)"/>
          </Result-map-merge-example>
      </Results>
  </xsl:template>
  
</xsl:stylesheet>

The "equivalent" of a sequential loop would not be xsl:for-each but rather xsl:iterate:

  <xsl:function name="mf:iterate-example" as="element(OriginalGenre)*">
      <xsl:param name="books" as="element(book)*"/>
      <xsl:iterate select="$books[price &lt; 10]/genre/data()">
          <xsl:param name="genres" as="xs:string*" select="()"/>
          <xsl:if test="not(. = $genres)">
            <OriginalGenre>{.}</OriginalGenre>
          </xsl:if>
          <xsl:next-iteration>
            <xsl:with-param name="genres" select="if (. = $genres) then $genres else ($genres, .)"/>
          </xsl:next-iteration>
      </xsl:iterate>
  </xsl:function>

https://xsltfiddle.liberty-development.net/bEzkTcU

Martin Honnen
  • 160,499
  • 6
  • 90
  • 110
0

There are a couple of issues that I see.

  • If you want to create multiple OriginalGenre elements, one for each value produced by the call to the function, then move that element inside of your function.
  • Inside of your for-each statement, the current context is changing. The xsl:if will be evaluated with the book as the context item, so it will not have a book child, and you want to ensure that it's genre is not the same as any preceding::genre, like this: <xsl:if test="not(genre = preceding::genre)">
  • the function was not bound to a namespace, so I just assigned local
  • instead of xsl:copy-of I would use xsl:sequence

I would do something like this:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0" xmlns:local="local">
    <xsl:output indent="yes"/>
    
    <xsl:function name="local:OriginalBook.Genre">
        <xsl:param name="book"/>
        <xsl:for-each select="$book[price &lt; 10]">
            <xsl:if test="not(genre = preceding::genre)">
                <OriginalGenre><xsl:value-of select="current()/genre"/></OriginalGenre>
            </xsl:if>
        </xsl:for-each>
    </xsl:function>
    
    <xsl:template match="/">
        <OriginalBook>  
            <xsl:sequence select="local:OriginalBook.Genre(/catalog/book)"/>
        </OriginalBook>
    </xsl:template>
</xsl:stylesheet>
Mads Hansen
  • 63,927
  • 12
  • 112
  • 147
  • Hi @Mads Hansen, Thanks for your answer, Now I added Element name as per your suggestion, I tried to change the if block(by not including book context), but am getting duplicate values still. – Mohammed Abdullah Aug 12 '20 at 05:22