Skip to main content

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

1use crate::eval::coercion::{to_number, to_string_val};
2use crate::eval::functions::check_arity;
3use crate::eval::functions::text::lenb::dbcs_char_width;
4use crate::types::{ErrorKind, Value};
5
6/// Convert a 1-based DBCS start byte to a char index, snapping forward if inside a char.
7fn dbcs_start_to_char_idx(s: &str, start_1based: usize) -> usize {
8    if start_1based <= 1 {
9        return 0;
10    }
11    let target = start_1based - 1; // 0-based
12    let mut pos = 0usize;
13    for (i, c) in s.chars().enumerate() {
14        let w = dbcs_char_width(c);
15        if pos >= target {
16            return i;
17        }
18        if pos < target && pos + w > target {
19            return i + 1; // snap forward past partial char
20        }
21        pos += w;
22    }
23    s.chars().count()
24}
25
26/// Convert a char index to a 1-based DBCS byte position.
27fn char_idx_to_dbcs_byte_end(s: &str, char_idx: usize) -> usize {
28    s.chars().take(char_idx).map(dbcs_char_width).sum::<usize>()
29}
30
31/// `REPLACEB(old_text, start_byte, num_bytes, new_text)` — DBCS byte-position replacement.
32pub fn replaceb_fn(args: &[Value]) -> Value {
33    if let Some(err) = check_arity(args, 4, 4) {
34        return err;
35    }
36    let text = match to_string_val(args[0].clone()) {
37        Ok(s) => s,
38        Err(e) => return e,
39    };
40    let start_num = match to_number(args[1].clone()) {
41        Ok(n) => n,
42        Err(e) => return e,
43    };
44    let num_bytes = match to_number(args[2].clone()) {
45        Ok(n) => n,
46        Err(e) => return e,
47    };
48    let new_text = match to_string_val(args[3].clone()) {
49        Ok(s) => s,
50        Err(e) => return e,
51    };
52    if start_num < 1.0 || num_bytes < 0.0 {
53        return Value::Error(ErrorKind::Value);
54    }
55    let start_dbcs = start_num as usize; // 1-based
56    let num = num_bytes as usize;
57    let chars: Vec<char> = text.chars().collect();
58    // Snap start forward to char boundary
59    let start_char = dbcs_start_to_char_idx(&text, start_dbcs);
60    // end DBCS byte = (snapped start DBCS byte) + num_bytes
61    let start_dbcs_snapped = char_idx_to_dbcs_byte_end(&text, start_char) + 1; // 1-based
62    let end_dbcs_target = start_dbcs_snapped.saturating_sub(1) + num; // 0-based end exclusive
63    // Find end char: walk from start_char taking num_bytes
64    let mut taken = 0usize;
65    let mut end_char = start_char;
66    for c in chars[start_char..].iter() {
67        let w = dbcs_char_width(*c);
68        if taken + w <= num {
69            taken += w;
70            end_char += 1;
71            if taken == num { break; }
72        } else {
73            // Partial char: skip (exclude from replacement range)
74            break;
75        }
76    }
77    // Build result: prefix + new_text + suffix
78    let prefix: String = chars[..start_char].iter().collect();
79    let suffix: String = chars[end_char..].iter().collect();
80    let _ = end_dbcs_target; // used for clarity only
81    Value::Text(format!("{}{}{}", prefix, new_text, suffix))
82}
83
84#[cfg(test)]
85mod tests;