This library is useful to evaluate a string expression like variable1 >= 2 && variable2 == "Value"
. The variables are provided by a dictionary [String : String]
representing the variables and their values. The complexity to evaluate a string expression is O(n)
Both Expression from Nick Lockwood and Eval from Lázló Teveli are interesintg alternatives. Exression is a ready to use framework and Lázló Teveli has produced a great work to deeply customize the usage of his framework. The goal of BooleanExpressionEvaluation is to focus on boolean expressions, when other expressions evaluation is not needed. Thus, the framework bears a little less complexity in its usage and customization.
Add the package to your dependencies in your Package.swift file
let package = Package (
...
dependencies: [
.package(url: "https://github.com/ABridoux/BooleanExpressionEvaluation.git", from: "1.0.0")
],
...
)
Or simply use Xcode menu File > Swift Packages > Add Package Dependency and copy/paste the git URL: https://github.com/ABridoux/BooleanExpressionEvaluation.git
Then import BooleanExpressionEvaluation in your file:
import BooleanExpressionEvaluation
To evaluate a String, create an Expression
, passing the string as the parameter of the init. Note that the initialisation can throw an error, as the string expression can contain incorrect elements. You can then call the evaluate()
function of the expression. This function can also throw an error, as the expression can have an incorrect grammar.
For example:
(Note that the use of the raw string syntax #""#
from Swift 5.0 is used to allow the use of double quotes without quoting them with \
)
let variables = ["userAge": "15", "userName": "Morty"]
let stringExpression = #"userAge > 10 && userName == "Morty""#
let expression: Expression
do {
expression = try Expression(stringExpression)
} catch {
// handle errors such as invalid elements in the expression
return
}
do {
let result = try expression.evaluate(with: variables)
print("The user is allowed to travel across dimensions: \(result)")
} catch {
// handle errors such as incorrect grammar in the expression
}
You can also create the Expression
and evaluate it in the same do{} catch{}
statement:
let variables = ["userAge": "15", "userName": "Morty"]
let stringExpression = #"userAge > 10 && userName == "Morty""#
do {
let result = try Expression(stringExpression).evaluate(with: variables)
} catch {
// handle errors such as invalid elements or incorrect grammar in the expression
return
}
Finally, a simple use case is to implement an extension of Expression
when you always evaluate them with a single source of variables, like a VariablesManager
singleton in your overall project.
extension Expression {
func evaluate() throws -> Bool {
return try evaluate(with: VariablesManager.shared.variables)
}
}
There are two types of operators available in an expression: comparison operators, to compare a variable and an other operand, and logic operators, to compare to boolean operands.
==
for equal!=
for different>
for greater than>=
for greater than or equal<
for lesser than<=
for lesser than or equalisIn
The result is true is the left operand is contained the right one which is provided as a list of string values. The right operand has to be filled with values separated by commas. For example: if the variableDucks
has "Riri, Fifi, Loulou" for value, the comparison'Riri' isIn Ducks
is evaluated as true. It's possible to escape a comma with "".hasPrefix
hasSuffix
contains
: true when the left string contains the right string.matches
: true when the left operand matches the right operand, given as a regular expression.
&&
for and||
for or!
for not which works on single boolean and parenthesised expressions.
You can define custom operators in an extension of the Operator
struct. Then add this operator to the Operator.models
set. The same applies for the LogicOperator
struct.
For example, you can define the hasPrefix
operator (note that those operators already exist).
extension Operator {
public static var hasPrefix: Operator { Operator("hasPrefix", isKeyword: true) { (lhs, rhs) in
guard let stringLhs = lhs as? String, let stringRhs = rhs as? String else {
throw ExpressionError.mismatchingType
}
return stringLhs.hasPrefix(stringRhs)
}}
}
Then, in the setup of your app:
Operator.models.insert(.hasPrefix)
Finally, you can simply add an operator directly:
Operator.models.insert(Operator("~=") { (lhs, rhs) in
guard let lhs = lhs as? String, let rhs = rhs as? String else { return nil }
return lhs.hasSuffix(rhs)
})
You can remove if you want the default operators, by calling the proper Operator.removeFromModels(:)
method. You can also directly override the behavior of a default operator, by updating the Operator.models
set with an operator which has the same description.
Note
As it is not possible for now to restrict a closure signature to a protocol without specifying the type as generic in the structure, we cannot allow only Comparable
operands in an operator evaluate
closure. Nonetheless, only strings, boolean and double are allowed as operands in this framework now. Moreover, you might want to compare a double and a string, with an opeator like count
for example. This would no be possible if the two operands were comparable with the same type.
You can compare a variable and an operand with a comparison operator. There are four types of operands.
String
which are quoted with double quotes onlyNumber
which group all numeric values, including floating onesBoolean
which are written as true or falseVariables
which have to begin by a letter (lower or upper case) and can contain hyphens-
and underscores_
. You can compare two variables. Note that the boolean variables can be written without the==
to evaluate if their state istrue
.
Given the following variables, here are some examples:
- "isUserAWizard": "true"
- "userName": "Gandalf"
- "userAge": "400"
- "fellowship": "Gandalf, Frodo, Sam, Aragorn, Gimli, Legolas, Boromir, Merry, Pippin"
- "hobbit": "Bilbo"
- "passphrase": "You shall not pass!"
isUserAWizard == true && hobbit == "Bilbo"
→ trueuserAge >= 400 || userName != "Saruman"
→ truefellowship <: hobbit
→ falseuserAge < 400 && userName == "Gandalf"
→ false(userAge < 400 && userName == "Gandalf) || fellowship <: "Aragorn"
→ trueisUserAWizard && passphrase == "You shall not pass!"
→ true
Variables are provided to the Expression
with a [String: String]
dictionary. When comparing two operands with a comparison operator, one of the operands at least has to be a variable. Otherwise, the comparison expression result is already known and the expression do not need to be evaluated.
A useful property of an Expression
is variables
, which is an array of the names of all the variables involved in the expression.
The default regular expression to match a variable is [a-zA-Z]{1}[a-zA-Z0-9_-]+
. You can choose to use an other regular expression by providing it when initialising an expression:
let expression = try? Expression("#variable >= 2", variablesRegexPattern: "[a-zA-Z#]{1}[a-zA-Z0-9#]+")
If you always use the same regular expression, you should consider to write an extension of Expression
to add the initialiser with this default expression. So with our last example:
extension Expression {
init(stringExpression: String) throws {
try self.init(stringExpression, variablesRegexPattern: "[a-zA-Z#]{1}[a-zA-Z0-9#]+")
}
}
Expression
implements the Codable
protocol, and it is encoded/decoded as a String
. Thus, you can try to decode a String
value as an expression. And encoding it will render a String
, describing it as a literal boolean expression.
Represents an element of the expression, like a variable, an operator or a number. There is four nested Enum
s to group the different elements of an expression:
ComparisonOperator
like>
or=
to evaluate a comparison between a variable and an other operandLogicOperator
to evaluate a result with two booleansBrackets
Operands
which always have an associated value, like a double, a boolean, a string or a variable
Act like an array of ExpressionElement
, although it is a struct
which implements the Collection
protocol.
Converts an expression which contains comparison expressions to a boolean expression, which contains only logic operators, boolean operands and brackets.
Uses the BooleanExpressionTokenizator
to get the different elements of the boolean expression and evaluate it. A new array is added into the expressionResults
array of arrays each time an opening bracket is met. When a closing bracket is met, the last created array is reduced to a boolean which is then injected into the previous array. The last created array is then deleted.