Home > database >  How to do a case insensitive lookup over a set of nodes in XSLT/XPATH?
How to do a case insensitive lookup over a set of nodes in XSLT/XPATH?

Time:09-28

Say I have this data:

<recipe name="Fruit Salad">
  <ingredient name="banana"/>
  <ingredient name="oRange"/>
  <ingredient name="APPLE"/>
</recipes>

and I have this other data:

<refrigerator>
  <item name="appLe"/>
  <item name="Grape"/>
</refrigerator>

and I want to find all the things needed for my recipes that are not already in my frig, to make a shopping list. The right answer is the set {banana, orange}

This doesn't work because lower-case() only takes a string. So does translate():

<xsl:variable name="missing-ingredients" select="recipe/ingredient/@name[not(lower-case(.)=lower-case(refrigerator/item/@name))]"/>

Without resorting to cheating (going outside xslt and xpath), how can I do this? I find it hard to imagine they didn't make a provision for this inside xpath, since node lookups are the bread and butter of xslt and xpath, and weren't not on the first versions anymore.

If I could just convert a list of nodes (a node-set, I suppose) to lowercase, I would be home free.

CodePudding user response:

There's a problem with this XPath expression:

recipe/ingredient/@name[not(lower-case(.)=lower-case(refrigerator/item/@name))]

The expresson refrigerator/item/@name yields a sequence of attributes, rather than a single string, as the lower-case() function expects. You need to lower-case each one of the items in that sequence, by passing them to the lower-case() function individually. e.g.

for 
   $stored-ingredient 
in 
   refrigerator/item/@name 
return 
   lower-case($stored-ingredient)

In XPath 3 there's a more concise alternative, the "simple map" operator !, which would look like this:

refrigerator/item/@name ! lower-case(.)

CodePudding user response:

For cross-references, I would use a key e.g.

  <xsl:param name="frig" expand-text="no">
    <refrigerator>
      <item name="appLe"/>
      <item name="Grape"/>
    </refrigerator>    
  </xsl:param>
  
  <xsl:key name="frig-item-by-name" match="refrigerator/item" use="lower-case(@name)"/>

Then you can select e.g.

  <xsl:template match="recipe">
    <section>
      <h1>{local-name()}</h1>
      <section>
        <h2>Available ingredients</h2>
        <xsl:where-populated>
          <ul>
            <xsl:apply-templates select="ingredient[key('frig-item-by-name', lower-case(@name), $frig)]"/>
          </ul>
        </xsl:where-populated>
      </section>
      <section>
        <h2>Ingredients to buy</h2>
        <xsl:where-populated>
          <ul>
            <xsl:apply-templates select="ingredient[not(key('frig-item-by-name', lower-case(@name), $frig))]"/>
          </ul>
        </xsl:where-populated>
      </section>
    </section>
  </xsl:template>

Of course, if the data of the frig is in the same document as the primary input, drop the parameter and drop the third argument of the key function calls. And of course instead of inlining the data, as I did for self-containedness of the example, if the data is in a second document, use e.g. <xsl:param name="frig" select="doc('frig-data.xml')"/>.

Complete sample to put above snippets into context:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    exclude-result-prefixes="#all"
    expand-text="yes"
    version="3.0">
  
  <xsl:param name="frig" expand-text="no">
    <refrigerator>
      <item name="appLe"/>
      <item name="Grape"/>
    </refrigerator>    
  </xsl:param>
  
  <xsl:key name="frig-item-by-name" match="refrigerator/item" use="lower-case(@name)"/>
  
  <xsl:template match="recipe">
    <section>
      <h1>{local-name()}</h1>
      <section>
        <h2>Available ingredients</h2>
        <xsl:where-populated>
          <ul>
            <xsl:apply-templates select="ingredient[key('frig-item-by-name', lower-case(@name), $frig)]"/>
          </ul>
        </xsl:where-populated>
      </section>
      <section>
        <h2>Ingredients to buy</h2>
        <xsl:where-populated>
          <ul>
            <xsl:apply-templates select="ingredient[not(key('frig-item-by-name', lower-case(@name), $frig))]"/>
          </ul>
        </xsl:where-populated>
      </section>
    </section>
  </xsl:template>
  
  <xsl:template match="ingredient">
    <li>{lower-case(@name)}</li>
  </xsl:template>

  <xsl:output method="html" indent="yes" html-version="5"/>

  <xsl:template match="/">
    <html>
      <head>
        <title>.NET XSLT Fiddle Example</title>
      </head>
      <body>
        <xsl:apply-templates/>
      </body>
    </html>
  </xsl:template>
  
</xsl:stylesheet>

CodePudding user response:

Here's my working XPath 2 answer:

<xsl:variable name="ingredients" select="recipe/ingredients/@name"/>
<xsl:variable name="inventory" select="refrigerator/item/@name"/> 
<xsl:variable name="lower-ingredients">
    <xsl:for-each select="$ingredients">
        <item>
            <xsl:value-of select="lower-case(.)"/>
        </item>
    </xsl:for-each>
</xsl:variable>
<xsl:variable name="lower-inventory">
    <xsl:for-each select="$inventory">
        <item>
            <xsl:value-of select="lower-case(.)"/>
        </item>
    </xsl:for-each>
</xsl:variable>
<xsl:variable name="missing-ingredients" select="$lower-ingredients/item[not(. = $lower-inventory/item)]"/>
  • Related