Skip to main content

ravenrs_sheet/engine/
fill.rs

1//! Fill: copy, AutoFill series (numbers, dates, weekdays, months, text with numbers, custom
2//! lists), Series dialog and Flash Fill.
3
4use std::sync::Arc;
5
6use crate::base::date::{MONTHS, WEEKDAYS, datetime_from_serial, days_in_month, serial_from_ymd};
7use crate::base::{CellRef, RangeRef, Value};
8use crate::model::{Cell, Formula, Workbook};
9use crate::numfmt::{FormatKind, NumberFormat};
10
11use crate::engine::Result;
12use crate::engine::cmd::Ctx;
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum FillMode {
16    Series,
17    /// The Ctrl+drag fill: the opposite of a plain drag. A lone number fills a
18    /// series stepping by 1; anything a plain drag would extend is copied.
19    Alternate,
20    Copy,
21    Formats,
22    /// Fill Without Formatting: extends like a plain drag, the targets keep their formats.
23    ValuesOnly,
24    /// Fill Series from the right-drag menu: extends like a plain drag, and a lone number counts up.
25    Extend,
26    /// Fill Days, Fill Weekdays, Fill Months or Fill Years: a source of dates steps by that unit.
27    /// A lone date steps by one unit; two or more step by the difference between the first two,
28    /// counted in that unit (one unit when they fall in the same one). A source that isn't dates
29    /// extends like a plain drag.
30    Dates(DateUnit),
31}
32
33/// The step of a [`FillMode::Dates`] fill.
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum DateUnit {
36    Day,
37    /// Monday to Friday: Saturday and Sunday are skipped.
38    Weekday,
39    Month,
40    Year,
41}
42
43impl FillMode {
44    /// The modes that continue the source's series rather than copy it.
45    fn extends_series(self) -> bool {
46        matches!(self, FillMode::Series | FillMode::Extend | FillMode::ValuesOnly | FillMode::Dates(_))
47    }
48}
49
50/// Whether a number format shows a calendar date (`YYYY-MM-DD` and `m/d/yyyy h:mm` do; `0 "days"`
51/// and `h:mm` don't).
52fn is_date_format(fmt: &str) -> bool {
53    NumberFormat::parse(fmt).kind() == FormatKind::Date
54}
55
56/// Whether the first cell of `src` holds a date, so a fill from it can step by days, weekdays,
57/// months or years: a number (not a formula) shown in a date format.
58pub fn starts_with_date(wb: &Workbook, sheet: usize, src: RangeRef) -> bool {
59    let Some(cell) = wb.sheet(sheet).and_then(|sh| sh.cell(src.start)) else { return false };
60    cell.formula.is_none() && cell.value.is_number() && is_date_format(wb.styles.get(cell.style).num_fmt.as_str())
61}
62
63/// Built-in custom lists.
64fn lists() -> Vec<Vec<String>> {
65    let short_days: Vec<String> = WEEKDAYS.iter().map(|d| d.get(..3).unwrap_or(d).to_string()).collect();
66    let short_months: Vec<String> = MONTHS.iter().map(|m| m.get(..3).unwrap_or(m).to_string()).collect();
67    vec![
68        short_days,
69        WEEKDAYS.iter().map(|s| s.to_string()).collect(),
70        short_months,
71        MONTHS.iter().map(|s| s.to_string()).collect(),
72        vec!["Q1".into(), "Q2".into(), "Q3".into(), "Q4".into()],
73    ]
74}
75
76/// The most cells one fill may write: the size cap the fill commands put on their ranges.
77pub const MAX_FILL_CELLS: u64 = 5_000_000;
78
79/// Whether `target` is a shape Excel's AutoFill accepts for `src`: it contains the source and the
80/// source sits in one of its corners. Excel refuses a target that doesn't contain the source
81/// (`2:3` into `A:A`, `H4:H9` into `D:F`) or holds it away from every corner (`H4:H9` into `H:H`).
82pub fn holds_source_in_corner(src: RangeRef, target: RangeRef) -> bool {
83    target.contains_range(&src)
84        && (target.start.row == src.start.row || target.end.row == src.end.row)
85        && (target.start.col == src.start.col || target.end.col == src.end.col)
86}
87
88/// Fills `target` (which contains `src` at one edge) from the pattern in `src`.
89pub fn fill(cx: &mut Ctx, sheet: usize, src: RangeRef, target: RangeRef, mode: FillMode) -> Result<()> {
90    let custom = cx.wb.custom_lists.clone();
91    let Some(sh) = cx.wb.sheet(sheet) else { return Ok(()) };
92    // Direction: down/up if target extends rows, else right/left.
93    let vertical = target.height() != src.height() || target.width() == src.width();
94    let forward = if vertical {
95        target.end.row > src.end.row || target.start.row == src.start.row
96    } else {
97        target.end.col > src.end.col || target.start.col == src.start.col
98    };
99    // Every source lane writes one cell per step past the source: bound that product, not the
100    // source and target separately (whole rows into whole columns was ~17 billion writes).
101    let (lane_count, steps) = if vertical {
102        let steps = if forward { target.end.row.saturating_sub(src.end.row) } else { src.start.row.saturating_sub(target.start.row) };
103        (src.width(), steps)
104    } else {
105        let steps = if forward { target.end.col.saturating_sub(src.end.col) } else { src.start.col.saturating_sub(target.start.col) };
106        (src.height(), steps)
107    };
108    if u64::from(lane_count).saturating_mul(u64::from(steps)) > MAX_FILL_CELLS {
109        return Err(crate::engine::EngineError::Other("The fill is too large.".into()));
110    }
111    let lanes: Vec<u32> = if vertical { (src.start.col..=src.end.col).collect() } else { (src.start.row..=src.end.row).collect() };
112    let src_len = if vertical { src.height() } else { src.width() };
113    let mut writes: Vec<(CellRef, Option<Cell>)> = Vec::new();
114    let mut pictures = Vec::new();
115    for lane in lanes {
116        let src_cells: Vec<(CellRef, Option<Cell>)> = (0..src_len)
117            .map(|i| {
118                let c = if vertical { CellRef::new(src.start.row + i, lane) } else { CellRef::new(lane, src.start.col + i) };
119                (c, sh.cell(c).cloned())
120            })
121            .collect();
122        let targets: Vec<CellRef> = if vertical {
123            let rows: Vec<u32> =
124                if forward { (src.end.row + 1..=target.end.row).collect() } else { (target.start.row..src.start.row).rev().collect() };
125            rows.into_iter().map(|r| CellRef::new(r, lane)).collect()
126        } else {
127            let cols: Vec<u32> =
128                if forward { (src.end.col + 1..=target.end.col).collect() } else { (target.start.col..src.start.col).rev().collect() };
129            cols.into_iter().map(|c| CellRef::new(lane, c)).collect()
130        };
131        let fmt = |c: &(CellRef, Option<Cell>)| cx.wb.styles.get(c.1.as_ref().map(|c| c.style).unwrap_or_default()).num_fmt.as_str();
132        let pattern = if mode.extends_series() || mode == FillMode::Alternate {
133            let first_fmt = src_cells.first().map(fmt).unwrap_or_default();
134            let dates = match mode {
135                FillMode::Dates(unit) => Pattern::dates(&src_cells, unit, cx.wb.date_system, first_fmt),
136                _ => None,
137            };
138            let detected = dates.unwrap_or_else(|| Pattern::detect(&src_cells, &custom, cx.wb.date_system, first_fmt));
139            match (mode, detected) {
140                (FillMode::Alternate, p) | (FillMode::Extend, p @ Pattern::Copy) => p.alternate(&src_cells),
141                (_, p) => p,
142            }
143        } else {
144            Pattern::Copy
145        };
146        // A source with no common pattern (`Product 1`, `On backorder`): each cell continues its
147        // own series, one step per repeat of the source.
148        let per_cell: Vec<Pattern> = if mode.extends_series() && matches!(pattern, Pattern::Copy) && src_cells.len() > 1 {
149            src_cells.iter().map(|c| Pattern::detect(std::slice::from_ref(c), &custom, cx.wb.date_system, fmt(c))).collect()
150        } else {
151            Vec::new()
152        };
153        for (k, t) in targets.iter().enumerate() {
154            let step = k as i64 + 1;
155            let idx = k % src_cells.len().max(1);
156            let src_i = if forward { idx } else { src_cells.len() - 1 - idx };
157            let (sc, scell) = src_cells.get(src_i).cloned().unwrap_or((src.start, None));
158            let mut cell = scell.clone().unwrap_or_default();
159            match mode {
160                FillMode::Formats => {
161                    let mut existing = sh.cell(*t).cloned().unwrap_or_default();
162                    existing.style = cell.style;
163                    writes.push((*t, Some(existing)));
164                    continue;
165                }
166                FillMode::ValuesOnly | FillMode::Series | FillMode::Alternate | FillMode::Copy | FillMode::Extend | FillMode::Dates(_) => {
167                    let style = if mode == FillMode::ValuesOnly { sh.cell(*t).map(|c| c.style).unwrap_or_default() } else { cell.style };
168                    if let Some(f) = &cell.formula {
169                        if let Some(e) = f.expr() {
170                            let shifted = crate::formula::adjust::shift_relative(e, t.row as i64 - sc.row as i64, t.col as i64 - sc.col as i64);
171                            cell.formula = Some(Arc::new(Formula::from_expr(shifted)));
172                            cell.value = Value::Empty;
173                        }
174                    } else if cell.py.is_none()
175                        && let Some(v) = match per_cell.get(src_i) {
176                            Some(p) => p.value(k as i64 / src_cells.len() as i64 + 1, forward, 1),
177                            None => pattern.value(step, forward, src_cells.len()),
178                        }
179                    {
180                        cell.value = v;
181                    }
182                    cell.style = style;
183                }
184            }
185            if let Some(picture) = sh.cell_pictures.get(&sc) {
186                pictures.push((*t, picture.clone()));
187            }
188            writes.push((*t, if cell.is_blank() { None } else { Some(cell) }));
189        }
190    }
191    let sh = cx.sheet_mut(sheet)?;
192    for (c, cell) in &writes {
193        match cell {
194            Some(x) if mode == FillMode::Formats => sh.cells.set(*c, x.clone()),
195            Some(x) => sh.set_cell(*c, x.clone()),
196            None => {
197                sh.remove_cell(*c);
198            }
199        }
200    }
201    sh.cell_pictures.extend(pictures);
202    for (c, _) in writes {
203        cx.touch(sheet, c);
204    }
205    Ok(())
206}
207
208#[derive(Clone, Debug)]
209enum Pattern {
210    Copy,
211    /// Linear numeric series: last value and step.
212    Linear {
213        last: f64,
214        first: f64,
215        step: f64,
216    },
217    /// Text with a trailing (or leading) number: `Item 1`, `Q1`.
218    TextNum {
219        prefix: String,
220        suffix: String,
221        last: i64,
222        first: i64,
223        step: i64,
224        width: usize,
225    },
226    /// A position in a list (days, months, custom).
227    List {
228        list: Vec<String>,
229        last: usize,
230        first: usize,
231        step: i64,
232        upper: bool,
233        lower: bool,
234    },
235    /// Dates stepping by months (same day each month) or years.
236    Months {
237        last: f64,
238        first: f64,
239        months: i64,
240        sys: crate::base::DateSystem,
241    },
242    /// Dates stepping by weekdays, skipping Saturday and Sunday.
243    Weekdays {
244        last: f64,
245        first: f64,
246        step: i64,
247        sys: crate::base::DateSystem,
248    },
249}
250
251impl Pattern {
252    /// The pattern of Fill Days, Fill Weekdays, Fill Months or Fill Years, or `None` when the
253    /// source isn't all dates (numbers, not formulas, under a date format in its first cell).
254    fn dates(cells: &[(CellRef, Option<Cell>)], unit: DateUnit, sys: crate::base::DateSystem, fmt: &str) -> Option<Pattern> {
255        if !is_date_format(fmt) {
256            return None;
257        }
258        let mut nums = Vec::with_capacity(cells.len());
259        for (_, cell) in cells {
260            let cell = cell.as_ref()?;
261            if cell.formula.is_some() || !cell.value.is_number() {
262                return None;
263            }
264            nums.push(cell.value.as_f64()?);
265        }
266        let first = *nums.first()?;
267        let last = *nums.last()?;
268        let second = nums.get(1).copied();
269        // The difference between the first two dates in the unit, and one unit when they fall in
270        // the same one.
271        let at_least_one = |n: i64| if n == 0 { 1 } else { n };
272        Some(match unit {
273            DateUnit::Day => {
274                let days = second.map_or(1.0, |b| b.floor() - first.floor());
275                Pattern::Linear { last, first, step: if days == 0.0 { 1.0 } else { days } }
276            }
277            DateUnit::Weekday => {
278                let step = match second {
279                    Some(b) => at_least_one(weekdays_between(sys, first, b)?),
280                    None => 1,
281                };
282                Pattern::Weekdays { last, first, step, sys }
283            }
284            DateUnit::Month | DateUnit::Year => {
285                let months = match second {
286                    Some(b) => {
287                        let (a, b) = (datetime_from_serial(sys, first)?, datetime_from_serial(sys, b)?);
288                        let years = b.year as i64 - a.year as i64;
289                        if unit == DateUnit::Year { at_least_one(years) * 12 } else { at_least_one(years * 12 + b.month as i64 - a.month as i64) }
290                    }
291                    None if unit == DateUnit::Year => 12,
292                    None => 1,
293                };
294                Pattern::Months { last, first, months, sys }
295            }
296        })
297    }
298
299    fn detect(cells: &[(CellRef, Option<Cell>)], custom: &[Vec<String>], sys: crate::base::DateSystem, fmt: &str) -> Pattern {
300        let vals: Vec<Value> = cells.iter().map(|(_, c)| c.as_ref().map(|c| c.value.clone()).unwrap_or_default()).collect();
301        // Formulas and Python cells copy.
302        if cells.iter().any(|(_, c)| c.as_ref().is_some_and(|c| c.formula.is_some() || c.py.is_some())) {
303            return Pattern::Copy;
304        }
305        // Numbers.
306        if !vals.is_empty() && vals.iter().all(|v| v.is_number()) {
307            let nums: Vec<f64> = vals.iter().filter_map(Value::as_f64).collect();
308            let first = nums.first().copied().unwrap_or(0.0);
309            let last = nums.last().copied().unwrap_or(0.0);
310            let is_date = fmt.contains('d') || fmt.contains('y') || (fmt.contains('m') && !fmt.contains('h'));
311            if nums.len() == 1 {
312                // A single number copies; a single date increments by a day, a single time by an hour.
313                return if is_date {
314                    Pattern::Linear { last, first, step: 1.0 }
315                } else if NumberFormat::parse(fmt).kind() == FormatKind::Time {
316                    Pattern::Linear { last, first, step: 1.0 / 24.0 }
317                } else {
318                    Pattern::Copy
319                };
320            }
321            // Dates that differ by whole months.
322            if is_date && nums.len() >= 2 {
323                let d0 = datetime_from_serial(sys, first);
324                let d1 = datetime_from_serial(sys, nums.get(1).copied().unwrap_or(first));
325                if let (Some(a), Some(b)) = (d0, d1)
326                    && a.day == b.day
327                    && (a.month != b.month || a.year != b.year)
328                {
329                    let months = (b.year as i64 - a.year as i64) * 12 + b.month as i64 - a.month as i64;
330                    return Pattern::Months { last, first, months, sys };
331                }
332            }
333            // Least-squares step for 2+ values (Excel uses a linear trend).
334            let n = nums.len() as f64;
335            let mean_x = (n - 1.0) / 2.0;
336            let mean_y = nums.iter().sum::<f64>() / n;
337            let mut num = 0.0;
338            let mut den = 0.0;
339            for (i, y) in nums.iter().enumerate() {
340                num += (i as f64 - mean_x) * (y - mean_y);
341                den += (i as f64 - mean_x).powi(2);
342            }
343            let step = if den == 0.0 { 0.0 } else { num / den };
344            let fitted_last = mean_y + step * (n - 1.0 - mean_x);
345            let fitted_first = mean_y - step * mean_x;
346            return Pattern::Linear { last: fitted_last, first: fitted_first, step };
347        }
348        // Text.
349        if !vals.is_empty() && vals.iter().all(|v| v.is_text()) {
350            let texts: Vec<String> = vals.iter().filter_map(|v| v.as_text().map(str::to_string)).collect();
351            let all_lists: Vec<Vec<String>> = custom.iter().cloned().chain(lists()).collect();
352            for list in &all_lists {
353                let idx: Vec<Option<usize>> = texts.iter().map(|t| list.iter().position(|l| l.eq_ignore_ascii_case(t))).collect();
354                if idx.iter().all(Option::is_some) {
355                    let idx: Vec<usize> = idx.into_iter().flatten().collect();
356                    let first = idx.first().copied().unwrap_or(0);
357                    let last = idx.last().copied().unwrap_or(0);
358                    let step = if idx.len() >= 2 { idx[1] as i64 - idx[0] as i64 } else { 1 };
359                    let t0 = texts.first().cloned().unwrap_or_default();
360                    let upper = t0.chars().all(|c| !c.is_lowercase()) && t0.chars().any(char::is_alphabetic) && t0.len() > 1;
361                    let lower = t0.chars().all(|c| !c.is_uppercase());
362                    return Pattern::List { list: list.clone(), last, first, step, upper, lower };
363                }
364            }
365            // Text with a number at the end (or start).
366            let split: Vec<Option<(String, i64, String, usize)>> = texts.iter().map(|t| split_num(t)).collect();
367            if split.iter().all(Option::is_some) {
368                let parts: Vec<(String, i64, String, usize)> = split.into_iter().flatten().collect();
369                if parts.iter().all(|p| p.0 == parts[0].0 && p.2 == parts[0].2) {
370                    let first = parts.first().map(|p| p.1).unwrap_or(0);
371                    let last = parts.last().map(|p| p.1).unwrap_or(0);
372                    let step = if parts.len() >= 2 { parts[1].1 - parts[0].1 } else { 1 };
373                    return Pattern::TextNum { prefix: parts[0].0.clone(), suffix: parts[0].2.clone(), last, first, step, width: parts[0].3 };
374                }
375            }
376        }
377        Pattern::Copy
378    }
379
380    /// The pattern of a Ctrl+drag, which swaps copying and extending: a single
381    /// number (which a plain drag copies) counts up by 1, and every series a
382    /// plain drag would extend (numbers, dates, lists, `Item 1`) is copied.
383    fn alternate(self, cells: &[(CellRef, Option<Cell>)]) -> Pattern {
384        match (self, cells) {
385            (Pattern::Copy, [(_, Some(cell))]) if cell.formula.is_none() => match cell.value.as_f64() {
386                Some(n) if cell.value.is_number() => Pattern::Linear { last: n, first: n, step: 1.0 },
387                _ => Pattern::Copy,
388            },
389            _ => Pattern::Copy,
390        }
391    }
392
393    /// The value `k` steps beyond the source (forward) or before it (backward).
394    fn value(&self, k: i64, forward: bool, _len: usize) -> Option<Value> {
395        let sign = if forward { 1 } else { -1 };
396        match self {
397            Pattern::Copy => None,
398            Pattern::Linear { last, first, step } => {
399                let base = if forward { *last } else { *first };
400                Some(Value::number(round15(base + step * (k * sign) as f64)))
401            }
402            Pattern::TextNum { prefix, suffix, last, first, step, width } => {
403                let base = if forward { *last } else { *first };
404                // Text numbers come from input and can sit near i64::MAX (`X 9223372036854775807`):
405                // an overflowing series yields nothing — the cell keeps its source value — rather
406                // than wrapping in release or panicking in debug.
407                let n = step.checked_mul(k)?.checked_mul(sign)?.checked_add(base)?;
408                // `Qtr3`, `Quarter 3` wrap after 4 like `Q3`.
409                let quarter = suffix.is_empty()
410                    && (1..=4).contains(&base)
411                    && ["q", "qtr", "quarter"].contains(&prefix.trim_end().to_ascii_lowercase().as_str());
412                let n = if quarter { n.checked_sub(1)?.rem_euclid(4) + 1 } else { n.checked_abs()? };
413                Some(Value::text(format!("{prefix}{n:0width$}{}", ordinal_suffix(n, suffix), width = *width)))
414            }
415            Pattern::List { list, last, first, step, upper, lower } => {
416                let len = list.len() as i64;
417                if len == 0 {
418                    return None;
419                }
420                let base = if forward { *last } else { *first } as i64;
421                let i = (base + step * k * sign).rem_euclid(len) as usize;
422                let s = list.get(i)?.clone();
423                Some(Value::text(if *upper {
424                    s.to_uppercase()
425                } else if *lower && !s.chars().next().is_some_and(char::is_uppercase) {
426                    s.to_lowercase()
427                } else {
428                    s
429                }))
430            }
431            Pattern::Months { last, first, months, sys } => {
432                let base = if forward { *last } else { *first };
433                let s = add_months_clamped(*sys, base, months.checked_mul(k * sign)?)?;
434                Some(Value::number(s))
435            }
436            Pattern::Weekdays { last, first, step, sys } => {
437                let base = if forward { *last } else { *first };
438                Some(Value::number(add_weekdays(*sys, base, step.checked_mul(k)?.checked_mul(sign)?)?))
439            }
440        }
441    }
442}
443
444/// Whether `serial` falls on Monday to Friday; `None` outside the supported date range.
445fn is_weekday(sys: crate::base::DateSystem, serial: f64) -> Option<bool> {
446    datetime_from_serial(sys, serial).map(|d| d.weekday != 0 && d.weekday != 6)
447}
448
449/// `serial` moved by `n` weekdays (back when `n` is negative), keeping its time of day. A start on
450/// a Saturday or Sunday moves to the next (or previous) weekday for the first step.
451fn add_weekdays(sys: crate::base::DateSystem, serial: f64, n: i64) -> Option<f64> {
452    if n == 0 {
453        return Some(serial);
454    }
455    let dir = n.signum() as f64;
456    let mut d = serial;
457    let next_weekday = |d: &mut f64| -> Option<()> {
458        loop {
459            *d += dir;
460            if is_weekday(sys, *d)? {
461                return Some(());
462            }
463        }
464    };
465    next_weekday(&mut d)?;
466    // From a weekday, every seven days hold five weekdays.
467    let rest = n.unsigned_abs() - 1;
468    d += dir * (rest / 5 * 7) as f64;
469    for _ in 0..rest % 5 {
470        next_weekday(&mut d)?;
471    }
472    datetime_from_serial(sys, d)?;
473    Some(d)
474}
475
476/// How many weekdays lie after `a` up to and including `b` (negative when `b` comes first),
477/// counting whole days; `None` outside the supported date range.
478fn weekdays_between(sys: crate::base::DateSystem, a: f64, b: f64) -> Option<i64> {
479    let (lo, hi, sign) = if b >= a { (a.floor(), b.floor(), 1) } else { (b.floor(), a.floor(), -1) };
480    let days = (hi - lo) as i64;
481    let mut count = days / 7 * 5;
482    let mut d = lo + (days / 7 * 7) as f64;
483    for _ in 0..days % 7 {
484        d += 1.0;
485        if is_weekday(sys, d)? {
486            count += 1;
487        }
488    }
489    Some(count * sign)
490}
491
492/// `serial` shifted by `months` calendar months, keeping the day of month but clamping it to the
493/// target month's last day (Jan 31 + 1 month = Feb 28/29). Always measured from the original date,
494/// so a series does not drift: Jan 31, Feb 28, Mar 31, Apr 30. The time of day is kept.
495fn add_months_clamped(sys: crate::base::DateSystem, serial: f64, months: i64) -> Option<f64> {
496    let d = datetime_from_serial(sys, serial)?;
497    let total = (d.year as i64).checked_mul(12)?.checked_add(d.month as i64 - 1)?.checked_add(months)?;
498    let year = total.div_euclid(12);
499    let month = total.rem_euclid(12) + 1;
500    let last = days_in_month(i32::try_from(year).ok()?, month as u32);
501    serial_from_ymd(sys, year, month, (d.day as i64).min(last as i64)).map(|day| day + serial - serial.floor())
502}
503
504fn round15(x: f64) -> f64 {
505    if x == 0.0 || !x.is_finite() {
506        return x;
507    }
508    let mag = x.abs().log10().floor() as i32;
509    let p = 10f64.powi(14 - mag);
510    if !p.is_finite() || p == 0.0 { x } else { (x * p).round() / p }
511}
512
513/// `1st Period` → `2nd Period`: an English ordinal ending is rewritten to suit the new number.
514fn ordinal_suffix(n: i64, suffix: &str) -> String {
515    let (Some(end), Some(rest)) = (suffix.get(..2), suffix.get(2..)) else { return suffix.to_string() };
516    if !["st", "nd", "rd", "th"].contains(&end.to_ascii_lowercase().as_str()) || rest.starts_with(char::is_alphanumeric) {
517        return suffix.to_string();
518    }
519    let new = match (n % 10, n % 100) {
520        (_, 11..=13) => "th",
521        (1, _) => "st",
522        (2, _) => "nd",
523        (3, _) => "rd",
524        _ => "th",
525    };
526    let new = if end.chars().all(|c| c.is_ascii_uppercase()) { new.to_ascii_uppercase() } else { new.to_string() };
527    format!("{new}{rest}")
528}
529
530/// `Item 007` → ("Item ", 7, "", 3); `3rd` style leading numbers → ("", 3, "rd", 1).
531fn split_num(t: &str) -> Option<(String, i64, String, usize)> {
532    let chars: Vec<char> = t.chars().collect();
533    let end = chars.iter().rposition(char::is_ascii_digit)?;
534    let mut start = end;
535    while start > 0 && chars.get(start - 1).is_some_and(char::is_ascii_digit) {
536        start -= 1;
537    }
538    let digits: String = chars.get(start..=end)?.iter().collect();
539    let n: i64 = digits.parse().ok()?;
540    let prefix: String = chars.get(..start)?.iter().collect();
541    let suffix: String = chars.get(end + 1..)?.iter().collect();
542    let width = if digits.starts_with('0') && digits.len() > 1 { digits.len() } else { 1 };
543    Some((prefix, n, suffix, width))
544}
545
546/// Options for filling a numeric or date series.
547pub struct SeriesOptions<'a> {
548    pub range: RangeRef,
549    pub rows: bool,
550    pub kind: &'a str,
551    pub step: f64,
552    pub stop: Option<f64>,
553    pub unit: &'a str,
554}
555
556/// Home › Fill › Series.
557pub fn series(cx: &mut Ctx, sheet: usize, options: SeriesOptions<'_>) -> Result<()> {
558    let SeriesOptions { range: r, rows, kind, step, stop, unit } = options;
559    let sys = cx.wb.date_system;
560    let Some(sh) = cx.wb.sheet(sheet) else { return Ok(()) };
561    let lanes: Vec<u32> = if rows { (r.start.row..=r.end.row).collect() } else { (r.start.col..=r.end.col).collect() };
562    let len = if rows { r.width() } else { r.height() };
563    let mut writes = Vec::new();
564    for lane in lanes {
565        let first_cell = if rows { CellRef::new(lane, r.start.col) } else { CellRef::new(r.start.row, lane) };
566        let Some(start) = sh.value(first_cell).as_f64() else { continue };
567        let style = sh.style_id(first_cell);
568        let mut v = start;
569        for i in 1..len {
570            let prev = v;
571            v = match kind {
572                "growth" => v * step,
573                "date" => {
574                    let d = datetime_from_serial(sys, v);
575                    match (unit, d) {
576                        ("month", Some(_)) => (step as i64).checked_mul(i as i64).and_then(|m| add_months_clamped(sys, start, m)).unwrap_or(v),
577                        ("year", Some(_)) => (step as i64).checked_mul(i as i64 * 12).and_then(|m| add_months_clamped(sys, start, m)).unwrap_or(v),
578                        // Whole weeks at a time: a huge step costs no more than a small one.
579                        ("weekday", Some(_)) => add_weekdays(sys, v, step as i64)
580                            .ok_or_else(|| crate::engine::EngineError::Other("The weekday series runs past the supported date range.".into()))?,
581                        _ => v + step,
582                    }
583                }
584                _ => v + step,
585            };
586            // The stop value bounds the series in the direction it runs (a growth by 0.5 falls).
587            if let Some(s) = stop
588                && ((v >= prev && round15(v) > s) || (v < prev && round15(v) < s))
589            {
590                break;
591            }
592            let c = if rows { CellRef::new(lane, r.start.col + i) } else { CellRef::new(r.start.row + i, lane) };
593            writes.push((c, Cell { value: Value::number(round15(v)), formula: None, style, py: None }));
594        }
595    }
596    let sh = cx.sheet_mut(sheet)?;
597    for (c, cell) in &writes {
598        sh.set_cell(*c, cell.clone());
599    }
600    for (c, _) in writes {
601        cx.touch(sheet, c);
602    }
603    Ok(())
604}
605
606/// Flash Fill: learns a transformation from the examples typed in the column of `at` (rows
607/// above it with values) from the column(s) to the left, and fills the remaining rows.
608/// Supported transformations: a token (word) of the source by index, concatenations of tokens
609/// with literal separators, upper/lower/proper case, and fixed-position substrings.
610pub fn flash_fill(cx: &mut Ctx, sheet: usize, at: CellRef) -> Result<usize> {
611    let Some(sh) = cx.wb.sheet(sheet) else { return Ok(0) };
612    if at.col == 0 {
613        return Ok(0);
614    }
615    let src_col = at.col - 1;
616    // Rows of the data region in the source column.
617    let mut top = at.row;
618    while top > 0 && !sh.value(CellRef::new(top - 1, src_col)).is_empty() {
619        top -= 1;
620    }
621    let mut bottom = at.row;
622    while bottom + 1 < crate::base::MAX_ROWS && !sh.value(CellRef::new(bottom + 1, src_col)).is_empty() && bottom - top < 1_000_000 {
623        bottom += 1;
624    }
625    // Header row: skip a first row whose target is text and source looks like a header? Keep simple.
626    let mut examples = Vec::new();
627    let mut todo = Vec::new();
628    // Flash Fill works on the text cells show (a date as formatted, not its serial number).
629    for r in top..=bottom {
630        let src = crate::engine::display::cell_text(&cx.wb, sh, CellRef::new(r, src_col));
631        if sh.value(CellRef::new(r, at.col)).is_empty() {
632            todo.push((r, src));
633        } else {
634            examples.push((src, crate::engine::display::cell_text(&cx.wb, sh, CellRef::new(r, at.col))));
635        }
636    }
637    if examples.is_empty() {
638        return Ok(0);
639    }
640    let Some(program) = learn(&examples) else { return Ok(0) };
641    let mut n = 0;
642    let style = sh.style_id(CellRef::new(top, at.col));
643    let mut writes = Vec::new();
644    for (r, src) in todo {
645        if let Some(out) = program.apply(&src) {
646            writes.push((CellRef::new(r, at.col), out));
647            n += 1;
648        }
649    }
650    let sh = cx.sheet_mut(sheet)?;
651    for (c, out) in &writes {
652        sh.set_cell(*c, Cell { value: Value::text(out.as_str()), formula: None, style, py: None });
653    }
654    for (c, _) in writes {
655        cx.touch(sheet, c);
656    }
657    Ok(n)
658}
659
660#[derive(Clone, Debug, PartialEq)]
661enum Piece {
662    Lit(String),
663    /// Token index (negative from the end) with a case transform.
664    Tok(i32, Case),
665    /// First character of a token.
666    Initial(i32, Case),
667}
668
669#[derive(Clone, Copy, Debug, PartialEq)]
670enum Case {
671    Same,
672    Upper,
673    Lower,
674    Proper,
675}
676
677#[derive(Clone, Debug)]
678struct Program(Vec<Piece>);
679
680fn tokens(s: &str) -> Vec<String> {
681    s.split(|c: char| !c.is_alphanumeric() && c != '\'').filter(|t| !t.is_empty()).map(str::to_string).collect()
682}
683
684fn apply_case(s: &str, c: Case) -> String {
685    match c {
686        Case::Same => s.to_string(),
687        Case::Upper => s.to_uppercase(),
688        Case::Lower => s.to_lowercase(),
689        Case::Proper => {
690            let mut out = String::new();
691            for (i, ch) in s.chars().enumerate() {
692                if i == 0 {
693                    out.extend(ch.to_uppercase());
694                } else {
695                    out.extend(ch.to_lowercase());
696                }
697            }
698            out
699        }
700    }
701}
702
703impl Program {
704    fn apply(&self, src: &str) -> Option<String> {
705        let toks = tokens(src);
706        let get = |i: i32| -> Option<&String> {
707            if i >= 0 { toks.get(i as usize) } else { toks.len().checked_sub(i.unsigned_abs() as usize).and_then(|j| toks.get(j)) }
708        };
709        let mut out = String::new();
710        for p in &self.0 {
711            match p {
712                Piece::Lit(l) => out.push_str(l),
713                Piece::Tok(i, c) => out.push_str(&apply_case(get(*i)?, *c)),
714                Piece::Initial(i, c) => out.push_str(&apply_case(&get(*i)?.chars().next()?.to_string(), *c)),
715            }
716        }
717        Some(out)
718    }
719}
720
721/// Finds a program consistent with every example.
722fn learn(examples: &[(String, String)]) -> Option<Program> {
723    let (src, dst) = examples.first()?;
724    let candidates = synthesize(src, dst);
725    candidates.into_iter().find(|p| examples.iter().all(|(s, d)| p.apply(s).as_deref() == Some(d.as_str())))
726}
727
728/// Candidate programs for one example, by greedy segmentation of the output into token matches
729/// and literals (tries several index conventions).
730fn synthesize(src: &str, dst: &str) -> Vec<Program> {
731    let toks = tokens(src);
732    let n = toks.len() as i32;
733    let mut out = Vec::new();
734    for prefer_neg in [false, true] {
735        let mut pieces = Vec::new();
736        let mut rest = dst;
737        let mut guard = 0;
738        while !rest.is_empty() && guard < 64 {
739            guard += 1;
740            // Longest token (with any case) that prefixes `rest`.
741            let mut best: Option<(usize, Piece)> = None;
742            for (i, t) in toks.iter().enumerate() {
743                let idx = if prefer_neg { i as i32 - n } else { i as i32 };
744                for c in [Case::Same, Case::Upper, Case::Lower, Case::Proper] {
745                    let cand = apply_case(t, c);
746                    if !cand.is_empty() && rest.starts_with(&cand) && best.as_ref().is_none_or(|(l, _)| cand.len() > *l) {
747                        best = Some((cand.len(), Piece::Tok(idx, c)));
748                    }
749                }
750            }
751            if best.as_ref().is_none_or(|(l, _)| *l <= 1) {
752                // Initial letters (e.g. "J. Smith").
753                for (i, t) in toks.iter().enumerate() {
754                    let idx = if prefer_neg { i as i32 - n } else { i as i32 };
755                    if let Some(ch) = t.chars().next() {
756                        for c in [Case::Same, Case::Upper, Case::Lower] {
757                            let cand = apply_case(&ch.to_string(), c);
758                            if rest.starts_with(&cand) && best.is_none() {
759                                best = Some((cand.len(), Piece::Initial(idx, c)));
760                            }
761                        }
762                    }
763                }
764            }
765            match best {
766                Some((len, piece)) => {
767                    pieces.push(piece);
768                    rest = rest.get(len..).unwrap_or("");
769                }
770                None => {
771                    let ch = rest.chars().next().unwrap_or(' ');
772                    match pieces.last_mut() {
773                        Some(Piece::Lit(l)) => l.push(ch),
774                        _ => pieces.push(Piece::Lit(ch.to_string())),
775                    }
776                    rest = rest.get(ch.len_utf8()..).unwrap_or("");
777                }
778            }
779        }
780        out.push(Program(pieces));
781    }
782    out
783}
784
785#[cfg(test)]
786mod tests {
787    use super::*;
788
789    #[test]
790    fn flash_fill_reads_shown_text() {
791        let mut s = crate::engine::Session::new();
792        s.new_workbook();
793        let values = serde_json::json!([["2024-01-15", "15/01/2024"], ["2023-12-03", null]]);
794        s.execute("range.setValues", serde_json::json!({"range": "A1", "values": values})).unwrap();
795        s.execute("selection.set", serde_json::json!({"cell": "B2"})).unwrap();
796        s.execute("data.flashFill", serde_json::json!({})).unwrap();
797        let sh = s.doc().unwrap().wb.active().unwrap();
798        assert_eq!(sh.value(CellRef::new(1, 1)), Value::from("03/12/2023"));
799    }
800
801    #[test]
802    fn series_keeps_value_equal_to_stop_after_float_drift() {
803        let mut s = crate::engine::Session::new();
804        s.new_workbook();
805        s.execute("cell.set", serde_json::json!({"cell": "A1", "input": "1"})).unwrap();
806        s.execute("edit.fillSeries", serde_json::json!({"range": "A1:A6", "type": "linear", "step": 0.1, "stop": 1.3})).unwrap();
807        s.execute("cell.set", serde_json::json!({"cell": "B1", "input": "0.1"})).unwrap();
808        s.execute("edit.fillSeries", serde_json::json!({"range": "B1:B4", "type": "growth", "step": 3, "stop": 0.3})).unwrap();
809        let sh = s.doc().unwrap().wb.active().unwrap();
810        let col = |c: u32, n: u32| (0..n).map(|r| sh.value(CellRef::new(r, c))).collect::<Vec<_>>();
811        let n = Value::number;
812        assert_eq!(col(0, 6), vec![n(1.0), n(1.1), n(1.2), n(1.3), Value::Empty, Value::Empty]);
813        assert_eq!(col(1, 4), vec![n(0.1), n(0.3), Value::Empty, Value::Empty]);
814    }
815
816    #[test]
817    fn flash_programs() {
818        let p = learn(&[("John Smith".into(), "Smith, John".into())]).unwrap();
819        assert_eq!(p.apply("Ada Lovelace").as_deref(), Some("Lovelace, Ada"));
820        let p = learn(&[("jane doe".into(), "JD".into())]).unwrap();
821        assert_eq!(p.apply("alan turing").as_deref(), Some("AT"));
822        let p = learn(&[("alice@example.com".into(), "alice".into())]).unwrap();
823        assert_eq!(p.apply("bob@example.com").as_deref(), Some("bob"));
824    }
825
826    #[test]
827    fn split_numbers() {
828        assert_eq!(split_num("Item 7"), Some(("Item ".into(), 7, "".into(), 1)));
829        assert_eq!(split_num("Q003x"), Some(("Q".into(), 3, "x".into(), 3)));
830        assert_eq!(split_num("abc"), None);
831    }
832
833    #[test]
834    fn times_quarters_ordinals_and_mixed_sources() {
835        let mut s = crate::engine::Session::new();
836        s.new_workbook();
837        let mut fill = |values: serde_json::Value, source: &str, target: &str| {
838            s.execute("range.setValues", serde_json::json!({"range": source.get(..2).unwrap(), "values": values})).unwrap();
839            s.execute("edit.autoFill", serde_json::json!({"source": source, "target": target})).unwrap();
840            let sh = s.doc().unwrap().wb.active().unwrap();
841            let r = RangeRef::parse(target).unwrap();
842            r.iter().map(|c| crate::engine::display::cell_text(&s.doc().unwrap().wb, sh, c)).collect::<Vec<_>>()
843        };
844        assert_eq!(fill(serde_json::json!([["9:00"]]), "A1", "A1:A3"), ["9:00", "10:00", "11:00"]);
845        assert_eq!(fill(serde_json::json!([["Qtr3"]]), "B1", "B1:B3"), ["Qtr3", "Qtr4", "Qtr1"]);
846        assert_eq!(fill(serde_json::json!([["Quarter 4"]]), "C1", "C1:C2"), ["Quarter 4", "Quarter 1"]);
847        assert_eq!(fill(serde_json::json!([["1st Period"]]), "D1", "D1:D4"), ["1st Period", "2nd Period", "3rd Period", "4th Period"]);
848        assert_eq!(
849            fill(serde_json::json!([["Product 1"], ["On backorder"]]), "E1:E2", "E1:E6"),
850            ["Product 1", "On backorder", "Product 2", "On backorder", "Product 3", "On backorder"]
851        );
852    }
853
854    #[test]
855    fn falling_growth_series_stops_at_its_stop_value() {
856        let mut s = crate::engine::Session::new();
857        s.new_workbook();
858        s.execute("cell.set", serde_json::json!({"cell": "A1", "input": "5"})).unwrap();
859        s.execute("edit.fillSeries", serde_json::json!({"range": "A1:A6", "type": "growth", "step": 0.5, "stop": 1})).unwrap();
860        let d = s.doc().unwrap();
861        let sh = d.wb.active().unwrap();
862        let growth: Vec<String> = (1..6).map(|r| crate::engine::display::cell_text(&d.wb, sh, CellRef::new(r, 0))).collect();
863        assert_eq!(growth, ["2.5", "1.25", "", "", ""]);
864    }
865
866    #[test]
867    fn text_series_near_i64_max_neither_wraps_nor_panics() {
868        let mut s = crate::engine::Session::new();
869        s.new_workbook();
870        s.execute("cell.set", serde_json::json!({"cell": "A1", "input": "X 0"})).unwrap();
871        s.execute("cell.set", serde_json::json!({"cell": "A2", "input": "X 9223372036854775807"})).unwrap();
872        // The detected step is i64::MAX, so the next value overflows i64. The cell keeps its
873        // source value instead of panicking in debug or wrapping in release.
874        s.execute("edit.autoFill", serde_json::json!({"source": "A1:A2", "target": "A1:A3"})).unwrap();
875        let d = s.doc().unwrap();
876        let sh = d.wb.active().unwrap();
877        assert_eq!(crate::engine::display::cell_text(&d.wb, sh, CellRef::new(2, 0)), "X 0");
878    }
879
880    #[test]
881    fn autofill_refuses_targets_that_do_not_extend_the_source() {
882        // Whole rows into whole columns once filled every source column down the whole sheet
883        // (~17 billion writes) and aborted the process on a 24-51 GB allocation (fuzz seeds 1064,
884        // 1114, 1775). Excel refuses every one of these shapes.
885        let mut s = crate::engine::Session::new();
886        s.new_workbook();
887        s.execute("range.setValues", serde_json::json!({"range": "A2", "values": [[1, "Item 1"], [2, "Item 2"]]})).unwrap();
888        s.execute("range.setValues", serde_json::json!({"range": "H4", "values": [[10], [20]]})).unwrap();
889        let before = s.doc().unwrap().wb.active().unwrap().cells.len();
890        for (mode, source, target) in [
891            ("series", "2:3", "A:A"),
892            ("alternate", "2:3", "A:A"),
893            ("series", "8:10", "C:E"),
894            ("copy", "24:24", "B:D"),
895            ("values", "H4:H9", "D:F"),
896            ("series", "H4:H9", "H:H"),
897            ("series", "A2:A3", "A1:A5"),
898            ("series", "A2:A6", "A2:A4"),
899            ("series", "B2:B3", "A2:A3"),
900        ] {
901            let r = s.execute("edit.autoFill", serde_json::json!({"mode": mode, "source": source, "target": target}));
902            assert!(r.is_err(), "{source} -> {target} should be refused");
903        }
904        assert_eq!(s.doc().unwrap().wb.active().unwrap().cells.len(), before, "a refused fill writes nothing");
905        // Targets holding the source in a corner still fill, whole rows and diagonal drags included.
906        s.execute("edit.autoFill", serde_json::json!({"source": "2:3", "target": "2:6"})).unwrap();
907        s.execute("edit.autoFill", serde_json::json!({"source": "A2:A3", "target": "A1:A3"})).unwrap();
908        let sh = s.doc().unwrap().wb.active().unwrap();
909        let text = |a: &str| crate::engine::display::cell_text(&s.doc().unwrap().wb, sh, CellRef::parse(a).unwrap());
910        assert_eq!([text("A1"), text("A6"), text("B6")], ["0", "5", "Item 5"]);
911        assert!(holds_source_in_corner(RangeRef::parse("B5").unwrap(), RangeRef::parse("B3:B5").unwrap()));
912        assert!(holds_source_in_corner(RangeRef::parse("A:A").unwrap(), RangeRef::parse("A:C").unwrap()));
913        assert!(holds_source_in_corner(RangeRef::parse("B5:C9").unwrap(), RangeRef::parse("B5:D13").unwrap()));
914        assert!(holds_source_in_corner(RangeRef::parse("B1:B2").unwrap(), RangeRef::parse("A1:B4").unwrap()));
915        assert!(!holds_source_in_corner(RangeRef::parse("A:A").unwrap(), RangeRef::parse("1:1").unwrap()));
916    }
917}