xmlxsltxslt-3.0xpath-3.0

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


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.


Solution

  • 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(.)