Merge pull request #38 from bittrance/string-operators
String operators and builtin functions
This commit is contained in:
commit
2f7d1c2dfe
@ -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"
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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),
|
||||||
|
@ -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 {
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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")]
|
||||||
|
@ -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 {
|
||||||
|
@ -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")]
|
||||||
|
Loading…
Reference in New Issue
Block a user