formualizer_eval/engine/
range_view.rs

1use crate::SheetId as EngineSheetId;
2use crate::args::CoercionPolicy;
3use crate::arrow_store;
4use crate::engine::DependencyGraph;
5use crate::stripes::NumericChunk;
6use formualizer_common::{ExcelError, LiteralValue};
7
8/// Unified view over a 2D range with efficient traversal utilities.
9pub struct RangeView<'a> {
10    backing: RangeBacking<'a>,
11    rows: usize,
12    cols: usize,
13}
14
15enum RangeBacking<'a> {
16    /// Borrowed 2D rows without cloning
17    Borrowed2D(&'a [Vec<LiteralValue>]),
18    /// Slice over the graph (values fetched on demand)
19    GraphSlice {
20        graph: &'a DependencyGraph,
21        sheet_id: EngineSheetId,
22        sr: u32,
23        sc: u32,
24        er: u32,
25        ec: u32,
26    },
27    /// Arrow-backed range view
28    Arrow(arrow_store::ArrowRangeView<'a>),
29    /// Hybrid view: prefers graph values (formulas/edits) and falls back to Arrow base values
30    Hybrid {
31        graph: &'a DependencyGraph,
32        sheet_id: EngineSheetId,
33        sr: u32,
34        sc: u32,
35        arrow: arrow_store::ArrowRangeView<'a>,
36    },
37}
38
39impl<'a> core::fmt::Debug for RangeView<'a> {
40    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
41        f.debug_struct("RangeView")
42            .field("rows", &self.rows)
43            .field("cols", &self.cols)
44            .field("kind", &self.kind_probe())
45            .finish()
46    }
47}
48
49#[derive(Copy, Clone, Debug, Eq, PartialEq)]
50pub enum RangeKind {
51    Empty,
52    NumericOnly,
53    TextOnly,
54    Mixed,
55}
56
57impl<'a> RangeView<'a> {
58    pub fn from_borrowed(rows: &'a [Vec<LiteralValue>]) -> Self {
59        let r = rows.len();
60        let c = rows.first().map(|r| r.len()).unwrap_or(0);
61        Self {
62            backing: RangeBacking::Borrowed2D(rows),
63            rows: r,
64            cols: c,
65        }
66    }
67    pub fn from_arrow(av: arrow_store::ArrowRangeView<'a>) -> Self {
68        let (rows, cols) = av.dims();
69        Self {
70            backing: RangeBacking::Arrow(av),
71            rows,
72            cols,
73        }
74    }
75
76    pub fn from_graph(
77        graph: &'a DependencyGraph,
78        sheet_id: EngineSheetId,
79        sr: u32,
80        sc: u32,
81        er: u32,
82        ec: u32,
83    ) -> Self {
84        // Normalize empty
85        let (rows, cols) = if er < sr || ec < sc {
86            (0, 0)
87        } else {
88            ((er - sr + 1) as usize, (ec - sc + 1) as usize)
89        };
90        Self {
91            backing: RangeBacking::GraphSlice {
92                graph,
93                sheet_id,
94                sr,
95                sc,
96                er,
97                ec,
98            },
99            rows,
100            cols,
101        }
102    }
103
104    #[inline]
105    pub fn dims(&self) -> (usize, usize) {
106        (self.rows, self.cols)
107    }
108
109    #[inline]
110    pub fn is_empty(&self) -> bool {
111        self.rows == 0 || self.cols == 0
112    }
113
114    pub fn kind_probe(&self) -> RangeKind {
115        match &self.backing {
116            RangeBacking::Borrowed2D(rows) => {
117                if rows.is_empty() || self.is_empty() {
118                    RangeKind::Empty
119                } else {
120                    // Quick scan with early exits
121                    let mut has_num = false;
122                    let mut has_text = false;
123                    let mut has_other = false;
124                    'outer: for r in rows.iter() {
125                        for v in r.iter() {
126                            match v {
127                                LiteralValue::Number(_)
128                                | LiteralValue::Int(_)
129                                | LiteralValue::Boolean(_)
130                                | LiteralValue::Empty => has_num = true,
131                                LiteralValue::Text(_) => has_text = true,
132                                _ => {
133                                    has_other = true;
134                                    break 'outer;
135                                }
136                            }
137                        }
138                    }
139                    if has_other || (has_num && has_text) {
140                        RangeKind::Mixed
141                    } else if has_text {
142                        RangeKind::TextOnly
143                    } else if has_num {
144                        RangeKind::NumericOnly
145                    } else {
146                        RangeKind::Empty
147                    }
148                }
149            }
150            RangeBacking::GraphSlice { .. } => RangeKind::Mixed, // unknown a priori
151            RangeBacking::Arrow(_) => RangeKind::Mixed, // probe conservatively; mixed possible
152            RangeBacking::Hybrid { .. } => RangeKind::Mixed,
153        }
154    }
155
156    pub fn as_1x1(&self) -> Option<LiteralValue> {
157        if self.rows == 0 || self.cols == 0 {
158            return None;
159        }
160        if self.rows == 1 && self.cols == 1 {
161            let mut out: Option<LiteralValue> = None;
162            let _ = self.for_each_cell(&mut |v| {
163                out = Some(v.clone());
164                Ok(())
165            });
166            return out;
167        }
168        None
169    }
170
171    /// Get a specific cell by row and column index (0-based).
172    /// Returns Empty for out-of-bounds access.
173    pub fn get_cell(&self, row: usize, col: usize) -> LiteralValue {
174        if row >= self.rows || col >= self.cols {
175            return LiteralValue::Empty;
176        }
177
178        match &self.backing {
179            RangeBacking::Borrowed2D(rows) => rows[row][col].clone(),
180            RangeBacking::GraphSlice {
181                graph,
182                sheet_id,
183                sr,
184                sc,
185                ..
186            } => {
187                let actual_row = sr + row as u32;
188                let actual_col = sc + col as u32;
189                let coord = crate::reference::Coord::new(actual_row, actual_col, true, true);
190                let addr = crate::reference::CellRef::new(*sheet_id, coord);
191                graph
192                    .get_vertex_id_for_address(&addr)
193                    .and_then(|id| graph.get_value(*id))
194                    .unwrap_or(LiteralValue::Empty)
195            }
196            RangeBacking::Arrow(av) => av.get_cell(row, col),
197            RangeBacking::Hybrid {
198                graph,
199                sheet_id,
200                sr,
201                sc,
202                arrow,
203            } => {
204                // Compute absolute address
205                let abs_row = sr.saturating_add(row as u32);
206                let abs_col = sc.saturating_add(col as u32);
207                let coord = crate::reference::Coord::new(abs_row, abs_col, true, true);
208                let addr = crate::reference::CellRef::new(*sheet_id, coord);
209                if let Some(vid) = graph.get_vertex_id_for_address(&addr) {
210                    if let Some(v) = graph.get_value(*vid) {
211                        return v;
212                    }
213                }
214                arrow.get_cell(row, col)
215            }
216        }
217    }
218
219    /// Row-major cell traversal. For borrowable backings, passes borrowed slices/values.
220    pub fn for_each_cell(
221        &self,
222        f: &mut dyn FnMut(&LiteralValue) -> Result<(), ExcelError>,
223    ) -> Result<(), ExcelError> {
224        match &self.backing {
225            RangeBacking::Borrowed2D(rows) => {
226                for r in rows.iter() {
227                    for v in r.iter() {
228                        f(v)?;
229                    }
230                }
231            }
232            RangeBacking::GraphSlice {
233                graph,
234                sheet_id,
235                sr,
236                sc,
237                er,
238                ec,
239            } => {
240                if *er < *sr || *ec < *sc {
241                    return Ok(());
242                }
243                let mut row = *sr;
244                while row <= *er {
245                    let mut col = *sc;
246                    while col <= *ec {
247                        let coord = crate::reference::Coord::new(row, col, true, true);
248                        let addr = crate::reference::CellRef::new(*sheet_id, coord);
249                        let v = graph
250                            .get_vertex_id_for_address(&addr)
251                            .and_then(|id| graph.get_value(*id))
252                            .unwrap_or(LiteralValue::Empty);
253                        f(&v)?;
254                        col += 1;
255                    }
256                    row += 1;
257                }
258            }
259            RangeBacking::Arrow(av) => {
260                for r in 0..self.rows {
261                    for c in 0..self.cols {
262                        let tmp = av.get_cell(r, c);
263                        f(&tmp)?;
264                    }
265                }
266            }
267            RangeBacking::Hybrid { .. } => {
268                for r in 0..self.rows {
269                    for c in 0..self.cols {
270                        let v = self.get_cell(r, c);
271                        f(&v)?;
272                    }
273                }
274            }
275        }
276        Ok(())
277    }
278
279    /// Visit each row as a borrowed slice when possible; otherwise uses a reusable buffer per row.
280    pub fn for_each_row(
281        &self,
282        f: &mut dyn FnMut(&[LiteralValue]) -> Result<(), ExcelError>,
283    ) -> Result<(), ExcelError> {
284        match &self.backing {
285            RangeBacking::Borrowed2D(rows) => {
286                for r in rows.iter() {
287                    f(&r[..])?;
288                }
289            }
290            RangeBacking::GraphSlice {
291                graph,
292                sheet_id,
293                sr,
294                sc,
295                er,
296                ec,
297            } => {
298                if *er < *sr || *ec < *sc {
299                    return Ok(());
300                }
301                let cols = (*ec - *sc + 1) as usize;
302                let mut buf: Vec<LiteralValue> = Vec::with_capacity(cols);
303                let mut row = *sr;
304                while row <= *er {
305                    buf.clear();
306                    let mut col = *sc;
307                    while col <= *ec {
308                        let coord = crate::reference::Coord::new(row, col, true, true);
309                        let addr = crate::reference::CellRef::new(*sheet_id, coord);
310                        let v = graph
311                            .get_vertex_id_for_address(&addr)
312                            .and_then(|id| graph.get_value(*id))
313                            .unwrap_or(LiteralValue::Empty);
314                        buf.push(v);
315                        col += 1;
316                    }
317                    f(&buf[..])?;
318                    row += 1;
319                }
320            }
321            RangeBacking::Arrow(av) => {
322                let mut buf: Vec<LiteralValue> = Vec::with_capacity(self.cols);
323                for r in 0..self.rows {
324                    buf.clear();
325                    for c in 0..self.cols {
326                        buf.push(av.get_cell(r, c));
327                    }
328                    f(&buf[..])?;
329                }
330            }
331            RangeBacking::Hybrid { .. } => {
332                let mut buf: Vec<LiteralValue> = Vec::with_capacity(self.cols);
333                for r in 0..self.rows {
334                    buf.clear();
335                    for c in 0..self.cols {
336                        buf.push(self.get_cell(r, c));
337                    }
338                    f(&buf[..])?;
339                }
340            }
341        }
342        Ok(())
343    }
344
345    /// Visit each column as a contiguous slice (buffered when needed)
346    pub fn for_each_col(
347        &self,
348        f: &mut dyn FnMut(&[LiteralValue]) -> Result<(), ExcelError>,
349    ) -> Result<(), ExcelError> {
350        match &self.backing {
351            RangeBacking::Borrowed2D(rows) => {
352                if self.cols == 0 {
353                    return Ok(());
354                }
355                let mut col_buf: Vec<LiteralValue> = Vec::with_capacity(self.rows);
356                for c in 0..self.cols {
357                    col_buf.clear();
358                    for r in 0..self.rows {
359                        col_buf.push(rows[r][c].clone());
360                    }
361                    f(&col_buf[..])?;
362                }
363            }
364            RangeBacking::GraphSlice {
365                graph,
366                sheet_id,
367                sr,
368                sc,
369                er,
370                ec,
371            } => {
372                if *er < *sr || *ec < *sc {
373                    return Ok(());
374                }
375                let rows = (*er - *sr + 1) as usize;
376                let mut col_buf: Vec<LiteralValue> = Vec::with_capacity(rows);
377                let mut col = *sc;
378                while col <= *ec {
379                    col_buf.clear();
380                    let mut row = *sr;
381                    while row <= *er {
382                        let coord = crate::reference::Coord::new(row, col, true, true);
383                        let addr = crate::reference::CellRef::new(*sheet_id, coord);
384                        let v = graph
385                            .get_vertex_id_for_address(&addr)
386                            .and_then(|id| graph.get_value(*id))
387                            .unwrap_or(LiteralValue::Empty);
388                        col_buf.push(v);
389                        row += 1;
390                    }
391                    f(&col_buf[..])?;
392                    col += 1;
393                }
394            }
395            RangeBacking::Arrow(av) => {
396                let mut col_buf: Vec<LiteralValue> = Vec::with_capacity(self.rows);
397                for c in 0..self.cols {
398                    col_buf.clear();
399                    for r in 0..self.rows {
400                        col_buf.push(av.get_cell(r, c));
401                    }
402                    f(&col_buf[..])?;
403                }
404            }
405            RangeBacking::Hybrid { .. } => {
406                let mut col_buf: Vec<LiteralValue> = Vec::with_capacity(self.rows);
407                for c in 0..self.cols {
408                    col_buf.clear();
409                    for r in 0..self.rows {
410                        col_buf.push(self.get_cell(r, c));
411                    }
412                    f(&col_buf[..])?;
413                }
414            }
415        }
416        Ok(())
417    }
418
419    /// If Arrow-backed, return the underlying ArrowRangeView for vectorized fast paths.
420    pub fn as_arrow(&self) -> Option<&arrow_store::ArrowRangeView<'a>> {
421        match &self.backing {
422            RangeBacking::Arrow(av) => Some(av),
423            RangeBacking::Hybrid { arrow, .. } => Some(arrow),
424            _ => None,
425        }
426    }
427
428    /// Get a numeric value at a specific cell, with coercion.
429    /// Returns None for empty cells or non-coercible values.
430    pub fn get_cell_numeric(&self, row: usize, col: usize, policy: CoercionPolicy) -> Option<f64> {
431        if row >= self.rows || col >= self.cols {
432            return None;
433        }
434
435        let val = self.get_cell(row, col);
436        pack_numeric(&val, policy).ok().flatten()
437    }
438
439    /// Numeric chunk iteration with coercion policy
440    pub fn numbers_chunked(
441        &self,
442        policy: CoercionPolicy,
443        min_chunk: usize,
444        f: &mut dyn FnMut(NumericChunk) -> Result<(), ExcelError>,
445    ) -> Result<(), ExcelError> {
446        let min_chunk = min_chunk.max(1);
447        let mut buf: Vec<f64> = Vec::with_capacity(min_chunk);
448        let mut flush = |buf: &mut Vec<f64>| -> Result<(), ExcelError> {
449            if buf.is_empty() {
450                return Ok(());
451            }
452            // SAFETY: read-only borrow for callback duration
453            let ptr = buf.as_ptr();
454            let len = buf.len();
455            let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
456            let chunk = NumericChunk {
457                data: slice,
458                validity: None,
459            };
460            f(chunk)?;
461            buf.clear();
462            Ok(())
463        };
464
465        match self.backing {
466            RangeBacking::Borrowed2D(rows) => {
467                for r in rows.iter() {
468                    for v in r.iter() {
469                        if let Some(n) = pack_numeric(v, policy)? {
470                            buf.push(n);
471                            if buf.len() >= min_chunk {
472                                flush(&mut buf)?;
473                            }
474                        }
475                    }
476                }
477                flush(&mut buf)?;
478            }
479            RangeBacking::GraphSlice { .. } => {
480                self.for_each_cell(&mut |v| {
481                    if let Some(n) = pack_numeric(v, policy)? {
482                        buf.push(n);
483                        if buf.len() >= min_chunk {
484                            flush(&mut buf)?;
485                        }
486                    }
487                    Ok(())
488                })?;
489                flush(&mut buf)?;
490            }
491            RangeBacking::Arrow(_) => {
492                self.for_each_cell(&mut |v| {
493                    if let Some(n) = pack_numeric(v, policy)? {
494                        buf.push(n);
495                        if buf.len() >= min_chunk {
496                            flush(&mut buf)?;
497                        }
498                    }
499                    Ok(())
500                })?;
501                flush(&mut buf)?;
502            }
503            RangeBacking::Hybrid { .. } => {
504                self.for_each_cell(&mut |v| {
505                    if let Some(n) = pack_numeric(v, policy)? {
506                        buf.push(n);
507                        if buf.len() >= min_chunk {
508                            flush(&mut buf)?;
509                        }
510                    }
511                    Ok(())
512                })?;
513                flush(&mut buf)?;
514            }
515        }
516
517        Ok(())
518    }
519}
520
521impl<'a> RangeView<'a> {
522    pub fn from_hybrid(
523        graph: &'a DependencyGraph,
524        sheet_id: EngineSheetId,
525        sr: u32,
526        sc: u32,
527        arrow: arrow_store::ArrowRangeView<'a>,
528    ) -> Self {
529        let (rows, cols) = arrow.dims();
530        Self {
531            backing: RangeBacking::Hybrid {
532                graph,
533                sheet_id,
534                sr,
535                sc,
536                arrow,
537            },
538            rows,
539            cols,
540        }
541    }
542}
543
544#[inline]
545fn pack_numeric(v: &LiteralValue, policy: CoercionPolicy) -> Result<Option<f64>, ExcelError> {
546    match policy {
547        CoercionPolicy::NumberLenientText => match v {
548            LiteralValue::Error(e) => Err(e.clone()),
549            LiteralValue::Empty => Ok(None),
550            other => Ok(crate::coercion::to_number_lenient(other).ok()),
551        },
552        CoercionPolicy::NumberStrict => match v {
553            LiteralValue::Error(e) => Err(e.clone()),
554            LiteralValue::Empty => Ok(None),
555            other => Ok(crate::coercion::to_number_strict(other).ok()),
556        },
557        _ => match v {
558            LiteralValue::Error(e) => Err(e.clone()),
559            _ => Ok(None),
560        },
561    }
562}
563
564#[cfg(test)]
565mod tests {
566    use super::*;
567
568    #[test]
569    fn borrowed2d_rows_are_borrowed() {
570        let row0 = vec![LiteralValue::Number(1.0), LiteralValue::Number(2.0)];
571        let row1 = vec![LiteralValue::Number(3.0), LiteralValue::Number(4.0)];
572        let data: Vec<Vec<LiteralValue>> = vec![row0, row1];
573        let rows_ptrs: Vec<*const LiteralValue> = data.iter().map(|r| r.as_ptr()).collect();
574        let view = RangeView::from_borrowed(&data);
575        let mut seen_ptrs: Vec<*const LiteralValue> = Vec::new();
576        view.for_each_row(&mut |row| {
577            seen_ptrs.push(row.as_ptr());
578            Ok(())
579        })
580        .unwrap();
581        assert_eq!(rows_ptrs.len(), seen_ptrs.len());
582        for (a, b) in rows_ptrs.iter().zip(seen_ptrs.iter()) {
583            assert_eq!(*a, *b, "row slices should be borrowed, not cloned");
584        }
585    }
586
587    #[test]
588    fn borrowed2d_numeric_chunking() {
589        let data: Vec<Vec<LiteralValue>> = vec![
590            vec![
591                LiteralValue::Number(1.0),
592                LiteralValue::Text("x".into()),
593                LiteralValue::Int(3),
594            ],
595            vec![
596                LiteralValue::Boolean(true),
597                LiteralValue::Empty,
598                LiteralValue::Number(2.5),
599            ],
600        ];
601        let view = RangeView::from_borrowed(&data);
602        let mut sum = 0.0f64;
603        view.numbers_chunked(CoercionPolicy::NumberLenientText, 2, &mut |chunk| {
604            for &n in chunk.data {
605                sum += n;
606            }
607            Ok(())
608        })
609        .unwrap();
610        assert!((sum - 7.5).abs() < 1e-9);
611    }
612
613    #[test]
614    fn flat_numeric_numbers_chunked_removed() {
615        // Flats removed: ensure borrowed path still works for chunking
616        let data: Vec<Vec<LiteralValue>> = vec![
617            vec![LiteralValue::Number(1.0), LiteralValue::Number(2.0)],
618            vec![LiteralValue::Number(3.0), LiteralValue::Number(4.0)],
619        ];
620        let view = RangeView::from_borrowed(&data);
621        assert_eq!(view.dims(), (2, 2));
622        let mut collected = Vec::new();
623        view.numbers_chunked(CoercionPolicy::NumberLenientText, 3, &mut |chunk| {
624            collected.extend_from_slice(chunk.data);
625            Ok(())
626        })
627        .unwrap();
628        assert_eq!(collected, vec![1.0, 2.0, 3.0, 4.0]);
629    }
630
631    #[test]
632    fn graph_slice_row_iteration_and_sum() {
633        use crate::engine::{Engine, EvalConfig};
634        use crate::test_workbook::TestWorkbook;
635        let mut engine = Engine::new(TestWorkbook::new(), EvalConfig::default());
636        let sheet = engine.default_sheet_name().to_string();
637        engine
638            .set_cell_value(&sheet, 1, 1, LiteralValue::Int(1))
639            .unwrap();
640        engine
641            .set_cell_value(&sheet, 1, 2, LiteralValue::Int(2))
642            .unwrap();
643        engine
644            .set_cell_value(&sheet, 2, 1, LiteralValue::Int(3))
645            .unwrap();
646        engine
647            .set_cell_value(&sheet, 2, 2, LiteralValue::Int(4))
648            .unwrap();
649
650        let sheet_id = engine.default_sheet_id();
651        let view = RangeView::from_graph(&engine.graph, sheet_id, 1, 1, 2, 2);
652        assert_eq!(view.dims(), (2, 2));
653        let mut rows_sum = Vec::new();
654        view.for_each_row(&mut |row| {
655            let mut s = 0.0;
656            for v in row {
657                if let LiteralValue::Int(i) = v {
658                    s += *i as f64
659                } else if let LiteralValue::Number(n) = v {
660                    s += *n
661                }
662            }
663            rows_sum.push(s);
664            Ok(())
665        })
666        .unwrap();
667        assert_eq!(rows_sum, vec![3.0, 7.0]);
668
669        let mut sum = 0.0;
670        RangeView::from_graph(&engine.graph, sheet_id, 1, 1, 2, 2)
671            .numbers_chunked(CoercionPolicy::NumberLenientText, 2, &mut |chunk| {
672                for &n in chunk.data {
673                    sum += n;
674                }
675                Ok(())
676            })
677            .unwrap();
678        assert_eq!(sum, 10.0);
679    }
680}