From a2d1d4a351e65e20d77e64509a8c87d786e128c1 Mon Sep 17 00:00:00 2001 From: Alexander Sennikov Date: Tue, 28 May 2024 16:25:09 +0200 Subject: [PATCH] feat: add 2 more string functions: $split and $replace (#33) * feat: add replace fn * feat: add split fn --- src/errors.rs | 12 +++ src/evaluator/functions.rs | 95 +++++++++++++++++++ src/lib.rs | 8 +- .../function-replace/case000.json | 0 .../function-replace/case001.json | 0 .../function-replace/case002.json | 0 .../function-replace/case003.json | 0 .../function-replace/case004.json | 0 .../function-replace/case005.json | 0 .../function-replace/case006.json | 0 .../function-replace/case007.json | 0 .../function-replace/case008.json | 0 .../function-replace/case009.json | 0 .../function-replace/case010.json | 0 .../function-replace/case011.json | 0 .../function-split/case000.json | 0 .../function-split/case001.json | 0 .../function-split/case002.json | 0 .../function-split/case003.json | 0 .../function-split/case004.json | 0 .../function-split/case005.json | 0 .../function-split/case006.json | 0 .../function-split/case007.json | 0 .../function-split/case008.json | 0 .../function-split/case009.json | 0 .../function-split/case010.json | 0 .../function-split/case011.json | 0 .../function-split/case012.json | 0 .../function-split/case013.json | 0 .../function-split/case014.json | 0 .../function-split/case015.json | 0 .../function-split/case016.json | 0 .../function-split/case017.json | 0 .../function-split/case018.json | 0 34 files changed, 112 insertions(+), 3 deletions(-) rename tests/testsuite/{skip => groups}/function-replace/case000.json (100%) rename tests/testsuite/{skip => groups}/function-replace/case001.json (100%) rename tests/testsuite/{skip => groups}/function-replace/case002.json (100%) rename tests/testsuite/{skip => groups}/function-replace/case003.json (100%) rename tests/testsuite/{skip => groups}/function-replace/case004.json (100%) rename tests/testsuite/{skip => groups}/function-replace/case005.json (100%) rename tests/testsuite/{skip => groups}/function-replace/case006.json (100%) rename tests/testsuite/{skip => groups}/function-replace/case007.json (100%) rename tests/testsuite/{skip => groups}/function-replace/case008.json (100%) rename tests/testsuite/{skip => groups}/function-replace/case009.json (100%) rename tests/testsuite/{skip => groups}/function-replace/case010.json (100%) rename tests/testsuite/{skip => groups}/function-replace/case011.json (100%) rename tests/testsuite/{skip => groups}/function-split/case000.json (100%) rename tests/testsuite/{skip => groups}/function-split/case001.json (100%) rename tests/testsuite/{skip => groups}/function-split/case002.json (100%) rename tests/testsuite/{skip => groups}/function-split/case003.json (100%) rename tests/testsuite/{skip => groups}/function-split/case004.json (100%) rename tests/testsuite/{skip => groups}/function-split/case005.json (100%) rename tests/testsuite/{skip => groups}/function-split/case006.json (100%) rename tests/testsuite/{skip => groups}/function-split/case007.json (100%) rename tests/testsuite/{skip => groups}/function-split/case008.json (100%) rename tests/testsuite/{skip => groups}/function-split/case009.json (100%) rename tests/testsuite/{skip => groups}/function-split/case010.json (100%) rename tests/testsuite/{skip => groups}/function-split/case011.json (100%) rename tests/testsuite/{skip => groups}/function-split/case012.json (100%) rename tests/testsuite/{skip => groups}/function-split/case013.json (100%) rename tests/testsuite/{skip => groups}/function-split/case014.json (100%) rename tests/testsuite/{skip => groups}/function-split/case015.json (100%) rename tests/testsuite/{skip => groups}/function-split/case016.json (100%) rename tests/testsuite/{skip => groups}/function-split/case017.json (100%) rename tests/testsuite/{skip => groups}/function-split/case018.json (100%) diff --git a/src/errors.rs b/src/errors.rs index af276ca5..103cea69 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -29,6 +29,9 @@ pub enum Error { D1009MultipleKeys(usize, String), D2014RangeOutOfBounds(usize, isize), D3001StringNotFinite(usize), + D3010EmptyPattern(usize), + D3011NegativeLimit(usize), + D3020NegativeLimit(usize), D3030NonNumericCast(usize, String), D3060SqrtNegative(usize, String), D3061PowUnrepresentable(usize, String, String), @@ -106,6 +109,9 @@ impl Error { Error::D1009MultipleKeys(..) => "D1009", Error::D2014RangeOutOfBounds(..) => "D2014", Error::D3001StringNotFinite(..) => "D3001", + Error::D3010EmptyPattern(..) => "D3010", + Error::D3011NegativeLimit(..) => "D3011", + Error::D3020NegativeLimit(..) => "D3020", Error::D3030NonNumericCast(..) => "D3030", Error::D3060SqrtNegative(..) => "D3060", Error::D3061PowUnrepresentable(..) => "D3061", @@ -196,6 +202,12 @@ impl fmt::Display for Error { write!(f, "{}: The size of the sequence allocated by the range operator (..) must not exceed 1e7. Attempted to allocate {}", p, s), D3001StringNotFinite(ref p) => write!(f, "{}: Attempting to invoke string function on Infinity or NaN", p), + D3010EmptyPattern(ref p) => + write!(f, "{}: Second argument of replace function cannot be an empty string", p), + D3011NegativeLimit(ref p) => + write!(f, "{}: Fourth argument of replace function must evaluate to a positive number", p), + D3020NegativeLimit(ref p) => + write!(f, "{}: Third argument of split function must evaluate to a positive number", p), D3030NonNumericCast(ref p, ref n) => write!(f, "{}: Unable to cast value to a number: {}", p, n), D3060SqrtNegative(ref p, ref n) => diff --git a/src/evaluator/functions.rs b/src/evaluator/functions.rs index bd94fcb9..4e4dfe04 100644 --- a/src/evaluator/functions.rs +++ b/src/evaluator/functions.rs @@ -589,6 +589,101 @@ pub fn fn_contains<'a>( )) } +pub fn fn_replace<'a>( + context: FunctionContext<'a, '_>, + args: &'a Value<'a>, +) -> Result<&'a Value<'a>> { + let str_value = &args[0]; + let pattern_value = &args[1]; + let replacement_value = &args[2]; + let limit_value = &args[3]; + + if str_value.is_undefined() { + return Ok(Value::undefined()); + } + + if pattern_value.is_string() && pattern_value.as_str().is_empty() { + return Err(Error::D3010EmptyPattern(context.char_index)); + } + + assert_arg!(str_value.is_string(), context, 1); + assert_arg!(pattern_value.is_string(), context, 2); + assert_arg!(replacement_value.is_string(), context, 3); + + let str_value = str_value.as_str(); + let pattern_value = pattern_value.as_str(); + let replacement_value = replacement_value.as_str(); + let limit_value = if limit_value.is_undefined() { + None + } else { + assert_arg!(limit_value.is_number(), context, 4); + if limit_value.as_isize().is_negative() { + return Err(Error::D3011NegativeLimit(context.char_index)); + } + Some(limit_value.as_isize()) + }; + + let replaced_string = if let Some(limit) = limit_value { + str_value.replacen( + &pattern_value.to_string(), + &replacement_value, + limit as usize, + ) + } else { + str_value.replace(&pattern_value.to_string(), &replacement_value) + }; + + Ok(Value::string(context.arena, replaced_string)) +} + +pub fn fn_split<'a>( + context: FunctionContext<'a, '_>, + args: &'a Value<'a>, +) -> Result<&'a Value<'a>> { + let str_value = &args[0]; + let separator_value = &args[1]; + let limit_value = &args[2]; + + if str_value.is_undefined() { + return Ok(Value::undefined()); + } + + assert_arg!(str_value.is_string(), context, 1); + assert_arg!(separator_value.is_string(), context, 2); + + let str_value = str_value.as_str(); + let separator_value = separator_value.as_str(); + let limit_value = if limit_value.is_undefined() { + None + } else { + assert_arg!(limit_value.is_number(), context, 4); + if limit_value.as_isize().is_negative() { + return Err(Error::D3020NegativeLimit(context.char_index)); + } + Some(limit_value.as_isize()) + }; + + let substrings: Vec<&str> = if let Some(limit) = limit_value { + str_value + .split(&separator_value.to_string()) + .take(limit as usize) + .collect() + } else { + str_value.split(&separator_value.to_string()).collect() + }; + + let substrings_count = substrings.len(); + + let result = Value::array_with_capacity(context.arena, substrings_count, ArrayFlags::empty()); + for (index, substring) in substrings.into_iter().enumerate() { + if substring.is_empty() && (index == 0 || index == substrings_count - 1) { + continue; + } + result.push(Value::string(context.arena, substring)); + } + Ok(result) +} + pub fn fn_abs<'a>(context: FunctionContext<'a, '_>, args: &'a Value<'a>) -> Result<&'a Value<'a>> { let arg = &args[0]; diff --git a/src/lib.rs b/src/lib.rs index 65d8f928..253b9337 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -124,6 +124,8 @@ impl<'a> JsonAta<'a> { bind_native!("abs", 1, fn_abs); bind_native!("append", 2, fn_append); bind_native!("assert", 2, fn_assert); + bind_native!("base64decode", 1, fn_base64_decode); + bind_native!("base64encode", 1, fn_base64_encode); bind_native!("boolean", 1, fn_boolean); bind_native!("ceil", 1, fn_ceil); bind_native!("contains", 2, fn_contains); @@ -145,16 +147,16 @@ impl<'a> JsonAta<'a> { bind_native!("not", 1, fn_not); bind_native!("number", 1, fn_number); bind_native!("power", 2, fn_power); + bind_native!("replace", 4, fn_replace); bind_native!("reverse", 1, fn_reverse); bind_native!("sort", 2, fn_sort); - bind_native!("string", 1, fn_string); + bind_native!("split", 3, fn_split); bind_native!("sqrt", 1, fn_sqrt); + bind_native!("string", 1, fn_string); bind_native!("substring", 3, fn_substring); bind_native!("sum", 1, fn_sum); bind_native!("trim", 1, fn_trim); bind_native!("uppercase", 1, fn_uppercase); - bind_native!("base64encode", 1, fn_base64_encode); - bind_native!("base64decode", 1, fn_base64_decode); let chain_ast = Some(parser::parse( "function($f, $g) { function($x){ $g($f($x)) } }", diff --git a/tests/testsuite/skip/function-replace/case000.json b/tests/testsuite/groups/function-replace/case000.json similarity index 100% rename from tests/testsuite/skip/function-replace/case000.json rename to tests/testsuite/groups/function-replace/case000.json diff --git a/tests/testsuite/skip/function-replace/case001.json b/tests/testsuite/groups/function-replace/case001.json similarity index 100% rename from tests/testsuite/skip/function-replace/case001.json rename to tests/testsuite/groups/function-replace/case001.json diff --git a/tests/testsuite/skip/function-replace/case002.json b/tests/testsuite/groups/function-replace/case002.json similarity index 100% rename from tests/testsuite/skip/function-replace/case002.json rename to tests/testsuite/groups/function-replace/case002.json diff --git a/tests/testsuite/skip/function-replace/case003.json b/tests/testsuite/groups/function-replace/case003.json similarity index 100% rename from tests/testsuite/skip/function-replace/case003.json rename to tests/testsuite/groups/function-replace/case003.json diff --git a/tests/testsuite/skip/function-replace/case004.json b/tests/testsuite/groups/function-replace/case004.json similarity index 100% rename from tests/testsuite/skip/function-replace/case004.json rename to tests/testsuite/groups/function-replace/case004.json diff --git a/tests/testsuite/skip/function-replace/case005.json b/tests/testsuite/groups/function-replace/case005.json similarity index 100% rename from tests/testsuite/skip/function-replace/case005.json rename to tests/testsuite/groups/function-replace/case005.json diff --git a/tests/testsuite/skip/function-replace/case006.json b/tests/testsuite/groups/function-replace/case006.json similarity index 100% rename from tests/testsuite/skip/function-replace/case006.json rename to tests/testsuite/groups/function-replace/case006.json diff --git a/tests/testsuite/skip/function-replace/case007.json b/tests/testsuite/groups/function-replace/case007.json similarity index 100% rename from tests/testsuite/skip/function-replace/case007.json rename to tests/testsuite/groups/function-replace/case007.json diff --git a/tests/testsuite/skip/function-replace/case008.json b/tests/testsuite/groups/function-replace/case008.json similarity index 100% rename from tests/testsuite/skip/function-replace/case008.json rename to tests/testsuite/groups/function-replace/case008.json diff --git a/tests/testsuite/skip/function-replace/case009.json b/tests/testsuite/groups/function-replace/case009.json similarity index 100% rename from tests/testsuite/skip/function-replace/case009.json rename to tests/testsuite/groups/function-replace/case009.json diff --git a/tests/testsuite/skip/function-replace/case010.json b/tests/testsuite/groups/function-replace/case010.json similarity index 100% rename from tests/testsuite/skip/function-replace/case010.json rename to tests/testsuite/groups/function-replace/case010.json diff --git a/tests/testsuite/skip/function-replace/case011.json b/tests/testsuite/groups/function-replace/case011.json similarity index 100% rename from tests/testsuite/skip/function-replace/case011.json rename to tests/testsuite/groups/function-replace/case011.json diff --git a/tests/testsuite/skip/function-split/case000.json b/tests/testsuite/groups/function-split/case000.json similarity index 100% rename from tests/testsuite/skip/function-split/case000.json rename to tests/testsuite/groups/function-split/case000.json diff --git a/tests/testsuite/skip/function-split/case001.json b/tests/testsuite/groups/function-split/case001.json similarity index 100% rename from tests/testsuite/skip/function-split/case001.json rename to tests/testsuite/groups/function-split/case001.json diff --git a/tests/testsuite/skip/function-split/case002.json b/tests/testsuite/groups/function-split/case002.json similarity index 100% rename from tests/testsuite/skip/function-split/case002.json rename to tests/testsuite/groups/function-split/case002.json diff --git a/tests/testsuite/skip/function-split/case003.json b/tests/testsuite/groups/function-split/case003.json similarity index 100% rename from tests/testsuite/skip/function-split/case003.json rename to tests/testsuite/groups/function-split/case003.json diff --git a/tests/testsuite/skip/function-split/case004.json b/tests/testsuite/groups/function-split/case004.json similarity index 100% rename from tests/testsuite/skip/function-split/case004.json rename to tests/testsuite/groups/function-split/case004.json diff --git a/tests/testsuite/skip/function-split/case005.json b/tests/testsuite/groups/function-split/case005.json similarity index 100% rename from tests/testsuite/skip/function-split/case005.json rename to tests/testsuite/groups/function-split/case005.json diff --git a/tests/testsuite/skip/function-split/case006.json b/tests/testsuite/groups/function-split/case006.json similarity index 100% rename from tests/testsuite/skip/function-split/case006.json rename to tests/testsuite/groups/function-split/case006.json diff --git a/tests/testsuite/skip/function-split/case007.json b/tests/testsuite/groups/function-split/case007.json similarity index 100% rename from tests/testsuite/skip/function-split/case007.json rename to tests/testsuite/groups/function-split/case007.json diff --git a/tests/testsuite/skip/function-split/case008.json b/tests/testsuite/groups/function-split/case008.json similarity index 100% rename from tests/testsuite/skip/function-split/case008.json rename to tests/testsuite/groups/function-split/case008.json diff --git a/tests/testsuite/skip/function-split/case009.json b/tests/testsuite/groups/function-split/case009.json similarity index 100% rename from tests/testsuite/skip/function-split/case009.json rename to tests/testsuite/groups/function-split/case009.json diff --git a/tests/testsuite/skip/function-split/case010.json b/tests/testsuite/groups/function-split/case010.json similarity index 100% rename from tests/testsuite/skip/function-split/case010.json rename to tests/testsuite/groups/function-split/case010.json diff --git a/tests/testsuite/skip/function-split/case011.json b/tests/testsuite/groups/function-split/case011.json similarity index 100% rename from tests/testsuite/skip/function-split/case011.json rename to tests/testsuite/groups/function-split/case011.json diff --git a/tests/testsuite/skip/function-split/case012.json b/tests/testsuite/groups/function-split/case012.json similarity index 100% rename from tests/testsuite/skip/function-split/case012.json rename to tests/testsuite/groups/function-split/case012.json diff --git a/tests/testsuite/skip/function-split/case013.json b/tests/testsuite/groups/function-split/case013.json similarity index 100% rename from tests/testsuite/skip/function-split/case013.json rename to tests/testsuite/groups/function-split/case013.json diff --git a/tests/testsuite/skip/function-split/case014.json b/tests/testsuite/groups/function-split/case014.json similarity index 100% rename from tests/testsuite/skip/function-split/case014.json rename to tests/testsuite/groups/function-split/case014.json diff --git a/tests/testsuite/skip/function-split/case015.json b/tests/testsuite/groups/function-split/case015.json similarity index 100% rename from tests/testsuite/skip/function-split/case015.json rename to tests/testsuite/groups/function-split/case015.json diff --git a/tests/testsuite/skip/function-split/case016.json b/tests/testsuite/groups/function-split/case016.json similarity index 100% rename from tests/testsuite/skip/function-split/case016.json rename to tests/testsuite/groups/function-split/case016.json diff --git a/tests/testsuite/skip/function-split/case017.json b/tests/testsuite/groups/function-split/case017.json similarity index 100% rename from tests/testsuite/skip/function-split/case017.json rename to tests/testsuite/groups/function-split/case017.json diff --git a/tests/testsuite/skip/function-split/case018.json b/tests/testsuite/groups/function-split/case018.json similarity index 100% rename from tests/testsuite/skip/function-split/case018.json rename to tests/testsuite/groups/function-split/case018.json