Builtin string functions - downcase, len, match(regex), replace(regex), trim, upcase. New dependency regex.

This commit is contained in:
Quest 2019-04-07 08:10:36 +02:00
parent a9c45307dd
commit be54931f76
7 changed files with 148 additions and 2 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,6 +214,12 @@ 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 |
| downcase | 1 | Returns lower-case version of string |
| len | 1 | Return the character length of string argument |
| match | 2 | Returns true if first string argument matches regex in second |
| replace | 3 | Returns string with matches replaced by third argument |
| trim | 1 | Strips whitespace from start and end of string |
| upcase | 1 | Returns upper-case version 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.

View File

@ -84,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

@ -167,6 +167,14 @@ pub enum EvalexprError {
divisor: Value, divisor: Value,
}, },
/// This 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,
@ -279,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.

View File

@ -1,8 +1,27 @@
#[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;
use Value; use Value;
#[cfg(feature = "regex_support")]
fn regex_with_local_errors(re_str: &str) -> Result<Regex, EvalexprError> {
match Regex::new(re_str) {
Ok(re) => Ok(re),
Err(regex::Error::Syntax(message)) =>
Err(EvalexprError::invalid_regex(re_str.to_string(), message)),
Err(regex::Error::CompiledTooBig(max_size)) =>
Err(EvalexprError::invalid_regex(
re_str.to_string(),
format!("Regex exceeded max size {}", max_size))
),
Err(err) => Err(EvalexprError::CustomMessage(err.to_string())),
}
}
pub fn builtin_function(identifier: &str) -> Option<Function> { pub fn builtin_function(identifier: &str) -> Option<Function> {
match identifier { match identifier {
"min" => Some(Function::new( "min" => Some(Function::new(
@ -53,6 +72,62 @@ pub fn builtin_function(identifier: &str) -> Option<Function> {
} }
}), }),
)), )),
// string functions
"downcase" => Some(Function::new(
Some(1),
Box::new(|arguments| {
let subject = expect_string(&arguments[0])?;
Ok(Value::from(subject.to_lowercase()))
}),
)),
"len" => Some(Function::new(
Some(1),
Box::new(|arguments| {
let subject = expect_string(&arguments[0])?;
Ok(Value::from(subject.len() as i64))
}),
)),
#[cfg(feature = "regex_support")]
"match" => Some(Function::new(
Some(2),
Box::new(|arguments| {
let subject = expect_string(&arguments[0])?;
let re_str = expect_string(&arguments[1])?;
match regex_with_local_errors(re_str) {
Ok(re) => Ok(Value::Boolean(re.is_match(subject))),
Err(err) => Err(err)
}
}),
)),
#[cfg(feature = "regex_support")]
"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_with_local_errors(re_str) {
Ok(re) => Ok(Value::String(re.replace_all(subject, repl).to_string())),
Err(err) => Err(err),
}
}),
)),
"trim" => Some(Function::new(
Some(1),
Box::new(|arguments| {
let subject = expect_string(&arguments[0])?;
Ok(Value::from(subject.trim()))
}),
)),
"upcase" => Some(Function::new(
Some(1),
Box::new(|arguments| {
let subject = expect_string(&arguments[0])?;
Ok(Value::from(subject.to_uppercase()))
}),
)),
_ => 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

@ -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("downcase(\"FOOBAR\")"),
Ok(Value::from("foobar"))
);
assert_eq!(
eval("len(\"foobar\")"),
Ok(Value::Int(6))
);
assert_eq!(
eval("trim(\" foo bar \")"),
Ok(Value::from("foo bar"))
);
assert_eq!(
eval("upcase(\"foobar\")"),
Ok(Value::from("FOOBAR"))
);
}
#[test]
#[cfg(feature = "regex_support")]
fn test_regex_functions() {
assert_eq!(
eval("match(\"foobar\", \"[ob]{3}\")"),
Ok(Value::Boolean(true))
);
assert_eq!(
eval("match(\"gazonk\", \"[ob]{3}\")"),
Ok(Value::Boolean(false))
);
match eval("match(\"foo\", \"[\")") {
Err(EvalexprError::InvalidRegex{ regex, message }) => {
assert_eq!(regex, "[");
assert!(message.contains("unclosed character class"));
},
v => panic!(v),
};
assert_eq!(
eval("replace(\"foobar\", \".*?(o+)\", \"b$1\")"),
Ok(Value::String("boobar".to_owned()))
);
assert_eq!(
eval("replace(\"foobar\", \".*?(i+)\", \"b$1\")"),
Ok(Value::String("foobar".to_owned()))
);
} }
#[test] #[test]