use crate::{Environment, Value, bail};
fn one_array(name: &str, mut args: Vec<Value>) -> crate::Result<Vec<Value>> {
if args.len() != 1 {
bail!("{name}() takes exactly one argument");
}
match args.pop().expect("length checked") {
Value::Array(values) => Ok(values),
_ => bail!("{name}() takes an array as the argument"),
}
}
fn flatten(values: Vec<Value>) -> Vec<Value> {
let mut stack = values.into_iter().rev().collect::<Vec<_>>();
let mut output = Vec::new();
while let Some(value) = stack.pop() {
match value {
Value::Array(values) => stack.extend(values.into_iter().rev()),
value => output.push(value),
}
}
output
}
pub fn add_collection_functions(env: &mut Environment) {
env.add_function("join", |call| {
if call.args.is_empty() || call.args.len() > 2 {
bail!("join() takes one or two arguments");
}
let separator = match call.args.get(1) {
Some(Value::String(separator)) => separator.as_str(),
Some(_) => bail!("join() separator must be a string"),
None => "",
};
let Value::Array(values) = &call.args[0] else {
bail!("join() takes an array as the first argument");
};
let values = values
.iter()
.map(|value| match value {
Value::String(value) => Ok(value.as_str()),
_ => bail!("join() array elements must be strings"),
})
.collect::<crate::Result<Vec<_>>>()?;
Ok(Value::from(values.join(separator)))
});
env.add_function("first", |call| {
Ok(one_array("first", call.args)?
.into_iter()
.next()
.unwrap_or_default())
});
env.add_function("last", |call| {
Ok(one_array("last", call.args)?
.into_iter()
.next_back()
.unwrap_or_default())
});
env.add_function("take", |call| {
if call.args.len() != 2 {
bail!("take() takes exactly two arguments");
}
let Value::Array(values) = &call.args[0] else {
bail!("take() takes an array as the first argument");
};
let Value::Integer(count) = call.args[1] else {
bail!("take() takes an integer as the second argument");
};
let count = usize::try_from(count).map_err(|_| {
crate::Error::ExprError("take() count cannot be negative".to_string())
})?;
Ok(Value::Array(
values.iter().take(count).cloned().collect(),
))
});
env.add_function("reverse", |call| {
let mut values = one_array("reverse", call.args)?;
values.reverse();
Ok(Value::Array(values))
});
env.add_function("uniq", |call| {
let mut output = Vec::new();
for value in one_array("uniq", call.args)? {
if !output
.iter()
.any(|candidate| crate::ast::operator::values_equal(candidate, &value))
{
output.push(value);
}
}
Ok(Value::Array(output))
});
env.add_function("concat", |call| {
if call.args.is_empty() {
bail!("concat() takes at least one argument");
}
let mut output = Vec::new();
for value in call.args {
match value {
Value::Array(values) => output.extend(values),
_ => bail!("concat() arguments must be arrays"),
}
}
Ok(Value::Array(output))
});
env.add_function("flatten", |call| {
let values = one_array("flatten", call.args)?;
Ok(Value::Array(flatten(values)))
});
}
#[cfg(test)]
mod tests {
use super::flatten;
use crate::Value;
#[test]
fn flatten_handles_deeply_nested_arrays_iteratively() {
let mut value = Value::Integer(1);
for _ in 0..20_000 {
value = Value::Array(vec![value]);
}
assert_eq!(flatten(vec![value]), vec![Value::Integer(1)]);
}
}