Skip to content

Commit

Permalink
Builtin string functions - downcase, len, match(regex), replace(regex…
Browse files Browse the repository at this point in the history
…), trim, upcase. New dependency regex.
  • Loading branch information
bittrance committed Apr 7, 2019
1 parent a9c4530 commit bf2bd51
Show file tree
Hide file tree
Showing 7 changed files with 137 additions and 2 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ name = "evalexpr"
path = "src/lib.rs"

[dependencies]
regex = { version = "1" }
serde = { version = "1", optional = true}
serde_derive = { version = "1", optional = true}

Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,12 @@ This crate offers a set of builtin functions.
|------------|-----------------|-------------|
| min | >= 1 | Returns the minimum 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.
They return the result as the type it was passed into the function.
Expand Down
1 change: 1 addition & 0 deletions src/error/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,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),
Expand Down
13 changes: 13 additions & 0 deletions src/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,14 @@ pub enum EvalexprError {
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.
ContextNotManipulable,

Expand Down Expand Up @@ -279,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.
Expand Down
71 changes: 71 additions & 0 deletions src/function/builtin.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,25 @@
use regex::Regex;

use crate::error::*;
use value::{FloatType, IntType};
use EvalexprError;
use Function;
use Value;

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> {
match identifier {
"min" => Some(Function::new(
Expand Down Expand Up @@ -53,6 +70,60 @@ 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))
}),
)),
"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)
}
}),
)),
"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,
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@
#![warn(missing_docs)]

extern crate regex;
#[cfg(test)]
extern crate ron;
#[cfg(feature = "serde_support")]
Expand Down
46 changes: 44 additions & 2 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,15 +275,57 @@ 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("downcase(\"FOOBAR\")"),
Ok(Value::from("foobar"))
);
assert_eq!(
eval("len(\"foobar\")"),
Ok(Value::Int(6))
);
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()))
);
assert_eq!(
eval("trim(\" foo bar \")"),
Ok(Value::from("foo bar"))
);
assert_eq!(
eval("upcase(\"foobar\")"),
Ok(Value::from("FOOBAR"))
);
}

#[test]
Expand Down

0 comments on commit bf2bd51

Please sign in to comment.