use logisheets_base::CellId;
use logisheets_parser::unparse::CellShift;
use crate::Workbook;
use crate::api::worksheet::Worksheet;
use crate::controller::display::Value;
use crate::edit_action::CellInput;
use crate::errors::{Error, Result};
#[derive(Debug, Clone, Copy)]
pub struct FillRange {
pub start_row: usize,
pub start_col: usize,
pub end_row: usize,
pub end_col: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Axis {
Vertical,
Horizontal,
}
enum SrcCell {
Formula(CellId),
Number(f64),
Raw(String),
}
impl Workbook {
pub fn predict_fill(
&self,
sheet_idx: usize,
src: FillRange,
dst: FillRange,
) -> Result<Vec<CellInput>> {
let ws = self.get_sheet_by_idx(sheet_idx)?;
let cells = ws.predict_fill(src, dst)?;
Ok(cells
.into_iter()
.map(|(row, col, content)| CellInput {
sheet_idx,
row,
col,
content,
})
.collect())
}
}
impl<'a> Worksheet<'a> {
pub(crate) fn predict_fill(
&self,
src: FillRange,
dst: FillRange,
) -> Result<Vec<(usize, usize, String)>> {
let axis = infer_axis(src, dst)?;
let mut out = Vec::new();
match axis {
Axis::Vertical => {
let src_positions: Vec<usize> = (src.start_row..=src.end_row).collect();
let targets: Vec<usize> = (dst.start_row..=dst.end_row).collect();
for col in src.start_col..=src.end_col {
let src_cells = self.collect_line(&src_positions, |&row| (row, col))?;
self.fill_line(
&src_positions,
&src_cells,
&targets,
axis,
|pos, content| out.push((pos, col, content)),
)?;
}
}
Axis::Horizontal => {
let src_positions: Vec<usize> = (src.start_col..=src.end_col).collect();
let targets: Vec<usize> = (dst.start_col..=dst.end_col).collect();
for row in src.start_row..=src.end_row {
let src_cells = self.collect_line(&src_positions, |&col| (row, col))?;
self.fill_line(
&src_positions,
&src_cells,
&targets,
axis,
|pos, content| out.push((row, pos, content)),
)?;
}
}
}
Ok(out)
}
fn collect_line(
&self,
positions: &[usize],
to_rc: impl Fn(&usize) -> (usize, usize),
) -> Result<Vec<SrcCell>> {
positions
.iter()
.map(|p| {
let (row, col) = to_rc(p);
let cell_id =
self.controller
.status
.navigator
.fetch_cell_id(&self.sheet_id, row, col)?;
if self.has_formula(&cell_id) {
Ok(SrcCell::Formula(cell_id))
} else {
Ok(match self.get_value_by_id(&cell_id)? {
Value::Number(n) => SrcCell::Number(n),
v => SrcCell::Raw(value_to_input(&v)),
})
}
})
.collect()
}
fn fill_line(
&self,
src_positions: &[usize],
src_cells: &[SrcCell],
targets: &[usize],
axis: Axis,
mut emit: impl FnMut(usize, String),
) -> Result<()> {
let n = src_cells.len();
if n == 0 {
return Ok(());
}
let first = src_positions[0] as i64;
let all_formula = src_cells.iter().all(|c| matches!(c, SrcCell::Formula(_)));
let arithmetic = (n >= 2).then(|| numeric_series(src_cells)).flatten();
for &t in targets {
let content = if let Some((v0, step)) = arithmetic {
let value = v0 + step * ((t as i64 - first) as f64);
format_number(value)
} else if all_formula {
let idx = (t as i64 - first).rem_euclid(n as i64) as usize;
self.shifted_formula(src_positions[idx], src_cells[idx].cell_id(), t, axis)?
} else {
let idx = (t as i64 - first).rem_euclid(n as i64) as usize;
match &src_cells[idx] {
SrcCell::Formula(id) => {
self.shifted_formula(src_positions[idx], *id, t, axis)?
}
SrcCell::Number(num) => format_number(*num),
SrcCell::Raw(s) => s.clone(),
}
};
emit(t, content);
}
Ok(())
}
fn shifted_formula(
&self,
src_pos: usize,
cell_id: CellId,
t: usize,
axis: Axis,
) -> Result<String> {
let delta = t as i32 - src_pos as i32;
let shift = match axis {
Axis::Vertical => CellShift::new(delta, 0),
Axis::Horizontal => CellShift::new(0, delta),
};
let f = self.get_formula_with_shift_by_id(&cell_id, shift)?;
Ok(format!("={}", f))
}
}
impl SrcCell {
fn cell_id(&self) -> CellId {
match self {
SrcCell::Formula(id) => *id,
_ => unreachable!("cell_id called on non-formula source cell"),
}
}
}
fn infer_axis(src: FillRange, dst: FillRange) -> Result<Axis> {
let same_cols = src.start_col == dst.start_col && src.end_col == dst.end_col;
let same_rows = src.start_row == dst.start_row && src.end_row == dst.end_row;
if same_cols && !same_rows {
Ok(Axis::Vertical)
} else if same_rows && !same_cols {
Ok(Axis::Horizontal)
} else {
Err(Error::PayloadError(
"fill source and target must align on a single axis".to_string(),
))
}
}
fn numeric_series(src_cells: &[SrcCell]) -> Option<(f64, f64)> {
let nums: Option<Vec<f64>> = src_cells
.iter()
.map(|c| match c {
SrcCell::Number(n) => Some(*n),
_ => None,
})
.collect();
let nums = nums?;
let n = nums.len();
if n < 2 {
return None;
}
let step = (nums[n - 1] - nums[0]) / (n as f64 - 1.0);
Some((nums[0], step))
}
fn value_to_input(v: &Value) -> String {
match v {
Value::Str(s) => s.clone(),
Value::Bool(b) => if *b { "TRUE" } else { "FALSE" }.to_string(),
Value::Number(n) => format_number(*n),
Value::Error(s) => s.clone(),
Value::Empty => String::new(),
}
}
fn format_number(n: f64) -> String {
format!("{}", n)
}
#[cfg(test)]
mod tests {
use super::FillRange;
use crate::Workbook;
use crate::edit_action::{CellInput, EditAction, EditPayload, PayloadsAction};
fn input(wb: &mut Workbook, row: usize, col: usize, content: &str) {
wb.handle_action(EditAction::Payloads(PayloadsAction {
payloads: vec![EditPayload::CellInput(CellInput {
sheet_idx: 0,
row,
col,
content: content.to_string(),
})],
undoable: true,
init: false,
}));
}
fn rng(start_row: usize, start_col: usize, end_row: usize, end_col: usize) -> FillRange {
FillRange {
start_row,
start_col,
end_row,
end_col,
}
}
#[test]
fn fill_arithmetic_down() {
let mut wb = Workbook::default();
input(&mut wb, 0, 0, "1");
input(&mut wb, 1, 0, "2");
let res = wb
.predict_fill(0, rng(0, 0, 1, 0), rng(2, 0, 4, 0))
.unwrap();
let contents: Vec<_> = res.iter().map(|c| (c.row, c.content.clone())).collect();
assert_eq!(
contents,
vec![
(2, "3".to_string()),
(3, "4".to_string()),
(4, "5".to_string())
]
);
}
#[test]
fn fill_formula_relative_shift() {
let mut wb = Workbook::default();
input(&mut wb, 0, 1, "=A1*2");
let res = wb
.predict_fill(0, rng(0, 1, 0, 1), rng(1, 1, 2, 1))
.unwrap();
let contents: Vec<_> = res.iter().map(|c| c.content.clone()).collect();
assert_eq!(contents, vec!["=A2 * 2".to_string(), "=A3 * 2".to_string()]);
}
#[test]
fn fill_formula_absolute_ref_unchanged() {
let mut wb = Workbook::default();
input(&mut wb, 0, 1, "=$A$1+A1");
let res = wb
.predict_fill(0, rng(0, 1, 0, 1), rng(1, 1, 1, 1))
.unwrap();
assert_eq!(res[0].content, "=$A$1 + A2".to_string());
}
#[test]
fn fill_single_value_copy() {
let mut wb = Workbook::default();
input(&mut wb, 0, 2, "hello");
let res = wb
.predict_fill(0, rng(0, 2, 0, 2), rng(1, 2, 2, 2))
.unwrap();
let contents: Vec<_> = res.iter().map(|c| c.content.clone()).collect();
assert_eq!(contents, vec!["hello".to_string(), "hello".to_string()]);
}
#[test]
fn fill_single_number_copies_not_increments() {
let mut wb = Workbook::default();
input(&mut wb, 0, 3, "5");
let res = wb
.predict_fill(0, rng(0, 3, 0, 3), rng(1, 3, 2, 3))
.unwrap();
let contents: Vec<_> = res.iter().map(|c| c.content.clone()).collect();
assert_eq!(contents, vec!["5".to_string(), "5".to_string()]);
}
#[test]
fn fill_horizontal_arithmetic() {
let mut wb = Workbook::default();
input(&mut wb, 0, 0, "10");
input(&mut wb, 0, 1, "20");
let res = wb
.predict_fill(0, rng(0, 0, 0, 1), rng(0, 2, 0, 3))
.unwrap();
let contents: Vec<_> = res.iter().map(|c| (c.col, c.content.clone())).collect();
assert_eq!(contents, vec![(2, "30".to_string()), (3, "40".to_string())]);
}
}