//! Fill: copy, AutoFill series (numbers, dates, weekdays, months, text with numbers, custom //! lists), Series dialog and Flash Fill. use std::sync::Arc; use gridcraft_core::date::{MONTHS, WEEKDAYS, datetime_from_serial, days_in_month, serial_from_ymd}; use gridcraft_core::{CellRef, RangeRef, Value}; use gridcraft_model::{Cell, Formula}; use gridcraft_numfmt::{FormatKind, NumberFormat}; use crate::Result; use crate::cmd::Ctx; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum FillMode { Series, /// The Ctrl+drag fill: the opposite of a plain drag. A lone number fills a /// series stepping by 1; anything a plain drag would extend is copied. Alternate, Copy, Formats, /// Fill Without Formatting: extends like a plain drag, the targets keep their formats. ValuesOnly, /// Fill Series from the right-drag menu: extends like a plain drag, and a lone number counts up. Extend, } impl FillMode { /// The modes that continue the source's series rather than copy it. fn extends_series(self) -> bool { matches!(self, FillMode::Series | FillMode::Extend | FillMode::ValuesOnly) } } /// Built-in custom lists. fn lists() -> Vec> { let short_days: Vec = WEEKDAYS.iter().map(|d| d.get(..3).unwrap_or(d).to_string()).collect(); let short_months: Vec = MONTHS.iter().map(|m| m.get(..3).unwrap_or(m).to_string()).collect(); vec![ short_days, WEEKDAYS.iter().map(|s| s.to_string()).collect(), short_months, MONTHS.iter().map(|s| s.to_string()).collect(), vec!["Q1".into(), "Q2".into(), "Q3".into(), "Q4".into()], ] } /// The most cells one fill may write: the size cap the fill commands put on their ranges. pub const MAX_FILL_CELLS: u64 = 5_000_000; /// Whether `target` is a shape Excel's AutoFill accepts for `src`: it contains the source and the /// source sits in one of its corners. Excel refuses a target that doesn't contain the source /// (`2:3` into `A:A`, `H4:H9` into `D:F`) or holds it away from every corner (`H4:H9` into `H:H`). pub fn holds_source_in_corner(src: RangeRef, target: RangeRef) -> bool { target.contains_range(&src) && (target.start.row == src.start.row || target.end.row == src.end.row) && (target.start.col == src.start.col || target.end.col == src.end.col) } /// Fills `target` (which contains `src` at one edge) from the pattern in `src`. pub fn fill(cx: &mut Ctx, sheet: usize, src: RangeRef, target: RangeRef, mode: FillMode) -> Result<()> { let custom = cx.wb.custom_lists.clone(); let Some(sh) = cx.wb.sheet(sheet) else { return Ok(()) }; // Direction: down/up if target extends rows, else right/left. let vertical = target.height() != src.height() || target.width() == src.width(); let forward = if vertical { target.end.row > src.end.row || target.start.row == src.start.row } else { target.end.col > src.end.col || target.start.col == src.start.col }; // Every source lane writes one cell per step past the source: bound that product, not the // source and target separately (whole rows into whole columns was ~17 billion writes). let (lane_count, steps) = if vertical { let steps = if forward { target.end.row.saturating_sub(src.end.row) } else { src.start.row.saturating_sub(target.start.row) }; (src.width(), steps) } else { let steps = if forward { target.end.col.saturating_sub(src.end.col) } else { src.start.col.saturating_sub(target.start.col) }; (src.height(), steps) }; if u64::from(lane_count).saturating_mul(u64::from(steps)) > MAX_FILL_CELLS { return Err(crate::EngineError::Other("The fill is too large.".into())); } let lanes: Vec = if vertical { (src.start.col..=src.end.col).collect() } else { (src.start.row..=src.end.row).collect() }; let src_len = if vertical { src.height() } else { src.width() }; let mut writes: Vec<(CellRef, Option)> = Vec::new(); let mut pictures = Vec::new(); for lane in lanes { let src_cells: Vec<(CellRef, Option)> = (0..src_len) .map(|i| { let c = if vertical { CellRef::new(src.start.row + i, lane) } else { CellRef::new(lane, src.start.col + i) }; (c, sh.cell(c).cloned()) }) .collect(); let targets: Vec = if vertical { let rows: Vec = if forward { (src.end.row + 1..=target.end.row).collect() } else { (target.start.row..src.start.row).rev().collect() }; rows.into_iter().map(|r| CellRef::new(r, lane)).collect() } else { let cols: Vec = if forward { (src.end.col + 1..=target.end.col).collect() } else { (target.start.col..src.start.col).rev().collect() }; cols.into_iter().map(|c| CellRef::new(lane, c)).collect() }; let fmt = |c: &(CellRef, Option)| cx.wb.styles.get(c.1.as_ref().map(|c| c.style).unwrap_or_default()).num_fmt.as_str(); let pattern = if mode.extends_series() || mode == FillMode::Alternate { let detected = Pattern::detect(&src_cells, &custom, cx.wb.date_system, src_cells.first().map(fmt).unwrap_or_default()); match (mode, detected) { (FillMode::Alternate, p) | (FillMode::Extend, p @ Pattern::Copy) => p.alternate(&src_cells), (_, p) => p, } } else { Pattern::Copy }; // A source with no common pattern (`Product 1`, `On backorder`): each cell continues its // own series, one step per repeat of the source. let per_cell: Vec = if mode.extends_series() && matches!(pattern, Pattern::Copy) && src_cells.len() > 1 { src_cells.iter().map(|c| Pattern::detect(std::slice::from_ref(c), &custom, cx.wb.date_system, fmt(c))).collect() } else { Vec::new() }; for (k, t) in targets.iter().enumerate() { let step = k as i64 + 1; let idx = k % src_cells.len().max(1); let src_i = if forward { idx } else { src_cells.len() - 1 - idx }; let (sc, scell) = src_cells.get(src_i).cloned().unwrap_or((src.start, None)); let mut cell = scell.clone().unwrap_or_default(); match mode { FillMode::Formats => { let mut existing = sh.cell(*t).cloned().unwrap_or_default(); existing.style = cell.style; writes.push((*t, Some(existing))); continue; } FillMode::ValuesOnly | FillMode::Series | FillMode::Alternate | FillMode::Copy | FillMode::Extend => { let style = if mode == FillMode::ValuesOnly { sh.cell(*t).map(|c| c.style).unwrap_or_default() } else { cell.style }; if let Some(f) = &cell.formula { if let Some(e) = f.expr() { let shifted = gridcraft_formula::adjust::shift_relative(e, t.row as i64 - sc.row as i64, t.col as i64 - sc.col as i64); cell.formula = Some(Arc::new(Formula::from_expr(shifted))); cell.value = Value::Empty; } } else if let Some(v) = match per_cell.get(src_i) { Some(p) => p.value(k as i64 / src_cells.len() as i64 + 1, forward, 1), None => pattern.value(step, forward, src_cells.len()), } { cell.value = v; } cell.style = style; } } if let Some(picture) = sh.cell_pictures.get(&sc) { pictures.push((*t, picture.clone())); } writes.push((*t, if cell.is_blank() { None } else { Some(cell) })); } } let sh = cx.sheet_mut(sheet)?; for (c, cell) in &writes { match cell { Some(x) if mode == FillMode::Formats => sh.cells.set(*c, x.clone()), Some(x) => sh.set_cell(*c, x.clone()), None => { sh.remove_cell(*c); } } } sh.cell_pictures.extend(pictures); for (c, _) in writes { cx.touch(sheet, c); } Ok(()) } #[derive(Clone, Debug)] enum Pattern { Copy, /// Linear numeric series: last value and step. Linear { last: f64, first: f64, step: f64, }, /// Text with a trailing (or leading) number: `Item 1`, `Q1`. TextNum { prefix: String, suffix: String, last: i64, first: i64, step: i64, width: usize, }, /// A position in a list (days, months, custom). List { list: Vec, last: usize, first: usize, step: i64, upper: bool, lower: bool, }, /// Dates stepping by months (same day each month) or years. Months { last: f64, first: f64, months: i64, sys: gridcraft_core::DateSystem, }, } impl Pattern { fn detect(cells: &[(CellRef, Option)], custom: &[Vec], sys: gridcraft_core::DateSystem, fmt: &str) -> Pattern { let vals: Vec = cells.iter().map(|(_, c)| c.as_ref().map(|c| c.value.clone()).unwrap_or_default()).collect(); if cells.iter().any(|(_, c)| c.as_ref().is_some_and(|c| c.formula.is_some())) { return Pattern::Copy; } // Numbers. if !vals.is_empty() && vals.iter().all(|v| v.is_number()) { let nums: Vec = vals.iter().filter_map(Value::as_f64).collect(); let first = nums.first().copied().unwrap_or(0.0); let last = nums.last().copied().unwrap_or(0.0); let is_date = fmt.contains('d') || fmt.contains('y') || (fmt.contains('m') && !fmt.contains('h')); if nums.len() == 1 { // A single number copies; a single date increments by a day, a single time by an hour. return if is_date { Pattern::Linear { last, first, step: 1.0 } } else if NumberFormat::parse(fmt).kind() == FormatKind::Time { Pattern::Linear { last, first, step: 1.0 / 24.0 } } else { Pattern::Copy }; } // Dates that differ by whole months. if is_date && nums.len() >= 2 { let d0 = datetime_from_serial(sys, first); let d1 = datetime_from_serial(sys, nums.get(1).copied().unwrap_or(first)); if let (Some(a), Some(b)) = (d0, d1) && a.day == b.day && (a.month != b.month || a.year != b.year) { let months = (b.year as i64 - a.year as i64) * 12 + b.month as i64 - a.month as i64; return Pattern::Months { last, first, months, sys }; } } // Least-squares step for 2+ values (Excel uses a linear trend). let n = nums.len() as f64; let mean_x = (n - 1.0) / 2.0; let mean_y = nums.iter().sum::() / n; let mut num = 0.0; let mut den = 0.0; for (i, y) in nums.iter().enumerate() { num += (i as f64 - mean_x) * (y - mean_y); den += (i as f64 - mean_x).powi(2); } let step = if den == 0.0 { 0.0 } else { num / den }; let fitted_last = mean_y + step * (n - 1.0 - mean_x); let fitted_first = mean_y - step * mean_x; return Pattern::Linear { last: fitted_last, first: fitted_first, step }; } // Text. if !vals.is_empty() && vals.iter().all(|v| v.is_text()) { let texts: Vec = vals.iter().filter_map(|v| v.as_text().map(str::to_string)).collect(); let all_lists: Vec> = custom.iter().cloned().chain(lists()).collect(); for list in &all_lists { let idx: Vec> = texts.iter().map(|t| list.iter().position(|l| l.eq_ignore_ascii_case(t))).collect(); if idx.iter().all(Option::is_some) { let idx: Vec = idx.into_iter().flatten().collect(); let first = idx.first().copied().unwrap_or(0); let last = idx.last().copied().unwrap_or(0); let step = if idx.len() >= 2 { idx[1] as i64 - idx[0] as i64 } else { 1 }; let t0 = texts.first().cloned().unwrap_or_default(); let upper = t0.chars().all(|c| !c.is_lowercase()) && t0.chars().any(char::is_alphabetic) && t0.len() > 1; let lower = t0.chars().all(|c| !c.is_uppercase()); return Pattern::List { list: list.clone(), last, first, step, upper, lower }; } } // Text with a number at the end (or start). let split: Vec> = texts.iter().map(|t| split_num(t)).collect(); if split.iter().all(Option::is_some) { let parts: Vec<(String, i64, String, usize)> = split.into_iter().flatten().collect(); if parts.iter().all(|p| p.0 == parts[0].0 && p.2 == parts[0].2) { let first = parts.first().map(|p| p.1).unwrap_or(0); let last = parts.last().map(|p| p.1).unwrap_or(0); let step = if parts.len() >= 2 { parts[1].1 - parts[0].1 } else { 1 }; return Pattern::TextNum { prefix: parts[0].0.clone(), suffix: parts[0].2.clone(), last, first, step, width: parts[0].3 }; } } } Pattern::Copy } /// The pattern of a Ctrl+drag, which swaps copying and extending: a single /// number (which a plain drag copies) counts up by 1, and every series a /// plain drag would extend (numbers, dates, lists, `Item 1`) is copied. fn alternate(self, cells: &[(CellRef, Option)]) -> Pattern { match (self, cells) { (Pattern::Copy, [(_, Some(cell))]) if cell.formula.is_none() => match cell.value.as_f64() { Some(n) if cell.value.is_number() => Pattern::Linear { last: n, first: n, step: 1.0 }, _ => Pattern::Copy, }, _ => Pattern::Copy, } } /// The value `k` steps beyond the source (forward) or before it (backward). fn value(&self, k: i64, forward: bool, _len: usize) -> Option { let sign = if forward { 1 } else { -1 }; match self { Pattern::Copy => None, Pattern::Linear { last, first, step } => { let base = if forward { *last } else { *first }; Some(Value::number(round15(base + step * (k * sign) as f64))) } Pattern::TextNum { prefix, suffix, last, first, step, width } => { let base = if forward { *last } else { *first }; // Text numbers come from input and can sit near i64::MAX (`X 9223372036854775807`): // an overflowing series yields nothing — the cell keeps its source value — rather // than wrapping in release or panicking in debug. let n = step.checked_mul(k)?.checked_mul(sign)?.checked_add(base)?; // `Qtr3`, `Quarter 3` wrap after 4 like `Q3`. let quarter = suffix.is_empty() && (1..=4).contains(&base) && ["q", "qtr", "quarter"].contains(&prefix.trim_end().to_ascii_lowercase().as_str()); let n = if quarter { n.checked_sub(1)?.rem_euclid(4) + 1 } else { n.checked_abs()? }; Some(Value::text(format!("{prefix}{n:0width$}{}", ordinal_suffix(n, suffix), width = *width))) } Pattern::List { list, last, first, step, upper, lower } => { let len = list.len() as i64; if len == 0 { return None; } let base = if forward { *last } else { *first } as i64; let i = (base + step * k * sign).rem_euclid(len) as usize; let s = list.get(i)?.clone(); Some(Value::text(if *upper { s.to_uppercase() } else if *lower && !s.chars().next().is_some_and(char::is_uppercase) { s.to_lowercase() } else { s })) } Pattern::Months { last, first, months, sys } => { let base = if forward { *last } else { *first }; let s = add_months_clamped(*sys, base, months.checked_mul(k * sign)?)?; Some(Value::number(s)) } } } } /// `serial` shifted by `months` calendar months, keeping the day of month but clamping it to the /// target month's last day (Jan 31 + 1 month = Feb 28/29). Always measured from the original date, /// so a series does not drift: Jan 31, Feb 28, Mar 31, Apr 30. fn add_months_clamped(sys: gridcraft_core::DateSystem, serial: f64, months: i64) -> Option { let d = datetime_from_serial(sys, serial)?; let total = (d.year as i64).checked_mul(12)?.checked_add(d.month as i64 - 1)?.checked_add(months)?; let year = total.div_euclid(12); let month = total.rem_euclid(12) + 1; let last = days_in_month(i32::try_from(year).ok()?, month as u32); serial_from_ymd(sys, year, month, (d.day as i64).min(last as i64)) } fn round15(x: f64) -> f64 { if x == 0.0 || !x.is_finite() { return x; } let mag = x.abs().log10().floor() as i32; let p = 10f64.powi(14 - mag); if !p.is_finite() || p == 0.0 { x } else { (x * p).round() / p } } /// `1st Period` → `2nd Period`: an English ordinal ending is rewritten to suit the new number. fn ordinal_suffix(n: i64, suffix: &str) -> String { let (Some(end), Some(rest)) = (suffix.get(..2), suffix.get(2..)) else { return suffix.to_string() }; if !["st", "nd", "rd", "th"].contains(&end.to_ascii_lowercase().as_str()) || rest.starts_with(char::is_alphanumeric) { return suffix.to_string(); } let new = match (n % 10, n % 100) { (_, 11..=13) => "th", (1, _) => "st", (2, _) => "nd", (3, _) => "rd", _ => "th", }; let new = if end.chars().all(|c| c.is_ascii_uppercase()) { new.to_ascii_uppercase() } else { new.to_string() }; format!("{new}{rest}") } /// `Item 007` → ("Item ", 7, "", 3); `3rd` style leading numbers → ("", 3, "rd", 1). fn split_num(t: &str) -> Option<(String, i64, String, usize)> { let chars: Vec = t.chars().collect(); let end = chars.iter().rposition(|c| c.is_ascii_digit())?; let mut start = end; while start > 0 && chars.get(start - 1).is_some_and(|c| c.is_ascii_digit()) { start -= 1; } let digits: String = chars.get(start..=end)?.iter().collect(); let n: i64 = digits.parse().ok()?; let prefix: String = chars.get(..start)?.iter().collect(); let suffix: String = chars.get(end + 1..)?.iter().collect(); let width = if digits.starts_with('0') && digits.len() > 1 { digits.len() } else { 1 }; Some((prefix, n, suffix, width)) } /// Home › Fill › Series. pub fn series(cx: &mut Ctx, sheet: usize, r: RangeRef, rows: bool, kind: &str, step: f64, stop: Option, unit: &str) -> Result<()> { let sys = cx.wb.date_system; let Some(sh) = cx.wb.sheet(sheet) else { return Ok(()) }; let lanes: Vec = if rows { (r.start.row..=r.end.row).collect() } else { (r.start.col..=r.end.col).collect() }; let len = if rows { r.width() } else { r.height() }; let mut writes = Vec::new(); for lane in lanes { let first_cell = if rows { CellRef::new(lane, r.start.col) } else { CellRef::new(r.start.row, lane) }; let Some(start) = sh.value(first_cell).as_f64() else { continue }; let style = sh.style_id(first_cell); let mut v = start; for i in 1..len { let prev = v; v = match kind { "growth" => v * step, "date" => { let d = datetime_from_serial(sys, v); match (unit, d) { ("month", Some(_)) => (step as i64).checked_mul(i as i64).and_then(|m| add_months_clamped(sys, start, m)).unwrap_or(v), ("year", Some(_)) => (step as i64).checked_mul(i as i64 * 12).and_then(|m| add_months_clamped(sys, start, m)).unwrap_or(v), ("weekday", Some(_)) => { let mut n = v; let mut left = step.abs() as i64; while left > 0 { n += step.signum(); let Some(day) = datetime_from_serial(sys, n) else { return Err(crate::EngineError::Other("The weekday series runs past the supported date range.".into())); }; if day.weekday != 0 && day.weekday != 6 { left -= 1; } } n } _ => v + step, } } _ => v + step, }; // The stop value bounds the series in the direction it runs (a growth by 0.5 falls). if let Some(s) = stop && ((v >= prev && v > s) || (v < prev && v < s)) { break; } let c = if rows { CellRef::new(lane, r.start.col + i) } else { CellRef::new(r.start.row + i, lane) }; writes.push((c, Cell { value: Value::number(round15(v)), formula: None, style })); } } let sh = cx.sheet_mut(sheet)?; for (c, cell) in &writes { sh.set_cell(*c, cell.clone()); } for (c, _) in writes { cx.touch(sheet, c); } Ok(()) } /// Flash Fill: learns a transformation from the examples typed in the column of `at` (rows /// above it with values) from the column(s) to the left, and fills the remaining rows. /// Supported transformations: a token (word) of the source by index, concatenations of tokens /// with literal separators, upper/lower/proper case, and fixed-position substrings. pub fn flash_fill(cx: &mut Ctx, sheet: usize, at: CellRef) -> Result { let Some(sh) = cx.wb.sheet(sheet) else { return Ok(0) }; if at.col == 0 { return Ok(0); } let src_col = at.col - 1; // Rows of the data region in the source column. let mut top = at.row; while top > 0 && !sh.value(CellRef::new(top - 1, src_col)).is_empty() { top -= 1; } let mut bottom = at.row; while bottom + 1 < gridcraft_core::MAX_ROWS && !sh.value(CellRef::new(bottom + 1, src_col)).is_empty() && bottom - top < 1_000_000 { bottom += 1; } // Header row: skip a first row whose target is text and source looks like a header? Keep simple. let mut examples = Vec::new(); let mut todo = Vec::new(); // Flash Fill works on the text cells show (a date as formatted, not its serial number). for r in top..=bottom { let src = crate::display::cell_text(&cx.wb, sh, CellRef::new(r, src_col)); if sh.value(CellRef::new(r, at.col)).is_empty() { todo.push((r, src)); } else { examples.push((src, crate::display::cell_text(&cx.wb, sh, CellRef::new(r, at.col)))); } } if examples.is_empty() { return Ok(0); } let Some(program) = learn(&examples) else { return Ok(0) }; let mut n = 0; let style = sh.style_id(CellRef::new(top, at.col)); let mut writes = Vec::new(); for (r, src) in todo { if let Some(out) = program.apply(&src) { writes.push((CellRef::new(r, at.col), out)); n += 1; } } let sh = cx.sheet_mut(sheet)?; for (c, out) in &writes { sh.set_cell(*c, Cell { value: Value::text(out.as_str()), formula: None, style }); } for (c, _) in writes { cx.touch(sheet, c); } Ok(n) } #[derive(Clone, Debug, PartialEq)] enum Piece { Lit(String), /// Token index (negative from the end) with a case transform. Tok(i32, Case), /// First character of a token. Initial(i32, Case), } #[derive(Clone, Copy, Debug, PartialEq)] enum Case { Same, Upper, Lower, Proper, } #[derive(Clone, Debug)] struct Program(Vec); fn tokens(s: &str) -> Vec { s.split(|c: char| !c.is_alphanumeric() && c != '\'').filter(|t| !t.is_empty()).map(str::to_string).collect() } fn apply_case(s: &str, c: Case) -> String { match c { Case::Same => s.to_string(), Case::Upper => s.to_uppercase(), Case::Lower => s.to_lowercase(), Case::Proper => { let mut out = String::new(); for (i, ch) in s.chars().enumerate() { if i == 0 { out.extend(ch.to_uppercase()); } else { out.extend(ch.to_lowercase()); } } out } } } impl Program { fn apply(&self, src: &str) -> Option { let toks = tokens(src); let get = |i: i32| -> Option<&String> { if i >= 0 { toks.get(i as usize) } else { toks.len().checked_sub(i.unsigned_abs() as usize).and_then(|j| toks.get(j)) } }; let mut out = String::new(); for p in &self.0 { match p { Piece::Lit(l) => out.push_str(l), Piece::Tok(i, c) => out.push_str(&apply_case(get(*i)?, *c)), Piece::Initial(i, c) => out.push_str(&apply_case(&get(*i)?.chars().next()?.to_string(), *c)), } } Some(out) } } /// Finds a program consistent with every example. fn learn(examples: &[(String, String)]) -> Option { let (src, dst) = examples.first()?; let candidates = synthesize(src, dst); candidates.into_iter().find(|p| examples.iter().all(|(s, d)| p.apply(s).as_deref() == Some(d.as_str()))) } /// Candidate programs for one example, by greedy segmentation of the output into token matches /// and literals (tries several index conventions). fn synthesize(src: &str, dst: &str) -> Vec { let toks = tokens(src); let n = toks.len() as i32; let mut out = Vec::new(); for prefer_neg in [false, true] { let mut pieces = Vec::new(); let mut rest = dst; let mut guard = 0; while !rest.is_empty() && guard < 64 { guard += 1; // Longest token (with any case) that prefixes `rest`. let mut best: Option<(usize, Piece)> = None; for (i, t) in toks.iter().enumerate() { let idx = if prefer_neg { i as i32 - n } else { i as i32 }; for c in [Case::Same, Case::Upper, Case::Lower, Case::Proper] { let cand = apply_case(t, c); if !cand.is_empty() && rest.starts_with(&cand) && best.as_ref().is_none_or(|(l, _)| cand.len() > *l) { best = Some((cand.len(), Piece::Tok(idx, c))); } } } if best.as_ref().is_none_or(|(l, _)| *l <= 1) { // Initial letters (e.g. "J. Smith"). for (i, t) in toks.iter().enumerate() { let idx = if prefer_neg { i as i32 - n } else { i as i32 }; if let Some(ch) = t.chars().next() { for c in [Case::Same, Case::Upper, Case::Lower] { let cand = apply_case(&ch.to_string(), c); if rest.starts_with(&cand) && best.is_none() { best = Some((cand.len(), Piece::Initial(idx, c))); } } } } } match best { Some((len, piece)) => { pieces.push(piece); rest = rest.get(len..).unwrap_or(""); } None => { let ch = rest.chars().next().unwrap_or(' '); match pieces.last_mut() { Some(Piece::Lit(l)) => l.push(ch), _ => pieces.push(Piece::Lit(ch.to_string())), } rest = rest.get(ch.len_utf8()..).unwrap_or(""); } } } out.push(Program(pieces)); } out } #[cfg(test)] mod tests { use super::*; #[test] fn flash_fill_reads_shown_text() { let mut s = crate::Session::new(); s.new_workbook(); let values = serde_json::json!([["2024-01-15", "15/01/2024"], ["2023-12-03", null]]); s.execute("range.setValues", serde_json::json!({"range": "A1", "values": values})).unwrap(); s.execute("selection.set", serde_json::json!({"cell": "B2"})).unwrap(); s.execute("data.flashFill", serde_json::json!({})).unwrap(); let sh = s.doc().unwrap().wb.active().unwrap(); assert_eq!(sh.value(CellRef::new(1, 1)), Value::from("03/12/2023")); } #[test] fn flash_programs() { let p = learn(&[("John Smith".into(), "Smith, John".into())]).unwrap(); assert_eq!(p.apply("Ada Lovelace").as_deref(), Some("Lovelace, Ada")); let p = learn(&[("jane doe".into(), "JD".into())]).unwrap(); assert_eq!(p.apply("alan turing").as_deref(), Some("AT")); let p = learn(&[("alice@example.com".into(), "alice".into())]).unwrap(); assert_eq!(p.apply("bob@example.com").as_deref(), Some("bob")); } #[test] fn split_numbers() { assert_eq!(split_num("Item 7"), Some(("Item ".into(), 7, "".into(), 1))); assert_eq!(split_num("Q003x"), Some(("Q".into(), 3, "x".into(), 3))); assert_eq!(split_num("abc"), None); } #[test] fn times_quarters_ordinals_and_mixed_sources() { let mut s = crate::Session::new(); s.new_workbook(); let mut fill = |values: serde_json::Value, source: &str, target: &str| { s.execute("range.setValues", serde_json::json!({"range": source.get(..2).unwrap(), "values": values})).unwrap(); s.execute("edit.autoFill", serde_json::json!({"source": source, "target": target})).unwrap(); let sh = s.doc().unwrap().wb.active().unwrap(); let r = RangeRef::parse(target).unwrap(); r.iter().map(|c| crate::display::cell_text(&s.doc().unwrap().wb, sh, c)).collect::>() }; assert_eq!(fill(serde_json::json!([["9:00"]]), "A1", "A1:A3"), ["9:00", "10:00", "11:00"]); assert_eq!(fill(serde_json::json!([["Qtr3"]]), "B1", "B1:B3"), ["Qtr3", "Qtr4", "Qtr1"]); assert_eq!(fill(serde_json::json!([["Quarter 4"]]), "C1", "C1:C2"), ["Quarter 4", "Quarter 1"]); assert_eq!(fill(serde_json::json!([["1st Period"]]), "D1", "D1:D4"), ["1st Period", "2nd Period", "3rd Period", "4th Period"]); assert_eq!( fill(serde_json::json!([["Product 1"], ["On backorder"]]), "E1:E2", "E1:E6"), ["Product 1", "On backorder", "Product 2", "On backorder", "Product 3", "On backorder"] ); } #[test] fn falling_growth_series_stops_at_its_stop_value() { let mut s = crate::Session::new(); s.new_workbook(); s.execute("cell.set", serde_json::json!({"cell": "A1", "input": "5"})).unwrap(); s.execute("edit.fillSeries", serde_json::json!({"range": "A1:A6", "type": "growth", "step": 0.5, "stop": 1})).unwrap(); let d = s.doc().unwrap(); let sh = d.wb.active().unwrap(); let growth: Vec = (1..6).map(|r| crate::display::cell_text(&d.wb, sh, CellRef::new(r, 0))).collect(); assert_eq!(growth, ["2.5", "1.25", "", "", ""]); } #[test] fn text_series_near_i64_max_neither_wraps_nor_panics() { let mut s = crate::Session::new(); s.new_workbook(); s.execute("cell.set", serde_json::json!({"cell": "A1", "input": "X 0"})).unwrap(); s.execute("cell.set", serde_json::json!({"cell": "A2", "input": "X 9223372036854775807"})).unwrap(); // The detected step is i64::MAX, so the next value overflows i64. The cell keeps its // source value instead of panicking in debug or wrapping in release. s.execute("edit.autoFill", serde_json::json!({"source": "A1:A2", "target": "A1:A3"})).unwrap(); let d = s.doc().unwrap(); let sh = d.wb.active().unwrap(); assert_eq!(crate::display::cell_text(&d.wb, sh, CellRef::new(2, 0)), "X 0"); } #[test] fn autofill_refuses_targets_that_do_not_extend_the_source() { // Whole rows into whole columns once filled every source column down the whole sheet // (~17 billion writes) and aborted the process on a 24-51 GB allocation (fuzz seeds 1064, // 1114, 1775). Excel refuses every one of these shapes. let mut s = crate::Session::new(); s.new_workbook(); s.execute("range.setValues", serde_json::json!({"range": "A2", "values": [[1, "Item 1"], [2, "Item 2"]]})).unwrap(); s.execute("range.setValues", serde_json::json!({"range": "H4", "values": [[10], [20]]})).unwrap(); let before = s.doc().unwrap().wb.active().unwrap().cells.len(); for (mode, source, target) in [ ("series", "2:3", "A:A"), ("alternate", "2:3", "A:A"), ("series", "8:10", "C:E"), ("copy", "24:24", "B:D"), ("values", "H4:H9", "D:F"), ("series", "H4:H9", "H:H"), ("series", "A2:A3", "A1:A5"), ("series", "A2:A6", "A2:A4"), ("series", "B2:B3", "A2:A3"), ] { let r = s.execute("edit.autoFill", serde_json::json!({"mode": mode, "source": source, "target": target})); assert!(r.is_err(), "{source} -> {target} should be refused"); } assert_eq!(s.doc().unwrap().wb.active().unwrap().cells.len(), before, "a refused fill writes nothing"); // Targets holding the source in a corner still fill, whole rows and diagonal drags included. s.execute("edit.autoFill", serde_json::json!({"source": "2:3", "target": "2:6"})).unwrap(); s.execute("edit.autoFill", serde_json::json!({"source": "A2:A3", "target": "A1:A3"})).unwrap(); let sh = s.doc().unwrap().wb.active().unwrap(); let text = |a: &str| crate::display::cell_text(&s.doc().unwrap().wb, sh, CellRef::parse(a).unwrap()); assert_eq!([text("A1"), text("A6"), text("B6")], ["0", "5", "Item 5"]); assert!(holds_source_in_corner(RangeRef::parse("B5").unwrap(), RangeRef::parse("B3:B5").unwrap())); assert!(holds_source_in_corner(RangeRef::parse("A:A").unwrap(), RangeRef::parse("A:C").unwrap())); assert!(holds_source_in_corner(RangeRef::parse("B5:C9").unwrap(), RangeRef::parse("B5:D13").unwrap())); assert!(holds_source_in_corner(RangeRef::parse("B1:B2").unwrap(), RangeRef::parse("A1:B4").unwrap())); assert!(!holds_source_in_corner(RangeRef::parse("A:A").unwrap(), RangeRef::parse("1:1").unwrap())); } }