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
21/// Returns the row number of a reference, or of the current cell when omitted.
22///
23/// `ROW` returns a 1-based row index.
24///
25/// # Remarks
26/// - With a range argument, `ROW` returns the first row in that reference.
27/// - Without arguments, it uses the row of the formula cell.
28/// - Full-column references such as `A:A` return `1`.
29/// - Invalid references return an error (`#REF!`/`#VALUE!` depending on context).
30///
31/// # Examples
32/// ```yaml,sandbox
33/// title: "Row of a single-cell reference"
34/// formula: '=ROW(B5)'
35/// expected: 5
36/// ```
37///
38/// ```yaml,sandbox
39/// title: "Row of a multi-cell range"
40/// formula: '=ROW(C3:E9)'
41/// expected: 3
42/// ```
43///
44/// ```yaml,docs
45/// related:
46///   - ROWS
47///   - COLUMN
48///   - ADDRESS
49/// faq:
50///   - q: "What does ROW return for a multi-cell reference?"
51///     a: "ROW returns the first row index of the reference, not an array of every row number."
52///   - q: "What if ROW() is called without arguments?"
53///     a: "It uses the formula cell position; if no current cell context exists, it returns #VALUE!."
54/// ```
55/// [formualizer-docgen:schema:start]
56/// Name: ROW
57/// Type: RowFn
58/// Min args: 0
59/// Max args: 1
60/// Variadic: false
61/// Signature: ROW(arg1?: range@range)
62/// Arg schema: arg1{kinds=range,required=false,shape=range,by_ref=true,coercion=None,max=None,repeating=None,default=false}
63/// Caps: PURE
64/// [formualizer-docgen:schema:end]
65impl Function for RowFn {
66    fn name(&self) -> &'static str {
67        "ROW"
68    }
69
70    fn min_args(&self) -> usize {
71        0
72    }
73
74    func_caps!(PURE);
75
76    fn arg_schema(&self) -> &'static [ArgSchema] {
77        use once_cell::sync::Lazy;
78        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
79            vec![
80                // Optional reference
81                ArgSchema {
82                    kinds: smallvec::smallvec![ArgKind::Range],
83                    required: false,
84                    by_ref: true,
85                    shape: ShapeKind::Range,
86                    coercion: CoercionPolicy::None,
87                    max: None,
88                    repeating: None,
89                    default: None,
90                },
91            ]
92        });
93        &SCHEMA
94    }
95
96    fn eval<'a, 'b, 'c>(
97        &self,
98        args: &'c [ArgumentHandle<'a, 'b>],
99        ctx: &dyn FunctionContext<'b>,
100    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
101        if args.is_empty() {
102            // Return current cell's row (1-based) if available
103            if let Some(cell_ref) = ctx.current_cell() {
104                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
105                    cell_ref.coord.row() as i64 + 1,
106                )));
107            }
108            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
109                ExcelError::new(ExcelErrorKind::Value),
110            )));
111        }
112
113        // Get reference
114        let reference = match args[0].as_reference_or_eval() {
115            Ok(r) => r,
116            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
117        };
118
119        // Extract row number from reference (1-based)
120        let row_1based = match &reference {
121            ReferenceType::Cell { row, .. } => *row as i64,
122            ReferenceType::Range {
123                start_row: Some(sr),
124                ..
125            } => *sr as i64,
126            // Full-column references like A:A use first row
127            ReferenceType::Range {
128                start_row: None,
129                end_row: None,
130                ..
131            } => 1,
132            // Fallback: resolve the reference and use the view origin
133            _ => match ctx.resolve_range_view(&reference, ctx.current_sheet()) {
134                Ok(view) => {
135                    if view.is_empty() {
136                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
137                            ExcelError::new(ExcelErrorKind::Ref),
138                        )));
139                    }
140                    view.start_row() as i64 + 1
141                }
142                Err(e) => {
143                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
144                }
145            },
146        };
147
148        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
149            row_1based,
150        )))
151    }
152}
153
154#[derive(Debug)]
155pub struct RowsFn;
156
157/// Returns the number of rows in a reference or array.
158///
159/// `ROWS` reports height, not data density.
160///
161/// # Remarks
162/// - For a single cell reference, returns `1`.
163/// - For full-column references (for example `A:A`), returns `1048576`.
164/// - For array literals, returns the outer array length.
165/// - Invalid references return an error.
166///
167/// # Examples
168/// ```yaml,sandbox
169/// title: "Count rows in a contiguous range"
170/// formula: '=ROWS(B2:D10)'
171/// expected: 9
172/// ```
173///
174/// ```yaml,sandbox
175/// title: "Count rows in a full column reference"
176/// formula: '=ROWS(A:A)'
177/// expected: 1048576
178/// ```
179///
180/// ```yaml,docs
181/// related:
182///   - ROW
183///   - COLUMNS
184///   - INDEX
185/// faq:
186///   - q: "Does ROWS count populated rows or reference height?"
187///     a: "ROWS returns reference height only; blanks inside the range do not reduce the count."
188///   - q: "How does ROWS behave for full-column references?"
189///     a: "A full-column reference (like A:A) returns the sheet row limit, 1048576."
190/// ```
191/// [formualizer-docgen:schema:start]
192/// Name: ROWS
193/// Type: RowsFn
194/// Min args: 1
195/// Max args: 1
196/// Variadic: false
197/// Signature: ROWS(arg1: any@range)
198/// Arg schema: arg1{kinds=any,required=true,shape=range,by_ref=false,coercion=None,max=None,repeating=None,default=false}
199/// Caps: PURE
200/// [formualizer-docgen:schema:end]
201impl Function for RowsFn {
202    fn name(&self) -> &'static str {
203        "ROWS"
204    }
205
206    fn min_args(&self) -> usize {
207        1
208    }
209
210    func_caps!(PURE);
211
212    fn arg_schema(&self) -> &'static [ArgSchema] {
213        use once_cell::sync::Lazy;
214        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
215            vec![
216                // Required reference/range or array
217                ArgSchema {
218                    kinds: smallvec::smallvec![ArgKind::Any],
219                    required: true,
220                    by_ref: false,
221                    shape: ShapeKind::Range,
222                    coercion: CoercionPolicy::None,
223                    max: None,
224                    repeating: None,
225                    default: None,
226                },
227            ]
228        });
229        &SCHEMA
230    }
231
232    fn eval<'a, 'b, 'c>(
233        &self,
234        args: &'c [ArgumentHandle<'a, 'b>],
235        ctx: &dyn FunctionContext<'b>,
236    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
237        const EXCEL_MAX_ROWS: i64 = 1_048_576;
238
239        if args.is_empty() {
240            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
241                ExcelError::new(ExcelErrorKind::Value),
242            )));
243        }
244
245        // Try to get reference first, fall back to array literal
246        if let Ok(reference) = args[0].as_reference_or_eval() {
247            // Calculate number of rows
248            let rows = match &reference {
249                ReferenceType::Cell { .. } => 1,
250                ReferenceType::Range {
251                    start_row: Some(sr),
252                    end_row: Some(er),
253                    ..
254                } => {
255                    if *er >= *sr {
256                        (*er - *sr + 1) as i64
257                    } else {
258                        1
259                    }
260                }
261                // Full-column references like A:A
262                ReferenceType::Range {
263                    start_row: None,
264                    end_row: None,
265                    ..
266                } => EXCEL_MAX_ROWS,
267                // Open-ended tail like A5:A
268                ReferenceType::Range {
269                    start_row: Some(sr),
270                    end_row: None,
271                    ..
272                } => EXCEL_MAX_ROWS.saturating_sub(*sr as i64).saturating_add(1),
273                // Open-ended head like A:A10 (treated as A1:A10)
274                ReferenceType::Range {
275                    start_row: None,
276                    end_row: Some(er),
277                    ..
278                } => *er as i64,
279                // Fallback for named ranges, table refs, etc.
280                _ => match ctx.resolve_range_view(&reference, ctx.current_sheet()) {
281                    Ok(view) => view.dims().0 as i64,
282                    Err(e) => {
283                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
284                    }
285                },
286            };
287            Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(rows)))
288        } else {
289            // Handle array literal
290            let v = args[0].value()?.into_literal();
291            let rows = match v {
292                LiteralValue::Array(arr) => arr.len() as i64,
293                _ => 1,
294            };
295            Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(rows)))
296        }
297    }
298}
299
300#[derive(Debug)]
301pub struct ColumnFn;
302
303/// Returns the column number of a reference, or of the current cell when omitted.
304///
305/// `COLUMN` returns a 1-based column index (`A` = 1).
306///
307/// # Remarks
308/// - With a range argument, `COLUMN` returns the first column in that reference.
309/// - Without arguments, it uses the column of the formula cell.
310/// - Full-row references such as `5:5` return `1`.
311/// - Invalid references return an error (`#REF!`/`#VALUE!` depending on context).
312///
313/// # Examples
314/// ```yaml,sandbox
315/// title: "Column of a single-cell reference"
316/// formula: '=COLUMN(C5)'
317/// expected: 3
318/// ```
319///
320/// ```yaml,sandbox
321/// title: "Column of a range"
322/// formula: '=COLUMN(B2:D4)'
323/// expected: 2
324/// ```
325///
326/// ```yaml,docs
327/// related:
328///   - COLUMNS
329///   - ROW
330///   - ADDRESS
331/// faq:
332///   - q: "What does COLUMN return for a range like B2:D4?"
333///     a: "COLUMN returns the first column index of the reference (2 for column B)."
334///   - q: "What if COLUMN() is used without a reference?"
335///     a: "It returns the formula cell's column number, or #VALUE! if current-cell context is unavailable."
336/// ```
337/// [formualizer-docgen:schema:start]
338/// Name: COLUMN
339/// Type: ColumnFn
340/// Min args: 0
341/// Max args: 1
342/// Variadic: false
343/// Signature: COLUMN(arg1?: range@range)
344/// Arg schema: arg1{kinds=range,required=false,shape=range,by_ref=true,coercion=None,max=None,repeating=None,default=false}
345/// Caps: PURE
346/// [formualizer-docgen:schema:end]
347impl Function for ColumnFn {
348    fn name(&self) -> &'static str {
349        "COLUMN"
350    }
351
352    fn min_args(&self) -> usize {
353        0
354    }
355
356    func_caps!(PURE);
357
358    fn arg_schema(&self) -> &'static [ArgSchema] {
359        use once_cell::sync::Lazy;
360        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
361            vec![
362                // Optional reference
363                ArgSchema {
364                    kinds: smallvec::smallvec![ArgKind::Range],
365                    required: false,
366                    by_ref: true,
367                    shape: ShapeKind::Range,
368                    coercion: CoercionPolicy::None,
369                    max: None,
370                    repeating: None,
371                    default: None,
372                },
373            ]
374        });
375        &SCHEMA
376    }
377
378    fn eval<'a, 'b, 'c>(
379        &self,
380        args: &'c [ArgumentHandle<'a, 'b>],
381        ctx: &dyn FunctionContext<'b>,
382    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
383        if args.is_empty() {
384            // Return current cell's column (1-based) if available
385            if let Some(cell_ref) = ctx.current_cell() {
386                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
387                    cell_ref.coord.col() as i64 + 1,
388                )));
389            }
390            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
391                ExcelError::new(ExcelErrorKind::Value),
392            )));
393        }
394
395        // Get reference
396        let reference = match args[0].as_reference_or_eval() {
397            Ok(r) => r,
398            Err(e) => return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e))),
399        };
400
401        // Extract column number from reference (1-based)
402        let col_1based = match &reference {
403            ReferenceType::Cell { col, .. } => *col as i64,
404            ReferenceType::Range {
405                start_col: Some(sc),
406                ..
407            } => *sc as i64,
408            // Full-row references like 1:1 use first column
409            ReferenceType::Range {
410                start_col: None,
411                end_col: None,
412                ..
413            } => 1,
414            // Fallback: resolve the reference and use the view origin
415            _ => match ctx.resolve_range_view(&reference, ctx.current_sheet()) {
416                Ok(view) => {
417                    if view.is_empty() {
418                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
419                            ExcelError::new(ExcelErrorKind::Ref),
420                        )));
421                    }
422                    view.start_col() as i64 + 1
423                }
424                Err(e) => {
425                    return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
426                }
427            },
428        };
429
430        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(
431            col_1based,
432        )))
433    }
434}
435
436#[derive(Debug)]
437pub struct ColumnsFn;
438
439/// Returns the number of columns in a reference or array.
440///
441/// `COLUMNS` reports width, not data density.
442///
443/// # Remarks
444/// - For a single cell reference, returns `1`.
445/// - For full-row references (for example `1:1`), returns `16384`.
446/// - For array literals, returns the first row width.
447/// - Invalid references return an error.
448///
449/// # Examples
450/// ```yaml,sandbox
451/// title: "Count columns in a rectangular range"
452/// formula: '=COLUMNS(B2:D10)'
453/// expected: 3
454/// ```
455///
456/// ```yaml,sandbox
457/// title: "Count columns in a full row reference"
458/// formula: '=COLUMNS(1:1)'
459/// expected: 16384
460/// ```
461///
462/// ```yaml,docs
463/// related:
464///   - COLUMN
465///   - ROWS
466///   - CHOOSECOLS
467/// faq:
468///   - q: "Does COLUMNS count non-empty cells?"
469///     a: "No. COLUMNS returns the width of the referenced array/range, including blank cells."
470///   - q: "What does COLUMNS return for a full-row reference like 1:1?"
471///     a: "It returns the sheet column limit, 16384."
472/// ```
473/// [formualizer-docgen:schema:start]
474/// Name: COLUMNS
475/// Type: ColumnsFn
476/// Min args: 1
477/// Max args: 1
478/// Variadic: false
479/// Signature: COLUMNS(arg1: any@range)
480/// Arg schema: arg1{kinds=any,required=true,shape=range,by_ref=false,coercion=None,max=None,repeating=None,default=false}
481/// Caps: PURE
482/// [formualizer-docgen:schema:end]
483impl Function for ColumnsFn {
484    fn name(&self) -> &'static str {
485        "COLUMNS"
486    }
487
488    fn min_args(&self) -> usize {
489        1
490    }
491
492    func_caps!(PURE);
493
494    fn arg_schema(&self) -> &'static [ArgSchema] {
495        use once_cell::sync::Lazy;
496        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
497            vec![
498                // Required reference/range or array
499                ArgSchema {
500                    kinds: smallvec::smallvec![ArgKind::Any],
501                    required: true,
502                    by_ref: false,
503                    shape: ShapeKind::Range,
504                    coercion: CoercionPolicy::None,
505                    max: None,
506                    repeating: None,
507                    default: None,
508                },
509            ]
510        });
511        &SCHEMA
512    }
513
514    fn eval<'a, 'b, 'c>(
515        &self,
516        args: &'c [ArgumentHandle<'a, 'b>],
517        ctx: &dyn FunctionContext<'b>,
518    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
519        const EXCEL_MAX_COLS: i64 = 16_384;
520
521        if args.is_empty() {
522            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
523                ExcelError::new(ExcelErrorKind::Value),
524            )));
525        }
526
527        // Try to get reference first, fall back to array literal
528        if let Ok(reference) = args[0].as_reference_or_eval() {
529            // Calculate number of columns
530            let cols = match &reference {
531                ReferenceType::Cell { .. } => 1,
532                ReferenceType::Range {
533                    start_col: Some(sc),
534                    end_col: Some(ec),
535                    ..
536                } => {
537                    if *ec >= *sc {
538                        (*ec - *sc + 1) as i64
539                    } else {
540                        1
541                    }
542                }
543                // Full-row references like 1:1
544                ReferenceType::Range {
545                    start_col: None,
546                    end_col: None,
547                    ..
548                } => EXCEL_MAX_COLS,
549                // Open-ended tail where start_col is known and end_col is omitted
550                ReferenceType::Range {
551                    start_col: Some(sc),
552                    end_col: None,
553                    ..
554                } => EXCEL_MAX_COLS.saturating_sub(*sc as i64).saturating_add(1),
555                // Open-ended head like :F (or equivalent parsed form)
556                ReferenceType::Range {
557                    start_col: None,
558                    end_col: Some(ec),
559                    ..
560                } => *ec as i64,
561                // Fallback for named ranges, table refs, etc.
562                _ => match ctx.resolve_range_view(&reference, ctx.current_sheet()) {
563                    Ok(view) => view.dims().1 as i64,
564                    Err(e) => {
565                        return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
566                    }
567                },
568            };
569            Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(cols)))
570        } else {
571            // Handle array literal
572            let v = args[0].value()?.into_literal();
573            let cols = match v {
574                LiteralValue::Array(arr) => arr.first().map(|r| r.len()).unwrap_or(0) as i64,
575                _ => 1,
576            };
577            Ok(crate::traits::CalcValue::Scalar(LiteralValue::Int(cols)))
578        }
579    }
580}
581
582#[cfg(test)]
583mod tests {
584    use super::*;
585    use crate::test_workbook::TestWorkbook;
586    use crate::{CellRef, Coord};
587    use formualizer_parse::parser::{ASTNode, ASTNodeType, ReferenceType};
588    use std::sync::Arc;
589
590    #[test]
591    fn row_with_reference() {
592        let wb = TestWorkbook::new().with_function(Arc::new(RowFn));
593        let ctx = wb.interpreter();
594        let f = ctx.context.get_function("", "ROW").unwrap();
595
596        // ROW(B5) -> 5
597        let b5_ref = ASTNode::new(
598            ASTNodeType::Reference {
599                original: "B5".into(),
600                reference: ReferenceType::cell(None, 5, 2),
601            },
602            None,
603        );
604
605        let args = vec![ArgumentHandle::new(&b5_ref, &ctx)];
606        let result = f
607            .dispatch(&args, &ctx.function_context(None))
608            .unwrap()
609            .into_literal();
610        assert_eq!(result, LiteralValue::Int(5));
611
612        // ROW(A1:C3) -> 1 (first row)
613        let range_ref = ASTNode::new(
614            ASTNodeType::Reference {
615                original: "A1:C3".into(),
616                reference: ReferenceType::range(None, Some(1), Some(1), Some(3), Some(3)),
617            },
618            None,
619        );
620
621        let args2 = vec![ArgumentHandle::new(&range_ref, &ctx)];
622        let result2 = f
623            .dispatch(&args2, &ctx.function_context(None))
624            .unwrap()
625            .into_literal();
626        assert_eq!(result2, LiteralValue::Int(1));
627    }
628
629    #[test]
630    fn row_no_arg_uses_current_cell_1_based() {
631        let wb = TestWorkbook::new().with_function(Arc::new(RowFn));
632        let ctx = wb.interpreter();
633        let f = ctx.context.get_function("", "ROW").unwrap();
634
635        let current = CellRef::new(0, Coord::from_excel(7, 4, false, false));
636        let result = f
637            .dispatch(&[], &ctx.function_context(Some(&current)))
638            .unwrap()
639            .into_literal();
640        assert_eq!(result, LiteralValue::Int(7));
641    }
642
643    #[test]
644    fn row_full_column_reference_returns_first_row() {
645        let wb = TestWorkbook::new().with_function(Arc::new(RowFn));
646        let ctx = wb.interpreter();
647        let f = ctx.context.get_function("", "ROW").unwrap();
648
649        // ROW(A:A) -> 1
650        let col_range_ref = ASTNode::new(
651            ASTNodeType::Reference {
652                original: "A:A".into(),
653                reference: ReferenceType::range(None, None, Some(1), None, Some(1)),
654            },
655            None,
656        );
657
658        let args = vec![ArgumentHandle::new(&col_range_ref, &ctx)];
659        let result = f
660            .dispatch(&args, &ctx.function_context(None))
661            .unwrap()
662            .into_literal();
663        assert_eq!(result, LiteralValue::Int(1));
664    }
665
666    #[test]
667    fn row_named_range_falls_back_to_resolved_range_view() {
668        let wb = TestWorkbook::new()
669            .with_named_range("MyRow", vec![vec![LiteralValue::Int(42)]])
670            .with_function(Arc::new(RowFn));
671        let ctx = wb.interpreter();
672        let f = ctx.context.get_function("", "ROW").unwrap();
673
674        let named_ref = ASTNode::new(
675            ASTNodeType::Reference {
676                original: "MyRow".into(),
677                reference: ReferenceType::NamedRange("MyRow".into()),
678            },
679            None,
680        );
681
682        let args = vec![ArgumentHandle::new(&named_ref, &ctx)];
683        let result = f
684            .dispatch(&args, &ctx.function_context(None))
685            .unwrap()
686            .into_literal();
687        assert_eq!(result, LiteralValue::Int(1));
688    }
689
690    #[test]
691    fn rows_function() {
692        let wb = TestWorkbook::new().with_function(Arc::new(RowsFn));
693        let ctx = wb.interpreter();
694        let f = ctx.context.get_function("", "ROWS").unwrap();
695
696        // ROWS(A1:A5) -> 5
697        let range_ref = ASTNode::new(
698            ASTNodeType::Reference {
699                original: "A1:A5".into(),
700                reference: ReferenceType::range(None, Some(1), Some(1), Some(5), Some(1)),
701            },
702            None,
703        );
704
705        let args = vec![ArgumentHandle::new(&range_ref, &ctx)];
706        let result = f
707            .dispatch(&args, &ctx.function_context(None))
708            .unwrap()
709            .into_literal();
710        assert_eq!(result, LiteralValue::Int(5));
711
712        // ROWS(B2:D10) -> 9
713        let range_ref2 = ASTNode::new(
714            ASTNodeType::Reference {
715                original: "B2:D10".into(),
716                reference: ReferenceType::range(None, Some(2), Some(2), Some(10), Some(4)),
717            },
718            None,
719        );
720
721        let args2 = vec![ArgumentHandle::new(&range_ref2, &ctx)];
722        let result2 = f
723            .dispatch(&args2, &ctx.function_context(None))
724            .unwrap()
725            .into_literal();
726        assert_eq!(result2, LiteralValue::Int(9));
727
728        // ROWS(A1) -> 1 (single cell)
729        let cell_ref = ASTNode::new(
730            ASTNodeType::Reference {
731                original: "A1".into(),
732                reference: ReferenceType::cell(None, 1, 1),
733            },
734            None,
735        );
736
737        let args3 = vec![ArgumentHandle::new(&cell_ref, &ctx)];
738        let result3 = f
739            .dispatch(&args3, &ctx.function_context(None))
740            .unwrap()
741            .into_literal();
742        assert_eq!(result3, LiteralValue::Int(1));
743    }
744
745    #[test]
746    fn rows_full_column_reference_returns_sheet_height() {
747        let wb = TestWorkbook::new().with_function(Arc::new(RowsFn));
748        let ctx = wb.interpreter();
749        let f = ctx.context.get_function("", "ROWS").unwrap();
750
751        // ROWS(A:A) -> 1048576
752        let col_range_ref = ASTNode::new(
753            ASTNodeType::Reference {
754                original: "A:A".into(),
755                reference: ReferenceType::range(None, None, Some(1), None, Some(1)),
756            },
757            None,
758        );
759
760        let args = vec![ArgumentHandle::new(&col_range_ref, &ctx)];
761        let result = f
762            .dispatch(&args, &ctx.function_context(None))
763            .unwrap()
764            .into_literal();
765        assert_eq!(result, LiteralValue::Int(1_048_576));
766    }
767
768    #[test]
769    fn rows_named_range_falls_back_to_resolved_range_view() {
770        let wb = TestWorkbook::new()
771            .with_named_range(
772                "MyRows",
773                vec![
774                    vec![LiteralValue::Int(1)],
775                    vec![LiteralValue::Int(2)],
776                    vec![LiteralValue::Int(3)],
777                ],
778            )
779            .with_function(Arc::new(RowsFn));
780        let ctx = wb.interpreter();
781        let f = ctx.context.get_function("", "ROWS").unwrap();
782
783        let named_ref = ASTNode::new(
784            ASTNodeType::Reference {
785                original: "MyRows".into(),
786                reference: ReferenceType::NamedRange("MyRows".into()),
787            },
788            None,
789        );
790
791        let args = vec![ArgumentHandle::new(&named_ref, &ctx)];
792        let result = f
793            .dispatch(&args, &ctx.function_context(None))
794            .unwrap()
795            .into_literal();
796        assert_eq!(result, LiteralValue::Int(3));
797    }
798
799    #[test]
800    fn column_with_reference() {
801        let wb = TestWorkbook::new().with_function(Arc::new(ColumnFn));
802        let ctx = wb.interpreter();
803        let f = ctx.context.get_function("", "COLUMN").unwrap();
804
805        // COLUMN(C5) -> 3
806        let c5_ref = ASTNode::new(
807            ASTNodeType::Reference {
808                original: "C5".into(),
809                reference: ReferenceType::cell(None, 5, 3),
810            },
811            None,
812        );
813
814        let args = vec![ArgumentHandle::new(&c5_ref, &ctx)];
815        let result = f
816            .dispatch(&args, &ctx.function_context(None))
817            .unwrap()
818            .into_literal();
819        assert_eq!(result, LiteralValue::Int(3));
820
821        // COLUMN(B2:D4) -> 2 (first column)
822        let range_ref = ASTNode::new(
823            ASTNodeType::Reference {
824                original: "B2:D4".into(),
825                reference: ReferenceType::range(None, Some(2), Some(2), Some(4), Some(4)),
826            },
827            None,
828        );
829
830        let args2 = vec![ArgumentHandle::new(&range_ref, &ctx)];
831        let result2 = f
832            .dispatch(&args2, &ctx.function_context(None))
833            .unwrap()
834            .into_literal();
835        assert_eq!(result2, LiteralValue::Int(2));
836    }
837
838    #[test]
839    fn column_no_arg_uses_current_cell_1_based() {
840        let wb = TestWorkbook::new().with_function(Arc::new(ColumnFn));
841        let ctx = wb.interpreter();
842        let f = ctx.context.get_function("", "COLUMN").unwrap();
843
844        let current = CellRef::new(0, Coord::from_excel(7, 4, false, false));
845        let result = f
846            .dispatch(&[], &ctx.function_context(Some(&current)))
847            .unwrap()
848            .into_literal();
849        assert_eq!(result, LiteralValue::Int(4));
850    }
851
852    #[test]
853    fn column_full_row_reference_returns_first_column() {
854        let wb = TestWorkbook::new().with_function(Arc::new(ColumnFn));
855        let ctx = wb.interpreter();
856        let f = ctx.context.get_function("", "COLUMN").unwrap();
857
858        // COLUMN(5:5) -> 1
859        let row_range_ref = ASTNode::new(
860            ASTNodeType::Reference {
861                original: "5:5".into(),
862                reference: ReferenceType::range(None, Some(5), None, Some(5), None),
863            },
864            None,
865        );
866
867        let args = vec![ArgumentHandle::new(&row_range_ref, &ctx)];
868        let result = f
869            .dispatch(&args, &ctx.function_context(None))
870            .unwrap()
871            .into_literal();
872        assert_eq!(result, LiteralValue::Int(1));
873    }
874
875    #[test]
876    fn column_named_range_falls_back_to_resolved_range_view() {
877        let wb = TestWorkbook::new()
878            .with_named_range("MyRange", vec![vec![LiteralValue::Int(42)]])
879            .with_function(Arc::new(ColumnFn));
880        let ctx = wb.interpreter();
881        let f = ctx.context.get_function("", "COLUMN").unwrap();
882
883        let named_ref = ASTNode::new(
884            ASTNodeType::Reference {
885                original: "MyRange".into(),
886                reference: ReferenceType::NamedRange("MyRange".into()),
887            },
888            None,
889        );
890
891        let args = vec![ArgumentHandle::new(&named_ref, &ctx)];
892        let result = f
893            .dispatch(&args, &ctx.function_context(None))
894            .unwrap()
895            .into_literal();
896        assert_eq!(result, LiteralValue::Int(1));
897    }
898
899    #[test]
900    fn columns_function() {
901        let wb = TestWorkbook::new().with_function(Arc::new(ColumnsFn));
902        let ctx = wb.interpreter();
903        let f = ctx.context.get_function("", "COLUMNS").unwrap();
904
905        // COLUMNS(A1:E1) -> 5
906        let range_ref = ASTNode::new(
907            ASTNodeType::Reference {
908                original: "A1:E1".into(),
909                reference: ReferenceType::range(None, Some(1), Some(1), Some(1), Some(5)),
910            },
911            None,
912        );
913
914        let args = vec![ArgumentHandle::new(&range_ref, &ctx)];
915        let result = f
916            .dispatch(&args, &ctx.function_context(None))
917            .unwrap()
918            .into_literal();
919        assert_eq!(result, LiteralValue::Int(5));
920
921        // COLUMNS(B2:D10) -> 3
922        let range_ref2 = ASTNode::new(
923            ASTNodeType::Reference {
924                original: "B2:D10".into(),
925                reference: ReferenceType::range(None, Some(2), Some(2), Some(10), Some(4)),
926            },
927            None,
928        );
929
930        let args2 = vec![ArgumentHandle::new(&range_ref2, &ctx)];
931        let result2 = f
932            .dispatch(&args2, &ctx.function_context(None))
933            .unwrap()
934            .into_literal();
935        assert_eq!(result2, LiteralValue::Int(3));
936
937        // COLUMNS(A1) -> 1 (single cell)
938        let cell_ref = ASTNode::new(
939            ASTNodeType::Reference {
940                original: "A1".into(),
941                reference: ReferenceType::cell(None, 1, 1),
942            },
943            None,
944        );
945
946        let args3 = vec![ArgumentHandle::new(&cell_ref, &ctx)];
947        let result3 = f
948            .dispatch(&args3, &ctx.function_context(None))
949            .unwrap()
950            .into_literal();
951        assert_eq!(result3, LiteralValue::Int(1));
952    }
953
954    #[test]
955    fn columns_full_row_reference_returns_sheet_width() {
956        let wb = TestWorkbook::new().with_function(Arc::new(ColumnsFn));
957        let ctx = wb.interpreter();
958        let f = ctx.context.get_function("", "COLUMNS").unwrap();
959
960        // COLUMNS(1:1) -> 16384
961        let row_range_ref = ASTNode::new(
962            ASTNodeType::Reference {
963                original: "1:1".into(),
964                reference: ReferenceType::range(None, Some(1), None, Some(1), None),
965            },
966            None,
967        );
968
969        let args = vec![ArgumentHandle::new(&row_range_ref, &ctx)];
970        let result = f
971            .dispatch(&args, &ctx.function_context(None))
972            .unwrap()
973            .into_literal();
974        assert_eq!(result, LiteralValue::Int(16_384));
975    }
976
977    #[test]
978    fn columns_named_range_falls_back_to_resolved_range_view() {
979        let wb = TestWorkbook::new()
980            .with_named_range(
981                "MyCols",
982                vec![
983                    vec![
984                        LiteralValue::Int(1),
985                        LiteralValue::Int(2),
986                        LiteralValue::Int(3),
987                    ],
988                    vec![
989                        LiteralValue::Int(4),
990                        LiteralValue::Int(5),
991                        LiteralValue::Int(6),
992                    ],
993                ],
994            )
995            .with_function(Arc::new(ColumnsFn));
996        let ctx = wb.interpreter();
997        let f = ctx.context.get_function("", "COLUMNS").unwrap();
998
999        let named_ref = ASTNode::new(
1000            ASTNodeType::Reference {
1001                original: "MyCols".into(),
1002                reference: ReferenceType::NamedRange("MyCols".into()),
1003            },
1004            None,
1005        );
1006
1007        let args = vec![ArgumentHandle::new(&named_ref, &ctx)];
1008        let result = f
1009            .dispatch(&args, &ctx.function_context(None))
1010            .unwrap()
1011            .into_literal();
1012        assert_eq!(result, LiteralValue::Int(3));
1013    }
1014
1015    #[test]
1016    fn rows_columns_reversed_range() {
1017        // A5:A1 (start_row > end_row) should treat as 1 row / 1 column per current implementation fallback
1018        let wb = TestWorkbook::new()
1019            .with_function(Arc::new(RowsFn))
1020            .with_function(Arc::new(ColumnsFn));
1021        let ctx = wb.interpreter();
1022        let rows_f = ctx.context.get_function("", "ROWS").unwrap();
1023        let cols_f = ctx.context.get_function("", "COLUMNS").unwrap();
1024        let rev_range = ASTNode::new(
1025            ASTNodeType::Reference {
1026                original: "A5:A1".into(),
1027                reference: ReferenceType::range(None, Some(5), Some(1), Some(1), Some(1)),
1028            },
1029            None,
1030        );
1031        let args = vec![ArgumentHandle::new(&rev_range, &ctx)];
1032        let r_count = rows_f
1033            .dispatch(&args, &ctx.function_context(None))
1034            .unwrap()
1035            .into_literal();
1036        let c_count = cols_f
1037            .dispatch(&args, &ctx.function_context(None))
1038            .unwrap()
1039            .into_literal();
1040        assert_eq!(r_count, LiteralValue::Int(1));
1041        assert_eq!(c_count, LiteralValue::Int(1));
1042    }
1043}