Skip to content

Commit 2f7d1c2

Browse files
authored
Merge pull request #38 from bittrance/string-operators
String operators and builtin functions
2 parents e328adb + dbf3949 commit 2f7d1c2

8 files changed

Lines changed: 207 additions & 19 deletions

File tree

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ name = "evalexpr"
1616
path = "src/lib.rs"
1717

1818
[dependencies]
19+
regex = { version = "1", optional = true}
1920
serde = { version = "1", optional = true}
2021
serde_derive = { version = "1", optional = true}
2122

2223
[features]
2324
serde_support = ["serde", "serde_derive"]
25+
regex_support = ["regex"]
2426

2527
[dev-dependencies]
2628
ron = "0.4"

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,9 +214,16 @@ This crate offers a set of builtin functions.
214214
|------------|-----------------|-------------|
215215
| min | >= 1 | Returns the minimum of the arguments |
216216
| max | >= 1 | Returns the maximum of the arguments |
217+
| len | 1 | Return the character length of string argument |
218+
| str::regex_matches | 2 | Returns true if first string argument matches regex in second |
219+
| str::regex_replace | 3 | Returns string with matches replaced by third argument |
220+
| str::to_lowercase | 1 | Returns lower-case version of string |
221+
| str::to_uppercase | 1 | Returns upper-case version of string |
222+
| str::trim | 1 | Strips whitespace from start and end of string |
217223

218224
The `min` and `max` functions can deal with a mixture of integer and floating point arguments.
219-
They return the result as the type it was passed into the function.
225+
They return the result as the type it was passed into the function. The regex functions require
226+
feature flag `regex_support`.
220227

221228
### Values
222229

src/error/display.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ impl fmt::Display for EvalexprError {
2424
ExpectedNumber { actual } => {
2525
write!(f, "Expected a Value::Float or Value::Int, but got {:?}.", actual)
2626
},
27+
ExpectedNumberOrString { actual } => {
28+
write!(f, "Expected a Value::Number or a Value::String, but got {:?}.", actual)
29+
},
2730
ExpectedBoolean { actual } => {
2831
write!(f, "Expected a Value::Boolean, but got {:?}.", actual)
2932
},
@@ -81,6 +84,7 @@ impl fmt::Display for EvalexprError {
8184
ModulationError { dividend, divisor } => {
8285
write!(f, "Error modulating {} % {}", dividend, divisor)
8386
},
87+
InvalidRegex { regex, message } => write!(f, "Regular expression {:?} is invalid: {:?}", regex, message),
8488
ContextNotManipulable => write!(f, "Cannot manipulate context"),
8589
IllegalEscapeSequence(string) => write!(f, "Illegal escape sequence: {}", string),
8690
CustomMessage(message) => write!(f, "Error: {}", message),

src/error/mod.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ pub enum EvalexprError {
5656
actual: Value,
5757
},
5858

59+
/// A numeric or string value was expected.
60+
/// Numeric values are the variants `Value::Int` and `Value::Float`.
61+
ExpectedNumberOrString {
62+
/// The actual value.
63+
actual: Value,
64+
},
65+
5966
/// A boolean value was expected.
6067
ExpectedBoolean {
6168
/// The actual value.
@@ -160,6 +167,14 @@ pub enum EvalexprError {
160167
divisor: Value,
161168
},
162169

170+
/// A regular expression could not be parsed
171+
InvalidRegex {
172+
/// The invalid regular expression
173+
regex: String,
174+
/// Failure message from the regex engine
175+
message: String,
176+
},
177+
163178
/// A modification was attempted on a `Context` that does not allow modifications.
164179
ContextNotManipulable,
165180

@@ -204,6 +219,11 @@ impl EvalexprError {
204219
EvalexprError::ExpectedNumber { actual }
205220
}
206221

222+
/// Constructs `Error::ExpectedNumberOrString{actual}`.
223+
pub fn expected_number_or_string(actual: Value) -> Self {
224+
EvalexprError::ExpectedNumberOrString { actual }
225+
}
226+
207227
/// Constructs `Error::ExpectedBoolean{actual}`.
208228
pub fn expected_boolean(actual: Value) -> Self {
209229
EvalexprError::ExpectedBoolean { actual }
@@ -267,6 +287,11 @@ impl EvalexprError {
267287
pub(crate) fn modulation_error(dividend: Value, divisor: Value) -> Self {
268288
EvalexprError::ModulationError { dividend, divisor }
269289
}
290+
291+
/// Constructs `EvalexprError::InvalidRegex(regex)`
292+
pub fn invalid_regex(regex: String, message: String) -> Self {
293+
EvalexprError::InvalidRegex{ regex, message }
294+
}
270295
}
271296

272297
/// 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<()> {
315340
}
316341
}
317342

343+
/// Returns `Ok(())` if the given value is a string or a numeric
344+
pub fn expect_number_or_string(actual: &Value) -> EvalexprResult<()> {
345+
match actual {
346+
Value::String(_) | Value::Float(_) | Value::Int(_) => Ok(()),
347+
_ => Err(EvalexprError::expected_number_or_string(actual.clone())),
348+
}
349+
}
350+
318351
/// Returns `Ok(bool)` if the given value is a `Value::Boolean`, or `Err(Error::ExpectedBoolean)` otherwise.
319352
pub fn expect_boolean(actual: &Value) -> EvalexprResult<bool> {
320353
match actual {

src/function/builtin.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
#[cfg(feature = "regex_support")]
2+
use regex::Regex;
3+
4+
use crate::error::*;
15
use value::{FloatType, IntType};
26
use EvalexprError;
37
use Function;
@@ -53,6 +57,63 @@ pub fn builtin_function(identifier: &str) -> Option<Function> {
5357
}
5458
}),
5559
)),
60+
61+
"len" => Some(Function::new(
62+
Some(1),
63+
Box::new(|arguments| {
64+
let subject = expect_string(&arguments[0])?;
65+
Ok(Value::from(subject.len() as i64))
66+
}),
67+
)),
68+
69+
// string functions
70+
71+
#[cfg(feature = "regex_support")]
72+
"str::regex_matches" => Some(Function::new(
73+
Some(2),
74+
Box::new(|arguments| {
75+
let subject = expect_string(&arguments[0])?;
76+
let re_str = expect_string(&arguments[1])?;
77+
match Regex::new(re_str) {
78+
Ok(re) => Ok(Value::Boolean(re.is_match(subject))),
79+
Err(err) => Err(EvalexprError::invalid_regex(re_str.to_string(), format!("{}", err)))
80+
}
81+
}),
82+
)),
83+
#[cfg(feature = "regex_support")]
84+
"str::regex_replace" => Some(Function::new(
85+
Some(3),
86+
Box::new(|arguments| {
87+
let subject = expect_string(&arguments[0])?;
88+
let re_str = expect_string(&arguments[1])?;
89+
let repl = expect_string(&arguments[2])?;
90+
match Regex::new(re_str) {
91+
Ok(re) => Ok(Value::String(re.replace_all(subject, repl).to_string())),
92+
Err(err) => Err(EvalexprError::invalid_regex(re_str.to_string(), format!("{}", err))),
93+
}
94+
}),
95+
)),
96+
"str::to_lowercase" => Some(Function::new(
97+
Some(1),
98+
Box::new(|arguments| {
99+
let subject = expect_string(&arguments[0])?;
100+
Ok(Value::from(subject.to_lowercase()))
101+
}),
102+
)),
103+
"str::to_uppercase" => Some(Function::new(
104+
Some(1),
105+
Box::new(|arguments| {
106+
let subject = expect_string(&arguments[0])?;
107+
Ok(Value::from(subject.to_uppercase()))
108+
}),
109+
)),
110+
"str::trim" => Some(Function::new(
111+
Some(1),
112+
Box::new(|arguments| {
113+
let subject = expect_string(&arguments[0])?;
114+
Ok(Value::from(subject.trim()))
115+
}),
116+
)),
56117
_ => None,
57118
}
58119
}

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,8 @@
347347
348348
#![warn(missing_docs)]
349349

350+
#[cfg(feature = "regex_support")]
351+
extern crate regex;
350352
#[cfg(test)]
351353
extern crate ron;
352354
#[cfg(feature = "serde_support")]

src/operator/mod.rs

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,15 @@ impl Operator for Add {
151151

152152
fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult<Value> {
153153
expect_operator_argument_amount(arguments.len(), 2)?;
154-
expect_number(&arguments[0])?;
155-
expect_number(&arguments[1])?;
156-
157-
if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
154+
expect_number_or_string(&arguments[0])?;
155+
expect_number_or_string(&arguments[1])?;
156+
157+
if let (Ok(a), Ok(b)) = (arguments[0].as_string(), arguments[1].as_string()) {
158+
let mut result = String::with_capacity(a.len() + b.len());
159+
result.push_str(&a);
160+
result.push_str(&b);
161+
Ok(Value::String(result))
162+
} else if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
158163
let result = a.checked_add(b);
159164
if let Some(result) = result {
160165
Ok(Value::Int(result))
@@ -400,10 +405,16 @@ impl Operator for Gt {
400405

401406
fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult<Value> {
402407
expect_operator_argument_amount(arguments.len(), 2)?;
403-
expect_number(&arguments[0])?;
404-
expect_number(&arguments[1])?;
408+
expect_number_or_string(&arguments[0])?;
409+
expect_number_or_string(&arguments[1])?;
405410

406-
if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
411+
if let (Ok(a), Ok(b)) = (arguments[0].as_string(), arguments[1].as_string()) {
412+
if a > b {
413+
Ok(Value::Boolean(true))
414+
} else {
415+
Ok(Value::Boolean(false))
416+
}
417+
} else if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
407418
if a > b {
408419
Ok(Value::Boolean(true))
409420
} else {
@@ -430,10 +441,16 @@ impl Operator for Lt {
430441

431442
fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult<Value> {
432443
expect_operator_argument_amount(arguments.len(), 2)?;
433-
expect_number(&arguments[0])?;
434-
expect_number(&arguments[1])?;
444+
expect_number_or_string(&arguments[0])?;
445+
expect_number_or_string(&arguments[1])?;
435446

436-
if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
447+
if let (Ok(a), Ok(b)) = (arguments[0].as_string(), arguments[1].as_string()) {
448+
if a < b {
449+
Ok(Value::Boolean(true))
450+
} else {
451+
Ok(Value::Boolean(false))
452+
}
453+
} else if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
437454
if a < b {
438455
Ok(Value::Boolean(true))
439456
} else {
@@ -460,10 +477,16 @@ impl Operator for Geq {
460477

461478
fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult<Value> {
462479
expect_operator_argument_amount(arguments.len(), 2)?;
463-
expect_number(&arguments[0])?;
464-
expect_number(&arguments[1])?;
480+
expect_number_or_string(&arguments[0])?;
481+
expect_number_or_string(&arguments[1])?;
465482

466-
if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
483+
if let (Ok(a), Ok(b)) = (arguments[0].as_string(), arguments[1].as_string()) {
484+
if a >= b {
485+
Ok(Value::Boolean(true))
486+
} else {
487+
Ok(Value::Boolean(false))
488+
}
489+
} else if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
467490
if a >= b {
468491
Ok(Value::Boolean(true))
469492
} else {
@@ -490,10 +513,16 @@ impl Operator for Leq {
490513

491514
fn eval(&self, arguments: &[Value], _context: &Context) -> EvalexprResult<Value> {
492515
expect_operator_argument_amount(arguments.len(), 2)?;
493-
expect_number(&arguments[0])?;
494-
expect_number(&arguments[1])?;
516+
expect_number_or_string(&arguments[0])?;
517+
expect_number_or_string(&arguments[1])?;
495518

496-
if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
519+
if let (Ok(a), Ok(b)) = (arguments[0].as_string(), arguments[1].as_string()) {
520+
if a <= b {
521+
Ok(Value::Boolean(true))
522+
} else {
523+
Ok(Value::Boolean(false))
524+
}
525+
} else if let (Ok(a), Ok(b)) = (arguments[0].as_int(), arguments[1].as_int()) {
497526
if a <= b {
498527
Ok(Value::Boolean(true))
499528
} else {

tests/integration.rs

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,15 +275,62 @@ fn test_n_ary_functions() {
275275
Ok(Value::Int(3))
276276
);
277277
assert_eq!(eval_with_context("count 5", &context), Ok(Value::Int(1)));
278+
}
278279

280+
#[test]
281+
fn test_builtin_functions() {
279282
assert_eq!(
280-
eval_with_context("min(4.0, 3)", &context),
283+
eval("min(4.0, 3)"),
281284
Ok(Value::Int(3))
282285
);
283286
assert_eq!(
284-
eval_with_context("max(4.0, 3)", &context),
287+
eval("max(4.0, 3)"),
285288
Ok(Value::Float(4.0))
286289
);
290+
assert_eq!(
291+
eval("len(\"foobar\")"),
292+
Ok(Value::Int(6))
293+
);
294+
assert_eq!(
295+
eval("str::to_lowercase(\"FOOBAR\")"),
296+
Ok(Value::from("foobar"))
297+
);
298+
assert_eq!(
299+
eval("str::to_uppercase(\"foobar\")"),
300+
Ok(Value::from("FOOBAR"))
301+
);
302+
assert_eq!(
303+
eval("str::trim(\" foo bar \")"),
304+
Ok(Value::from("foo bar"))
305+
);
306+
}
307+
308+
#[test]
309+
#[cfg(feature = "regex_support")]
310+
fn test_regex_functions() {
311+
assert_eq!(
312+
eval("str::regex_matches(\"foobar\", \"[ob]{3}\")"),
313+
Ok(Value::Boolean(true))
314+
);
315+
assert_eq!(
316+
eval("str::regex_matches(\"gazonk\", \"[ob]{3}\")"),
317+
Ok(Value::Boolean(false))
318+
);
319+
match eval("str::regex_matches(\"foo\", \"[\")") {
320+
Err(EvalexprError::InvalidRegex{ regex, message }) => {
321+
assert_eq!(regex, "[");
322+
assert!(message.contains("unclosed character class"));
323+
},
324+
v => panic!(v),
325+
};
326+
assert_eq!(
327+
eval("str::regex_replace(\"foobar\", \".*?(o+)\", \"b$1\")"),
328+
Ok(Value::String("boobar".to_owned()))
329+
);
330+
assert_eq!(
331+
eval("str::regex_replace(\"foobar\", \".*?(i+)\", \"b$1\")"),
332+
Ok(Value::String("foobar".to_owned()))
333+
);
287334
}
288335

289336
#[test]
@@ -551,6 +598,9 @@ fn test_strings() {
551598
eval_boolean_with_context("a == \"a string\"", &context),
552599
Ok(true)
553600
);
601+
assert_eq!(eval("\"a\" + \"b\""), Ok(Value::from("ab")));
602+
assert_eq!(eval("\"a\" > \"b\""), Ok(Value::from(false)));
603+
assert_eq!(eval("\"a\" < \"b\""), Ok(Value::from(true)));
554604
}
555605

556606
#[cfg(feature = "serde")]

0 commit comments

Comments
 (0)