gridcraft/crates/functions/src/text.rs

1182 lines
45 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Text functions.
use gridcraft_core::parse::parse_number_text;
use gridcraft_core::{CellError, Value, number_to_text};
use crate::criteria::wildcard_find;
use crate::util::{
A, MAX_CELLS, MAX_TEXT, R, S, arg, array_val, as_array, boolean, has, num, num_val, opt_bool, opt_int, opt_num, round_half_away, scalar, text,
text_val,
};
use crate::{Arg, Ctx, FnSpec, VAR};
const TJ: &[bool] = &[false, true, false];
const TB: &[bool] = &[true, false, true, true, true, false];
const TS: &[bool] = &[true, false, false, true, true, false];
const ATT: &[bool] = &[false, true];
fn chars(s: &str) -> Vec<char> {
s.chars().collect()
}
fn check_len(s: String) -> R<Value> {
if s.len() > MAX_TEXT && s.chars().count() > MAX_TEXT {
return Err(CellError::Value);
}
text_val(s)
}
/// Pushes `piece` onto `out`, failing once the result exceeds the cell limit.
fn push_capped(out: &mut String, piece: &str) -> R<()> {
out.push_str(piece);
if out.len() > MAX_TEXT * 4 {
return Err(CellError::Value);
}
Ok(())
}
fn concatenate(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let mut out = String::new();
for x in a {
push_capped(&mut out, &scalar(x).to_text()?)?;
}
check_len(out)
}
fn concat(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let mut out = String::new();
for x in a {
for v in as_array(&x.value).iter() {
push_capped(&mut out, &v.to_text()?)?;
}
}
check_len(out)
}
fn textjoin(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let delims: Vec<String> = as_array(&arg(a, 0)?.value).iter().map(|v| v.to_text()).collect::<R<_>>()?;
let ignore = boolean(a, 1)?;
let mut items = Vec::new();
for x in a.iter().skip(2) {
for v in as_array(&x.value).iter() {
let s = v.to_text()?;
if ignore && s.is_empty() {
continue;
}
items.push(s);
}
}
let mut out = String::new();
for (i, s) in items.iter().enumerate() {
if i > 0 && !delims.is_empty() {
push_capped(&mut out, &delims[(i - 1) % delims.len()])?;
}
push_capped(&mut out, s)?;
}
check_len(out)
}
fn count_arg(a: &[Arg], i: usize, default: f64) -> R<usize> {
let n = opt_num(a, i, default)?;
if n < 0.0 || n.is_nan() {
return Err(CellError::Value);
}
Ok(if n > 1e9 { 1_000_000_000 } else { n.trunc() as usize })
}
fn left(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let s = text(a, 0)?;
let n = count_arg(a, 1, 1.0)?;
text_val(s.chars().take(n).collect::<String>())
}
fn right(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let s = chars(&text(a, 0)?);
let n = count_arg(a, 1, 1.0)?.min(s.len());
text_val(s[s.len() - n..].iter().collect::<String>())
}
fn mid(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let s = text(a, 0)?;
let start = num(a, 1)?;
if start.is_nan() || start < 1.0 {
return Err(CellError::Value);
}
let n = count_arg(a, 2, 0.0)?;
let start = if start > 1e9 { 1_000_000_000 } else { start as usize };
text_val(s.chars().skip(start - 1).take(n).collect::<String>())
}
fn len(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
num_val(text(a, 0)?.chars().count() as f64)
}
fn lower(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
text_val(text(a, 0)?.to_lowercase())
}
fn upper(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
text_val(text(a, 0)?.to_uppercase())
}
fn proper(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let s = text(a, 0)?;
let mut out = String::with_capacity(s.len());
let mut prev_letter = false;
for ch in s.chars() {
if ch.is_alphabetic() {
if prev_letter {
out.extend(ch.to_lowercase());
} else {
out.extend(ch.to_uppercase());
}
prev_letter = true;
} else {
out.push(ch);
prev_letter = false;
}
}
text_val(out)
}
fn trim(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let s = text(a, 0)?;
text_val(s.split(' ').filter(|w| !w.is_empty()).collect::<Vec<_>>().join(" "))
}
fn clean(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
text_val(text(a, 0)?.chars().filter(|c| (*c as u32) >= 32).collect::<String>())
}
fn substitute(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let s = text(a, 0)?;
let old = text(a, 1)?;
let new = text(a, 2)?;
if old.is_empty() {
return text_val(s);
}
if has(a, 3) {
let inst = num(a, 3)?;
if inst.is_nan() || inst < 1.0 {
return Err(CellError::Value);
}
let inst = inst.trunc() as usize;
match s.match_indices(old.as_str()).nth(inst - 1) {
Some((pos, _)) => {
let mut out = String::with_capacity(s.len() + new.len());
out.push_str(&s[..pos]);
out.push_str(&new);
out.push_str(&s[pos + old.len()..]);
check_len(out)
}
None => text_val(s),
}
} else {
let count = s.matches(old.as_str()).count();
if new.len() > old.len() && s.len() + count.saturating_mul(new.len() - old.len()) > MAX_TEXT * 4 {
return Err(CellError::Value);
}
check_len(s.replace(old.as_str(), &new))
}
}
fn replace(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let s = chars(&text(a, 0)?);
let start = num(a, 1)?;
if start.is_nan() || start < 1.0 {
return Err(CellError::Value);
}
let n = count_arg(a, 2, 0.0)?;
let new = text(a, 3)?;
let start = (if start > 1e9 { 1_000_000_000 } else { start as usize } - 1).min(s.len());
let end = start.saturating_add(n).min(s.len());
let mut out: String = s[..start].iter().collect();
out.push_str(&new);
out.extend(s[end..].iter());
check_len(out)
}
fn rept(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let s = text(a, 0)?;
let n = num(a, 1)?;
if n.is_nan() || n < 0.0 {
return Err(CellError::Value);
}
let n = n.trunc();
let total = s.chars().count() as f64 * n;
if total > MAX_TEXT as f64 {
return Err(CellError::Value);
}
text_val(s.repeat(n as usize))
}
fn find_impl(a: &[Arg], wild: bool) -> R<Value> {
let needle = text(a, 0)?;
let hay = text(a, 1)?;
let start = opt_num(a, 2, 1.0)?;
let hc = chars(&hay);
if (start.is_nan() || start < 1.0) || start > hc.len().max(1) as f64 {
return Err(CellError::Value);
}
let start = start as usize - 1;
if needle.is_empty() {
return num_val((start + 1) as f64);
}
let pos = if wild {
wildcard_find(&needle, &hay, start)
} else {
let nc = chars(&needle);
(start..hc.len()).find(|&i| hc.get(i..i + nc.len()).is_some_and(|w| w == nc.as_slice()))
};
match pos {
Some(p) => num_val((p + 1) as f64),
None => Err(CellError::Value),
}
}
fn find(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
find_impl(a, false)
}
fn search(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
find_impl(a, true)
}
fn exact(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
Ok(Value::Bool(text(a, 0)? == text(a, 1)?))
}
/// Windows-1252 code points 128–159.
const CP1252: [u32; 32] = [
0x20AC, 0x81, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x8D, 0x017D, 0x8F, 0x90, 0x2018, 0x2019,
0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x9D, 0x017E, 0x0178,
];
fn char_fn(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let n = num(a, 0)?.trunc();
if !(1.0..=255.0).contains(&n) {
return Err(CellError::Value);
}
let n = n as u32;
let cp = if (128..160).contains(&n) { CP1252[(n - 128) as usize] } else { n };
text_val(char::from_u32(cp).map(String::from).unwrap_or_default())
}
fn code(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let s = text(a, 0)?;
let ch = s.chars().next().ok_or(CellError::Value)?;
let cp = ch as u32;
let code = if cp < 128 || (160..256).contains(&cp) {
cp
} else if let Some(i) = CP1252.iter().position(|&x| x == cp) {
128 + i as u32
} else {
63
};
num_val(code as f64)
}
fn unichar(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let n = num(a, 0)?.trunc();
if !(1.0..=1_114_111.0).contains(&n) {
return Err(CellError::Value);
}
match char::from_u32(n as u32) {
Some(c) => text_val(c.to_string()),
None => Err(CellError::NA),
}
}
fn unicode(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let s = text(a, 0)?;
let ch = s.chars().next().ok_or(CellError::Value)?;
num_val(ch as u32 as f64)
}
/// VALUE: a blank cell reads as 0, but empty or blank text is not a number (observed:
/// `VALUE("")` is `#VALUE!`).
fn value(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
match scalar(arg(a, 0)?) {
Value::Number(n) => num_val(n),
Value::Empty => num_val(0.0),
Value::Error(e) => Err(e),
Value::Text(t) => parse_number_text(&t).ok_or(CellError::Value).and_then(num_val),
_ => Err(CellError::Value),
}
}
fn numbervalue(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let s = text(a, 0)?;
let dec = if a.len() > 1 { text(a, 1)?.chars().next().ok_or(CellError::Value)? } else { '.' };
let grp = if a.len() > 2 { text(a, 2)?.chars().next().ok_or(CellError::Value)? } else { ',' };
if dec == grp {
return Err(CellError::Value);
}
let mut body: String = s.chars().filter(|c| !c.is_whitespace()).collect();
if body.is_empty() {
return num_val(0.0);
}
let mut pct = 0;
while let Some(b) = body.strip_suffix('%') {
body = b.to_string();
pct += 1;
}
let mut seen_dec = false;
let mut out = String::new();
for c in body.chars() {
if c == dec {
if seen_dec {
return Err(CellError::Value);
}
seen_dec = true;
out.push('.');
} else if c == grp {
if seen_dec {
return Err(CellError::Value);
}
} else {
out.push(c);
}
}
let mut n = parse_number_text(&out).ok_or(CellError::Value)?;
for _ in 0..pct {
n /= 100.0;
}
num_val(n)
}
/// Formats `x` (already rounded) with `decimals` places and optional thousands separators.
fn format_fixed(x: f64, decimals: i64, commas: bool) -> String {
let d = decimals.max(0) as usize;
let s = format!("{:.*}", d, x.abs());
let (int_part, frac) = match s.split_once('.') {
Some((i, f)) => (i.to_string(), Some(f.to_string())),
None => (s.clone(), None),
};
let int_part = if commas {
let b = int_part.as_bytes();
let mut o = String::new();
for (i, ch) in b.iter().enumerate() {
if i > 0 && (b.len() - i) % 3 == 0 {
o.push(',');
}
o.push(char::from(*ch));
}
o
} else {
int_part
};
let mut out = int_part;
if let Some(f) = frac {
out.push('.');
out.push_str(&f);
}
out
}
fn rounded(a: &[Arg], default_dec: f64) -> R<(f64, i64)> {
let x = num(a, 0)?;
let d = opt_num(a, 1, default_dec)?.trunc();
if d > 127.0 {
return Err(CellError::Value);
}
let d = d.max(-308.0) as i64;
let r = round_half_away(x, d as i32);
if !r.is_finite() || r.abs() >= 1e300 {
return Err(CellError::Num);
}
Ok((r, d))
}
fn fixed(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let (r, d) = rounded(a, 2.0)?;
let no_commas = opt_bool(a, 2, false)?;
let body = format_fixed(r, d, !no_commas);
text_val(if r < 0.0 { format!("-{body}") } else { body })
}
fn dollar(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let (r, d) = rounded(a, 2.0)?;
let body = format_fixed(r, d, true);
text_val(if r < 0.0 { format!("(${body})") } else { format!("${body}") })
}
// ---------------------------------------------------------------------------------------------
// BAHTTEXT
const THAI_DIGITS: [&str; 10] = ["ศูนย์", "หนึ่ง", "สอง", "สาม", "สี่", "ห้า", "หก", "เจ็ด", "แปด", "เก้า"];
/// Units, tens, hundreds, thousands, ten thousands, hundred thousands.
const THAI_PLACES: [&str; 6] = ["", "สิบ", "ร้อย", "พัน", "หมื่น", "แสน"];
/// Thai words for a group of up to six decimal digits (most significant first); empty for zero.
/// One in the tens is สิบ, two is ยี่สิบ, and a final one after higher digits is เอ็ด.
fn thai_group(digits: &[u8]) -> String {
let n = digits.len();
let higher = digits.iter().take(n.saturating_sub(1)).any(|d| *d != 0);
let mut out = String::new();
for (i, &d) in digits.iter().enumerate() {
let place = n - 1 - i;
match (place, d) {
(_, 0) => {}
(0, 1) if higher => out.push_str("เอ็ด"),
(1, 1) => out.push_str("สิบ"),
(1, 2) => out.push_str("ยี่สิบ"),
_ => {
out.push_str(THAI_DIGITS.get(d as usize).copied().unwrap_or_default());
out.push_str(THAI_PLACES.get(place).copied().unwrap_or_default());
}
}
}
out
}
/// Thai words for a whole number written in decimal digits: groups of six digits, each but the
/// last followed by ล้าน (so 10^12 is หนึ่งล้านล้าน).
fn thai_number(digits: &str) -> String {
let d: Vec<u8> = digits.bytes().filter(u8::is_ascii_digit).map(|b| b - b'0').skip_while(|x| *x == 0).collect();
if d.is_empty() {
return "ศูนย์".to_string();
}
let first = match d.len() % 6 {
0 => 6,
r => r,
};
let mut out = String::new();
let mut start = 0;
let mut len = first;
while let Some(group) = d.get(start..start + len) {
out.push_str(&thai_group(group));
start += len;
len = 6;
if start < d.len() {
out.push_str("ล้าน");
}
}
out
}
/// BAHTTEXT(number): the amount in Thai words with บาท, rounded to satang (สตางค์); a whole
/// number of baht ends in ถ้วน and a negative amount starts with ลบ.
fn bahttext(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let x = round_half_away(num(a, 0)?, 2);
if !x.is_finite() {
return Err(CellError::Num);
}
let fixed = format!("{:.2}", x.abs());
let (whole, frac) = fixed.split_once('.').unwrap_or((fixed.as_str(), "00"));
let baht = whole.bytes().any(|b| b != b'0');
let satang = frac.bytes().any(|b| b != b'0');
let mut out = String::new();
if x < 0.0 && (baht || satang) {
out.push_str("ลบ");
}
if baht || !satang {
out.push_str(&thai_number(whole));
out.push_str("บาท");
}
if satang {
out.push_str(&thai_number(frac));
out.push_str("สตางค์");
} else {
out.push_str("ถ้วน");
}
text_val(out)
}
fn t_fn(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
match scalar(arg(a, 0)?) {
Value::Text(t) => Ok(Value::Text(t)),
Value::Error(e) => Err(e),
_ => text_val(""),
}
}
fn identity(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
text_val(text(a, 0)?)
}
/// Lower-cased char sequence with one char per input char (keeps positions aligned).
fn fold(c: &[char]) -> Vec<char> {
c.iter().map(|ch| ch.to_lowercase().next().unwrap_or(*ch)).collect()
}
/// Delimiter list from a scalar or array argument.
fn delimiters(v: &Value) -> R<Vec<Vec<char>>> {
as_array(v).iter().map(|d| d.to_text().map(|s| chars(&s))).collect()
}
/// Non-overlapping delimiter matches (start, end) in char positions, scanning left to right and
/// preferring the longest delimiter at each position. Empty delimiters are ignored unless all
/// delimiters are empty, in which case every position matches with zero length.
fn find_matches(text: &[char], delims: &[Vec<char>], insensitive: bool) -> Vec<(usize, usize)> {
let t = if insensitive { fold(text) } else { text.to_vec() };
let ds: Vec<Vec<char>> = delims.iter().filter(|d| !d.is_empty()).map(|d| if insensitive { fold(d) } else { d.clone() }).collect();
if ds.is_empty() {
return (0..=t.len()).map(|i| (i, i)).collect();
}
let mut out = Vec::new();
let mut i = 0;
while i < t.len() {
let best = ds.iter().filter(|d| t.get(i..i + d.len()).is_some_and(|w| w == d.as_slice())).map(|d| d.len()).max();
match best {
Some(l) => {
out.push((i, i + l));
i += l;
}
None => i += 1,
}
}
out
}
fn text_before_after(a: &[Arg], before: bool) -> R<Value> {
let s = chars(&text(a, 0)?);
let delims = delimiters(&arg(a, 1)?.value)?;
let inst = opt_int(a, 2, 1)?;
let mode = opt_int(a, 3, 0)?;
let match_end = opt_bool(a, 4, false)?;
if !(0..=1).contains(&mode) || inst == 0 || inst.unsigned_abs() as usize > s.len().max(1) {
return Err(CellError::Value);
}
let mut m = find_matches(&s, &delims, mode == 1);
if match_end {
if inst > 0 {
if m.last().is_none_or(|x| x.0 != s.len()) {
m.push((s.len(), s.len()));
}
} else if m.first().is_none_or(|x| x.0 != 0) {
m.insert(0, (0, 0));
}
}
let k = inst.unsigned_abs() as usize;
let hit = if inst > 0 { m.get(k - 1) } else { m.len().checked_sub(k).and_then(|i| m.get(i)) };
match hit {
Some(&(st, en)) => text_val(if before { s[..st].iter().collect::<String>() } else { s[en..].iter().collect::<String>() }),
None => match a.get(5) {
Some(x) => Ok(scalar(x)),
None => Err(CellError::NA),
},
}
}
fn textbefore(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
text_before_after(a, true)
}
fn textafter(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
text_before_after(a, false)
}
fn split_by(s: &[char], delims: &[Vec<char>], insensitive: bool) -> Vec<Vec<char>> {
if delims.iter().all(|d| d.is_empty()) {
return vec![s.to_vec()];
}
let mut out = Vec::new();
let mut last = 0;
for (st, en) in find_matches(s, delims, insensitive) {
out.push(s[last..st].to_vec());
last = en;
}
out.push(s[last..].to_vec());
out
}
fn textsplit(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let s = chars(&text(a, 0)?);
let col_d = if has(a, 1) { delimiters(&arg(a, 1)?.value)? } else { vec![] };
let row_d = if has(a, 2) { delimiters(&arg(a, 2)?.value)? } else { vec![] };
if col_d.iter().all(|d| d.is_empty()) && row_d.iter().all(|d| d.is_empty()) {
return Err(CellError::Value);
}
let ignore = opt_bool(a, 3, false)?;
let mode = opt_int(a, 4, 0)?;
if !(0..=1).contains(&mode) {
return Err(CellError::Value);
}
let pad = if has(a, 5) { scalar(arg(a, 5)?) } else { Value::Error(CellError::NA) };
let mut rows: Vec<Vec<String>> = Vec::new();
for r in split_by(&s, &row_d, mode == 1) {
if ignore && r.is_empty() {
continue;
}
let cells: Vec<String> =
split_by(&r, &col_d, mode == 1).into_iter().filter(|c| !(ignore && c.is_empty())).map(|c| c.into_iter().collect()).collect();
if ignore && cells.is_empty() {
continue;
}
rows.push(cells);
}
if rows.is_empty() {
return Err(CellError::Calc);
}
let ncols = rows.iter().map(Vec::len).max().unwrap_or(1).max(1);
let nrows = rows.len();
if nrows.saturating_mul(ncols) > MAX_CELLS {
return Err(CellError::Num);
}
let mut data = Vec::with_capacity(nrows * ncols);
for r in rows {
let n = r.len();
data.extend(r.into_iter().map(Value::from));
data.extend(std::iter::repeat_n(pad.clone(), ncols - n));
}
array_val(nrows, ncols, data)
}
fn value_text(v: &Value, strict: bool) -> String {
match v {
Value::Text(t) if strict => format!("\"{}\"", t.replace('"', "\"\"")),
Value::Text(t) => t.to_string(),
Value::Error(e) => e.as_str().to_string(),
Value::Number(n) => number_to_text(*n),
other => other.to_text().unwrap_or_default(),
}
}
fn format_arg(a: &[Arg], i: usize) -> R<bool> {
match opt_int(a, i, 0)? {
0 => Ok(false),
1 => Ok(true),
_ => Err(CellError::Value),
}
}
fn valuetotext(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let strict = format_arg(a, 1)?;
check_len(value_text(&scalar(arg(a, 0)?), strict))
}
fn arraytotext(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let strict = format_arg(a, 1)?;
let arr = as_array(&arg(a, 0)?.value);
let mut out = String::new();
if strict {
out.push('{');
}
for r in 0..arr.rows {
for c in 0..arr.cols {
if r > 0 && c == 0 {
out.push_str(if strict { ";" } else { ", " });
} else if c > 0 {
out.push_str(if strict { "," } else { ", " });
}
push_capped(&mut out, &value_text(arr.get(r, c).unwrap_or(&Value::Empty), strict))?;
}
}
if strict {
out.push('}');
}
check_len(out)
}
fn build_regex(pattern: &str, insensitive: bool) -> R<regex::Regex> {
if pattern.len() > MAX_TEXT {
return Err(CellError::Value);
}
regex::RegexBuilder::new(pattern)
.case_insensitive(insensitive)
.size_limit(1 << 22)
.dfa_size_limit(1 << 22)
.nest_limit(100)
.build()
.map_err(|_| CellError::Value)
}
fn case_arg(a: &[Arg], i: usize) -> R<bool> {
match opt_int(a, i, 0)? {
0 => Ok(false),
1 => Ok(true),
_ => Err(CellError::Value),
}
}
fn regextest(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let s = text(a, 0)?;
let re = build_regex(&text(a, 1)?, case_arg(a, 2)?)?;
Ok(Value::Bool(re.is_match(&s)))
}
fn regexextract(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let s = text(a, 0)?;
let mode = opt_int(a, 2, 0)?;
let re = build_regex(&text(a, 1)?, case_arg(a, 3)?)?;
match mode {
0 => re.find(&s).map(|m| Value::from(m.as_str())).ok_or(CellError::NA),
1 => {
let all: Vec<Value> = re.find_iter(&s).take(1_000_000).map(|m| Value::from(m.as_str())).collect();
if all.is_empty() {
return Err(CellError::NA);
}
array_val(all.len(), 1, all)
}
2 => {
let caps = re.captures(&s).ok_or(CellError::NA)?;
if caps.len() <= 1 {
return Ok(Value::from(caps.get(0).map_or("", |m| m.as_str())));
}
let groups: Vec<Value> = (1..caps.len()).map(|i| Value::from(caps.get(i).map_or("", |m| m.as_str()))).collect();
array_val(1, groups.len(), groups)
}
_ => Err(CellError::Value),
}
}
/// Converts Excel-style `$1` references to the regex crate's unambiguous `${1}` form.
fn convert_replacement(r: &str) -> String {
let mut out = String::with_capacity(r.len() + 4);
let c: Vec<char> = r.chars().collect();
let mut i = 0;
while i < c.len() {
let ch = c[i];
if ch == '$' && c.get(i + 1).is_some_and(|d| d.is_ascii_digit()) {
let mut j = i + 1;
while c.get(j).is_some_and(|d| d.is_ascii_digit()) {
j += 1;
}
out.push_str("${");
out.extend(c[i + 1..j].iter());
out.push('}');
i = j;
} else {
out.push(ch);
i += 1;
}
}
out
}
fn regexreplace(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
let s = text(a, 0)?;
let rep = convert_replacement(&text(a, 2)?);
let occ = opt_int(a, 3, 0)?;
let re = build_regex(&text(a, 1)?, case_arg(a, 4)?)?;
if occ == 0 {
return check_len(re.replace_all(&s, rep.as_str()).into_owned());
}
let caps: Vec<regex::Captures> = re.captures_iter(&s).collect();
let k = occ.unsigned_abs() as usize;
let idx = if occ > 0 { k.checked_sub(1) } else { caps.len().checked_sub(k) };
let Some(cap) = idx.and_then(|i| caps.get(i)) else { return text_val(s) };
let Some(m) = cap.get(0) else { return text_val(s) };
let mut out = String::new();
out.push_str(&s[..m.start()]);
cap.expand(&rep, &mut out);
out.push_str(&s[m.end()..]);
check_len(out)
}
pub(crate) fn specs() -> Vec<FnSpec> {
vec![
f!("CONCATENATE", 1, VAR, Text, S, "CONCATENATE(text1, [text2], ...)", "Joins several text values into one.", concatenate),
f!("CONCAT", 1, VAR, Text, A, "CONCAT(text1, [text2], ...)", "Joins text from values and every cell of ranges, without a separator.", concat),
f!(
"TEXTJOIN",
3,
VAR,
Text,
TJ,
"TEXTJOIN(delimiter, ignore_empty, text1, [text2], ...)",
"Joins text from values and ranges with a delimiter, optionally skipping empty items.",
textjoin
),
f!("LEFT", 1, 2, Text, S, "LEFT(text, [num_chars])", "Returns the first characters of a text.", left),
f!("LEFTB", 1, 2, Text, S, "LEFTB(text, [num_bytes])", "Byte version of LEFT (same as LEFT for single-byte text).", left),
f!("RIGHT", 1, 2, Text, S, "RIGHT(text, [num_chars])", "Returns the last characters of a text.", right),
f!("RIGHTB", 1, 2, Text, S, "RIGHTB(text, [num_bytes])", "Byte version of RIGHT (same as RIGHT for single-byte text).", right),
f!("MID", 3, 3, Text, S, "MID(text, start_num, num_chars)", "Returns characters from the middle of a text.", mid),
f!("MIDB", 3, 3, Text, S, "MIDB(text, start_num, num_bytes)", "Byte version of MID (same as MID for single-byte text).", mid),
f!("LEN", 1, 1, Text, S, "LEN(text)", "Counts the characters in a text.", len),
f!("LENB", 1, 1, Text, S, "LENB(text)", "Byte version of LEN (same as LEN for single-byte text).", len),
f!("LOWER", 1, 1, Text, S, "LOWER(text)", "Converts text to lower case.", lower),
f!("UPPER", 1, 1, Text, S, "UPPER(text)", "Converts text to upper case.", upper),
f!("PROPER", 1, 1, Text, S, "PROPER(text)", "Capitalises the first letter of each word and lower-cases the rest.", proper),
f!("TRIM", 1, 1, Text, S, "TRIM(text)", "Removes leading and trailing spaces and collapses runs of spaces to one.", trim),
f!("CLEAN", 1, 1, Text, S, "CLEAN(text)", "Removes non-printable control characters from text.", clean),
f!(
"SUBSTITUTE",
3,
4,
Text,
S,
"SUBSTITUTE(text, old_text, new_text, [instance_num])",
"Replaces occurrences of one text with another, optionally only a given occurrence.",
substitute
),
f!("REPLACE", 4, 4, Text, S, "REPLACE(old_text, start_num, num_chars, new_text)", "Replaces part of a text by position.", replace),
f!("REPLACEB", 4, 4, Text, S, "REPLACEB(old_text, start_num, num_bytes, new_text)", "Byte version of REPLACE.", replace),
f!("REPT", 2, 2, Text, S, "REPT(text, number_times)", "Repeats a text a given number of times.", rept),
f!("FIND", 2, 3, Text, S, "FIND(find_text, within_text, [start_num])", "Finds the position of one text in another (case-sensitive).", find),
f!("FINDB", 2, 3, Text, S, "FINDB(find_text, within_text, [start_num])", "Byte version of FIND.", find),
f!(
"SEARCH",
2,
3,
Text,
S,
"SEARCH(find_text, within_text, [start_num])",
"Finds the position of one text in another, ignoring case and allowing wildcards.",
search
),
f!("SEARCHB", 2, 3, Text, S, "SEARCHB(find_text, within_text, [start_num])", "Byte version of SEARCH.", search),
f!("EXACT", 2, 2, Text, S, "EXACT(text1, text2)", "TRUE when two texts are identical, including case.", exact),
f!("CHAR", 1, 1, Text, S, "CHAR(number)", "Returns the character for a code from the Windows-1252 character set.", char_fn),
f!("CODE", 1, 1, Text, S, "CODE(text)", "Returns the Windows-1252 code of the first character of a text.", code),
f!("UNICHAR", 1, 1, Text, S, "UNICHAR(number)", "Returns the character for a Unicode code point.", unichar),
f!("UNICODE", 1, 1, Text, S, "UNICODE(text)", "Returns the Unicode code point of the first character of a text.", unicode),
f!("VALUE", 1, 1, Text, S, "VALUE(text)", "Converts text that looks like a number, date or time into a number.", value),
f!(
"NUMBERVALUE",
1,
3,
Text,
S,
"NUMBERVALUE(text, [decimal_separator], [group_separator])",
"Converts text to a number using the given decimal and group separators.",
numbervalue
),
f!(
"FIXED",
1,
3,
Text,
S,
"FIXED(number, [decimals], [no_commas])",
"Rounds a number and formats it as text with fixed decimals and optional thousands separators.",
fixed
),
f!("DOLLAR", 1, 2, Text, S, "DOLLAR(number, [decimals])", "Formats a number as currency text.", dollar),
f!("BAHTTEXT", 1, 1, Text, S, "BAHTTEXT(number)", "Spells out a number in Thai words with Baht.", bahttext),
f!("T", 1, 1, Text, S, "T(value)", "Returns the value if it is text, otherwise empty text.", t_fn),
f!(
"TEXTBEFORE",
2,
6,
Text,
TB,
"TEXTBEFORE(text, delimiter, [instance_num], [match_mode], [match_end], [if_not_found])",
"Returns the text that comes before a delimiter.",
textbefore
),
f!(
"TEXTAFTER",
2,
6,
Text,
TB,
"TEXTAFTER(text, delimiter, [instance_num], [match_mode], [match_end], [if_not_found])",
"Returns the text that comes after a delimiter.",
textafter
),
f!(
"TEXTSPLIT",
2,
6,
Text,
TS,
"TEXTSPLIT(text, col_delimiter, [row_delimiter], [ignore_empty], [match_mode], [pad_with])",
"Splits text into a grid of columns and rows at the given delimiters.",
textsplit
),
f!(
"VALUETOTEXT",
1,
2,
Text,
S,
"VALUETOTEXT(value, [format])",
"Converts any value to text, optionally in strict (quoted) form.",
valuetotext
),
f!(
"ARRAYTOTEXT",
1,
2,
Text,
ATT,
"ARRAYTOTEXT(array, [format])",
"Converts an array to text, either as a comma list or as a strict array constant.",
arraytotext
),
f!(
"REGEXTEST",
2,
3,
Text,
S,
"REGEXTEST(text, pattern, [case_sensitivity])",
"TRUE when a regular expression matches somewhere in the text.",
regextest
),
f!(
"REGEXEXTRACT",
2,
4,
Text,
S,
"REGEXEXTRACT(text, pattern, [return_mode], [case_sensitivity])",
"Extracts the first match, all matches or the capture groups of a regular expression.",
regexextract
),
f!(
"REGEXREPLACE",
3,
5,
Text,
S,
"REGEXREPLACE(text, pattern, replacement, [occurrence], [case_sensitivity])",
"Replaces matches of a regular expression, all of them or a chosen occurrence.",
regexreplace
),
f!("ASC", 1, 1, Text, S, "ASC(text)", "Converts full-width characters to half-width (no change for single-byte text).", identity),
f!("DBCS", 1, 1, Text, S, "DBCS(text)", "Converts half-width characters to full-width (no change in this locale).", identity),
f!("JIS", 1, 1, Text, S, "JIS(text)", "Converts half-width characters to full-width (no change in this locale).", identity),
f!("PHONETIC", 1, 1, Text, S, "PHONETIC(reference)", "Returns the phonetic (furigana) text; here the text itself.", identity),
]
}
#[cfg(test)]
mod tests {
use gridcraft_core::{CellError, Value};
use crate::util::testutil::*;
#[test]
fn bahttext() {
let bt = |x: f64| ev("BAHTTEXT", vec![n(x)]);
// Microsoft's example.
is_text(bt(1234.0), "หนึ่งพันสองร้อยสามสิบสี่บาทถ้วน");
// Tens, เอ็ด, satang, millions and beyond, sign and rounding to satang.
is_text(bt(0.0), "ศูนย์บาทถ้วน");
is_text(bt(21.25), "ยี่สิบเอ็ดบาทยี่สิบห้าสตางค์");
is_text(bt(0.5), "ห้าสิบสตางค์");
is_text(bt(101.0), "หนึ่งร้อยเอ็ดบาทถ้วน");
is_text(bt(1_000_000.0), "หนึ่งล้านบาทถ้วน");
is_text(bt(11_000_000.0), "สิบเอ็ดล้านบาทถ้วน");
is_text(bt(2_000_005_000_000.0), "สองล้านห้าล้านบาทถ้วน");
is_text(bt(1e12), "หนึ่งล้านล้านบาทถ้วน");
is_text(bt(-15.5), "ลบสิบห้าบาทห้าสิบสตางค์");
is_text(bt(1.999), "สองบาทถ้วน");
is_text(bt(0.005), "หนึ่งสตางค์");
is_text(bt(-0.001), "ศูนย์บาทถ้วน");
is_err(ev("BAHTTEXT", vec![t("abc")]), CellError::Value);
// Huge numbers give text (or #NUM! where rounding overflows), never a panic.
assert!(matches!(bt(-1e300), Value::Text(_)));
assert!(matches!(bt(f64::MAX), Value::Text(_) | Value::Error(CellError::Num)));
}
#[test]
fn concat_family() {
is_text(ev("CONCATENATE", vec![t("a"), n(1.5), b(true)]), "a1.5TRUE");
is_text(ev("CONCAT", vec![av(arr(vec![vec![tv("a"), tv("b")], vec![nv(1.0), Value::Empty]])), t("!")]), "ab1!");
is_text(ev("TEXTJOIN", vec![t("-"), b(true), av(arr(vec![vec![tv("a"), tv(""), tv("b")]])), t("c")]), "a-b-c");
is_text(ev("TEXTJOIN", vec![t("-"), b(false), av(arr(vec![vec![tv("a"), tv(""), tv("b")]]))]), "a--b");
is_text(ev("TEXTJOIN", vec![av(row_t(&[",", ";"])), b(true), t("a"), t("b"), t("c")]), "a,b;c");
is_err(ev("CONCAT", vec![t("a"), e(CellError::NA)]), CellError::NA);
// Lifting: CONCATENATE over an array.
assert_eq!(rows_of(&ev("CONCATENATE", vec![av(row_t(&["a", "b"])), t("x")])), vec![vec![tv("ax"), tv("bx")]]);
}
fn row_t(xs: &[&str]) -> Value {
arr(vec![xs.iter().map(|x| tv(x)).collect()])
}
#[test]
fn slicing() {
is_text(ev("LEFT", vec![t("Hello")]), "H");
is_text(ev("LEFT", vec![t("Hello"), n(3.0)]), "Hel");
is_text(ev("LEFT", vec![t("Hello"), n(99.0)]), "Hello");
is_err(ev("LEFT", vec![t("Hello"), n(-1.0)]), CellError::Value);
is_text(ev("RIGHT", vec![t("Hello"), n(2.0)]), "lo");
is_text(ev("RIGHT", vec![t("héllo"), n(4.0)]), "éllo");
is_text(ev("MID", vec![t("Hello"), n(2.0), n(3.0)]), "ell");
is_text(ev("MID", vec![t("Hello"), n(10.0), n(3.0)]), "");
is_err(ev("MID", vec![t("Hello"), n(0.0), n(3.0)]), CellError::Value);
close(ev("LEN", vec![t("héllo")]), 5.0);
close(ev("LEN", vec![n(123.45)]), 6.0);
close(ev("LEN", vec![empty()]), 0.0);
is_text(ev("LEFT", vec![n(12345.0), n(2.0)]), "12");
}
#[test]
fn case_and_cleanup() {
is_text(ev("UPPER", vec![t("abc")]), "ABC");
is_text(ev("LOWER", vec![t("ABC")]), "abc");
is_text(ev("PROPER", vec![t("this is a TITLE")]), "This Is A Title");
is_text(ev("PROPER", vec![t("2-way street's")]), "2-Way Street'S");
is_text(ev("TRIM", vec![t(" a b c ")]), "a b c");
is_text(ev("CLEAN", vec![t("a\u{7}b\nc")]), "abc");
}
#[test]
fn substitute_replace_rept() {
is_text(ev("SUBSTITUTE", vec![t("a-b-c"), t("-"), t("+")]), "a+b+c");
is_text(ev("SUBSTITUTE", vec![t("a-b-c"), t("-"), t("+"), n(2.0)]), "a-b+c");
is_text(ev("SUBSTITUTE", vec![t("a-b-c"), t("-"), t("+"), n(5.0)]), "a-b-c");
is_err(ev("SUBSTITUTE", vec![t("a"), t("a"), t("b"), n(0.0)]), CellError::Value);
is_text(ev("SUBSTITUTE", vec![t("abc"), t(""), t("x")]), "abc");
is_text(ev("REPLACE", vec![t("abcdef"), n(2.0), n(3.0), t("XY")]), "aXYef");
is_text(ev("REPLACE", vec![t("abc"), n(10.0), n(1.0), t("Z")]), "abcZ");
is_text(ev("REPT", vec![t("ab"), n(3.0)]), "ababab");
is_text(ev("REPT", vec![t("ab"), n(0.0)]), "");
is_err(ev("REPT", vec![t("ab"), n(-1.0)]), CellError::Value);
is_err(ev("REPT", vec![t("ab"), n(20000.0)]), CellError::Value);
is_err(ev("REPT", vec![t("ab"), n(1e300)]), CellError::Value);
}
#[test]
fn find_search() {
close(ev("FIND", vec![t("b"), t("abcb")]), 2.0);
close(ev("FIND", vec![t("b"), t("abcb"), n(3.0)]), 4.0);
is_err(ev("FIND", vec![t("B"), t("abc")]), CellError::Value);
close(ev("FIND", vec![t(""), t("abc")]), 1.0);
is_err(ev("FIND", vec![t("a"), t("abc"), n(0.0)]), CellError::Value);
close(ev("SEARCH", vec![t("B"), t("abc")]), 2.0);
close(ev("SEARCH", vec![t("c*"), t("abcd")]), 3.0);
close(ev("SEARCH", vec![t("?d"), t("abcd")]), 3.0);
close(ev("SEARCH", vec![t("~*"), t("a*b")]), 2.0);
is_err(ev("SEARCH", vec![t("z"), t("abc")]), CellError::Value);
is_err(ev("SEARCH", vec![t("a"), t("abc"), n(5.0)]), CellError::Value);
for f in ["FIND", "SEARCH"] {
close(ev(f, vec![t(""), t("abc"), n(3.0)]), 3.0);
is_err(ev(f, vec![t(""), t("abc"), n(4.0)]), CellError::Value);
}
assert_eq!(ev("EXACT", vec![t("a"), t("A")]), Value::Bool(false));
assert_eq!(ev("EXACT", vec![t("a"), t("a")]), Value::Bool(true));
}
#[test]
fn codes() {
is_text(ev("CHAR", vec![n(65.0)]), "A");
is_text(ev("CHAR", vec![n(128.0)]), "€");
is_err(ev("CHAR", vec![n(0.0)]), CellError::Value);
is_err(ev("CHAR", vec![n(256.0)]), CellError::Value);
close(ev("CODE", vec![t("A")]), 65.0);
close(ev("CODE", vec![t("€")]), 128.0);
is_err(ev("CODE", vec![t("")]), CellError::Value);
is_text(ev("UNICHAR", vec![n(9731.0)]), "☃");
close(ev("UNICODE", vec![t("☃")]), 9731.0);
is_err(ev("UNICHAR", vec![n(0.0)]), CellError::Value);
is_err(ev("UNICHAR", vec![n(55296.0)]), CellError::NA);
}
/// VALUE cases observed in Excel (ValueFunctionOfBlank, FormulaEvalTestData); a blank cell
/// still reads as 0.
#[test]
fn value_observed() {
let cases: Vec<(crate::Arg, Result<f64, CellError>)> = vec![
(t(""), Err(CellError::Value)),
(t("ABC"), Err(CellError::Value)),
(t("0.33"), Ok(0.33)),
(t("$1,000"), Ok(1000.0)),
(rf(Value::Empty), Ok(0.0)),
];
for (arg, want) in cases {
match want {
Ok(x) => close(ev("VALUE", vec![arg]), x),
Err(err) => is_err(ev("VALUE", vec![arg]), err),
}
}
}
#[test]
fn numbers_from_text() {
close(ev("VALUE", vec![t("1,234.5")]), 1234.5);
close(ev("VALUE", vec![t("50%")]), 0.5);
close(ev("VALUE", vec![t("$10")]), 10.0);
close(ev("VALUE", vec![t("12:00")]), 0.5);
close(ev("VALUE", vec![t("2020-01-01")]), 43831.0);
is_err(ev("VALUE", vec![t("abc")]), CellError::Value);
close(ev("NUMBERVALUE", vec![t("2.500,27"), t(","), t(".")]), 2500.27);
close(ev("NUMBERVALUE", vec![t("3.5%")]), 0.035);
close(ev("NUMBERVALUE", vec![t(" 1 000 ")]), 1000.0);
close(ev("NUMBERVALUE", vec![t("")]), 0.0);
is_err(ev("NUMBERVALUE", vec![t("1.2.3")]), CellError::Value);
is_text(ev("FIXED", vec![n(1234.567), n(1.0)]), "1,234.6");
is_text(ev("FIXED", vec![n(1234.567), n(-1.0)]), "1,230");
is_text(ev("FIXED", vec![n(-1234.567), n(1.0), b(true)]), "-1234.6");
is_text(ev("FIXED", vec![n(44.332)]), "44.33");
is_text(ev("FIXED", vec![n(2.675), n(2.0)]), "2.68");
is_text(ev("DOLLAR", vec![n(1234.567)]), "$1,234.57");
is_text(ev("DOLLAR", vec![n(-1234.567), n(1.0)]), "($1,234.6)");
is_text(ev("DOLLAR", vec![n(0.5), n(0.0)]), "$1");
is_text(ev("T", vec![t("x")]), "x");
is_text(ev("T", vec![n(1.0)]), "");
}
#[test]
fn before_after() {
is_text(ev("TEXTBEFORE", vec![t("Red riding hood"), t(" ")]), "Red");
is_text(ev("TEXTBEFORE", vec![t("Red riding hood"), t(" "), n(2.0)]), "Red riding");
is_text(ev("TEXTBEFORE", vec![t("Red riding hood"), t(" "), n(-1.0)]), "Red riding");
is_text(ev("TEXTAFTER", vec![t("Red riding hood"), t(" ")]), "riding hood");
is_text(ev("TEXTAFTER", vec![t("Red riding hood"), t(" "), n(-1.0)]), "hood");
is_err(ev("TEXTAFTER", vec![t("abc"), t("x")]), CellError::NA);
is_text(ev("TEXTAFTER", vec![t("abc"), t("x"), n(1.0), n(0.0), n(0.0), t("none")]), "none");
is_text(ev("TEXTAFTER", vec![t("aXb"), t("x"), n(1.0), n(1.0)]), "b");
is_err(ev("TEXTAFTER", vec![t("aXb"), t("x")]), CellError::NA);
is_text(ev("TEXTBEFORE", vec![t("abc"), t("x"), n(1.0), n(0.0), n(1.0)]), "abc");
is_text(ev("TEXTAFTER", vec![t("a-b+c"), av(row_t(&["+", "-"]))]), "b+c");
is_err(ev("TEXTBEFORE", vec![t("abc"), t("b"), n(0.0)]), CellError::Value);
is_text(ev("TEXTBEFORE", vec![t("abc"), t("")]), "");
is_text(ev("TEXTAFTER", vec![t("abc"), t("")]), "abc");
}
#[test]
fn split() {
let v = ev("TEXTSPLIT", vec![t("a,b;c,d"), t(","), t(";")]);
assert_eq!(rows_of(&v), vec![vec![tv("a"), tv("b")], vec![tv("c"), tv("d")]]);
let v = ev("TEXTSPLIT", vec![t("a,b;c"), t(","), t(";")]);
assert_eq!(rows_of(&v), vec![vec![tv("a"), tv("b")], vec![tv("c"), Value::Error(CellError::NA)]]);
let v = ev("TEXTSPLIT", vec![t("a,,b"), t(","), empty(), b(true)]);
assert_eq!(rows_of(&v), vec![vec![tv("a"), tv("b")]]);
let v = ev("TEXTSPLIT", vec![t("a,,b"), t(",")]);
assert_eq!(rows_of(&v), vec![vec![tv("a"), tv(""), tv("b")]]);
let v = ev("TEXTSPLIT", vec![t("a b-c"), av(row_t(&[" ", "-"]))]);
assert_eq!(rows_of(&v), vec![vec![tv("a"), tv("b"), tv("c")]]);
let v = ev("TEXTSPLIT", vec![t("1;2"), empty(), t(";")]);
assert_eq!(rows_of(&v), vec![vec![tv("1")], vec![tv("2")]]);
let v = ev("TEXTSPLIT", vec![t("aXbxc"), t("x"), empty(), b(false), n(1.0)]);
assert_eq!(rows_of(&v), vec![vec![tv("a"), tv("b"), tv("c")]]);
let v = ev("TEXTSPLIT", vec![t("a,b;c"), t(","), t(";"), b(false), n(0.0), t("-")]);
assert_eq!(rows_of(&v)[1], vec![tv("c"), tv("-")]);
}
#[test]
fn split_rejects_oversized_padded_grid() {
// One row of 8000 columns followed by 8000 one-cell rows: the text is under the cell-text limit, the padded grid is not.
let text = format!("{}{}", "a,".repeat(8000), "b;".repeat(8000));
is_err(ev("TEXTSPLIT", vec![t(&text), t(","), t(";")]), CellError::Num);
}
#[test]
fn to_text() {
is_text(ev("VALUETOTEXT", vec![t("a\"b")]), "a\"b");
is_text(ev("VALUETOTEXT", vec![t("ab"), n(1.0)]), "\"ab\"");
is_text(ev("VALUETOTEXT", vec![n(1.5), n(1.0)]), "1.5");
is_text(ev("VALUETOTEXT", vec![e(CellError::Div0)]), "#DIV/0!");
is_err(ev("VALUETOTEXT", vec![n(1.0), n(2.0)]), CellError::Value);
let a = av(arr(vec![vec![nv(1.0), tv("a")], vec![Value::Bool(true), Value::Error(CellError::NA)]]));
is_text(ev("ARRAYTOTEXT", vec![a.clone()]), "1, a, TRUE, #N/A");
is_text(ev("ARRAYTOTEXT", vec![a, n(1.0)]), "{1,\"a\";TRUE,#N/A}");
}
#[test]
fn regex() {
assert_eq!(ev("REGEXTEST", vec![t("abc123"), t("[0-9]+")]), Value::Bool(true));
assert_eq!(ev("REGEXTEST", vec![t("ABC"), t("abc")]), Value::Bool(false));
assert_eq!(ev("REGEXTEST", vec![t("ABC"), t("abc"), n(1.0)]), Value::Bool(true));
is_err(ev("REGEXTEST", vec![t("a"), t("(")]), CellError::Value);
is_text(ev("REGEXEXTRACT", vec![t("tel 555-1234 or 555-9876"), t("[0-9]{3}-[0-9]{4}")]), "555-1234");
let v = ev("REGEXEXTRACT", vec![t("tel 555-1234 or 555-9876"), t("[0-9]{3}-[0-9]{4}"), n(1.0)]);
assert_eq!(rows_of(&v), vec![vec![tv("555-1234")], vec![tv("555-9876")]]);
let v = ev("REGEXEXTRACT", vec![t("John Smith"), t("(\\w+) (\\w+)"), n(2.0)]);
assert_eq!(rows_of(&v), vec![vec![tv("John"), tv("Smith")]]);
is_err(ev("REGEXEXTRACT", vec![t("abc"), t("z")]), CellError::NA);
is_text(ev("REGEXREPLACE", vec![t("a1b22c"), t("[0-9]+"), t("#")]), "a#b#c");
is_text(ev("REGEXREPLACE", vec![t("a1b22c"), t("[0-9]+"), t("#"), n(2.0)]), "a1b#c");
is_text(ev("REGEXREPLACE", vec![t("a1b22c"), t("[0-9]+"), t("#"), n(-2.0)]), "a#b22c");
is_text(ev("REGEXREPLACE", vec![t("John Smith"), t("(\\w+) (\\w+)"), t("$2, $1")]), "Smith, John");
is_text(ev("REGEXREPLACE", vec![t("ab"), t("(a)"), t("$1x")]), "axb");
}
}