Skip to main content

truecalc_core/eval/functions/text/regexreplace/
mod.rs

1use crate::eval::coercion::to_string_val;
2use crate::eval::functions::check_arity;
3use crate::types::{ErrorKind, Value};
4use regex_lite::Regex;
5
6/// `REGEXREPLACE(text, pattern, replacement)` -- replaces ALL non-overlapping matches
7/// of pattern in text with replacement. Matches GS semantics: zero-length matches
8/// after non-empty matches are included (unlike regex_lite default behaviour).
9pub fn regexreplace_fn(args: &[Value]) -> Value {
10    if let Some(err) = check_arity(args, 3, 3) { return err; }
11    let text = match &args[0] {
12        Value::Text(s) => s.clone(),
13        Value::Empty => String::new(),
14        Value::Error(e) => return Value::Error(e.clone()),
15        _ => return Value::Error(ErrorKind::Value),
16    };
17    let pattern = match to_string_val(args[1].clone()) {
18        Ok(s) => s,
19        Err(e) => return e,
20    };
21    let replacement = match to_string_val(args[2].clone()) {
22        Ok(s) => s,
23        Err(e) => return e,
24    };
25    let re = match Regex::new(&pattern) {
26        Ok(r) => r,
27        Err(_) => return Value::Error(ErrorKind::Ref),
28    };
29    // GS includes zero-length matches after non-empty matches.
30    // regex_lite skips them; we scan manually to match GS semantics.
31    let text_bytes = text.as_bytes();
32    let text_len = text_bytes.len();
33    let mut result = String::new();
34    let mut pos = 0usize;
35    while pos <= text_len {
36        let slice = &text[pos..];
37        if let Some(m) = re.find(slice) {
38            let abs_start = pos + m.start();
39            let abs_end = pos + m.end();
40            result.push_str(&text[pos..abs_start]);
41            result.push_str(&replacement);
42            if m.start() == m.end() {
43                // Zero-length match: copy current byte to advance
44                if abs_end < text_len {
45                    // Safety: abs_end is a valid UTF-8 char boundary because
46                    // regex_lite only matches at char boundaries.
47                    let next_char_end = text[abs_end..].chars().next()
48                        .map(|c| abs_end + c.len_utf8())
49                        .unwrap_or(text_len);
50                    result.push_str(&text[abs_end..next_char_end]);
51                    pos = next_char_end;
52                } else {
53                    pos = abs_end + 1;
54                }
55            } else {
56                pos = abs_end;
57            }
58        } else {
59            result.push_str(&text[pos..]);
60            break;
61        }
62    }
63    Value::Text(result)
64}
65
66#[cfg(test)]
67mod tests;