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"
[dependencies]
regex = { version = "1", optional = true}
serde = { version = "1", optional = true}
serde_derive = { version = "1", optional = true}
[features]
serde_support = ["serde", "serde_derive"]
regex_support = ["regex"]
[dev-dependencies]
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 |
| 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.
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

View File

@ -24,6 +24,9 @@ impl fmt::Display for EvalexprError {
ExpectedNumber { 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 } => {
write!(f, "Expected a Value::Boolean, but got {:?}.", actual)
},
@ -81,6 +84,7 @@ impl fmt::Display for EvalexprError {
ModulationError { 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"),
IllegalEscapeSequence(string) => write!(f, "Illegal escape sequence: {}", string),
CustomMessage(message) => write!(f, "Error: {}", message),

View File

@ -56,6 +56,13 @@ pub enum EvalexprError {
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.
ExpectedBoolean {
/// The actual value.
@ -160,6 +167,14 @@ pub enum EvalexprError {
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.
ContextNotManipulable,
@ -204,6 +219,11 @@ impl EvalexprError {
EvalexprError::ExpectedNumber { actual }
}
/// Constructs `Error::ExpectedNumberOrString{actual}`.
pub fn expected_number_or_string(actual: Value) -> Self {
EvalexprError::ExpectedNumberOrString { actual }
}
/// Constructs `Error::ExpectedBoolean{actual}`.
pub fn expected_boolean(actual: Value) -> Self {
EvalexprError::ExpectedBoolean { actual }
@ -267,6 +287,11 @@ impl EvalexprError {
pub(crate) fn modulation_error(dividend: Value, divisor: Value) -> Self {
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.
@ -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.
pub fn expect_boolean(actual: &Value) -> EvalexprResult<bool> {
match actual {

View File

@ -1,3 +1,7 @@
#[cfg(feature = "regex_support")]
use regex::Regex;
use crate::error::*;
use value::{FloatType, IntType};
use EvalexprError;
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,
}
}

View File

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

View File

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

View File

@ -275,15 +275,62 @@ fn test_n_ary_functions() {
Ok(Value::Int(3))
);
assert_eq!(eval_with_context("count 5", &context), Ok(Value::Int(1)));
}
#[test]
fn test_builtin_functions() {
assert_eq!(
eval_with_context("min(4.0, 3)", &context),
eval("min(4.0, 3)"),
Ok(Value::Int(3))
);
assert_eq!(
eval_with_context("max(4.0, 3)", &context),
eval("max(4.0, 3)"),
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]
@ -551,6 +598,9 @@ fn test_strings() {
eval_boolean_with_context("a == \"a string\"", &context),
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")]