Merge pull request #38 from bittrance/string-operators

String operators and builtin functions
This commit is contained in:
ISibboI 2019-04-13 18:02:24 +02:00 committed by GitHub
commit 2f7d1c2dfe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 206 additions and 18 deletions

View File

@ -16,11 +16,13 @@ name = "evalexpr"
path = "src/lib.rs" path = "src/lib.rs"
[dependencies] [dependencies]
regex = { version = "1", optional = true}
serde = { version = "1", optional = true} serde = { version = "1", optional = true}
serde_derive = { version = "1", optional = true} serde_derive = { version = "1", optional = true}
[features] [features]
serde_support = ["serde", "serde_derive"] serde_support = ["serde", "serde_derive"]
regex_support = ["regex"]
[dev-dependencies] [dev-dependencies]
ron = "0.4" ron = "0.4"

View File

@ -214,9 +214,16 @@ This crate offers a set of builtin functions.
|------------|-----------------|-------------| |------------|-----------------|-------------|
| min | >= 1 | Returns the minimum of the arguments | | min | >= 1 | Returns the minimum of the arguments |
| max | >= 1 | Returns the maximum of the arguments | | max | >= 1 | Returns the maximum of the arguments |
| len | 1 | Return the character length of string argument |
| str::regex_matches | 2 | Returns true if first string argument matches regex in second |
| str::regex_replace | 3 | Returns string with matches replaced by third argument |
| str::to_lowercase | 1 | Returns lower-case version of string |
| str::to_uppercase | 1 | Returns upper-case version of string |
| str::trim | 1 | Strips whitespace from start and end of string |
The `min` and `max` functions can deal with a mixture of integer and floating point arguments. The `min` and `max` functions can deal with a mixture of integer and floating point arguments.
They return the result as the type it was passed into the function. They return the result as the type it was passed into the function. The regex functions require
feature flag `regex_support`.
### Values ### Values

View File

@ -24,6 +24,9 @@ impl fmt::Display for EvalexprError {
ExpectedNumber { actual } => { ExpectedNumber { actual } => {
write!(f, "Expected a Value::Float or Value::Int, but got {:?}.", actual) write!(f, "Expected a Value::Float or Value::Int, but got {:?}.", actual)
}, },
ExpectedNumberOrString { actual } => {
write!(f, "Expected a Value::Number or a Value::String, but got {:?}.", actual)
},
ExpectedBoolean { actual } => { ExpectedBoolean { actual } => {
write!(f, "Expected a Value::Boolean, but got {:?}.", actual) write!(f, "Expected a Value::Boolean, but got {:?}.", actual)
}, },
@ -81,6 +84,7 @@ impl fmt::Display for EvalexprError {
ModulationError { dividend, divisor } => { ModulationError { dividend, divisor } => {
write!(f, "Error modulating {} % {}", dividend, divisor) write!(f, "Error modulating {} % {}", dividend, divisor)
}, },
InvalidRegex { regex, message } => write!(f, "Regular expression {:?} is invalid: {:?}", regex, message),
ContextNotManipulable => write!(f, "Cannot manipulate context"), ContextNotManipulable => write!(f, "Cannot manipulate context"),
IllegalEscapeSequence(string) => write!(f, "Illegal escape sequence: {}", string), IllegalEscapeSequence(string) => write!(f, "Illegal escape sequence: {}", string),
CustomMessage(message) => write!(f, "Error: {}", message), CustomMessage(message) => write!(f, "Error: {}", message),

View File

@ -56,6 +56,13 @@ pub enum EvalexprError {
actual: Value, actual: Value,
}, },
/// A numeric or string value was expected.
/// Numeric values are the variants `Value::Int` and `Value::Float`.
ExpectedNumberOrString {
/// The actual value.
actual: Value,
},
/// A boolean value was expected. /// A boolean value was expected.
ExpectedBoolean { ExpectedBoolean {
/// The actual value. /// The actual value.
@ -160,6 +167,14 @@ pub enum EvalexprError {
divisor: Value, divisor: Value,
}, },
/// A regular expression could not be parsed
InvalidRegex {
/// The invalid regular expression
regex: String,
/// Failure message from the regex engine
message: String,
},
/// A modification was attempted on a `Context` that does not allow modifications. /// A modification was attempted on a `Context` that does not allow modifications.
ContextNotManipulable, ContextNotManipulable,
@ -204,6 +219,11 @@ impl EvalexprError {
EvalexprError::ExpectedNumber { actual } EvalexprError::ExpectedNumber { actual }
} }
/// Constructs `Error::ExpectedNumberOrString{actual}`.
pub fn expected_number_or_string(actual: Value) -> Self {
EvalexprError::ExpectedNumberOrString { actual }
}
/// Constructs `Error::ExpectedBoolean{actual}`. /// Constructs `Error::ExpectedBoolean{actual}`.
pub fn expected_boolean(actual: Value) -> Self { pub fn expected_boolean(actual: Value) -> Self {
EvalexprError::ExpectedBoolean { actual } EvalexprError::ExpectedBoolean { actual }
@ -267,6 +287,11 @@ impl EvalexprError {
pub(crate) fn modulation_error(dividend: Value, divisor: Value) -> Self { pub(crate) fn modulation_error(dividend: Value, divisor: Value) -> Self {
EvalexprError::ModulationError { dividend, divisor } EvalexprError::ModulationError { dividend, divisor }
} }
/// Constructs `EvalexprError::InvalidRegex(regex)`
pub fn invalid_regex(regex: String, message: String) -> Self {
EvalexprError::InvalidRegex{ regex, message }
}
} }
/// Returns `Ok(())` if the actual and expected parameters are equal, and `Err(Error::WrongOperatorArgumentAmount)` otherwise. /// Returns `Ok(())` if the actual and expected parameters are equal, and `Err(Error::WrongOperatorArgumentAmount)` otherwise.
@ -315,6 +340,14 @@ pub fn expect_number(actual: &Value) -> EvalexprResult<()> {
} }
} }
/// Returns `Ok(())` if the given value is a string or a numeric
pub fn expect_number_or_string(actual: &Value) -> EvalexprResult<()> {
match actual {
Value::String(_) | Value::Float(_) | Value::Int(_) => Ok(()),
_ => Err(EvalexprError::expected_number_or_string(actual.clone())),
}
}
/// Returns `Ok(bool)` if the given value is a `Value::Boolean`, or `Err(Error::ExpectedBoolean)` otherwise. /// Returns `Ok(bool)` if the given value is a `Value::Boolean`, or `Err(Error::ExpectedBoolean)` otherwise.
pub fn expect_boolean(actual: &Value) -> EvalexprResult<bool> { pub fn expect_boolean(actual: &Value) -> EvalexprResult<bool> {
match actual { match actual {

View File

@ -1,3 +1,7 @@
#[cfg(feature = "regex_support")]
use regex::Regex;
use crate::error::*;
use value::{FloatType, IntType}; use value::{FloatType, IntType};
use EvalexprError; use EvalexprError;
use Function; use Function;
@ -53,6 +57,63 @@ pub fn builtin_function(identifier: &str) -> Option<Function> {
} }
}), }),
)), )),
"len" => Some(Function::new(
Some(1),
Box::new(|arguments| {
let subject = expect_string(&arguments[0])?;
Ok(Value::from(subject.len() as i64))
}),
)),
// string functions
#[cfg(feature = "regex_support")]
"str::regex_matches" => Some(Function::new(
Some(2),
Box::new(|arguments| {
let subject = expect_string(&arguments[0])?;
let re_str = expect_string(&arguments[1])?;
match Regex::new(re_str) {
Ok(re) => Ok(Value::Boolean(re.is_match(subject))),
Err(err) => Err(EvalexprError::invalid_regex(re_str.to_string(), format!("{}", err)))
}
}),
)),
#[cfg(feature = "regex_support")]
"str::regex_replace" => Some(Function::new(
Some(3),
Box::new(|arguments| {
let subject = expect_string(&arguments[0])?;
let re_str = expect_string(&arguments[1])?;
let repl = expect_string(&arguments[2])?;
match Regex::new(re_str) {
Ok(re) => Ok(Value::String(re.replace_all(subject, repl).to_string())),
Err(err) => Err(EvalexprError::invalid_regex(re_str.to_string(), format!("{}", err))),
}
}),
)),
"str::to_lowercase" => Some(Function::new(
Some(1),
Box::new(|arguments| {
let subject = expect_string(&arguments[0])?;
Ok(Value::from(subject.to_lowercase()))
}),
)),
"str::to_uppercase" => Some(Function::new(
Some(1),
Box::new(|arguments| {
let subject = expect_string(&arguments[0])?;
Ok(Value::from(subject.to_uppercase()))
}),
)),
"str::trim" => Some(Function::new(
Some(1),
Box::new(|arguments| {
let subject = expect_string(&arguments[0])?;
Ok(Value::from(subject.trim()))
}),
)),
_ => None, _ => None,
} }
} }

View File

@ -347,6 +347,8 @@
#![warn(missing_docs)] #![warn(missing_docs)]
#[cfg(feature = "regex_support")]
extern crate regex;
#[cfg(test)] #[cfg(test)]
extern crate ron; extern crate ron;
#[cfg(feature = "serde_support")] #[cfg(feature = "serde_support")]

View File

@ -151,10 +151,15 @@ impl Operator for Add {
fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult<Value> { fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult<Value> {
expect_operator_argument_amount(arguments.len(), 2)?; expect_operator_argument_amount(arguments.len(), 2)?;
expect_number(&arguments[0])?; expect_number_or_string(&arguments[0])?;
expect_number(&arguments[1])?; expect_number_or_string(&arguments[1])?;
if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) { if let (Ok(a), Ok(b)) = (arguments[0].as_string(), arguments[1].as_string()) {
let mut result = String::with_capacity(a.len() + b.len());
result.push_str(&a);
result.push_str(&b);
Ok(Value::String(result))
} else if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
let result = a.checked_add(b); let result = a.checked_add(b);
if let Some(result) = result { if let Some(result) = result {
Ok(Value::Int(result)) Ok(Value::Int(result))
@ -400,10 +405,16 @@ impl Operator for Gt {
fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult<Value> { fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult<Value> {
expect_operator_argument_amount(arguments.len(), 2)?; expect_operator_argument_amount(arguments.len(), 2)?;
expect_number(&arguments[0])?; expect_number_or_string(&arguments[0])?;
expect_number(&arguments[1])?; expect_number_or_string(&arguments[1])?;
if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) { if let (Ok(a), Ok(b)) = (arguments[0].as_string(), arguments[1].as_string()) {
if a > b {
Ok(Value::Boolean(true))
} else {
Ok(Value::Boolean(false))
}
} else if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
if a > b { if a > b {
Ok(Value::Boolean(true)) Ok(Value::Boolean(true))
} else { } else {
@ -430,10 +441,16 @@ impl Operator for Lt {
fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult<Value> { fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult<Value> {
expect_operator_argument_amount(arguments.len(), 2)?; expect_operator_argument_amount(arguments.len(), 2)?;
expect_number(&arguments[0])?; expect_number_or_string(&arguments[0])?;
expect_number(&arguments[1])?; expect_number_or_string(&arguments[1])?;
if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) { if let (Ok(a), Ok(b)) = (arguments[0].as_string(), arguments[1].as_string()) {
if a < b {
Ok(Value::Boolean(true))
} else {
Ok(Value::Boolean(false))
}
} else if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
if a < b { if a < b {
Ok(Value::Boolean(true)) Ok(Value::Boolean(true))
} else { } else {
@ -460,10 +477,16 @@ impl Operator for Geq {
fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult<Value> { fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult<Value> {
expect_operator_argument_amount(arguments.len(), 2)?; expect_operator_argument_amount(arguments.len(), 2)?;
expect_number(&arguments[0])?; expect_number_or_string(&arguments[0])?;
expect_number(&arguments[1])?; expect_number_or_string(&arguments[1])?;
if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) { if let (Ok(a), Ok(b)) = (arguments[0].as_string(), arguments[1].as_string()) {
if a >= b {
Ok(Value::Boolean(true))
} else {
Ok(Value::Boolean(false))
}
} else if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
if a >= b { if a >= b {
Ok(Value::Boolean(true)) Ok(Value::Boolean(true))
} else { } else {
@ -490,10 +513,16 @@ impl Operator for Leq {
fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult<Value> { fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult<Value> {
expect_operator_argument_amount(arguments.len(), 2)?; expect_operator_argument_amount(arguments.len(), 2)?;
expect_number(&arguments[0])?; expect_number_or_string(&arguments[0])?;
expect_number(&arguments[1])?; expect_number_or_string(&arguments[1])?;
if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) { if let (Ok(a), Ok(b)) = (arguments[0].as_string(), arguments[1].as_string()) {
if a <= b {
Ok(Value::Boolean(true))
} else {
Ok(Value::Boolean(false))
}
} else if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
if a <= b { if a <= b {
Ok(Value::Boolean(true)) Ok(Value::Boolean(true))
} else { } else {

View File

@ -275,15 +275,62 @@ fn test_n_ary_functions() {
Ok(Value::Int(3)) Ok(Value::Int(3))
); );
assert_eq!(eval_with_context("count 5", &context), Ok(Value::Int(1))); assert_eq!(eval_with_context("count 5", &context), Ok(Value::Int(1)));
}
#[test]
fn test_builtin_functions() {
assert_eq!( assert_eq!(
eval_with_context("min(4.0, 3)", &context), eval("min(4.0, 3)"),
Ok(Value::Int(3)) Ok(Value::Int(3))
); );
assert_eq!( assert_eq!(
eval_with_context("max(4.0, 3)", &context), eval("max(4.0, 3)"),
Ok(Value::Float(4.0)) Ok(Value::Float(4.0))
); );
assert_eq!(
eval("len(\"foobar\")"),
Ok(Value::Int(6))
);
assert_eq!(
eval("str::to_lowercase(\"FOOBAR\")"),
Ok(Value::from("foobar"))
);
assert_eq!(
eval("str::to_uppercase(\"foobar\")"),
Ok(Value::from("FOOBAR"))
);
assert_eq!(
eval("str::trim(\" foo bar \")"),
Ok(Value::from("foo bar"))
);
}
#[test]
#[cfg(feature = "regex_support")]
fn test_regex_functions() {
assert_eq!(
eval("str::regex_matches(\"foobar\", \"[ob]{3}\")"),
Ok(Value::Boolean(true))
);
assert_eq!(
eval("str::regex_matches(\"gazonk\", \"[ob]{3}\")"),
Ok(Value::Boolean(false))
);
match eval("str::regex_matches(\"foo\", \"[\")") {
Err(EvalexprError::InvalidRegex{ regex, message }) => {
assert_eq!(regex, "[");
assert!(message.contains("unclosed character class"));
},
v => panic!(v),
};
assert_eq!(
eval("str::regex_replace(\"foobar\", \".*?(o+)\", \"b$1\")"),
Ok(Value::String("boobar".to_owned()))
);
assert_eq!(
eval("str::regex_replace(\"foobar\", \".*?(i+)\", \"b$1\")"),
Ok(Value::String("foobar".to_owned()))
);
} }
#[test] #[test]
@ -551,6 +598,9 @@ fn test_strings() {
eval_boolean_with_context("a == \"a string\"", &context), eval_boolean_with_context("a == \"a string\"", &context),
Ok(true) Ok(true)
); );
assert_eq!(eval("\"a\" + \"b\""), Ok(Value::from("ab")));
assert_eq!(eval("\"a\" > \"b\""), Ok(Value::from(false)));
assert_eq!(eval("\"a\" < \"b\""), Ok(Value::from(true)));
} }
#[cfg(feature = "serde")] #[cfg(feature = "serde")]