Last week a reader left a comment on my blog asking what the third argument of the List.Contains() M function (somewhat cryptically called equationCriteria) does. I didn’t know, so I did some research and found out that lots of the List.* functions have the same argument. The documentation for List.Distinct() has a few examples but no real explanation of how they work. It also says:
For more information about equationCriteria, see Parameter Values.
…but there’s no link or indication where the documentation on ‘Parameter Values’ can be found. A bit more digging led me to the bottom of this page:
Equation criteria
Equation criteria for list values can be specified as either a
A function value that is either
A key selector that determines the value in the list to apply the equality criteria, or
A comparer function that is used to specify the kind of comparison to apply. Built in comparer functions can be specified, see section for Comparer functions.
A list value which has
Exactly two items
The first element is the key selector as specified above
The second element is a comparer as specified above.
Still not exactly helpful, is it? After a bit of time testing, though, I think I’ve worked out how what’s possible with the equationCriteria argument and this blog post will, I hope, help any future M coders who are struggling with the same question. Let’s see some examples…
The basics
First of all, the basics. The following expression using List.Contains() returns TRUE because the text value “apples” appears in the list {“apples”, “oranges”, “pears”}:
List.Contains({"apples", "oranges", "pears"}, "apples")
The following returns FALSE because the text value “grapes” does not appear in the list {“apples”, “oranges”, “pears”}:
List.Contains({"apples", "oranges", "pears"}, "grapes")
However there are lots of different ways that text values can be compared and the equationCriteria argument allows you to specify which rules to follow.
Case sensitivity and culture
If you’ve written any M code you’ll know that it is case sensitive. As a result, the following returns FALSE:
List.Contains({"apples", "oranges", "pears"}, "Apples")
What happens if you want to do a case-insensitive comparison though? This is where the Comparer functions come in. The Comparer.FromCulture() function returns a function that compares two values according to the rules of a given culture or locale and optionally ignore case, and can be used in the equationCriteria argument. The following example returns TRUE:
List.Contains( {"apples", "oranges", "pears"}, "Apples", Comparer.FromCulture("en-GB", true) )
In this case Comparer.FromCulture("en-GB", true) returns a function that compares two values for the English – Great Britain culture (for a full list of culture values, see the Language Tag column of the table on this page); the second, optional argument here makes the function ignore case when making the comparison. The function that Comparer.FromCulture() returns is then used by List.Contains() to make the comparison.
Rather than specify a culture you can also use the Culture.Current function to return the current system culture. For me, Culture.Current returns the value “en-GB” because I live in Great Britain and have my PC configured to use a British English locale:
The following example shows how Culture.Current can be used with Comparer.FromCulture and also returns TRUE, at least for me:
List.Contains( {"apples", "oranges", "pears"}, "Apples", Comparer.FromCulture( Culture.Current, true ) )
If you’re curious to see an example where different cultures produce different results here’s one I stole from this article on string comparisons and sorting in .NET. Apparently in English the character æ is treated the same as the combination of the two characters ae but this is not the case in Danish. As a result the following returns TRUE:
List.Contains( {"aepples", "oranges", "pears"}, "æpples", Comparer.FromCulture( "en-GB", true ) )
Whereas this returns FALSE:
List.Contains( {"aepples", "oranges", "pears"}, "æpples", Comparer.FromCulture( "da-DK", true ) )
Ordinal comparisons
If you don’t want all the uncertainty of cultures and case sensitivity you can just make an ordinal comparison, which will compare two strings by finding the unicode character value for each character in each string and compare those values. To do this you can use the Comparer.Ordinal() function. The following returns FALSE:
List.Contains( {"apples", "oranges", "pears"}, "Apples", Comparer.Ordinal )
…because “a” is not the same unicode character as “A”, and so “apples” and “Apples” are not treated as equal.
Custom comparer functions
As the documentation hints you can also write your own function to do the comparison. A comparer function is just – as far as I can see – a function that has two arguments and returns a logical value. Here’s an example of a custom function that takes two text values, x and y, and returns true if the first three characters of x are the same as y:
(x as text, y as text)=>Text.Start(x,3)=y
It can be used with List.Contains() as in the following example, which returns TRUE:
List.Contains( {"apples", "oranges", "pears"}, "app", (x as text, y as text)=>Text.Start(x,3)=y )
What must be happening here is that the function is called three times, every value in the list {“apples”, “oranges”,”pears”} is being passed to the x argument and for each call “app” is passed to y; because the first three characters of “apples” are “app” the function returns true in this case, so List.Contains() returns true.
Key selectors
If you’re working with a list of records you might only want to do the comparison on one field in the record, and this is what key selectors allow you to do. The following example, which returns TRUE:
List.Contains( {[Fruit="apples", Colour="Red"], [Fruit="oranges", Colour="Orange"], [Fruit="pears", Colour="Green"]}, [Fruit="apples", Colour="Russet"], each [Fruit] )
…does so because it only compares the Fruit field in each record, and the Fruit fields in [Fruit=”apples”, Colour=”Red”] and [Fruit=”apples”, Colour=”Russet”] are indeed the same. However the following example returns FALSE:
List.Contains( {[Fruit="apples", Colour="Red"], [Fruit="oranges", Colour="Orange"], [Fruit="pears", Colour="Green"]}, [Fruit="apples", Colour="Russet"], each [Colour] )
…because the Colour “Russet” does not appear anywhere in the Colour field of any of the records in the first parameter.
Combining key selectors and comparison functions
Finally, as the documentation suggests, you can combine the above methods of comparison by passing a list containing two items to equationCriteria: the first item in the list must be a key selector, the second must be a comparer function. For example, the following returns TRUE:
List.Contains( {[Fruit="apples", Colour="Red"], [Fruit="oranges", Colour="Orange"], [Fruit="pears", Colour="Green"]}, [Fruit="Apples", Colour="Russet"], {each [Fruit], Comparer.FromCulture("en-GB", true)} )
…because it only looks at the Fruit field of each record, and it does a case-insensitive comparison using the en-GB culture, so “apples” and “Apples” are equal.