Skip to main content

formualizer_eval/builtins/lookup/
reference_info.rs

1//! Reference information functions: ROW, ROWS, COLUMN, COLUMNS
2//!
3//! Excel semantics:
4//! - ROW([reference]) - Returns the row number of a reference
5//! - ROWS(array) - Returns the number of rows in a reference
6//! - COLUMN([reference]) - Returns the column number of a reference
7//! - COLUMNS(array) - Returns the number of columns in a reference
8//!
9//! Without arguments, ROW and COLUMN return the current cell's position
10
11use crate::args::{ArgSchema, CoercionPolicy, ShapeKind};
12use crate::function::Function;
13use crate::traits::{ArgumentHandle, FunctionContext};
14use formualizer_common::{ArgKind, ExcelError, ExcelErrorKind, LiteralValue};
15use formualizer_macros::func_caps;
16use formualizer_parse::parser::ReferenceType;
17
18#[derive(Debug)]
19pub struct RowFn;
20
21impl Function for RowFn {
22    fn name(&self) -> &'static str {
23        "ROW"
24    }
25
26    fn min_args(&self) -> usize {
27        0
28    }
29
30    func_caps!(PURE);
31
32    fn arg_schema(&self) -> &'static [ArgSchema] {
33        use once_cell::sync::Lazy;
34        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
35            vec![
36                // Optional reference
37                ArgSchema {
38                    kinds: smallvec::smallvec![ArgKind::Range],
39                    required: false,
40                    by_ref: true,
41                    shape: ShapeKind::Range,
42                    coercion: CoercionPolicy::None,
43                    max: None,
44                    repeating: None,
45                    default: None,
46                },
47            ]
48        });
49        &SCHEMA
50    }
51
52    fn eval<'a, 'b, 'c>(
53        &self,
54        args: &'c [ArgumentHandle<'a, 'b>],
55        ctx: &dyn FunctionContext<'b>,
56    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
57        if args.is_empty() {
58            // Return current cell's row (1-based) if available
59            if let Some(cell_ref) = ctx.current_cell() {
60                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
61                    cell_ref.coord.row() as i64 + 1,
62                )));
63            }
64            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
65                ExcelError::new(ExcelErrorKind::Value),
66            )));
67        }
68
69        // Get reference
70        let reference = match args[0].as_reference_or_eval() {
71            Ok(r) => r,
72            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
73        };
74
75        // Extract row number from reference (1-based)
76        let row_1based = match &reference {
77            ReferenceType::Cell { row, .. } => *row as i64,
78            ReferenceType::Range {
79                start_row: Some(sr),
80                ..
81            } => *sr as i64,
82            // Full-column references like A:A use first row
83            ReferenceType::Range {
84                start_row: None,
85                end_row: None,
86                ..
87            } => 1,
88            // Fallback: resolve the reference and use the view origin
89            _ => match ctx.resolve_range_view(&reference, ctx.current_sheet()) {
90                Ok(view) => {
91                    if view.is_empty() {
92                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
93                            ExcelError::new(ExcelErrorKind::Ref),
94                        )));
95                    }
96                    view.start_row() as i64 + 1
97                }
98                Err(e) => {
99                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
100                }
101            },
102        };
103
104        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
105            row_1based,
106        )))
107    }
108}
109
110#[derive(Debug)]
111pub struct RowsFn;
112
113impl Function for RowsFn {
114    fn name(&self) -> &'static str {
115        "ROWS"
116    }
117
118    fn min_args(&self) -> usize {
119        1
120    }
121
122    func_caps!(PURE);
123
124    fn arg_schema(&self) -> &'static [ArgSchema] {
125        use once_cell::sync::Lazy;
126        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
127            vec![
128                // Required reference/range or array
129                ArgSchema {
130                    kinds: smallvec::smallvec![ArgKind::Any],
131                    required: true,
132                    by_ref: false,
133                    shape: ShapeKind::Range,
134                    coercion: CoercionPolicy::None,
135                    max: None,
136                    repeating: None,
137                    default: None,
138                },
139            ]
140        });
141        &SCHEMA
142    }
143
144    fn eval<'a, 'b, 'c>(
145        &self,
146        args: &'c [ArgumentHandle<'a, 'b>],
147        ctx: &dyn FunctionContext<'b>,
148    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
149        const EXCEL_MAX_ROWS: i64 = 1_048_576;
150
151        if args.is_empty() {
152            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
153                ExcelError::new(ExcelErrorKind::Value),
154            )));
155        }
156
157        // Try to get reference first, fall back to array literal
158        if let Ok(reference) = args[0].as_reference_or_eval() {
159            // Calculate number of rows
160            let rows = match &reference {
161                ReferenceType::Cell { .. } => 1,
162                ReferenceType::Range {
163                    start_row: Some(sr),
164                    end_row: Some(er),
165                    ..
166                } => {
167                    if *er >= *sr {
168                        (*er - *sr + 1) as i64
169                    } else {
170                        1
171                    }
172                }
173                // Full-column references like A:A
174                ReferenceType::Range {
175                    start_row: None,
176                    end_row: None,
177                    ..
178                } => EXCEL_MAX_ROWS,
179                // Open-ended tail like A5:A
180                ReferenceType::Range {
181                    start_row: Some(sr),
182                    end_row: None,
183                    ..
184                } => EXCEL_MAX_ROWS.saturating_sub(*sr as i64).saturating_add(1),
185                // Open-ended head like A:A10 (treated as A1:A10)
186                ReferenceType::Range {
187                    start_row: None,
188                    end_row: Some(er),
189                    ..
190                } => *er as i64,
191                // Fallback for named ranges, table refs, etc.
192                _ => match ctx.resolve_range_view(&reference, ctx.current_sheet()) {
193                    Ok(view) => view.dims().0 as i64,
194                    Err(e) => {
195                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
196                    }
197                },
198            };
199            Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(rows)))
200        } else {
201            // Handle array literal
202            let v = args[0].value()?.into_literal();
203            let rows = match v {
204                LiteralValue::Array(arr) => arr.len() as i64,
205                _ => 1,
206            };
207            Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(rows)))
208        }
209    }
210}
211
212#[derive(Debug)]
213pub struct ColumnFn;
214
215impl Function for ColumnFn {
216    fn name(&self) -> &'static str {
217        "COLUMN"
218    }
219
220    fn min_args(&self) -> usize {
221        0
222    }
223
224    func_caps!(PURE);
225
226    fn arg_schema(&self) -> &'static [ArgSchema] {
227        use once_cell::sync::Lazy;
228        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
229            vec![
230                // Optional reference
231                ArgSchema {
232                    kinds: smallvec::smallvec![ArgKind::Range],
233                    required: false,
234                    by_ref: true,
235                    shape: ShapeKind::Range,
236                    coercion: CoercionPolicy::None,
237                    max: None,
238                    repeating: None,
239                    default: None,
240                },
241            ]
242        });
243        &SCHEMA
244    }
245
246    fn eval<'a, 'b, 'c>(
247        &self,
248        args: &'c [ArgumentHandle<'a, 'b>],
249        ctx: &dyn FunctionContext<'b>,
250    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
251        if args.is_empty() {
252            // Return current cell's column (1-based) if available
253            if let Some(cell_ref) = ctx.current_cell() {
254                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
255                    cell_ref.coord.col() as i64 + 1,
256                )));
257            }
258            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
259                ExcelError::new(ExcelErrorKind::Value),
260            )));
261        }
262
263        // Get reference
264        let reference = match args[0].as_reference_or_eval() {
265            Ok(r) => r,
266            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
267        };
268
269        // Extract column number from reference (1-based)
270        let col_1based = match &reference {
271            ReferenceType::Cell { col, .. } => *col as i64,
272            ReferenceType::Range {
273                start_col: Some(sc),
274                ..
275            } => *sc as i64,
276            // Full-row references like 1:1 use first column
277            ReferenceType::Range {
278                start_col: None,
279                end_col: None,
280                ..
281            } => 1,
282            // Fallback: resolve the reference and use the view origin
283            _ => match ctx.resolve_range_view(&reference, ctx.current_sheet()) {
284                Ok(view) => {
285                    if view.is_empty() {
286                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
287                            ExcelError::new(ExcelErrorKind::Ref),
288                        )));
289                    }
290                    view.start_col() as i64 + 1
291                }
292                Err(e) => {
293                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
294                }
295            },
296        };
297
298        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
299            col_1based,
300        )))
301    }
302}
303
304#[derive(Debug)]
305pub struct ColumnsFn;
306
307impl Function for ColumnsFn {
308    fn name(&self) -> &'static str {
309        "COLUMNS"
310    }
311
312    fn min_args(&self) -> usize {
313        1
314    }
315
316    func_caps!(PURE);
317
318    fn arg_schema(&self) -> &'static [ArgSchema] {
319        use once_cell::sync::Lazy;
320        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
321            vec![
322                // Required reference/range or array
323                ArgSchema {
324                    kinds: smallvec::smallvec![ArgKind::Any],
325                    required: true,
326                    by_ref: false,
327                    shape: ShapeKind::Range,
328                    coercion: CoercionPolicy::None,
329                    max: None,
330                    repeating: None,
331                    default: None,
332                },
333            ]
334        });
335        &SCHEMA
336    }
337
338    fn eval<'a, 'b, 'c>(
339        &self,
340        args: &'c [ArgumentHandle<'a, 'b>],
341        ctx: &dyn FunctionContext<'b>,
342    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
343        const EXCEL_MAX_COLS: i64 = 16_384;
344
345        if args.is_empty() {
346            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
347                ExcelError::new(ExcelErrorKind::Value),
348            )));
349        }
350
351        // Try to get reference first, fall back to array literal
352        if let Ok(reference) = args[0].as_reference_or_eval() {
353            // Calculate number of columns
354            let cols = match &reference {
355                ReferenceType::Cell { .. } => 1,
356                ReferenceType::Range {
357                    start_col: Some(sc),
358                    end_col: Some(ec),
359                    ..
360                } => {
361                    if *ec >= *sc {
362                        (*ec - *sc + 1) as i64
363                    } else {
364                        1
365                    }
366                }
367                // Full-row references like 1:1
368                ReferenceType::Range {
369                    start_col: None,
370                    end_col: None,
371                    ..
372                } => EXCEL_MAX_COLS,
373                // Open-ended tail where start_col is known and end_col is omitted
374                ReferenceType::Range {
375                    start_col: Some(sc),
376                    end_col: None,
377                    ..
378                } => EXCEL_MAX_COLS.saturating_sub(*sc as i64).saturating_add(1),
379                // Open-ended head like :F (or equivalent parsed form)
380                ReferenceType::Range {
381                    start_col: None,
382                    end_col: Some(ec),
383                    ..
384                } => *ec as i64,
385                // Fallback for named ranges, table refs, etc.
386                _ => match ctx.resolve_range_view(&reference, ctx.current_sheet()) {
387                    Ok(view) => view.dims().1 as i64,
388                    Err(e) => {
389                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
390                    }
391                },
392            };
393            Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(cols)))
394        } else {
395            // Handle array literal
396            let v = args[0].value()?.into_literal();
397            let cols = match v {
398                LiteralValue::Array(arr) => arr.first().map(|r| r.len()).unwrap_or(0) as i64,
399                _ => 1,
400            };
401            Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(cols)))
402        }
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409    use crate::test_workbook::TestWorkbook;
410    use crate::{CellRef, Coord};
411    use formualizer_parse::parser::{ASTNode, ASTNodeType, ReferenceType};
412    use std::sync::Arc;
413
414    #[test]
415    fn row_with_reference() {
416        let wb = TestWorkbook::new().with_function(Arc::new(RowFn));
417        let ctx = wb.interpreter();
418        let f = ctx.context.get_function("", "ROW").unwrap();
419
420        // ROW(B5) -> 5
421        let b5_ref = ASTNode::new(
422            ASTNodeType::Reference {
423                original: "B5".into(),
424                reference: ReferenceType::cell(None, 5, 2),
425            },
426            None,
427        );
428
429        let args = vec![ArgumentHandle::new(&b5_ref, &ctx)];
430        let result = f
431            .dispatch(&args, &ctx.function_context(None))
432            .unwrap()
433            .into_literal();
434        assert_eq!(result, LiteralValue::Int(5));
435
436        // ROW(A1:C3) -> 1 (first row)
437        let range_ref = ASTNode::new(
438            ASTNodeType::Reference {
439                original: "A1:C3".into(),
440                reference: ReferenceType::range(None, Some(1), Some(1), Some(3), Some(3)),
441            },
442            None,
443        );
444
445        let args2 = vec![ArgumentHandle::new(&range_ref, &ctx)];
446        let result2 = f
447            .dispatch(&args2, &ctx.function_context(None))
448            .unwrap()
449            .into_literal();
450        assert_eq!(result2, LiteralValue::Int(1));
451    }
452
453    #[test]
454    fn row_no_arg_uses_current_cell_1_based() {
455        let wb = TestWorkbook::new().with_function(Arc::new(RowFn));
456        let ctx = wb.interpreter();
457        let f = ctx.context.get_function("", "ROW").unwrap();
458
459        let current = CellRef::new(0, Coord::from_excel(7, 4, false, false));
460        let result = f
461            .dispatch(&[], &ctx.function_context(Some(&current)))
462            .unwrap()
463            .into_literal();
464        assert_eq!(result, LiteralValue::Int(7));
465    }
466
467    #[test]
468    fn row_full_column_reference_returns_first_row() {
469        let wb = TestWorkbook::new().with_function(Arc::new(RowFn));
470        let ctx = wb.interpreter();
471        let f = ctx.context.get_function("", "ROW").unwrap();
472
473        // ROW(A:A) -> 1
474        let col_range_ref = ASTNode::new(
475            ASTNodeType::Reference {
476                original: "A:A".into(),
477                reference: ReferenceType::range(None, None, Some(1), None, Some(1)),
478            },
479            None,
480        );
481
482        let args = vec![ArgumentHandle::new(&col_range_ref, &ctx)];
483        let result = f
484            .dispatch(&args, &ctx.function_context(None))
485            .unwrap()
486            .into_literal();
487        assert_eq!(result, LiteralValue::Int(1));
488    }
489
490    #[test]
491    fn row_named_range_falls_back_to_resolved_range_view() {
492        let wb = TestWorkbook::new()
493            .with_named_range("MyRow", vec![vec![LiteralValue::Int(42)]])
494            .with_function(Arc::new(RowFn));
495        let ctx = wb.interpreter();
496        let f = ctx.context.get_function("", "ROW").unwrap();
497
498        let named_ref = ASTNode::new(
499            ASTNodeType::Reference {
500                original: "MyRow".into(),
501                reference: ReferenceType::NamedRange("MyRow".into()),
502            },
503            None,
504        );
505
506        let args = vec![ArgumentHandle::new(&named_ref, &ctx)];
507        let result = f
508            .dispatch(&args, &ctx.function_context(None))
509            .unwrap()
510            .into_literal();
511        assert_eq!(result, LiteralValue::Int(1));
512    }
513
514    #[test]
515    fn rows_function() {
516        let wb = TestWorkbook::new().with_function(Arc::new(RowsFn));
517        let ctx = wb.interpreter();
518        let f = ctx.context.get_function("", "ROWS").unwrap();
519
520        // ROWS(A1:A5) -> 5
521        let range_ref = ASTNode::new(
522            ASTNodeType::Reference {
523                original: "A1:A5".into(),
524                reference: ReferenceType::range(None, Some(1), Some(1), Some(5), Some(1)),
525            },
526            None,
527        );
528
529        let args = vec![ArgumentHandle::new(&range_ref, &ctx)];
530        let result = f
531            .dispatch(&args, &ctx.function_context(None))
532            .unwrap()
533            .into_literal();
534        assert_eq!(result, LiteralValue::Int(5));
535
536        // ROWS(B2:D10) -> 9
537        let range_ref2 = ASTNode::new(
538            ASTNodeType::Reference {
539                original: "B2:D10".into(),
540                reference: ReferenceType::range(None, Some(2), Some(2), Some(10), Some(4)),
541            },
542            None,
543        );
544
545        let args2 = vec![ArgumentHandle::new(&range_ref2, &ctx)];
546        let result2 = f
547            .dispatch(&args2, &ctx.function_context(None))
548            .unwrap()
549            .into_literal();
550        assert_eq!(result2, LiteralValue::Int(9));
551
552        // ROWS(A1) -> 1 (single cell)
553        let cell_ref = ASTNode::new(
554            ASTNodeType::Reference {
555                original: "A1".into(),
556                reference: ReferenceType::cell(None, 1, 1),
557            },
558            None,
559        );
560
561        let args3 = vec![ArgumentHandle::new(&cell_ref, &ctx)];
562        let result3 = f
563            .dispatch(&args3, &ctx.function_context(None))
564            .unwrap()
565            .into_literal();
566        assert_eq!(result3, LiteralValue::Int(1));
567    }
568
569    #[test]
570    fn rows_full_column_reference_returns_sheet_height() {
571        let wb = TestWorkbook::new().with_function(Arc::new(RowsFn));
572        let ctx = wb.interpreter();
573        let f = ctx.context.get_function("", "ROWS").unwrap();
574
575        // ROWS(A:A) -> 1048576
576        let col_range_ref = ASTNode::new(
577            ASTNodeType::Reference {
578                original: "A:A".into(),
579                reference: ReferenceType::range(None, None, Some(1), None, Some(1)),
580            },
581            None,
582        );
583
584        let args = vec![ArgumentHandle::new(&col_range_ref, &ctx)];
585        let result = f
586            .dispatch(&args, &ctx.function_context(None))
587            .unwrap()
588            .into_literal();
589        assert_eq!(result, LiteralValue::Int(1_048_576));
590    }
591
592    #[test]
593    fn rows_named_range_falls_back_to_resolved_range_view() {
594        let wb = TestWorkbook::new()
595            .with_named_range(
596                "MyRows",
597                vec![
598                    vec![LiteralValue::Int(1)],
599                    vec![LiteralValue::Int(2)],
600                    vec![LiteralValue::Int(3)],
601                ],
602            )
603            .with_function(Arc::new(RowsFn));
604        let ctx = wb.interpreter();
605        let f = ctx.context.get_function("", "ROWS").unwrap();
606
607        let named_ref = ASTNode::new(
608            ASTNodeType::Reference {
609                original: "MyRows".into(),
610                reference: ReferenceType::NamedRange("MyRows".into()),
611            },
612            None,
613        );
614
615        let args = vec![ArgumentHandle::new(&named_ref, &ctx)];
616        let result = f
617            .dispatch(&args, &ctx.function_context(None))
618            .unwrap()
619            .into_literal();
620        assert_eq!(result, LiteralValue::Int(3));
621    }
622
623    #[test]
624    fn column_with_reference() {
625        let wb = TestWorkbook::new().with_function(Arc::new(ColumnFn));
626        let ctx = wb.interpreter();
627        let f = ctx.context.get_function("", "COLUMN").unwrap();
628
629        // COLUMN(C5) -> 3
630        let c5_ref = ASTNode::new(
631            ASTNodeType::Reference {
632                original: "C5".into(),
633                reference: ReferenceType::cell(None, 5, 3),
634            },
635            None,
636        );
637
638        let args = vec![ArgumentHandle::new(&c5_ref, &ctx)];
639        let result = f
640            .dispatch(&args, &ctx.function_context(None))
641            .unwrap()
642            .into_literal();
643        assert_eq!(result, LiteralValue::Int(3));
644
645        // COLUMN(B2:D4) -> 2 (first column)
646        let range_ref = ASTNode::new(
647            ASTNodeType::Reference {
648                original: "B2:D4".into(),
649                reference: ReferenceType::range(None, Some(2), Some(2), Some(4), Some(4)),
650            },
651            None,
652        );
653
654        let args2 = vec![ArgumentHandle::new(&range_ref, &ctx)];
655        let result2 = f
656            .dispatch(&args2, &ctx.function_context(None))
657            .unwrap()
658            .into_literal();
659        assert_eq!(result2, LiteralValue::Int(2));
660    }
661
662    #[test]
663    fn column_no_arg_uses_current_cell_1_based() {
664        let wb = TestWorkbook::new().with_function(Arc::new(ColumnFn));
665        let ctx = wb.interpreter();
666        let f = ctx.context.get_function("", "COLUMN").unwrap();
667
668        let current = CellRef::new(0, Coord::from_excel(7, 4, false, false));
669        let result = f
670            .dispatch(&[], &ctx.function_context(Some(&current)))
671            .unwrap()
672            .into_literal();
673        assert_eq!(result, LiteralValue::Int(4));
674    }
675
676    #[test]
677    fn column_full_row_reference_returns_first_column() {
678        let wb = TestWorkbook::new().with_function(Arc::new(ColumnFn));
679        let ctx = wb.interpreter();
680        let f = ctx.context.get_function("", "COLUMN").unwrap();
681
682        // COLUMN(5:5) -> 1
683        let row_range_ref = ASTNode::new(
684            ASTNodeType::Reference {
685                original: "5:5".into(),
686                reference: ReferenceType::range(None, Some(5), None, Some(5), None),
687            },
688            None,
689        );
690
691        let args = vec![ArgumentHandle::new(&row_range_ref, &ctx)];
692        let result = f
693            .dispatch(&args, &ctx.function_context(None))
694            .unwrap()
695            .into_literal();
696        assert_eq!(result, LiteralValue::Int(1));
697    }
698
699    #[test]
700    fn column_named_range_falls_back_to_resolved_range_view() {
701        let wb = TestWorkbook::new()
702            .with_named_range("MyRange", vec![vec![LiteralValue::Int(42)]])
703            .with_function(Arc::new(ColumnFn));
704        let ctx = wb.interpreter();
705        let f = ctx.context.get_function("", "COLUMN").unwrap();
706
707        let named_ref = ASTNode::new(
708            ASTNodeType::Reference {
709                original: "MyRange".into(),
710                reference: ReferenceType::NamedRange("MyRange".into()),
711            },
712            None,
713        );
714
715        let args = vec![ArgumentHandle::new(&named_ref, &ctx)];
716        let result = f
717            .dispatch(&args, &ctx.function_context(None))
718            .unwrap()
719            .into_literal();
720        assert_eq!(result, LiteralValue::Int(1));
721    }
722
723    #[test]
724    fn columns_function() {
725        let wb = TestWorkbook::new().with_function(Arc::new(ColumnsFn));
726        let ctx = wb.interpreter();
727        let f = ctx.context.get_function("", "COLUMNS").unwrap();
728
729        // COLUMNS(A1:E1) -> 5
730        let range_ref = ASTNode::new(
731            ASTNodeType::Reference {
732                original: "A1:E1".into(),
733                reference: ReferenceType::range(None, Some(1), Some(1), Some(1), Some(5)),
734            },
735            None,
736        );
737
738        let args = vec![ArgumentHandle::new(&range_ref, &ctx)];
739        let result = f
740            .dispatch(&args, &ctx.function_context(None))
741            .unwrap()
742            .into_literal();
743        assert_eq!(result, LiteralValue::Int(5));
744
745        // COLUMNS(B2:D10) -> 3
746        let range_ref2 = ASTNode::new(
747            ASTNodeType::Reference {
748                original: "B2:D10".into(),
749                reference: ReferenceType::range(None, Some(2), Some(2), Some(10), Some(4)),
750            },
751            None,
752        );
753
754        let args2 = vec![ArgumentHandle::new(&range_ref2, &ctx)];
755        let result2 = f
756            .dispatch(&args2, &ctx.function_context(None))
757            .unwrap()
758            .into_literal();
759        assert_eq!(result2, LiteralValue::Int(3));
760
761        // COLUMNS(A1) -> 1 (single cell)
762        let cell_ref = ASTNode::new(
763            ASTNodeType::Reference {
764                original: "A1".into(),
765                reference: ReferenceType::cell(None, 1, 1),
766            },
767            None,
768        );
769
770        let args3 = vec![ArgumentHandle::new(&cell_ref, &ctx)];
771        let result3 = f
772            .dispatch(&args3, &ctx.function_context(None))
773            .unwrap()
774            .into_literal();
775        assert_eq!(result3, LiteralValue::Int(1));
776    }
777
778    #[test]
779    fn columns_full_row_reference_returns_sheet_width() {
780        let wb = TestWorkbook::new().with_function(Arc::new(ColumnsFn));
781        let ctx = wb.interpreter();
782        let f = ctx.context.get_function("", "COLUMNS").unwrap();
783
784        // COLUMNS(1:1) -> 16384
785        let row_range_ref = ASTNode::new(
786            ASTNodeType::Reference {
787                original: "1:1".into(),
788                reference: ReferenceType::range(None, Some(1), None, Some(1), None),
789            },
790            None,
791        );
792
793        let args = vec![ArgumentHandle::new(&row_range_ref, &ctx)];
794        let result = f
795            .dispatch(&args, &ctx.function_context(None))
796            .unwrap()
797            .into_literal();
798        assert_eq!(result, LiteralValue::Int(16_384));
799    }
800
801    #[test]
802    fn columns_named_range_falls_back_to_resolved_range_view() {
803        let wb = TestWorkbook::new()
804            .with_named_range(
805                "MyCols",
806                vec![
807                    vec![
808                        LiteralValue::Int(1),
809                        LiteralValue::Int(2),
810                        LiteralValue::Int(3),
811                    ],
812                    vec![
813                        LiteralValue::Int(4),
814                        LiteralValue::Int(5),
815                        LiteralValue::Int(6),
816                    ],
817                ],
818            )
819            .with_function(Arc::new(ColumnsFn));
820        let ctx = wb.interpreter();
821        let f = ctx.context.get_function("", "COLUMNS").unwrap();
822
823        let named_ref = ASTNode::new(
824            ASTNodeType::Reference {
825                original: "MyCols".into(),
826                reference: ReferenceType::NamedRange("MyCols".into()),
827            },
828            None,
829        );
830
831        let args = vec![ArgumentHandle::new(&named_ref, &ctx)];
832        let result = f
833            .dispatch(&args, &ctx.function_context(None))
834            .unwrap()
835            .into_literal();
836        assert_eq!(result, LiteralValue::Int(3));
837    }
838
839    #[test]
840    fn rows_columns_reversed_range() {
841        // A5:A1 (start_row > end_row) should treat as 1 row / 1 column per current implementation fallback
842        let wb = TestWorkbook::new()
843            .with_function(Arc::new(RowsFn))
844            .with_function(Arc::new(ColumnsFn));
845        let ctx = wb.interpreter();
846        let rows_f = ctx.context.get_function("", "ROWS").unwrap();
847        let cols_f = ctx.context.get_function("", "COLUMNS").unwrap();
848        let rev_range = ASTNode::new(
849            ASTNodeType::Reference {
850                original: "A5:A1".into(),
851                reference: ReferenceType::range(None, Some(5), Some(1), Some(1), Some(1)),
852            },
853            None,
854        );
855        let args = vec![ArgumentHandle::new(&rev_range, &ctx)];
856        let r_count = rows_f
857            .dispatch(&args, &ctx.function_context(None))
858            .unwrap()
859            .into_literal();
860        let c_count = cols_f
861            .dispatch(&args, &ctx.function_context(None))
862            .unwrap()
863            .into_literal();
864        assert_eq!(r_count, LiteralValue::Int(1));
865        assert_eq!(c_count, LiteralValue::Int(1));
866    }
867}