use crate::data::context::EvalError;
use crate::data::value::Val;
use crate::builtins::BuiltinMethod;
use std::sync::Arc;
pub fn slice_apply(recv: Val, start: i64, end: Option<i64>) -> Val {
let (parent, base_off, view_len): (Arc<str>, usize, usize) = match recv {
Val::Str(s) => {
let l = s.len();
(s, 0, l)
}
Val::StrSlice(r) => {
let parent = r.to_arc();
let plen = parent.len();
(parent, 0, plen)
}
other => return other,
};
let view = &parent[base_off..base_off + view_len];
let blen = view.len();
if view.is_ascii() {
let start_u = if start < 0 {
blen.saturating_sub((-start) as usize)
} else {
(start as usize).min(blen)
};
let end_u = match end {
Some(e) if e < 0 => blen.saturating_sub((-e) as usize),
Some(e) => (e as usize).min(blen),
None => blen,
};
let start_u = start_u.min(end_u);
if start_u == 0 && end_u == blen {
return Val::Str(parent);
}
return Val::StrSlice(crate::data::tape::StrRef::slice(
parent,
base_off + start_u,
base_off + end_u,
));
}
let chars: Vec<(usize, char)> = view.char_indices().collect();
let n = chars.len() as i64;
let resolve = |i: i64| -> usize {
let r = if i < 0 { n + i } else { i };
r.clamp(0, n) as usize
};
let s_idx = resolve(start);
let e_idx = match end {
Some(e) => resolve(e),
None => n as usize,
};
let s_idx = s_idx.min(e_idx);
let s_b = chars.get(s_idx).map(|c| c.0).unwrap_or(view.len());
let e_b = chars.get(e_idx).map(|c| c.0).unwrap_or(view.len());
if s_b == 0 && e_b == view.len() {
return Val::Str(parent);
}
Val::StrSlice(crate::data::tape::StrRef::slice(
parent,
base_off + s_b,
base_off + e_b,
))
}
#[inline]
pub fn split_apply(recv: &Val, sep: &str) -> Option<Val> {
let s: &str = match recv {
Val::Str(s) => s.as_ref(),
Val::StrSlice(r) => r.as_str(),
_ => return None,
};
Some(Val::arr(
s.split(sep)
.map(|p| Val::Str(Arc::<str>::from(p)))
.collect(),
))
}
#[inline]
pub fn chunk_apply(items: &[Val], n: usize) -> Vec<Val> {
let n = n.max(1);
items.chunks(n).map(|c| Val::arr(c.to_vec())).collect()
}
#[inline]
pub fn window_apply(items: &[Val], n: usize) -> Vec<Val> {
let n = n.max(1);
items.windows(n).map(|w| Val::arr(w.to_vec())).collect()
}
#[inline]
pub fn replace_apply(recv: Val, needle: &str, replacement: &str, all: bool) -> Option<Val> {
let s: Arc<str> = match recv {
Val::Str(s) => s,
Val::StrSlice(r) => r.to_arc(),
_ => return None,
};
if !s.contains(needle) {
return Some(Val::Str(s));
}
let out = if all {
s.replace(needle, replacement)
} else {
s.replacen(needle, replacement, 1)
};
Some(Val::Str(Arc::<str>::from(out)))
}
#[inline]
fn map_str_owned(recv: &Val, f: impl FnOnce(&str) -> String) -> Option<Val> {
let s = recv.as_str_ref()?;
Some(Val::Str(Arc::<str>::from(f(s).as_str())))
}
#[inline]
pub fn upper_apply(recv: &Val) -> Option<Val> {
map_str_owned(recv, |s| {
if s.is_ascii() {
let mut buf = s.to_owned();
buf.make_ascii_uppercase();
buf
} else {
s.to_uppercase()
}
})
}
#[inline]
pub fn lower_apply(recv: &Val) -> Option<Val> {
map_str_owned(recv, |s| {
if s.is_ascii() {
let mut buf = s.to_owned();
buf.make_ascii_lowercase();
buf
} else {
s.to_lowercase()
}
})
}
#[inline]
pub fn trim_apply(recv: &Val) -> Option<Val> {
map_str_owned(recv, |s| s.trim().to_owned())
}
#[inline]
pub fn trim_left_apply(recv: &Val) -> Option<Val> {
map_str_owned(recv, |s| s.trim_start().to_owned())
}
#[inline]
pub fn trim_right_apply(recv: &Val) -> Option<Val> {
map_str_owned(recv, |s| s.trim_end().to_owned())
}
#[inline]
pub fn capitalize_apply(recv: &Val) -> Option<Val> {
map_str_owned(recv, |s| {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars();
if let Some(first) = chars.next() {
for c in first.to_uppercase() {
out.push(c);
}
out.push_str(&chars.as_str().to_lowercase());
}
out
})
}
#[inline]
pub fn title_case_apply(recv: &Val) -> Option<Val> {
map_str_owned(recv, |s| {
let mut out = String::with_capacity(s.len());
let mut at_start = true;
for c in s.chars() {
if c.is_whitespace() {
out.push(c);
at_start = true;
} else if at_start {
for u in c.to_uppercase() {
out.push(u);
}
at_start = false;
} else {
for l in c.to_lowercase() {
out.push(l);
}
}
}
out
})
}
#[inline]
pub fn html_escape_apply(recv: &Val) -> Option<Val> {
map_str_owned(recv, |s| {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'&' => out.push_str("&"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(c),
}
}
out
})
}
#[inline]
pub fn html_unescape_apply(recv: &Val) -> Option<Val> {
map_str_owned(recv, |s| {
s.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace(""", "\"")
.replace("'", "'")
})
}
#[inline]
pub fn url_encode_apply(recv: &Val) -> Option<Val> {
map_str_owned(recv, |s| {
let mut out = String::with_capacity(s.len());
for b in s.as_bytes() {
let b = *b;
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(b as char)
}
_ => {
use std::fmt::Write;
let _ = write!(out, "%{:02X}", b);
}
}
}
out
})
}
#[inline]
pub fn url_decode_apply(recv: &Val) -> Option<Val> {
map_str_owned(recv, |s| {
let bytes = s.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
let h1 = char::from(bytes[i + 1]).to_digit(16);
let h2 = char::from(bytes[i + 2]).to_digit(16);
if let (Some(h1), Some(h2)) = (h1, h2) {
out.push((h1 * 16 + h2) as u8);
i += 3;
continue;
}
} else if bytes[i] == b'+' {
out.push(b' ');
i += 1;
continue;
}
out.push(bytes[i]);
i += 1;
}
String::from_utf8_lossy(&out).into_owned()
})
}
#[inline]
pub fn to_base64_apply(recv: &Val) -> Option<Val> {
map_str_owned(recv, |s| {
crate::builtins::helpers::base64_encode(s.as_bytes())
})
}
#[inline]
pub fn dedent_apply(recv: &Val) -> Option<Val> {
map_str_owned(recv, |s| {
let min_indent = s
.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| l.len() - l.trim_start().len())
.min()
.unwrap_or(0);
s.lines()
.map(|l| {
if l.len() >= min_indent {
&l[min_indent..]
} else {
l
}
})
.collect::<Vec<_>>()
.join("\n")
})
}
#[inline]
pub fn snake_case_apply(recv: &Val) -> Option<Val> {
map_str_owned(recv, |s| {
crate::builtins::helpers::split_words_lower(s).join("_")
})
}
#[inline]
pub fn kebab_case_apply(recv: &Val) -> Option<Val> {
map_str_owned(recv, |s| {
crate::builtins::helpers::split_words_lower(s).join("-")
})
}
#[inline]
pub fn camel_case_apply(recv: &Val) -> Option<Val> {
map_str_owned(recv, |s| {
let parts = crate::builtins::helpers::split_words_lower(s);
let mut out = String::with_capacity(s.len());
for (i, p) in parts.iter().enumerate() {
if i == 0 {
out.push_str(p);
} else {
crate::builtins::helpers::upper_first_into(p, &mut out);
}
}
out
})
}
#[inline]
pub fn pascal_case_apply(recv: &Val) -> Option<Val> {
map_str_owned(recv, |s| {
let parts = crate::builtins::helpers::split_words_lower(s);
let mut out = String::with_capacity(s.len());
for p in parts.iter() {
crate::builtins::helpers::upper_first_into(p, &mut out);
}
out
})
}
#[inline]
pub fn reverse_str_apply(recv: &Val) -> Option<Val> {
map_str_owned(recv, |s| s.chars().rev().collect::<String>())
}
#[inline]
pub fn map_str_val(recv: &Val, f: impl FnOnce(&str) -> Val) -> Option<Val> {
Some(f(recv.as_str_ref()?))
}
#[inline]
pub fn lines_apply(recv: &Val) -> Option<Val> {
map_str_val(recv, |s| {
Val::arr(s.lines().map(|l| Val::Str(Arc::from(l))).collect())
})
}
#[inline]
pub fn words_apply(recv: &Val) -> Option<Val> {
map_str_val(recv, |s| {
Val::arr(
s.split_whitespace()
.map(|w| Val::Str(Arc::from(w)))
.collect(),
)
})
}
#[inline]
pub fn chars_apply(recv: &Val) -> Option<Val> {
map_str_val(recv, |s| {
Val::arr(
s.chars()
.map(|c| Val::Str(Arc::from(c.to_string())))
.collect(),
)
})
}
#[inline]
pub fn chars_of_apply(recv: &Val) -> Option<Val> {
map_str_val(recv, |s| {
let mut out: Vec<Val> = Vec::new();
let mut tmp = [0u8; 4];
for c in s.chars() {
let utf8 = c.encode_utf8(&mut tmp);
out.push(Val::Str(Arc::from(utf8.as_ref())));
}
Val::arr(out)
})
}
#[inline]
pub fn bytes_of_apply(recv: &Val) -> Option<Val> {
map_str_val(recv, |s| {
let v: Vec<i64> = s.as_bytes().iter().map(|&b| b as i64).collect();
Val::int_vec(v)
})
}
#[inline]
pub fn ceil_apply(recv: &Val) -> Option<Val> {
match recv {
Val::Int(n) => Some(Val::Int(*n)),
Val::Float(f) => Some(Val::Int(f.ceil() as i64)),
_ => None,
}
}
#[inline]
pub fn try_ceil_apply(recv: &Val) -> Result<Option<Val>, EvalError> {
ceil_apply(recv)
.map(Some)
.ok_or_else(|| EvalError("ceil: expected number".into()))
}
#[inline]
pub fn floor_apply(recv: &Val) -> Option<Val> {
match recv {
Val::Int(n) => Some(Val::Int(*n)),
Val::Float(f) => Some(Val::Int(f.floor() as i64)),
_ => None,
}
}
#[inline]
pub fn try_floor_apply(recv: &Val) -> Result<Option<Val>, EvalError> {
floor_apply(recv)
.map(Some)
.ok_or_else(|| EvalError("floor: expected number".into()))
}
#[inline]
pub fn round_apply(recv: &Val) -> Option<Val> {
match recv {
Val::Int(n) => Some(Val::Int(*n)),
Val::Float(f) => Some(Val::Int(f.round() as i64)),
_ => None,
}
}
#[inline]
pub fn try_round_apply(recv: &Val) -> Result<Option<Val>, EvalError> {
round_apply(recv)
.map(Some)
.ok_or_else(|| EvalError("round: expected number".into()))
}
#[inline]
pub fn abs_apply(recv: &Val) -> Option<Val> {
match recv {
Val::Int(n) => Some(Val::Int(n.wrapping_abs())),
Val::Float(f) => Some(Val::Float(f.abs())),
_ => None,
}
}
#[inline]
pub fn try_abs_apply(recv: &Val) -> Result<Option<Val>, EvalError> {
abs_apply(recv)
.map(Some)
.ok_or_else(|| EvalError("abs: expected number".into()))
}
#[inline]
pub fn parse_int_apply(recv: &Val) -> Option<Val> {
map_str_val(recv, |s| {
s.trim().parse::<i64>().map(Val::Int).unwrap_or(Val::Null)
})
}
#[inline]
pub fn parse_float_apply(recv: &Val) -> Option<Val> {
map_str_val(recv, |s| {
s.trim().parse::<f64>().map(Val::Float).unwrap_or(Val::Null)
})
}
#[inline]
pub fn parse_bool_apply(recv: &Val) -> Option<Val> {
map_str_val(recv, |s| match s.trim().to_ascii_lowercase().as_str() {
"true" | "yes" | "1" | "on" => Val::Bool(true),
"false" | "no" | "0" | "off" => Val::Bool(false),
_ => Val::Null,
})
}
#[inline]
pub fn from_base64_apply(recv: &Val) -> Option<Val> {
map_str_val(recv, |s| match crate::builtins::helpers::base64_decode(s) {
Ok(bytes) => Val::Str(Arc::from(String::from_utf8_lossy(&bytes).as_ref())),
Err(_) => Val::Null,
})
}
#[inline]
pub fn repeat_apply(recv: &Val, n: usize) -> Option<Val> {
Some(Val::Str(Arc::from(recv.as_str_ref()?.repeat(n))))
}
#[inline]
pub fn strip_prefix_apply(recv: &Val, prefix: &str) -> Option<Val> {
let s = recv.as_str_ref()?;
Some(match s.strip_prefix(prefix) {
Some(stripped) => Val::Str(Arc::<str>::from(stripped)),
None => recv.clone(),
})
}
#[inline]
pub fn strip_suffix_apply(recv: &Val, suffix: &str) -> Option<Val> {
let s = recv.as_str_ref()?;
Some(match s.strip_suffix(suffix) {
Some(stripped) => Val::Str(Arc::<str>::from(stripped)),
None => recv.clone(),
})
}
#[inline]
pub fn pad_left_apply(recv: &Val, width: usize, fill: char) -> Option<Val> {
let s = recv.as_str_ref()?;
let n = s.chars().count();
if n >= width {
return Some(recv.clone());
}
let pad: String = std::iter::repeat(fill).take(width - n).collect();
Some(Val::Str(Arc::from(pad + s)))
}
#[inline]
pub fn pad_right_apply(recv: &Val, width: usize, fill: char) -> Option<Val> {
let s = recv.as_str_ref()?;
let n = s.chars().count();
if n >= width {
return Some(recv.clone());
}
let pad: String = std::iter::repeat(fill).take(width - n).collect();
Some(Val::Str(Arc::from(s.to_string() + &pad)))
}
#[inline]
pub fn center_apply(recv: &Val, width: usize, fill: char) -> Option<Val> {
let s = recv.as_str_ref()?;
let cur = s.chars().count();
if cur >= width {
return Some(recv.clone());
}
let total = width - cur;
let left = total / 2;
let right = total - left;
let mut out = String::with_capacity(s.len() + total);
for _ in 0..left {
out.push(fill);
}
out.push_str(s);
for _ in 0..right {
out.push(fill);
}
Some(Val::Str(Arc::from(out)))
}
#[inline]
pub fn indent_apply(recv: &Val, n: usize) -> Option<Val> {
let s = recv.as_str_ref()?;
let prefix: String = std::iter::repeat(' ').take(n).collect();
let out = s
.lines()
.map(|l| format!("{}{}", prefix, l))
.collect::<Vec<_>>()
.join("\n");
Some(Val::Str(Arc::from(out)))
}
#[inline]
pub fn scan_apply(recv: &Val, pat: &str) -> Option<Val> {
let s = recv.as_str_ref()?;
let mut out: Vec<Val> = Vec::new();
if !pat.is_empty() {
let mut start = 0usize;
while let Some(pos) = s[start..].find(pat) {
out.push(Val::Str(Arc::from(pat)));
start += pos + pat.len();
}
}
Some(Val::arr(out))
}
#[inline]
pub fn numeric_aggregate_apply(recv: &Val, method: BuiltinMethod) -> Val {
match recv {
Val::IntVec(a) => return numeric_aggregate_i64(a, method),
Val::FloatVec(a) => return numeric_aggregate_f64(a, method),
Val::Arr(a) => numeric_aggregate_values(a, method),
_ => Val::Null,
}
}
#[inline]
pub fn numeric_aggregate_projected_apply<F>(
recv: &Val,
method: BuiltinMethod,
mut eval: F,
) -> Result<Val, EvalError>
where
F: FnMut(&Val) -> Result<Val, EvalError>,
{
let items = recv
.as_vals()
.ok_or_else(|| EvalError("expected array for numeric aggregate".into()))?;
let mut vals = Vec::with_capacity(items.len());
for item in items.iter() {
let v = eval(item)?;
if v.is_number() {
vals.push(v);
}
}
Ok(numeric_aggregate_values(&vals, method))
}
#[inline]
fn numeric_aggregate_i64(a: &[i64], method: BuiltinMethod) -> Val {
match method {
BuiltinMethod::Sum => Val::Int(a.iter().fold(0i64, |acc, n| acc.wrapping_add(*n))),
BuiltinMethod::Avg => {
if a.is_empty() {
Val::Null
} else {
let s = a.iter().fold(0i64, |acc, n| acc.wrapping_add(*n));
Val::Float(s as f64 / a.len() as f64)
}
}
BuiltinMethod::Min => a.iter().min().copied().map(Val::Int).unwrap_or(Val::Null),
BuiltinMethod::Max => a.iter().max().copied().map(Val::Int).unwrap_or(Val::Null),
_ => Val::Null,
}
}
#[inline]
fn numeric_aggregate_f64(a: &[f64], method: BuiltinMethod) -> Val {
match method {
BuiltinMethod::Sum => Val::Float(a.iter().sum()),
BuiltinMethod::Avg => {
if a.is_empty() {
Val::Null
} else {
Val::Float(a.iter().sum::<f64>() / a.len() as f64)
}
}
BuiltinMethod::Min => a
.iter()
.copied()
.reduce(f64::min)
.map(Val::Float)
.unwrap_or(Val::Null),
BuiltinMethod::Max => a
.iter()
.copied()
.reduce(f64::max)
.map(Val::Float)
.unwrap_or(Val::Null),
_ => Val::Null,
}
}
#[inline]
fn numeric_aggregate_values(a: &[Val], method: BuiltinMethod) -> Val {
match method {
BuiltinMethod::Sum => {
let mut i_acc: i64 = 0;
let mut f_acc: f64 = 0.0;
let mut floated = false;
for v in a {
match v {
Val::Int(n) if !floated => i_acc = i_acc.wrapping_add(*n),
Val::Int(n) => f_acc += *n as f64,
Val::Float(f) if !floated => {
f_acc = i_acc as f64 + *f;
floated = true;
}
Val::Float(f) => f_acc += *f,
_ => {}
}
}
if floated {
Val::Float(f_acc)
} else {
Val::Int(i_acc)
}
}
BuiltinMethod::Avg => {
let mut sum = 0.0;
let mut n = 0usize;
for v in a {
match v {
Val::Int(i) => {
sum += *i as f64;
n += 1;
}
Val::Float(f) => {
sum += *f;
n += 1;
}
_ => {}
}
}
if n == 0 {
Val::Null
} else {
Val::Float(sum / n as f64)
}
}
BuiltinMethod::Min | BuiltinMethod::Max => {
let want_max = method == BuiltinMethod::Max;
let mut best: Option<Val> = None;
let mut best_f = 0.0;
for v in a {
if !v.is_number() {
continue;
}
let vf = v.as_f64().unwrap_or(0.0);
let replace = match best {
None => true,
Some(_) if want_max => vf > best_f,
Some(_) => vf < best_f,
};
if replace {
best_f = vf;
best = Some(v.clone());
}
}
best.unwrap_or(Val::Null)
}
_ => Val::Null,
}
}