pub(crate) fn shift_formula(formula: &str, dr: i32, dc: i32) -> String {
if dr == 0 && dc == 0 {
return formula.to_string();
}
let bytes: Vec<char> = formula.chars().collect();
let mut out = String::with_capacity(formula.len());
let mut i = 0usize;
let mut in_string = false;
while i < bytes.len() {
let c = bytes[i];
if in_string {
out.push(c);
if c == '"' {
if bytes.get(i + 1) == Some(&'"') {
out.push('"');
i += 2;
continue;
}
in_string = false;
}
i += 1;
continue;
}
if c == '"' {
in_string = true;
out.push(c);
i += 1;
continue;
}
match scan_ref(&bytes, i) {
Some((end, r)) => {
out.push_str(&shift_one(&r, dr, dc));
i = end;
}
None => {
out.push(c);
i += 1;
}
}
}
out
}
struct ParsedRef {
col_abs: bool,
row_abs: bool,
col: usize,
row: usize,
}
fn scan_ref(s: &[char], i: usize) -> Option<(usize, ParsedRef)> {
if i > 0 {
let p = s[i - 1];
if p.is_alphanumeric() || p == '_' || p == '.' || p == '$' {
return None;
}
}
let mut j = i;
let col_abs = s.get(j) == Some(&'$');
if col_abs {
j += 1;
}
let letter_start = j;
while j < s.len() && s[j].is_ascii_alphabetic() {
j += 1;
}
let letters: String = s[letter_start..j].iter().collect();
if letters.is_empty() || letters.len() > 3 {
return None;
}
let row_abs = s.get(j) == Some(&'$');
if row_abs {
j += 1;
}
let digit_start = j;
while j < s.len() && s[j].is_ascii_digit() {
j += 1;
}
if digit_start == j {
return None;
}
let digits: String = s[digit_start..j].iter().collect();
if let Some(&n) = s.get(j) {
if n.is_alphanumeric() || n == '_' || n == '(' {
return None;
}
}
let row: usize = digits.parse().ok()?;
if row == 0 {
return None;
}
Some((
j,
ParsedRef {
col_abs,
row_abs,
col: crate::sqref::col_to_idx(&letters.to_ascii_uppercase()),
row: row - 1,
},
))
}
fn shift_one(r: &ParsedRef, dr: i32, dc: i32) -> String {
let row = if r.row_abs {
Some(r.row)
} else {
offset(r.row, dr)
};
let col = if r.col_abs {
Some(r.col)
} else {
offset(r.col, dc)
};
match (row, col) {
(Some(row), Some(col)) => format!(
"{}{}{}{}",
if r.col_abs { "$" } else { "" },
crate::sqref::col_to_letters(col),
if r.row_abs { "$" } else { "" },
row + 1
),
_ => "#REF!".to_string(),
}
}
fn offset(v: usize, d: i32) -> Option<usize> {
let n = v as i64 + d as i64;
if n < 0 { None } else { Some(n as usize) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shifts_relative_keeps_absolute() {
assert_eq!(shift_formula("$B1>10", 2, 0), "$B3>10");
assert_eq!(shift_formula("B$1>10", 2, 1), "C$1>10");
assert_eq!(shift_formula("$B$1>10", 2, 3), "$B$1>10");
assert_eq!(shift_formula("B1>10", 2, 3), "E3>10");
}
#[test]
fn zero_delta_is_identity() {
assert_eq!(shift_formula("A1+$B$2", 0, 0), "A1+$B$2");
}
#[test]
fn leaves_string_literals_alone() {
assert_eq!(
shift_formula("IF(A1=\"B1\",\"C2\",A1)", 1, 0),
"IF(A2=\"B1\",\"C2\",A2)"
);
assert_eq!(
shift_formula("A1&\"say \"\"B1\"\" ok\"", 1, 0),
"A2&\"say \"\"B1\"\" ok\""
);
}
#[test]
fn leaves_function_names_and_identifiers_alone() {
assert_eq!(shift_formula("LOG10(A1)", 1, 0), "LOG10(A2)");
assert_eq!(shift_formula("SUM(A1:A5)", 1, 0), "SUM(A2:A6)");
assert_eq!(shift_formula("MYNAME1+A1", 1, 0), "MYNAME1+A2");
}
#[test]
fn keeps_sheet_qualifier() {
assert_eq!(shift_formula("Sheet1!A1>0", 1, 0), "Sheet1!A2>0");
}
#[test]
fn off_sheet_becomes_ref_error() {
assert_eq!(shift_formula("B2>0", -5, 0), "#REF!>0");
assert_eq!(shift_formula("B2>0", 0, -5), "#REF!>0");
}
#[test]
fn multi_letter_columns() {
assert_eq!(shift_formula("AA1>0", 1, 1), "AB2>0");
assert_eq!(shift_formula("ZZ1>0", 0, 1), "AAA1>0");
}
}