Skip to content

Commit

Permalink
Merge pull request #38 from bittrance/string-operators
Browse files Browse the repository at this point in the history
String operators and builtin functions
  • Loading branch information
ISibboI authored Apr 13, 2019
2 parents e328adb + dbf3949 commit 2f7d1c2
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 19 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions src/error/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
Expand Down Expand Up @@ -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),
Expand Down
33 changes: 33 additions & 0 deletions src/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,

Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
61 changes: 61 additions & 0 deletions src/function/builtin.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
#[cfg(feature = "regex_support")]
use regex::Regex;

use crate::error::*;
use value::{FloatType, IntType};
use EvalexprError;
use Function;
Expand Down Expand Up @@ -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,
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,8 @@
#![warn(missing_docs)]

#[cfg(feature = "regex_support")]
extern crate regex;
#[cfg(test)]
extern crate ron;
#[cfg(feature = "serde_support")]
Expand Down
61 changes: 45 additions & 16 deletions src/operator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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])?;

if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
expect_number_or_string(&arguments[0])?;
expect_number_or_string(&arguments[1])?;

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))
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
54 changes: 52 additions & 2 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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")]
Expand Down

0 comments on commit 2f7d1c2

Please sign in to comment.