Human readable conditions and filter
best companion.
A Clojure and ClojureScript library to write expressive
predicate functions.
Advantages:
- Predicate functions are very expressive and easy to read
- All built-in comparators are
nil
safe. - All built-in comparators which operate on strings have a case insensitive version
- Very fast execution (same as hand crafted version)
- Very easy to embed in your DSL
- Support for glob pattern matching (like:
*.txt
)
Maps are everywhere is Clojure. And when dealing with loads of maps it is important to work comfortably and make your code as readable as possible. However when it comes to filter maps building a clean predicate function becomes increasingly harder. Even harder with nested maps or when it required to combine predicate function with logical operators.
The purpose of this library is to simplify the construction of predicate
functions. A predicate function is a function which takes a value and
return a truthy or a falsey value. f(x) -> truthy | falsey
. These
are often used together with function like filter
to retain only items
which match a specific condition.
where
allows to build very expressive predicate functions and nil
-safe.
To use this library add the following dependency in your project.clj
[com.brunobonacci/where "0.5.6"]
then require the library
(ns your-ns
(:require [where.core :refer [where]])
The signature of the function is:
(where extractor comparator value)
extractor
is a function which is applied to every item passed and it has to extract the value from the map which ultimately needs to be compared.comparator
is any binary comparator such as:=
,not=
,>
,<
, etc It is just a function with takes two value and return true or falsevalue
is the value to compare against.
(where extractor comparator target)
;;=> predicate function
(where :field = "value")
Use the keyword as a function to extract the value(where :age > 18)
Comparator can be any clojure function which take two arities.(where (comp :zip :address) not= "ABC123")
Nested maps can be reached by composing functions.(where [:age <= 24])
The condition can be expressed as a vector of 3 elements for better composition.
(where [:and [:age <= 24] [:country = "USA"]])
:and
can be used to connect predicates logically for which all the conditions must be truthy.(where [:or [:country = "USA"] [:country = "Italy]])
:or
can be used to connect predicates logically for which one the conditions must be truthy.(where [:not [:country = "USA"]])
:not
can be used to negate the logical value of a single predicate.
The generic comparator accept any Clojure value.
(where :country :is? "Italy")
- like=
(where :country :is-not? "Italy")
- likenot=
(where :country :in? ["Italy" "France" "USA"])
- truthy if it matches any of the values listed(where :country :not-in? ["Italy" "France" "USA"])
- falsey if it matches any of the values listed
All String comparators they expect a String and are nil
safe (don't
throw NullPointerException like they String.class counterparts),
:matches
require a valid Pattern.
(where :country :starts-with? "Ita")
- likeString/startsWith
(where :country :ends-with? "ly")
- likeString/endsWith
(where :country :contains? "tal")
- likeString/indexOf != -1
(where :country :matches? #"United.*")
- likere-find
(where :country :matches-exactly? #"United.*")
- likere-matches
(where :filename :glob-matches? "*.txt")
- like glob pattern or wildcard matching?
for any character,*
any number of any characters
All String comparators they expect a String and are nil
safe (don't
throw NullPointerException like they String.class counterparts),
:MATCHES
require a valid Pattern.
(where :country :IS? "italy")
- likeString/.equalsIgnoreCase
(where :country :STARTS-WITH? "ITA")
- likeString/startsWith
, but case insensitive(where :country :ENDS-WITH? "ly")
- likeString/endsWith
, but case insensitive(where :country :CONTAINS? "tal")
- likeString/indexOf != -1
, but case insensitive(where :country :IN? ["ITALY" "france"])
- truthy if it matches any of the values listed, but case insensitive(where :country :MATCHES? #"united.*")
- likere-find
, but case insensitive(where :country :MATCHES-EXACTLY? #"united.*")
- likere-matches
, but case insensitive(where :filename :GLOB-MATCHES? "*.txt")
- like:glob-matches?
, but case insensitive
All String comparators they expect a String and are nil
safe (don't
throw NullPointerException like they String.class counterparts),
:not-matches
and :NOT-MATCHES
require a valid Pattern.
(where :country :not-starts-with? "Ita")
- same as(complement (where :country :starts-with? "Ita"))
(where :country :not-ends-with? "ly")
- same as(complement (where :country :ends-with? "ly"))
(where :country :not-contains? "tal")
- same as(complement (where :country :contains? "tal"))
(where :country :not-matches? #"United.*")
- same as(complement (where :country :matches? #"United.*"))
(where :country :not-matches-exactly? #"United.*")
- same as(complement (where :country :matches-exactly? #"United.*"))
(where :filename :not-glob-matches? "*.txt")
- same as(complement (where :filename :not-glob-matches? "*.txt"))
(where :country :IS-NOT? "italy")
- same as(complement (where :country :IS? "italy"))
(where :country :NOT-STARTS-WITH? "ITA")
- same as(complement (where :country :STARTS-WITH? "ITA"))
(where :country :NOT-ENDS-WITH? "ly")
- same as(complement (where :country :ENDS-WITH? "ly"))
(where :country :NOT-CONTAINS? "tal")
- same as(complement (where :country :CONTAINS? "tal"))
(where :country :NOT-IN? ["ITALY" "france"])
- same as(complement (where :country :IN? ["ITALY" "france"]))
(where :country :NOT-MATCHES? #"united.*")
- same as(complement (where :country :MATCHES? #"united.*"))
(where :country :NOT-MATCHES-EXACTLY? #"united.*")
- same as(complement (where :country :MATCHES-EXACTLY? #"united.*"))
(where :filename :NOT-GLOB-MATCHES? "*.txt")
- same as(complement (where :filename :NOT-GLOB-MATCHES? "*.txt"))
All numerical comparators are nil
safe (don't throw
NullPointerException) when one of the argument is nil.
(where :age :between? [18 34])
- truthy for all number between 18 and 34 (included)(where :age :strictly-between? [18 34])
- truthy for all number between 18 and 34 (excluded)(where :age :range? [18 34])
- truthy for all number between 18 (inlcuded) and 34 (excluded)(where :age :in? [18 22 34 16])
- truthy if the :age is in the given list of values(where :age :not-between? [18 34])
- falsey for all number between 18 and 34 (included)(where :age :not-strictly-between? [18 34])
- falsey for all number between 18 and 34 (excluded)(where :age :not-range? [18 34])
- falsey for all number between 18 (inlcuded) and 34 (excluded)(where :age :not-in? [18 22 34 16])
- falsey if the :age is in the given list of values
There are a number of common compartors which are provided as built-in functions. These comparators allow for much simpler and expressive code than their respective Clojure's counterparts. Additionally all built-in comparators have the following properties:
- All built-in comparator are
nil
safe - All built-in comparator have a complement operator (which starts with
not
) - All built-in string comparator have a case insensitive version (uppercase)
Comparator | Complement (not) | Case-insensitive | Insensitive Complement |
---|---|---|---|
:is? | :is-not? | :IS? | :IS-NOT? |
:starts-with? | :not-starts-with? | :STARTS-WITH? | :NOT-STARTS-WITH? |
:ends-with? | :not-ends-with? | :ENDS-WITH? | :NOT-ENDS-WITH? |
:contains? | :not-contains? | :CONTAINS? | :NOT-CONTAINS? |
:in? | :not-in? | :IN? | :NOT-IN? |
:matches? | :not-matches? | :MATCHES? | :NOT-MATCHES? |
:matches-exactly? | :not-matches-exactly? | :MATCHES-EXACTLY? | :NOT-MATCHES-EXACTLY? |
:glob-matches? | :not-glob-matches? | :GLOB-MATCHES? | :NOT-GLOB-MATCHES? |
Comparator | Example |
---|---|
:is? | (where :country :is? "USA") |
:starts-with? | (where :country :starts-with? "US") |
:ends-with? | (where :country :ends-with? "SA") |
:contains? | (where :country :contains? "SA") |
:in? | (where :country :in? ["USA" "Italy" "France"]) |
:matches? | (where :country :matches? #"United.*") |
Comparator | Complement (not) |
---|---|
:between? | :not-between? |
:strictly-between? | :not-strictly-between? |
:range? | :not-range? |
:in? | :not-in? |
Comparator | Example | True for |
---|---|---|
:between? | (where :age :between? [18 21]) |
18, 19, 20, 21 |
:strictly-between? | (where :age :strictly-between? [18 21]) |
19, 20 |
:range? | (where :age :range? [18 21]) |
18, 19, 20 |
:in? | (where :age :in? [18 20 22 24]) |
18, 20, 22, 24 |
where
can be used also with numbers and strings. When the extractor doesn't apply
then simply use the two-arity version of the function.
For example:
(filter (where > 5) (range 10))
;; => (6 7 8 9)
(filter (where ends-with "er") ["warrior" "singer" "player"])
;; => ("singer" "player")
See more examples.
Build profiles are by Clojure version:
lein build-all
Here are things I'm considering to add.
- more numerical comparators with (nil/type-safety such as: :> :< etc)
- date comparison (before, after, between)
- support for composition with custom predicates
(where [:and f1 f2 f3])
- a set of useful/common extractors and comparators.
- support for unary predicates
Copyright © 2015 - 2021 Bruno Bonacci - Distributed under the Apache License v2.0