Skip to main content

formualizer_eval/engine/
range_view.rs

1use crate::arrow_store;
2use crate::arrow_store::IngestBuilder;
3use crate::engine::CancelToken;
4use crate::stripes::NumericChunk;
5use arrow_array::Array;
6use arrow_schema::DataType;
7use formualizer_common::{CoercionPolicy, DateSystem, ExcelError, LiteralValue};
8use std::sync::Arc;
9
10#[cfg(test)]
11pub(crate) mod range_work {
12    use std::cell::Cell;
13
14    #[derive(Clone, Copy, Debug, Default, serde::Serialize)]
15    pub(crate) struct Work {
16        pub iterators: usize,
17        pub search_probes: usize,
18        pub candidates: usize,
19        pub segments: usize,
20        pub segment_rows: usize,
21        pub generic_columns: usize,
22        pub null_arrays: usize,
23        pub null_slots: usize,
24        pub provider_requests: [usize; 4],
25        pub provider_builds: [usize; 4],
26        pub provider_slots: [usize; 4],
27    }
28
29    thread_local! {
30        static WORK: Cell<Option<Work>> = const { Cell::new(None) };
31    }
32
33    pub(crate) fn begin() {
34        WORK.with(|work| work.set(Some(Work::default())));
35    }
36
37    pub(crate) fn take() -> Work {
38        WORK.with(|work| work.replace(None).unwrap_or_default())
39    }
40
41    #[inline]
42    pub(crate) fn record(f: impl FnOnce(&mut Work)) {
43        WORK.with(|work| {
44            if let Some(mut value) = work.get() {
45                f(&mut value);
46                work.set(Some(value));
47            }
48        });
49    }
50}
51
52#[derive(Clone)]
53pub enum RangeBacking<'a> {
54    Borrowed(&'a arrow_store::ArrowSheet),
55    Owned(Arc<arrow_store::ArrowSheet>),
56}
57
58/// Unified view over a 2D range with efficient traversal utilities.
59/// Phase 4: Arrow-only backing.
60#[derive(Clone)]
61pub struct RangeView<'a> {
62    backing: RangeBacking<'a>,
63    sr: usize,
64    sc: usize,
65    er: usize,
66    ec: usize,
67    rows: usize,
68    cols: usize,
69    cancel_token: Option<CancelToken>,
70}
71
72impl<'a> core::fmt::Debug for RangeView<'a> {
73    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
74        f.debug_struct("RangeView")
75            .field("rows", &self.rows)
76            .field("cols", &self.cols)
77            .field("kind", &self.kind_probe())
78            .finish()
79    }
80}
81
82#[derive(Copy, Clone, Debug, Eq, PartialEq)]
83pub enum RangeKind {
84    Empty,
85    NumericOnly,
86    TextOnly,
87    Mixed,
88}
89
90pub struct ChunkCol {
91    pub numbers: Option<arrow_array::ArrayRef>,
92    pub booleans: Option<arrow_array::ArrayRef>,
93    pub text: Option<arrow_array::ArrayRef>,
94    pub errors: Option<arrow_array::ArrayRef>,
95    pub type_tag: arrow_array::ArrayRef,
96}
97
98pub struct ChunkSlice {
99    pub row_start: usize, // relative to view top
100    pub row_len: usize,
101    pub cols: Vec<ChunkCol>,
102}
103
104struct RowSegment {
105    chunk_idx: usize,
106    chunk_offset: usize,
107    row_start: usize,
108    row_len: usize,
109}
110
111struct RowSegmentIterator<'a> {
112    view: &'a RangeView<'a>,
113    chunks: Option<core::ops::Range<usize>>,
114}
115
116impl Iterator for RowSegmentIterator<'_> {
117    type Item = Result<RowSegment, ExcelError>;
118
119    fn next(&mut self) -> Option<Self::Item> {
120        if self
121            .view
122            .cancel_token
123            .as_ref()
124            .is_some_and(CancelToken::is_cancelled)
125        {
126            return Some(Err(ExcelError::new(
127                formualizer_common::ExcelErrorKind::Cancelled,
128            )));
129        }
130        let sheet = self.view.sheet();
131        let starts = &sheet.chunk_starts;
132        let sheet_rows = sheet.nrows as usize;
133        let row_end = self.view.er.min(sheet_rows.saturating_sub(1));
134        let chunks = self.chunks.get_or_insert_with(|| {
135            // Match row coverage independently of logical column/zero-sized subview dimensions.
136            if sheet_rows == 0 || starts.is_empty() || self.view.sr > row_end {
137                return 0..0;
138            }
139            let first = starts
140                .partition_point(|&start| {
141                    #[cfg(test)]
142                    range_work::record(|w| w.search_probes += 1);
143                    start <= self.view.sr
144                })
145                .saturating_sub(1);
146            let end = starts.partition_point(|&start| {
147                #[cfg(test)]
148                range_work::record(|w| w.search_probes += 1);
149                start <= row_end
150            });
151            first..end
152        });
153        for ci in chunks.by_ref() {
154            #[cfg(test)]
155            range_work::record(|w| w.candidates += 1);
156            let start = starts[ci];
157            let end = starts.get(ci + 1).copied().unwrap_or(sheet_rows);
158            let len = end.saturating_sub(start);
159            if len == 0 {
160                continue;
161            }
162            let lo = start.max(self.view.sr);
163            let hi = (start + len - 1).min(row_end);
164            if lo > hi {
165                continue;
166            }
167            let row_len = hi - lo + 1;
168            #[cfg(test)]
169            range_work::record(|w| {
170                w.segments += 1;
171                w.segment_rows += row_len;
172            });
173            return Some(Ok(RowSegment {
174                chunk_idx: ci,
175                chunk_offset: lo - start,
176                row_start: lo - self.view.sr,
177                row_len,
178            }));
179        }
180        None
181    }
182}
183
184pub struct RowChunkIterator<'a> {
185    segments: RowSegmentIterator<'a>,
186}
187
188impl Iterator for RowChunkIterator<'_> {
189    type Item = Result<ChunkSlice, ExcelError>;
190
191    fn next(&mut self) -> Option<Self::Item> {
192        let segment = match self.segments.next()? {
193            Ok(segment) => segment,
194            Err(error) => return Some(Err(error)),
195        };
196        let view = self.segments.view;
197        let sheet = view.sheet();
198        let ci = segment.chunk_idx;
199        let rel_off = segment.chunk_offset;
200        let seg_len = segment.row_len;
201        let mut cols = Vec::with_capacity(view.cols);
202        for col_idx in view.sc..=view.ec {
203            #[cfg(test)]
204            range_work::record(|w| w.generic_columns += 1);
205            if col_idx >= sheet.columns.len() {
206                #[cfg(test)]
207                range_work::record(|w| {
208                    w.null_arrays += 4;
209                    w.null_slots += 4 * seg_len;
210                });
211                let numbers = Some(arrow_array::new_null_array(&DataType::Float64, seg_len));
212                let booleans = Some(arrow_array::new_null_array(&DataType::Boolean, seg_len));
213                let text = Some(arrow_array::new_null_array(&DataType::Utf8, seg_len));
214                let errors = Some(arrow_array::new_null_array(&DataType::UInt8, seg_len));
215                let type_tag: arrow_array::ArrayRef =
216                    Arc::new(arrow_array::UInt8Array::from(vec![
217                        arrow_store::TypeTag::Empty
218                            as u8;
219                        seg_len
220                    ]));
221                cols.push(ChunkCol {
222                    numbers,
223                    booleans,
224                    text,
225                    errors,
226                    type_tag,
227                });
228            } else {
229                let col = &sheet.columns[col_idx];
230                let Some(ch) = col.chunk(ci) else {
231                    #[cfg(test)]
232                    range_work::record(|w| {
233                        w.null_arrays += 4;
234                        w.null_slots += 4 * seg_len;
235                    });
236                    let numbers = Some(arrow_array::new_null_array(&DataType::Float64, seg_len));
237                    let booleans = Some(arrow_array::new_null_array(&DataType::Boolean, seg_len));
238                    let text = Some(arrow_array::new_null_array(&DataType::Utf8, seg_len));
239                    let errors = Some(arrow_array::new_null_array(&DataType::UInt8, seg_len));
240                    let type_tag: arrow_array::ArrayRef =
241                        Arc::new(arrow_array::UInt8Array::from(vec![
242                            arrow_store::TypeTag::Empty
243                                as u8;
244                            seg_len
245                        ]));
246                    cols.push(ChunkCol {
247                        numbers,
248                        booleans,
249                        text,
250                        errors,
251                        type_tag,
252                    });
253                    continue;
254                };
255
256                let numbers_base: arrow_array::ArrayRef = ch.numbers_or_null();
257                let booleans_base: arrow_array::ArrayRef = ch.booleans_or_null();
258                let text_base: arrow_array::ArrayRef = ch.text_or_null();
259                let errors_base: arrow_array::ArrayRef = ch.errors_or_null();
260
261                let numbers = Some(numbers_base.slice(rel_off, seg_len));
262                let booleans = Some(booleans_base.slice(rel_off, seg_len));
263                let text = Some(text_base.slice(rel_off, seg_len));
264                let errors = Some(errors_base.slice(rel_off, seg_len));
265                let type_tag: arrow_array::ArrayRef = Arc::new(ch.type_tag.slice(rel_off, seg_len));
266                cols.push(ChunkCol {
267                    numbers,
268                    booleans,
269                    text,
270                    errors,
271                    type_tag,
272                });
273            }
274        }
275        Some(Ok(ChunkSlice {
276            row_start: segment.row_start,
277            row_len: seg_len,
278            cols,
279        }))
280    }
281}
282
283impl<'a> RangeView<'a> {
284    pub(crate) fn new(
285        backing: RangeBacking<'a>,
286        sr: usize,
287        sc: usize,
288        er: usize,
289        ec: usize,
290        rows: usize,
291        cols: usize,
292    ) -> Self {
293        Self {
294            backing,
295            sr,
296            sc,
297            er,
298            ec,
299            rows,
300            cols,
301            cancel_token: None,
302        }
303    }
304
305    /// Attaches a shared cancellation handle to cancellation-aware range walks.
306    ///
307    /// Cloning a [`CancelToken`] shares its signal without allocating. Retrieve
308    /// a context token once before a hot loop and poll
309    /// [`CancelToken::is_cancelled`] periodically.
310    #[must_use]
311    pub fn with_cancel_token(mut self, token: Option<CancelToken>) -> Self {
312        self.cancel_token = token;
313        self
314    }
315
316    #[inline]
317    pub fn sheet(&self) -> &arrow_store::ArrowSheet {
318        match &self.backing {
319            RangeBacking::Borrowed(s) => s,
320            RangeBacking::Owned(s) => s,
321        }
322    }
323
324    pub fn from_owned_rows(
325        rows: Vec<Vec<LiteralValue>>,
326        date_system: DateSystem,
327    ) -> RangeView<'static> {
328        Self::try_from_owned_rows(rows, date_system, None)
329            .expect("uncancelled RangeView conversion")
330    }
331
332    pub(crate) fn try_from_owned_rows(
333        rows: Vec<Vec<LiteralValue>>,
334        date_system: DateSystem,
335        cancel_token: Option<CancelToken>,
336    ) -> Result<RangeView<'static>, ExcelError> {
337        let nrows = rows.len();
338        let ncols = rows.iter().map(|r| r.len()).max().unwrap_or(0);
339
340        let chunk_rows = 32 * 1024;
341        let mut ib = IngestBuilder::new("__tmp", ncols, chunk_rows, date_system);
342
343        for mut r in rows {
344            if cancel_token.as_ref().is_some_and(CancelToken::is_cancelled) {
345                return Err(ExcelError::new(
346                    formualizer_common::ExcelErrorKind::Cancelled,
347                ));
348            }
349            r.resize(ncols, LiteralValue::Empty);
350            ib.append_row(&r).expect("append_row for RangeView");
351        }
352
353        let sheet = Arc::new(ib.finish());
354
355        if nrows == 0 || ncols == 0 {
356            return Ok(RangeView {
357                backing: RangeBacking::Owned(sheet),
358                sr: 1,
359                sc: 1,
360                er: 0,
361                ec: 0,
362                rows: 0,
363                cols: 0,
364                cancel_token,
365            });
366        }
367
368        Ok(RangeView {
369            backing: RangeBacking::Owned(sheet),
370            sr: 0,
371            sc: 0,
372            er: nrows - 1,
373            ec: ncols - 1,
374            rows: nrows,
375            cols: ncols,
376            cancel_token,
377        })
378    }
379
380    pub fn dims(&self) -> (usize, usize) {
381        (self.rows, self.cols)
382    }
383
384    pub fn expand_to(&self, rows: usize, cols: usize) -> RangeView<'a> {
385        let er = self.sr + rows.saturating_sub(1);
386        let ec = self.sc + cols.saturating_sub(1);
387        RangeView {
388            backing: match &self.backing {
389                RangeBacking::Borrowed(s) => RangeBacking::Borrowed(s),
390                RangeBacking::Owned(s) => RangeBacking::Owned(s.clone()),
391            },
392            sr: self.sr,
393            sc: self.sc,
394            er,
395            ec,
396            rows,
397            cols,
398            cancel_token: self.cancel_token.clone(),
399        }
400    }
401
402    pub fn sub_view(&self, rs: usize, cs: usize, rows: usize, cols: usize) -> RangeView<'a> {
403        let abs_sr = self.sr + rs;
404        let abs_sc = self.sc + cs;
405        let er = abs_sr + rows.saturating_sub(1);
406        let ec = abs_sc + cols.saturating_sub(1);
407        RangeView {
408            backing: match &self.backing {
409                RangeBacking::Borrowed(s) => RangeBacking::Borrowed(s),
410                RangeBacking::Owned(s) => RangeBacking::Owned(s.clone()),
411            },
412            sr: abs_sr,
413            sc: abs_sc,
414            er,
415            ec,
416            rows,
417            cols,
418            cancel_token: self.cancel_token.clone(),
419        }
420    }
421
422    #[inline]
423    pub fn is_empty(&self) -> bool {
424        self.rows == 0 || self.cols == 0
425    }
426
427    /// Absolute 0-based start row of this view.
428    pub fn start_row(&self) -> usize {
429        self.sr
430    }
431    /// Absolute 0-based end row of this view (inclusive).
432    pub fn end_row(&self) -> usize {
433        self.er
434    }
435    /// Absolute 0-based start column of this view.
436    pub fn start_col(&self) -> usize {
437        self.sc
438    }
439    /// Absolute 0-based end column of this view (inclusive).
440    pub fn end_col(&self) -> usize {
441        self.ec
442    }
443    /// Whether this view reads a workbook sheet, as opposed to rows owned by
444    /// the view itself (array results, and names bound to a constant or a
445    /// formula), whose backing is a temporary sheet with no cell addresses.
446    pub(crate) fn is_sheet_backed(&self) -> bool {
447        matches!(self.backing, RangeBacking::Borrowed(_))
448    }
449
450    /// Owning sheet name.
451    pub fn sheet_name(&self) -> &str {
452        &self.sheet().name
453    }
454
455    pub fn kind_probe(&self) -> RangeKind {
456        if self.is_empty() {
457            return RangeKind::Empty;
458        }
459
460        let mut has_num = false;
461        let mut has_text = false;
462
463        for r in 0..self.rows {
464            for c in 0..self.cols {
465                match self.get_cell(r, c) {
466                    LiteralValue::Empty => {}
467                    LiteralValue::Number(_) | LiteralValue::Int(_) => has_num = true,
468                    LiteralValue::Text(_) => has_text = true,
469                    _ => return RangeKind::Mixed,
470                }
471                if has_num && has_text {
472                    return RangeKind::Mixed;
473                }
474            }
475        }
476
477        match (has_num, has_text) {
478            (false, false) => RangeKind::Empty,
479            (true, false) => RangeKind::NumericOnly,
480            (false, true) => RangeKind::TextOnly,
481            (true, true) => RangeKind::Mixed,
482        }
483    }
484
485    pub fn as_1x1(&self) -> Option<LiteralValue> {
486        if self.rows == 1 && self.cols == 1 {
487            Some(self.get_cell(0, 0))
488        } else {
489            None
490        }
491    }
492
493    /// Get a specific cell by row and column index (0-based).
494    /// Returns Empty for out-of-bounds access.
495    pub fn get_cell(&self, row: usize, col: usize) -> LiteralValue {
496        if row >= self.rows || col >= self.cols {
497            return LiteralValue::Empty;
498        }
499        let abs_row = self.sr + row;
500        let abs_col = self.sc + col;
501        let sheet = self.sheet();
502        let sheet_rows = sheet.nrows as usize;
503        if abs_row >= sheet_rows {
504            return LiteralValue::Empty;
505        }
506        if abs_col >= sheet.columns.len() {
507            return LiteralValue::Empty;
508        }
509        let col_ref = &sheet.columns[abs_col];
510        // Locate chunk by binary searching start offsets
511        let chunk_starts = &sheet.chunk_starts;
512        let ch_idx = match chunk_starts.binary_search(&abs_row) {
513            Ok(i) => i,
514            Err(0) => 0,
515            Err(i) => i - 1,
516        };
517        let Some(ch) = col_ref.chunk(ch_idx) else {
518            return LiteralValue::Empty;
519        };
520        let row_start = chunk_starts[ch_idx];
521        let in_off = abs_row - row_start;
522        // Overlay takes precedence: user edits over computed over base.
523        let cascade = arrow_store::OverlayCascade::new(&ch.overlay, &ch.computed_overlay);
524        if let Some(ov) = cascade.get_scalar(in_off) {
525            return ov.to_literal_for(sheet.date_system);
526        }
527        // Read tag and route to lane
528        let tag_u8 = ch.type_tag.value(in_off);
529        match arrow_store::TypeTag::from_u8(tag_u8) {
530            arrow_store::TypeTag::Empty => LiteralValue::Empty,
531            arrow_store::TypeTag::Number => {
532                if let Some(arr) = &ch.numbers {
533                    if arr.is_null(in_off) {
534                        return LiteralValue::Empty;
535                    }
536                    LiteralValue::Number(arr.value(in_off))
537                } else {
538                    LiteralValue::Empty
539                }
540            }
541            arrow_store::TypeTag::DateTime | arrow_store::TypeTag::Duration => {
542                if let Some(arr) = &ch.numbers {
543                    if arr.is_null(in_off) {
544                        LiteralValue::Empty
545                    } else {
546                        LiteralValue::Number(arr.value(in_off))
547                    }
548                } else {
549                    LiteralValue::Empty
550                }
551            }
552            arrow_store::TypeTag::Boolean => {
553                if let Some(arr) = &ch.booleans {
554                    if arr.is_null(in_off) {
555                        return LiteralValue::Empty;
556                    }
557                    LiteralValue::Boolean(arr.value(in_off))
558                } else {
559                    LiteralValue::Empty
560                }
561            }
562            arrow_store::TypeTag::Text => {
563                if let Some(arr) = &ch.text {
564                    if arr.is_null(in_off) {
565                        return LiteralValue::Empty;
566                    }
567                    let sa = arr
568                        .as_any()
569                        .downcast_ref::<arrow_array::StringArray>()
570                        .unwrap();
571                    LiteralValue::Text(sa.value(in_off).to_string())
572                } else {
573                    LiteralValue::Empty
574                }
575            }
576            arrow_store::TypeTag::Error => {
577                if let Some(arr) = &ch.errors {
578                    if arr.is_null(in_off) {
579                        return LiteralValue::Empty;
580                    }
581                    let kind = arrow_store::unmap_error_code(arr.value(in_off));
582                    LiteralValue::Error(ExcelError::new(kind))
583                } else {
584                    LiteralValue::Empty
585                }
586            }
587            arrow_store::TypeTag::Pending => LiteralValue::Pending,
588        }
589    }
590
591    /// Iterate overlapping chunks by row segment.
592    pub fn iter_row_chunks(&self) -> RowChunkIterator<'_> {
593        RowChunkIterator {
594            segments: self.iter_row_segments(),
595        }
596    }
597
598    fn iter_row_segments(&self) -> RowSegmentIterator<'_> {
599        #[cfg(test)]
600        range_work::record(|w| w.iterators += 1);
601        RowSegmentIterator {
602            view: self,
603            chunks: None,
604        }
605    }
606
607    /// Row-major cell traversal.
608    pub fn for_each_cell(
609        &self,
610        f: &mut dyn FnMut(&LiteralValue) -> Result<(), ExcelError>,
611    ) -> Result<(), ExcelError> {
612        for res in self.iter_row_chunks() {
613            let cs = res?;
614            for r in 0..cs.row_len {
615                for c in 0..self.cols {
616                    let tmp = self.get_cell(cs.row_start + r, c);
617                    f(&tmp)?;
618                }
619            }
620        }
621        Ok(())
622    }
623
624    /// Visit each row as a borrowed slice (buffered).
625    pub fn for_each_row(
626        &self,
627        f: &mut dyn FnMut(&[LiteralValue]) -> Result<(), ExcelError>,
628    ) -> Result<(), ExcelError> {
629        let mut buf: Vec<LiteralValue> = Vec::with_capacity(self.cols);
630        for r in 0..self.rows {
631            buf.clear();
632            for c in 0..self.cols {
633                buf.push(self.get_cell(r, c));
634            }
635            f(&buf[..])?;
636        }
637        Ok(())
638    }
639
640    /// Visit each column as a contiguous slice (buffered).
641    pub fn for_each_col(
642        &self,
643        f: &mut dyn FnMut(&[LiteralValue]) -> Result<(), ExcelError>,
644    ) -> Result<(), ExcelError> {
645        let mut col_buf: Vec<LiteralValue> = Vec::with_capacity(self.rows);
646        for c in 0..self.cols {
647            col_buf.clear();
648            for r in 0..self.rows {
649                col_buf.push(self.get_cell(r, c));
650            }
651            f(&col_buf[..])?;
652        }
653        Ok(())
654    }
655
656    /// Get a numeric value at a specific cell, with coercion.
657    /// Returns None for empty cells or non-coercible values.
658    pub fn get_cell_numeric(&self, row: usize, col: usize, policy: CoercionPolicy) -> Option<f64> {
659        if row >= self.rows || col >= self.cols {
660            return None;
661        }
662
663        let val = self.get_cell(row, col);
664        pack_numeric(&val, policy).ok().flatten()
665    }
666
667    /// Numeric chunk iteration with coercion policy.
668    pub fn numbers_chunked(
669        &self,
670        policy: CoercionPolicy,
671        min_chunk: usize,
672        f: &mut dyn FnMut(NumericChunk) -> Result<(), ExcelError>,
673    ) -> Result<(), ExcelError> {
674        // Fast path for Arrow numbers lane when policy allows ignoring non-numeric cells in ranges (standard Excel behavior for SUM/AVERAGE/etc over ranges)
675        if matches!(policy, CoercionPolicy::NumberStrict) {
676            for res in self.numbers_slices() {
677                let (_, _, cols) = res?;
678                for col in cols {
679                    if col.null_count() < col.len() {
680                        let data = col.values();
681                        // If there are nulls, we need to handle them.
682                        // Currently NumericChunk doesn't have a perfect way to represent sparse Arrow slices
683                        // without copying if we want a contiguous f64 slice.
684                        // For now, we can just provide the raw data and the validity mask if it exists.
685
686                        let validity = if col.null_count() > 0 {
687                            // Extract validity mask.
688                            // Note: This is still slightly awkward with the current NumericChunk design.
689                            None // TODO: Implement validity mask propagation
690                        } else {
691                            None
692                        };
693
694                        if col.null_count() == 0 {
695                            f(NumericChunk { data, validity })?;
696                        } else {
697                            // Fallback for nulls: iterate and push to a small buffer
698                            let mut buf = Vec::with_capacity(col.len());
699                            for i in 0..col.len() {
700                                if !col.is_null(i) {
701                                    buf.push(col.value(i));
702                                }
703                            }
704                            if !buf.is_empty() {
705                                f(NumericChunk {
706                                    data: &buf,
707                                    validity: None,
708                                })?;
709                            }
710                        }
711                    }
712                }
713            }
714            return Ok(());
715        }
716
717        let min_chunk = min_chunk.max(1);
718        let mut buf: Vec<f64> = Vec::with_capacity(min_chunk);
719        let mut flush = |buf: &mut Vec<f64>| -> Result<(), ExcelError> {
720            if buf.is_empty() {
721                return Ok(());
722            }
723            // SAFETY: read-only borrow for callback duration
724            let ptr = buf.as_ptr();
725            let len = buf.len();
726            let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
727            let chunk = NumericChunk {
728                data: slice,
729                validity: None,
730            };
731            f(chunk)?;
732            buf.clear();
733            Ok(())
734        };
735
736        self.for_each_cell(&mut |v| {
737            if let Some(n) = pack_numeric(v, policy)? {
738                buf.push(n);
739                if buf.len() >= min_chunk {
740                    flush(&mut buf)?;
741                }
742            }
743            Ok(())
744        })?;
745        flush(&mut buf)?;
746
747        Ok(())
748    }
749
750    /// Typed numeric slices per row-segment: (row_start, row_len, per-column Float64 arrays)
751    pub fn numbers_slices(
752        &self,
753    ) -> impl Iterator<Item = Result<(usize, usize, Vec<Arc<arrow_array::Float64Array>>), ExcelError>> + '_
754    {
755        self.iter_row_segments().map(move |res| {
756            let segment = res?;
757            let mut out_cols = Vec::with_capacity(self.cols);
758            let sheet = self.sheet();
759            for col_idx in self.sc..=self.ec {
760                let Some(ch) = sheet
761                    .columns
762                    .get(col_idx)
763                    .and_then(|col| col.chunk(segment.chunk_idx))
764                else {
765                    #[cfg(test)]
766                    range_work::record(|w| {
767                        w.null_arrays += 1;
768                        w.null_slots += segment.row_len;
769                    });
770                    out_cols.push(Arc::new(arrow_array::Float64Array::new_null(
771                        segment.row_len,
772                    )));
773                    continue;
774                };
775                let base = ch
776                    .numbers_or_null()
777                    .slice(segment.chunk_offset, segment.row_len);
778                let range = segment.chunk_offset..segment.chunk_offset + segment.row_len;
779                let cascade = arrow_store::OverlayCascade::new(&ch.overlay, &ch.computed_overlay);
780                out_cols.push(if cascade.has_any_in_range(range.clone()) {
781                    ch.merged_numbers(range.clone())
782                        .unwrap_or_else(|| cascade.select_numbers(range, &base))
783                } else {
784                    Arc::new(base)
785                });
786            }
787            Ok((segment.row_start, segment.row_len, out_cols))
788        })
789    }
790
791    /// Typed boolean slices per row-segment, overlay-aware via zip.
792    pub fn booleans_slices(
793        &self,
794    ) -> impl Iterator<Item = Result<(usize, usize, Vec<Arc<arrow_array::BooleanArray>>), ExcelError>> + '_
795    {
796        self.iter_row_chunks().map(move |res| {
797            let cs = res?;
798            let mut out_cols: Vec<Arc<arrow_array::BooleanArray>> =
799                Vec::with_capacity(cs.cols.len());
800            let sheet = self.sheet();
801            let chunk_starts = &sheet.chunk_starts;
802
803            for (local_c, col_idx) in (self.sc..=self.ec).enumerate() {
804                let base = cs.cols[local_c]
805                    .booleans
806                    .as_ref()
807                    .expect("booleans lane exists")
808                    .clone();
809                let base_ba = base
810                    .as_any()
811                    .downcast_ref::<arrow_array::BooleanArray>()
812                    .unwrap()
813                    .clone();
814                let base_arc = Arc::new(base_ba);
815
816                // Identify chunk and overlay segment
817                let abs_seg_start = self.sr + cs.row_start;
818                let ch_idx = match chunk_starts.binary_search(&abs_seg_start) {
819                    Ok(i) => i,
820                    Err(0) => 0,
821                    Err(i) => i - 1,
822                };
823                if col_idx >= sheet.columns.len() {
824                    out_cols.push(base_arc);
825                    continue;
826                }
827                let col = &sheet.columns[col_idx];
828                let Some(ch) = col.chunk(ch_idx) else {
829                    out_cols.push(base_arc);
830                    continue;
831                };
832                let rel_off = (self.sr + cs.row_start) - chunk_starts[ch_idx];
833                let seg_range = rel_off..(rel_off + cs.row_len);
834                let cascade = arrow_store::OverlayCascade::new(&ch.overlay, &ch.computed_overlay);
835                if cascade.has_any_in_range(seg_range.clone()) {
836                    let base_ba = base
837                        .as_any()
838                        .downcast_ref::<arrow_array::BooleanArray>()
839                        .unwrap();
840                    out_cols.push(cascade.select_booleans(seg_range, base_ba));
841                } else {
842                    out_cols.push(base_arc);
843                }
844            }
845            Ok((cs.row_start, cs.row_len, out_cols))
846        })
847    }
848
849    /// Text slices per row-segment (erased as ArrayRef for Utf8 today; future Dict/View support).
850    pub fn text_slices(
851        &self,
852    ) -> impl Iterator<Item = Result<(usize, usize, Vec<arrow_array::ArrayRef>), ExcelError>> + '_
853    {
854        self.iter_row_chunks().map(move |res| {
855            let cs = res?;
856            let mut out_cols: Vec<arrow_array::ArrayRef> = Vec::with_capacity(cs.cols.len());
857            let sheet = self.sheet();
858            let chunk_starts = &sheet.chunk_starts;
859
860            for (local_c, col_idx) in (self.sc..=self.ec).enumerate() {
861                let base = cs.cols[local_c]
862                    .text
863                    .as_ref()
864                    .expect("text lane exists")
865                    .clone();
866                let abs_seg_start = self.sr + cs.row_start;
867                let ch_idx = match chunk_starts.binary_search(&abs_seg_start) {
868                    Ok(i) => i,
869                    Err(0) => 0,
870                    Err(i) => i - 1,
871                };
872                if col_idx >= sheet.columns.len() {
873                    out_cols.push(base.clone());
874                    continue;
875                }
876                let col = &sheet.columns[col_idx];
877                let Some(ch) = col.chunk(ch_idx) else {
878                    out_cols.push(base.clone());
879                    continue;
880                };
881                let rel_off = (self.sr + cs.row_start) - chunk_starts[ch_idx];
882                let seg_range = rel_off..(rel_off + cs.row_len);
883                let cascade = arrow_store::OverlayCascade::new(&ch.overlay, &ch.computed_overlay);
884                if cascade.has_any_in_range(seg_range.clone()) {
885                    let base_sa = base
886                        .as_any()
887                        .downcast_ref::<arrow_array::StringArray>()
888                        .unwrap();
889                    out_cols.push(cascade.select_text(seg_range, base_sa));
890                } else {
891                    out_cols.push(base.clone());
892                }
893            }
894            Ok((cs.row_start, cs.row_len, out_cols))
895        })
896    }
897
898    /// Typed lowered text slices per row-segment, overlay-aware via zip.
899    pub fn lowered_text_slices(
900        &self,
901    ) -> impl Iterator<Item = Result<(usize, usize, Vec<Arc<arrow_array::StringArray>>), ExcelError>> + '_
902    {
903        self.iter_row_chunks().map(move |res| {
904            let cs = res?;
905            let mut out_cols: Vec<Arc<arrow_array::StringArray>> =
906                Vec::with_capacity(cs.cols.len());
907            let sheet = self.sheet();
908            let chunk_starts = &sheet.chunk_starts;
909
910            for (local_c, col_idx) in (self.sc..=self.ec).enumerate() {
911                // Identify chunk
912                let abs_seg_start = self.sr + cs.row_start;
913                let ch_idx = match chunk_starts.binary_search(&abs_seg_start) {
914                    Ok(i) => i,
915                    Err(0) => 0,
916                    Err(i) => i - 1,
917                };
918                if col_idx >= sheet.columns.len() {
919                    out_cols.push(Arc::new(arrow_array::StringArray::new_null(cs.row_len)));
920                    continue;
921                }
922                let col = &sheet.columns[col_idx];
923                let Some(ch) = col.chunk(ch_idx) else {
924                    out_cols.push(Arc::new(arrow_array::StringArray::new_null(cs.row_len)));
925                    continue;
926                };
927                let rel_off = (self.sr + cs.row_start) - chunk_starts[ch_idx];
928                let seg_range = rel_off..(rel_off + cs.row_len);
929
930                let base_lowered = ch.text_lower_or_null();
931                let base_seg = base_lowered.slice(rel_off, cs.row_len);
932                let base_sa = base_seg
933                    .as_any()
934                    .downcast_ref::<arrow_array::StringArray>()
935                    .expect("lowered slice downcast");
936
937                let cascade = arrow_store::OverlayCascade::new(&ch.overlay, &ch.computed_overlay);
938                if cascade.has_any_in_range(seg_range.clone()) {
939                    out_cols.push(cascade.select_lowered_text(seg_range, base_sa));
940                } else {
941                    out_cols.push(Arc::new(base_sa.clone()));
942                }
943            }
944            Ok((cs.row_start, cs.row_len, out_cols))
945        })
946    }
947
948    /// Typed error-code slices per row-segment.
949    pub fn errors_slices(
950        &self,
951    ) -> impl Iterator<Item = Result<(usize, usize, Vec<Arc<arrow_array::UInt8Array>>), ExcelError>> + '_
952    {
953        self.iter_row_segments().map(move |res| {
954            let segment = res?;
955            let mut out_cols = Vec::with_capacity(self.cols);
956            let sheet = self.sheet();
957            for col_idx in self.sc..=self.ec {
958                let Some(ch) = sheet
959                    .columns
960                    .get(col_idx)
961                    .and_then(|col| col.chunk(segment.chunk_idx))
962                else {
963                    #[cfg(test)]
964                    range_work::record(|w| {
965                        w.null_arrays += 1;
966                        w.null_slots += segment.row_len;
967                    });
968                    out_cols.push(Arc::new(arrow_array::UInt8Array::new_null(segment.row_len)));
969                    continue;
970                };
971                let base = ch
972                    .errors_or_null()
973                    .slice(segment.chunk_offset, segment.row_len);
974                let range = segment.chunk_offset..segment.chunk_offset + segment.row_len;
975                let cascade = arrow_store::OverlayCascade::new(&ch.overlay, &ch.computed_overlay);
976                out_cols.push(if cascade.has_any_in_range(range.clone()) {
977                    ch.merged_errors(range.clone())
978                        .unwrap_or_else(|| cascade.select_errors(range, &base))
979                } else {
980                    Arc::new(base)
981                });
982            }
983            Ok((segment.row_start, segment.row_len, out_cols))
984        })
985    }
986
987    /// Typed type-tag slices per row-segment.
988    pub fn type_tags_slices(
989        &self,
990    ) -> impl Iterator<Item = Result<(usize, usize, Vec<Arc<arrow_array::UInt8Array>>), ExcelError>> + '_
991    {
992        self.iter_row_chunks().map(move |res| {
993            let cs = res?;
994            let mut out_cols: Vec<Arc<arrow_array::UInt8Array>> = Vec::with_capacity(cs.cols.len());
995            let sheet = self.sheet();
996            let chunk_starts = &sheet.chunk_starts;
997
998            for (local_c, col_idx) in (self.sc..=self.ec).enumerate() {
999                let base = cs.cols[local_c].type_tag.clone();
1000                let base_ta = base
1001                    .as_any()
1002                    .downcast_ref::<arrow_array::UInt8Array>()
1003                    .unwrap()
1004                    .clone();
1005                let base_arc = Arc::new(base_ta);
1006
1007                let abs_seg_start = self.sr + cs.row_start;
1008                let ch_idx = match chunk_starts.binary_search(&abs_seg_start) {
1009                    Ok(i) => i,
1010                    Err(0) => 0,
1011                    Err(i) => i - 1,
1012                };
1013                if col_idx >= sheet.columns.len() {
1014                    out_cols.push(base_arc);
1015                    continue;
1016                }
1017                let col = &sheet.columns[col_idx];
1018                let Some(ch) = col.chunk(ch_idx) else {
1019                    out_cols.push(base_arc);
1020                    continue;
1021                };
1022                let rel_off = (self.sr + cs.row_start) - chunk_starts[ch_idx];
1023                let seg_range = rel_off..(rel_off + cs.row_len);
1024                let cascade = arrow_store::OverlayCascade::new(&ch.overlay, &ch.computed_overlay);
1025                if cascade.has_any_in_range(seg_range.clone()) {
1026                    let base_ta = base
1027                        .as_any()
1028                        .downcast_ref::<arrow_array::UInt8Array>()
1029                        .unwrap();
1030                    out_cols.push(cascade.select_type_tags(seg_range, base_ta));
1031                } else {
1032                    out_cols.push(base_arc);
1033                }
1034            }
1035            Ok((cs.row_start, cs.row_len, out_cols))
1036        })
1037    }
1038
1039    /// Build per-column concatenated lowered text arrays for this view.
1040    /// Uses per-chunk lowered cache for base text and merges overlays via zip_select.
1041    pub fn lowered_text_columns(&self) -> Vec<arrow_array::ArrayRef> {
1042        use crate::compute_prelude::concat_arrays;
1043
1044        let mut out: Vec<arrow_array::ArrayRef> = Vec::with_capacity(self.cols);
1045        if self.rows == 0 || self.cols == 0 {
1046            return out;
1047        }
1048        let sheet = self.sheet();
1049        let chunk_starts = &sheet.chunk_starts;
1050        // Clamp to physically materialized sheet rows; this view may be logically larger (e.g. A:A).
1051        let sheet_rows = sheet.nrows as usize;
1052        if sheet_rows == 0 || self.sr >= sheet_rows {
1053            for _ in 0..self.cols {
1054                out.push(arrow_array::new_null_array(&DataType::Utf8, 0));
1055            }
1056            return out;
1057        }
1058        let row_end = self.er.min(sheet_rows.saturating_sub(1));
1059        let physical_len = row_end.saturating_sub(self.sr) + 1;
1060        for col_idx in self.sc..=self.ec {
1061            let mut segs: Vec<arrow_array::ArrayRef> = Vec::new();
1062            if col_idx >= sheet.columns.len() {
1063                // OOB: nulls across rows
1064                segs.push(arrow_array::new_null_array(&DataType::Utf8, physical_len));
1065            } else {
1066                let col_ref = &sheet.columns[col_idx];
1067                for (ci, &start) in chunk_starts.iter().enumerate() {
1068                    let chunk_end = chunk_starts
1069                        .get(ci + 1)
1070                        .copied()
1071                        .unwrap_or(sheet.nrows as usize);
1072                    let len = chunk_end.saturating_sub(start);
1073                    if len == 0 {
1074                        continue;
1075                    }
1076                    let end = start + len - 1;
1077                    let is = start.max(self.sr);
1078                    let ie = end.min(row_end);
1079                    if is > ie {
1080                        continue;
1081                    }
1082                    let seg_len = ie - is + 1;
1083                    let rel_off = is - start;
1084                    if let Some(ch) = col_ref.chunk(ci) {
1085                        // Overlay-aware lowered segment
1086                        let seg_range = rel_off..(rel_off + seg_len);
1087                        let cascade =
1088                            arrow_store::OverlayCascade::new(&ch.overlay, &ch.computed_overlay);
1089                        if cascade.has_any_in_range(seg_range.clone()) {
1090                            let base_lowered = ch.text_lower_or_null();
1091                            let base_seg = base_lowered.slice(rel_off, seg_len);
1092                            let base_sa = base_seg
1093                                .as_any()
1094                                .downcast_ref::<arrow_array::StringArray>()
1095                                .expect("lowered slice downcast");
1096                            segs.push(cascade.select_lowered_text(seg_range, base_sa));
1097                        } else {
1098                            // No overlay: slice from lowered base
1099                            let lowered = ch.text_lower_or_null();
1100                            segs.push(lowered.slice(rel_off, seg_len));
1101                        }
1102                    } else {
1103                        segs.push(arrow_array::new_null_array(&DataType::Utf8, seg_len));
1104                    }
1105                }
1106            }
1107            // Ensure concat has at least one segment (can happen on sparse/empty sheets).
1108            if segs.is_empty() {
1109                segs.push(arrow_array::new_null_array(&DataType::Utf8, physical_len));
1110            }
1111            // Concat segments for this column
1112            let anys: Vec<&dyn arrow_array::Array> = segs
1113                .iter()
1114                .map(|a| a.as_ref() as &dyn arrow_array::Array)
1115                .collect();
1116            let conc = concat_arrays(&anys).expect("concat lowered segments");
1117            out.push(conc);
1118        }
1119        out
1120    }
1121
1122    /// Slice typed float arrays for a specific row interval (relative to view).
1123    pub fn slice_numbers(
1124        &self,
1125        rel_start: usize,
1126        len: usize,
1127    ) -> Vec<Option<Arc<arrow_array::Float64Array>>> {
1128        let abs_start = self.sr + rel_start;
1129        let abs_end = abs_start + len;
1130        let sheet = self.sheet();
1131        let chunk_starts = &sheet.chunk_starts;
1132
1133        let mut out_cols = Vec::with_capacity(self.cols);
1134        for col_idx in self.sc..=self.ec {
1135            if col_idx >= sheet.columns.len() {
1136                out_cols.push(None);
1137                continue;
1138            }
1139            let col = &sheet.columns[col_idx];
1140
1141            let start_ch_idx = match chunk_starts.binary_search(&abs_start) {
1142                Ok(i) => i,
1143                Err(0) => 0,
1144                Err(i) => i - 1,
1145            };
1146
1147            let mut segments: Vec<Arc<arrow_array::Float64Array>> = Vec::new();
1148            let mut null_only = true;
1149
1150            let mut curr = abs_start;
1151            let mut remaining = len;
1152            let mut ch_idx = start_ch_idx;
1153
1154            while remaining > 0 && ch_idx < chunk_starts.len() {
1155                let ch_start = chunk_starts[ch_idx];
1156                let ch_end = chunk_starts
1157                    .get(ch_idx + 1)
1158                    .copied()
1159                    .unwrap_or(sheet.nrows as usize);
1160                let ch_len = ch_end.saturating_sub(ch_start);
1161                if ch_len == 0 {
1162                    ch_idx += 1;
1163                    continue;
1164                }
1165
1166                let overlap_start = curr.max(ch_start);
1167                let overlap_end = ch_end.min(abs_end);
1168
1169                if overlap_start < overlap_end {
1170                    let seg_len = overlap_end - overlap_start;
1171                    let rel_off_in_chunk = overlap_start - ch_start;
1172
1173                    if let Some(ch) = col.chunk(ch_idx) {
1174                        let base_nums_arc = ch.numbers_or_null();
1175                        let base_nums = base_nums_arc.as_ref();
1176
1177                        let seg_range = rel_off_in_chunk..(rel_off_in_chunk + seg_len);
1178                        let cascade =
1179                            arrow_store::OverlayCascade::new(&ch.overlay, &ch.computed_overlay);
1180
1181                        let final_arr = if cascade.has_any_in_range(seg_range.clone()) {
1182                            let base_slice = base_nums.slice(rel_off_in_chunk, seg_len);
1183                            let base_fa = base_slice
1184                                .as_any()
1185                                .downcast_ref::<arrow_array::Float64Array>()
1186                                .unwrap();
1187                            cascade.select_numbers(seg_range, base_fa).as_ref().clone()
1188                        } else {
1189                            let sl = base_nums.slice(rel_off_in_chunk, seg_len);
1190                            sl.as_any()
1191                                .downcast_ref::<arrow_array::Float64Array>()
1192                                .unwrap()
1193                                .clone()
1194                        };
1195
1196                        if final_arr.null_count() < final_arr.len() {
1197                            null_only = false;
1198                        }
1199                        segments.push(Arc::new(final_arr));
1200                    } else {
1201                        segments.push(Arc::new(arrow_array::Float64Array::new_null(seg_len)));
1202                    }
1203                    curr += seg_len;
1204                    remaining -= seg_len;
1205                }
1206                ch_idx += 1;
1207            }
1208
1209            if remaining > 0 {
1210                segments.push(Arc::new(arrow_array::Float64Array::new_null(remaining)));
1211            }
1212
1213            if segments.len() == 1 {
1214                if null_only && segments[0].null_count() == segments[0].len() {
1215                    out_cols.push(None);
1216                } else {
1217                    out_cols.push(Some(segments.pop().unwrap()));
1218                }
1219            } else {
1220                let refs: Vec<&dyn Array> =
1221                    segments.iter().map(|a| a.as_ref() as &dyn Array).collect();
1222                let c = crate::compute_prelude::concat_arrays(&refs).expect("concat slice");
1223                let fa = c
1224                    .as_any()
1225                    .downcast_ref::<arrow_array::Float64Array>()
1226                    .unwrap()
1227                    .clone();
1228                out_cols.push(Some(Arc::new(fa)));
1229            }
1230        }
1231        out_cols
1232    }
1233
1234    /// Slice typed lowered text arrays for a specific row interval (relative to view).
1235    pub fn slice_lowered_text(
1236        &self,
1237        rel_start: usize,
1238        len: usize,
1239    ) -> Vec<Option<Arc<arrow_array::StringArray>>> {
1240        let abs_start = self.sr + rel_start;
1241        let abs_end = abs_start + len;
1242        let sheet = self.sheet();
1243        let chunk_starts = &sheet.chunk_starts;
1244
1245        let mut out_cols = Vec::with_capacity(self.cols);
1246        for col_idx in self.sc..=self.ec {
1247            if col_idx >= sheet.columns.len() {
1248                out_cols.push(None);
1249                continue;
1250            }
1251            let col = &sheet.columns[col_idx];
1252            let start_ch_idx = match chunk_starts.binary_search(&abs_start) {
1253                Ok(i) => i,
1254                Err(0) => 0,
1255                Err(i) => i - 1,
1256            };
1257
1258            let mut segments: Vec<Arc<arrow_array::StringArray>> = Vec::new();
1259            let mut null_only = true;
1260
1261            let mut curr = abs_start;
1262            let mut remaining = len;
1263            let mut ch_idx = start_ch_idx;
1264
1265            while remaining > 0 && ch_idx < chunk_starts.len() {
1266                let ch_start = chunk_starts[ch_idx];
1267                let ch_end = chunk_starts
1268                    .get(ch_idx + 1)
1269                    .copied()
1270                    .unwrap_or(sheet.nrows as usize);
1271                let ch_len = ch_end.saturating_sub(ch_start);
1272                if ch_len == 0 {
1273                    ch_idx += 1;
1274                    continue;
1275                }
1276
1277                let overlap_start = curr.max(ch_start);
1278                let overlap_end = ch_end.min(abs_end);
1279
1280                if overlap_start < overlap_end {
1281                    let seg_len = overlap_end - overlap_start;
1282                    let rel_off_in_chunk = overlap_start - ch_start;
1283
1284                    if let Some(ch) = col.chunk(ch_idx) {
1285                        let base_lowered = ch.text_lower_or_null();
1286                        let seg_range = rel_off_in_chunk..(rel_off_in_chunk + seg_len);
1287                        let cascade =
1288                            arrow_store::OverlayCascade::new(&ch.overlay, &ch.computed_overlay);
1289
1290                        let final_arr = if cascade.has_any_in_range(seg_range.clone()) {
1291                            let base_slice = base_lowered.slice(rel_off_in_chunk, seg_len);
1292                            let base_sa = base_slice
1293                                .as_any()
1294                                .downcast_ref::<arrow_array::StringArray>()
1295                                .unwrap();
1296                            cascade
1297                                .select_lowered_text(seg_range, base_sa)
1298                                .as_ref()
1299                                .clone()
1300                        } else {
1301                            let sl = base_lowered.slice(rel_off_in_chunk, seg_len);
1302                            sl.as_any()
1303                                .downcast_ref::<arrow_array::StringArray>()
1304                                .unwrap()
1305                                .clone()
1306                        };
1307
1308                        if final_arr.null_count() < final_arr.len() {
1309                            null_only = false;
1310                        }
1311                        segments.push(Arc::new(final_arr));
1312                    } else {
1313                        segments.push(Arc::new(arrow_array::StringArray::new_null(seg_len)));
1314                    }
1315                    curr += seg_len;
1316                    remaining -= seg_len;
1317                }
1318                ch_idx += 1;
1319            }
1320
1321            if remaining > 0 {
1322                segments.push(Arc::new(arrow_array::StringArray::new_null(remaining)));
1323            }
1324
1325            if segments.len() == 1 {
1326                if null_only && segments[0].null_count() == segments[0].len() {
1327                    out_cols.push(None);
1328                } else {
1329                    out_cols.push(Some(segments.pop().unwrap()));
1330                }
1331            } else {
1332                let refs: Vec<&dyn Array> =
1333                    segments.iter().map(|a| a.as_ref() as &dyn Array).collect();
1334                let c = crate::compute_prelude::concat_arrays(&refs).expect("concat text");
1335                let sa = c
1336                    .as_any()
1337                    .downcast_ref::<arrow_array::StringArray>()
1338                    .unwrap()
1339                    .clone();
1340                out_cols.push(Some(Arc::new(sa)));
1341            }
1342        }
1343        out_cols
1344    }
1345}
1346
1347#[inline]
1348fn pack_numeric(v: &LiteralValue, policy: CoercionPolicy) -> Result<Option<f64>, ExcelError> {
1349    match policy {
1350        CoercionPolicy::NumberLenientText => match v {
1351            LiteralValue::Error(e) => Err(e.clone()),
1352            LiteralValue::Empty => Ok(None),
1353            other => Ok(crate::coercion::to_number_lenient(other).ok()),
1354        },
1355        CoercionPolicy::NumberStrict => match v {
1356            LiteralValue::Error(e) => Err(e.clone()),
1357            LiteralValue::Empty => Ok(None),
1358            other => Ok(crate::coercion::to_number_strict(other).ok()),
1359        },
1360        _ => match v {
1361            LiteralValue::Error(e) => Err(e.clone()),
1362            _ => Ok(None),
1363        },
1364    }
1365}
1366
1367#[cfg(test)]
1368mod tests {
1369    use super::*;
1370
1371    #[test]
1372    fn owned_rows_numeric_chunking() {
1373        let data: Vec<Vec<LiteralValue>> = vec![
1374            vec![
1375                LiteralValue::Number(1.0),
1376                LiteralValue::Text("x".into()),
1377                LiteralValue::Number(3.0),
1378            ],
1379            vec![
1380                LiteralValue::Boolean(true),
1381                LiteralValue::Empty,
1382                LiteralValue::Number(2.5),
1383            ],
1384        ];
1385        let view = RangeView::from_owned_rows(data, DateSystem::Excel1900);
1386        let mut sum = 0.0f64;
1387        view.numbers_chunked(CoercionPolicy::NumberLenientText, 2, &mut |chunk| {
1388            for &n in chunk.data {
1389                sum += n;
1390            }
1391            Ok(())
1392        })
1393        .unwrap();
1394        assert!((sum - 7.5).abs() < 1e-9);
1395    }
1396
1397    #[test]
1398    fn as_1x1_works() {
1399        let view = RangeView::from_owned_rows(
1400            vec![vec![LiteralValue::Number(7.0)]],
1401            DateSystem::Excel1900,
1402        );
1403        assert_eq!(view.as_1x1(), Some(LiteralValue::Number(7.0)));
1404    }
1405
1406    #[test]
1407    fn pre_cancelled_token_stops_owned_row_construction() {
1408        let token = CancelToken::new();
1409        token.cancel();
1410
1411        let error = RangeView::try_from_owned_rows(
1412            vec![vec![LiteralValue::Number(1.0)]],
1413            DateSystem::Excel1900,
1414            Some(token),
1415        )
1416        .unwrap_err();
1417
1418        assert_eq!(error.kind, formualizer_common::ExcelErrorKind::Cancelled);
1419    }
1420
1421    #[test]
1422    fn pre_cancelled_token_stops_row_chunk_iteration() {
1423        let token = CancelToken::new();
1424        token.cancel();
1425        let view = RangeView::from_owned_rows(
1426            vec![vec![LiteralValue::Number(1.0)]],
1427            DateSystem::Excel1900,
1428        )
1429        .with_cancel_token(Some(token));
1430
1431        let Some(Err(error)) = view.iter_row_chunks().next() else {
1432            panic!("pre-cancelled chunk iteration should return cancellation");
1433        };
1434
1435        assert_eq!(error.kind, formualizer_common::ExcelErrorKind::Cancelled);
1436    }
1437}
1438#[cfg(test)]
1439mod bounded_projection_tests {
1440    use super::*;
1441    use crate::arrow_store::{ArrowSheet, OverlayFragment, OverlayValue};
1442    use arrow_array::{Float64Array, UInt8Array};
1443    use formualizer_common::ExcelErrorKind;
1444
1445    // Deliberately retain the exhaustive baseline intersection algorithm as an oracle.
1446    fn segments(view: &RangeView<'_>) -> Vec<(usize, usize, usize, usize)> {
1447        let sheet = view.sheet();
1448        let mut out = Vec::new();
1449        let row_end = view.er.min((sheet.nrows as usize).saturating_sub(1));
1450        for (ci, &start) in sheet.chunk_starts.iter().enumerate() {
1451            let end = sheet
1452                .chunk_starts
1453                .get(ci + 1)
1454                .copied()
1455                .unwrap_or(sheet.nrows as usize);
1456            let len = end.saturating_sub(start);
1457            if len == 0 {
1458                continue;
1459            }
1460            let lo = start.max(view.sr);
1461            let hi = (start + len - 1).min(row_end);
1462            if lo <= hi {
1463                out.push((ci, lo - start, lo - view.sr, hi - lo + 1));
1464            }
1465        }
1466        out
1467    }
1468
1469    fn assert_projection(view: &RangeView<'_>) {
1470        let expected = segments(view);
1471        let generic: Vec<_> = view.iter_row_chunks().map(Result::unwrap).collect();
1472        assert_eq!(
1473            generic
1474                .iter()
1475                .map(|s| (s.row_start, s.row_len))
1476                .collect::<Vec<_>>(),
1477            expected.iter().map(|s| (s.2, s.3)).collect::<Vec<_>>()
1478        );
1479        let numbers: Vec<_> = view.numbers_slices().map(Result::unwrap).collect();
1480        let errors: Vec<_> = view.errors_slices().map(Result::unwrap).collect();
1481        assert_eq!(numbers.len(), expected.len());
1482        assert_eq!(errors.len(), expected.len());
1483        for (seg, &(ci, offset, row_start, len)) in expected.iter().enumerate() {
1484            assert_eq!((numbers[seg].0, numbers[seg].1), (row_start, len));
1485            assert_eq!((errors[seg].0, errors[seg].1), (row_start, len));
1486            let columns = (view.sc..=view.ec).count();
1487            assert_eq!(generic[seg].cols.len(), columns);
1488            assert_eq!(numbers[seg].2.len(), columns);
1489            assert_eq!(errors[seg].2.len(), columns);
1490            for (c, absolute) in (view.sc..=view.ec).enumerate() {
1491                let (base_n, base_e) =
1492                    match view.sheet().columns.get(absolute).and_then(|c| c.chunk(ci)) {
1493                        Some(ch) => {
1494                            // Access physical lanes directly, without the optimized cursor/providers.
1495                            let n = ch
1496                                .numbers
1497                                .as_ref()
1498                                .map(|a| a.slice(offset, len))
1499                                .unwrap_or_else(|| Float64Array::new_null(len));
1500                            let e = ch
1501                                .errors
1502                                .as_ref()
1503                                .map(|a| a.slice(offset, len))
1504                                .unwrap_or_else(|| UInt8Array::new_null(len));
1505                            let cascade =
1506                                arrow_store::OverlayCascade::new(&ch.overlay, &ch.computed_overlay);
1507                            (
1508                                cascade.select_numbers(offset..offset + len, &n),
1509                                cascade.select_errors(offset..offset + len, &e),
1510                            )
1511                        }
1512                        None => (
1513                            Arc::new(Float64Array::new_null(len)),
1514                            Arc::new(UInt8Array::new_null(len)),
1515                        ),
1516                    };
1517                let actual_n = &numbers[seg].2[c];
1518                let actual_e = &errors[seg].2[c];
1519                assert_eq!(actual_n.len(), len);
1520                assert_eq!(actual_e.len(), len);
1521                for i in 0..len {
1522                    assert_eq!(actual_n.is_null(i), base_n.is_null(i));
1523                    assert_eq!(actual_e.is_null(i), base_e.is_null(i));
1524                    if !base_n.is_null(i) {
1525                        assert_eq!(actual_n.value(i).to_bits(), base_n.value(i).to_bits());
1526                    }
1527                    if !base_e.is_null(i) {
1528                        assert_eq!(actual_e.value(i), base_e.value(i));
1529                    }
1530                }
1531            }
1532        }
1533    }
1534
1535    fn fixture(chunk_rows: usize) -> ArrowSheet {
1536        let mut ingest = IngestBuilder::new("S", 3, chunk_rows, DateSystem::Excel1900);
1537        for row in 0..11 {
1538            ingest
1539                .append_row(&[
1540                    LiteralValue::Number(row as f64),
1541                    if row % 3 == 0 {
1542                        LiteralValue::Error(ExcelError::new(ExcelErrorKind::Div))
1543                    } else {
1544                        LiteralValue::Empty
1545                    },
1546                    LiteralValue::Text(format!("r{row}")),
1547                ])
1548                .unwrap();
1549        }
1550        let mut sheet = ingest.finish();
1551        sheet.ensure_row_capacity(29);
1552        for (row, value) in [
1553            (1, OverlayValue::Empty),
1554            (3, OverlayValue::Text(Arc::from("mask"))),
1555            (
1556                7,
1557                OverlayValue::Error(arrow_store::map_error_code(ExcelErrorKind::Na)),
1558            ),
1559            (22, OverlayValue::Number(17.0)),
1560            (28, OverlayValue::Pending),
1561        ] {
1562            let (ci, off) = sheet.chunk_of_row(row).unwrap();
1563            let chunk = sheet.ensure_column_chunk_mut(0, ci).unwrap();
1564            chunk.computed_overlay.set(off, OverlayValue::Number(99.0));
1565            chunk.overlay.set(off, value);
1566        }
1567        for (row, user) in [(0, OverlayValue::Empty), (8, OverlayValue::Number(17.0))] {
1568            let (ci, off) = sheet.chunk_of_row(row).unwrap();
1569            let ch = sheet.ensure_column_chunk_mut(1, ci).unwrap();
1570            ch.computed_overlay.set(
1571                off,
1572                OverlayValue::Error(arrow_store::map_error_code(ExcelErrorKind::Na)),
1573            );
1574            ch.overlay.set(off, user);
1575        }
1576        sheet
1577    }
1578
1579    #[test]
1580    fn range_projection_matches_independent_oracle_for_all_small_bounds() {
1581        for chunk_rows in [1, 3, 8, 32] {
1582            let sheet = fixture(chunk_rows);
1583            for sr in 0..=31 {
1584                for er in 0..=31 {
1585                    for (sc, ec) in [(0, 0), (1, 2), (0, 4), (4, 5), (2, 1)] {
1586                        assert_projection(&sheet.range_view(sr, sc, er, ec));
1587                    }
1588                }
1589            }
1590            let base = sheet.range_view(2, 1, 10, 2);
1591            for rows in [0, 1, 9, 40] {
1592                for cols in [0, 1, 4] {
1593                    assert_projection(&base.sub_view(1, 1, rows, cols));
1594                    assert_projection(&base.expand_to(rows, cols));
1595                }
1596            }
1597        }
1598        let empty = IngestBuilder::new("S", 1, 4, DateSystem::Excel1900).finish();
1599        assert_projection(&empty.range_view(0, 0, 7, 3));
1600    }
1601
1602    #[test]
1603    fn range_projection_preserves_dense_run_and_partial_overlay_paths() {
1604        for run in [false, true] {
1605            let mut sheet = fixture(32);
1606            let chunk = &mut sheet.columns[0].chunks[0];
1607            chunk.overlay.clear();
1608            chunk.computed_overlay.clear();
1609            let values = vec![OverlayValue::Number(7.0); 29];
1610            let fragment = if run {
1611                OverlayFragment::run_range(0, values)
1612            } else {
1613                OverlayFragment::dense_range(0, values)
1614            }
1615            .unwrap();
1616            chunk.computed_overlay.apply_fragment(fragment);
1617            let view = sheet.range_view(2, 0, 9, 0);
1618            arrow_store::reset_overlay_select_stats();
1619            let numeric = view.numbers_slices().next().unwrap().unwrap();
1620            assert_eq!(numeric.2[0].value(0), 7.0);
1621            let stats = arrow_store::snapshot_overlay_select_stats();
1622            assert_eq!(stats.zip_select_calls, 0);
1623            assert_eq!(stats.direct_dense_slices, usize::from(!run));
1624            assert_eq!(stats.direct_run_materializations, usize::from(run));
1625            assert_projection(&view);
1626            let chunk = &mut sheet.columns[0].chunks[0];
1627            for (off, value) in [
1628                (3, OverlayValue::Empty),
1629                (4, OverlayValue::Pending),
1630                (5, OverlayValue::DateTime(45000.25)),
1631                (6, OverlayValue::Duration(0.5)),
1632                (7, OverlayValue::Boolean(true)),
1633                (
1634                    8,
1635                    OverlayValue::Error(arrow_store::map_error_code(ExcelErrorKind::Ref)),
1636                ),
1637                (9, OverlayValue::Text(Arc::from("not numeric"))),
1638            ] {
1639                chunk.overlay.set(off, value);
1640            }
1641            assert_projection(&sheet.range_view(2, 0, 10, 3));
1642        }
1643    }
1644
1645    #[test]
1646    fn range_discovery_work_is_bounded_and_projection_is_lane_local() {
1647        let mut ingest = IngestBuilder::new("S", 1, 256, DateSystem::Excel1900);
1648        for _ in 0..256 {
1649            ingest.append_row(&[LiteralValue::Number(1.0)]).unwrap();
1650        }
1651        let mut sheet = ingest.finish();
1652        sheet.ensure_row_capacity(256 * 4096);
1653        for start in [0, 256 * 2048 + 1, 256 * 4096 - 8, 256 * 4096 + 8] {
1654            let view = sheet.range_view(start, 0, start + 7, 2);
1655            range_work::begin();
1656            let generic: Vec<_> = view.iter_row_chunks().collect();
1657            let work = range_work::take();
1658            assert_eq!(work.candidates, generic.len());
1659            assert_eq!(work.segments, generic.len());
1660            assert!(work.search_probes <= 32);
1661            range_work::begin();
1662            let _: Vec<_> = view.numbers_slices().collect();
1663            let numeric = range_work::take();
1664            assert_eq!(numeric.generic_columns, 0);
1665            assert_eq!(numeric.provider_requests[1..], [0, 0, 0]);
1666            assert!(numeric.null_arrays <= 3);
1667            range_work::begin();
1668            let _: Vec<_> = view.errors_slices().collect();
1669            let errors = range_work::take();
1670            assert_eq!(errors.generic_columns, 0);
1671            assert_eq!(errors.provider_requests[0], 0);
1672            assert_eq!(errors.provider_requests[1], 0);
1673            assert_eq!(errors.provider_requests[3], 0);
1674        }
1675        let view = sheet.range_view(0, 0, sheet.nrows as usize - 1, 0);
1676        range_work::begin();
1677        assert!(view.iter_row_chunks().next().unwrap().is_ok());
1678        let work = range_work::take();
1679        assert_eq!(work.candidates, 1);
1680        assert_eq!(work.segments, 1);
1681    }
1682
1683    #[test]
1684    fn range_cold_numeric_does_not_initialize_unused_null_providers() {
1685        let mut ingest = IngestBuilder::new("S", 1, 32768, DateSystem::Excel1900);
1686        for _ in 0..32768 {
1687            ingest.append_row(&[LiteralValue::Number(1.0)]).unwrap();
1688        }
1689        let sheet = ingest.finish();
1690        let view = sheet.range_view(1, 0, 8, 0);
1691        range_work::begin();
1692        view.numbers_slices().next().unwrap().unwrap();
1693        let numeric = range_work::take();
1694        assert_eq!(numeric.provider_requests, [1, 0, 0, 0]);
1695        assert_eq!(numeric.provider_builds, [0; 4]);
1696        range_work::begin();
1697        view.errors_slices().next().unwrap().unwrap();
1698        let errors = range_work::take();
1699        assert_eq!(errors.provider_requests, [0, 0, 1, 0]);
1700        assert_eq!(errors.provider_builds, [0, 0, 1, 0]);
1701        assert_eq!(errors.provider_slots, [0, 0, 32768, 0]);
1702    }
1703
1704    #[test]
1705    fn range_cancellation_keeps_empty_exhausted_and_between_segment_errors() {
1706        let sheet = fixture(3);
1707        for bounds in [(0, 0, 28, 0), (30, 0, 35, 0), (3, 0, 1, 0), (0, 2, 5, 1)] {
1708            let token = CancelToken::new();
1709            let view = sheet
1710                .range_view(bounds.0, bounds.1, bounds.2, bounds.3)
1711                .with_cancel_token(Some(token.clone()));
1712            let mut generic = view.iter_row_chunks();
1713            let mut numeric = view.numbers_slices();
1714            let mut errors = view.errors_slices();
1715            token.cancel();
1716            for _ in 0..2 {
1717                assert_eq!(
1718                    generic.next().unwrap().err().unwrap().kind,
1719                    ExcelErrorKind::Cancelled
1720                );
1721                assert_eq!(
1722                    numeric.next().unwrap().err().unwrap().kind,
1723                    ExcelErrorKind::Cancelled
1724                );
1725                assert_eq!(
1726                    errors.next().unwrap().err().unwrap().kind,
1727                    ExcelErrorKind::Cancelled
1728                );
1729            }
1730        }
1731        let token = CancelToken::new();
1732        let view = sheet
1733            .range_view(0, 0, 28, 0)
1734            .with_cancel_token(Some(token.clone()));
1735        let mut numeric = view.numbers_slices();
1736        assert!(numeric.next().unwrap().is_ok());
1737        token.cancel();
1738        assert_eq!(
1739            numeric.next().unwrap().err().unwrap().kind,
1740            ExcelErrorKind::Cancelled
1741        );
1742        let token = CancelToken::new();
1743        let view = sheet
1744            .range_view(30, 0, 35, 0)
1745            .with_cancel_token(Some(token.clone()));
1746        let mut numeric = view.numbers_slices();
1747        assert!(numeric.next().is_none());
1748        token.cancel();
1749        assert_eq!(
1750            numeric.next().unwrap().err().unwrap().kind,
1751            ExcelErrorKind::Cancelled
1752        );
1753    }
1754
1755    #[test]
1756    fn range_projection_keeps_numeric_bits_and_engine_error_order() {
1757        let values = [
1758            0.0,
1759            -0.0,
1760            f64::INFINITY,
1761            f64::NEG_INFINITY,
1762            f64::from_bits(0x7ff8000000000001),
1763        ];
1764        let view = RangeView::from_owned_rows(
1765            values
1766                .iter()
1767                .map(|n| vec![LiteralValue::Number(*n)])
1768                .collect(),
1769            DateSystem::Excel1900,
1770        );
1771        assert_projection(&view);
1772        let slice = view.numbers_slices().next().unwrap().unwrap().2.remove(0);
1773        for (i, n) in values.iter().enumerate() {
1774            assert_eq!(slice.value(i).to_bits(), n.to_bits());
1775        }
1776        for chunk_rows in [1, 2] {
1777            let mut engine = crate::engine::Engine::new(
1778                crate::test_workbook::TestWorkbook::new(),
1779                crate::engine::EvalConfig {
1780                    arrow_storage_enabled: true,
1781                    delta_overlay_enabled: true,
1782                    write_formula_overlay_enabled: true,
1783                    enable_parallel: false,
1784                    ..Default::default()
1785                },
1786            );
1787            let mut ingest = engine.begin_bulk_ingest_arrow();
1788            ingest.add_sheet("Source", 2, chunk_rows);
1789            ingest
1790                .append_row(
1791                    "Source",
1792                    &[
1793                        LiteralValue::Number(1.0),
1794                        LiteralValue::Error(ExcelError::new(ExcelErrorKind::Na)),
1795                    ],
1796                )
1797                .unwrap();
1798            ingest
1799                .append_row(
1800                    "Source",
1801                    &[
1802                        LiteralValue::Error(ExcelError::new(ExcelErrorKind::Div)),
1803                        LiteralValue::Number(2.0),
1804                    ],
1805                )
1806                .unwrap();
1807            ingest.finish().unwrap();
1808            engine
1809                .set_cell_formula(
1810                    "Result",
1811                    1,
1812                    1,
1813                    formualizer_parse::parser::parse("=SUM(Source!A1:B2)").unwrap(),
1814                )
1815                .unwrap();
1816            engine.evaluate_all().unwrap();
1817            let expected = if chunk_rows == 1 {
1818                ExcelErrorKind::Na
1819            } else {
1820                ExcelErrorKind::Div
1821            };
1822            assert!(
1823                matches!(engine.get_cell_value("Result", 1, 1), Some(LiteralValue::Error(e)) if e.kind == expected)
1824            );
1825        }
1826    }
1827
1828    #[test]
1829    fn range_arrays_outlive_owned_view_and_new_views_follow_structural_edits() {
1830        let numbers = {
1831            let view = RangeView::from_owned_rows(
1832                vec![vec![LiteralValue::Number(-0.0)]],
1833                DateSystem::Excel1900,
1834            );
1835            view.numbers_slices().next().unwrap().unwrap().2.remove(0)
1836        };
1837        assert_eq!(numbers.value(0).to_bits(), (-0.0f64).to_bits());
1838        let errors = {
1839            let view = RangeView::from_owned_rows(
1840                vec![vec![LiteralValue::Error(ExcelError::new(
1841                    ExcelErrorKind::Div,
1842                ))]],
1843                DateSystem::Excel1900,
1844            );
1845            view.errors_slices().next().unwrap().unwrap().2.remove(0)
1846        };
1847        assert_eq!(
1848            errors.value(0),
1849            arrow_store::map_error_code(ExcelErrorKind::Div)
1850        );
1851        let mut sheet = fixture(3);
1852        let chunks_before: usize = sheet.columns.iter().map(|c| c.total_chunk_count()).sum();
1853        assert_projection(&sheet.range_view(0, 0, 40, 4));
1854        assert_eq!(
1855            sheet
1856                .columns
1857                .iter()
1858                .map(|c| c.total_chunk_count())
1859                .sum::<usize>(),
1860            chunks_before
1861        );
1862        sheet.insert_rows(4, 2);
1863        assert_projection(&sheet.range_view(1, 0, 35, 4));
1864        sheet.delete_rows(2, 1);
1865        assert_projection(&sheet.range_view(0, 0, 35, 4));
1866        sheet.ensure_row_capacity(50);
1867        assert_projection(&sheet.range_view(0, 0, 55, 4));
1868        sheet.insert_columns(1, 1);
1869        assert_projection(&sheet.range_view(0, 0, 55, 4));
1870        sheet.delete_columns(0, 1);
1871        assert_projection(&sheet.range_view(0, 0, 55, 4));
1872    }
1873}