Skip to main content

formualizer_eval/builtins/lookup/
address.rs

1//! ADDRESS function - creates a cell reference as text
2//!
3//! Excel semantics:
4//! - ADDRESS(row_num, column_num, [abs_num], [a1], [sheet_text])
5//! - abs_num: 1 = absolute ($A$1), 2 = abs row (A$1), 3 = abs col ($A1), 4 = relative (A1)
6//! - a1: TRUE = A1 style, FALSE = R1C1 style
7//! - sheet_text: optional sheet name to include
8
9use crate::args::{ArgSchema, CoercionPolicy, ShapeKind};
10use crate::function::Function;
11use crate::traits::{ArgumentHandle, FunctionContext};
12use formualizer_common::{
13    ArgKind, ExcelError, ExcelErrorKind, LiteralValue, col_letters_from_1based,
14};
15use formualizer_macros::func_caps;
16
17/// Convert a column number to Excel letter notation (1 = A, 27 = AA, etc.)
18fn column_to_letters(col: u32) -> String {
19    col_letters_from_1based(col).unwrap_or_default()
20}
21
22#[derive(Debug)]
23pub struct AddressFn;
24
25/// Returns a cell reference as text from row and column numbers.
26///
27/// `ADDRESS` can emit either A1 or R1C1 notation and optionally prefix the address with a
28/// sheet name.
29///
30/// # Remarks
31/// - `abs_num` defaults to `1` (`$A$1`) and supports values `1..4`.
32/// - `a1` defaults to `TRUE`; `FALSE` returns R1C1-style text.
33/// - Valid row range is `1..1048576`; valid column range is `1..16384`.
34/// - Out-of-range row/column values or invalid `abs_num` return `#VALUE!`.
35/// - If `sheet_text` contains spaces or special characters, it is quoted.
36///
37/// # Examples
38/// ```yaml,sandbox
39/// title: "Absolute A1 reference"
40/// formula: '=ADDRESS(2,3)'
41/// expected: "$C$2"
42/// ```
43///
44/// ```yaml,sandbox
45/// title: "Relative R1C1 reference with sheet"
46/// formula: '=ADDRESS(5,3,4,FALSE,"Data Sheet")'
47/// expected: "'Data Sheet'!R[5]C[3]"
48/// ```
49///
50/// ```yaml,docs
51/// related:
52///   - INDEX
53///   - OFFSET
54///   - INDIRECT
55/// faq:
56///   - q: "What happens when abs_num is outside 1..4?"
57///     a: "ADDRESS returns #VALUE!; only 1 (absolute), 2 (absolute row), 3 (absolute column), and 4 (relative) are valid."
58///   - q: "Why does sheet_text sometimes add single quotes?"
59///     a: "Sheet names containing spaces or special characters are quoted and internal apostrophes are escaped to keep a valid reference string."
60/// ```
61/// [formualizer-docgen:schema:start]
62/// Name: ADDRESS
63/// Type: AddressFn
64/// Min args: 2
65/// Max args: 5
66/// Variadic: false
67/// Signature: ADDRESS(arg1: number@scalar, arg2: number@scalar, arg3?: number@scalar, arg4?: logical@scalar, arg5?: text@scalar)
68/// Arg schema: arg1{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberStrict,max=None,repeating=None,default=false}; arg2{kinds=number,required=true,shape=scalar,by_ref=false,coercion=NumberStrict,max=None,repeating=None,default=false}; arg3{kinds=number,required=false,shape=scalar,by_ref=false,coercion=NumberStrict,max=None,repeating=None,default=true}; arg4{kinds=logical,required=false,shape=scalar,by_ref=false,coercion=Logical,max=None,repeating=None,default=true}; arg5{kinds=text,required=false,shape=scalar,by_ref=false,coercion=None,max=None,repeating=None,default=false}
69/// Caps: PURE
70/// [formualizer-docgen:schema:end]
71impl Function for AddressFn {
72    fn name(&self) -> &'static str {
73        "ADDRESS"
74    }
75
76    fn min_args(&self) -> usize {
77        2
78    }
79
80    func_caps!(PURE);
81
82    fn arg_schema(&self) -> &'static [ArgSchema] {
83        use once_cell::sync::Lazy;
84        static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
85            vec![
86                // row_num (required, strict number)
87                ArgSchema {
88                    kinds: smallvec::smallvec![ArgKind::Number],
89                    required: true,
90                    by_ref: false,
91                    shape: ShapeKind::Scalar,
92                    coercion: CoercionPolicy::NumberStrict,
93                    max: None,
94                    repeating: None,
95                    default: None,
96                },
97                // column_num (required, strict number)
98                ArgSchema {
99                    kinds: smallvec::smallvec![ArgKind::Number],
100                    required: true,
101                    by_ref: false,
102                    shape: ShapeKind::Scalar,
103                    coercion: CoercionPolicy::NumberStrict,
104                    max: None,
105                    repeating: None,
106                    default: None,
107                },
108                // abs_num (optional, default 1)
109                ArgSchema {
110                    kinds: smallvec::smallvec![ArgKind::Number],
111                    required: false,
112                    by_ref: false,
113                    shape: ShapeKind::Scalar,
114                    coercion: CoercionPolicy::NumberStrict,
115                    max: None,
116                    repeating: None,
117                    default: Some(LiteralValue::Int(1)),
118                },
119                // a1 (optional, default TRUE)
120                ArgSchema {
121                    kinds: smallvec::smallvec![ArgKind::Logical],
122                    required: false,
123                    by_ref: false,
124                    shape: ShapeKind::Scalar,
125                    coercion: CoercionPolicy::Logical,
126                    max: None,
127                    repeating: None,
128                    default: Some(LiteralValue::Boolean(true)),
129                },
130                // sheet_text (optional)
131                ArgSchema {
132                    kinds: smallvec::smallvec![ArgKind::Text],
133                    required: false,
134                    by_ref: false,
135                    shape: ShapeKind::Scalar,
136                    coercion: CoercionPolicy::None,
137                    max: None,
138                    repeating: None,
139                    default: None,
140                },
141            ]
142        });
143        &SCHEMA
144    }
145
146    fn eval<'a, 'b, 'c>(
147        &self,
148        args: &'c [ArgumentHandle<'a, 'b>],
149        _ctx: &dyn FunctionContext<'b>,
150    ) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
151        if args.len() < 2 {
152            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
153                ExcelError::new(ExcelErrorKind::Value),
154            )));
155        }
156
157        // Get row number
158        let row_val = args[0].value()?.into_literal();
159        if let LiteralValue::Error(e) = row_val {
160            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
161        }
162        let row = match row_val {
163            LiteralValue::Number(n) => n as i64,
164            LiteralValue::Int(i) => i,
165            _ => {
166                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
167                    ExcelError::new(ExcelErrorKind::Value),
168                )));
169            }
170        };
171
172        if !(1..=1_048_576).contains(&row) {
173            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
174                ExcelError::new(ExcelErrorKind::Value),
175            )));
176        }
177
178        // Get column number
179        let col_val = args[1].value()?.into_literal();
180        if let LiteralValue::Error(e) = col_val {
181            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
182        }
183        let col = match col_val {
184            LiteralValue::Number(n) => n as i64,
185            LiteralValue::Int(i) => i,
186            _ => {
187                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
188                    ExcelError::new(ExcelErrorKind::Value),
189                )));
190            }
191        };
192
193        if !(1..=16384).contains(&col) {
194            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
195                ExcelError::new(ExcelErrorKind::Value),
196            )));
197        }
198
199        // Get abs_num (default 1 = absolute)
200        let abs_num = if args.len() > 2 {
201            let abs_val = args[2].value()?.into_literal();
202            if let LiteralValue::Error(e) = abs_val {
203                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
204            }
205            match abs_val {
206                LiteralValue::Number(n) => n as i64,
207                LiteralValue::Int(i) => i,
208                _ => 1,
209            }
210        } else {
211            1
212        };
213
214        if !(1..=4).contains(&abs_num) {
215            return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
216                ExcelError::new(ExcelErrorKind::Value),
217            )));
218        }
219
220        // Get a1 (default TRUE = A1 notation)
221        let a1_style = if args.len() > 3 {
222            let a1_val = args[3].value()?.into_literal();
223            if let LiteralValue::Error(e) = a1_val {
224                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
225            }
226            match a1_val {
227                LiteralValue::Boolean(b) => b,
228                _ => true,
229            }
230        } else {
231            true
232        };
233
234        // Get sheet name (optional)
235        let sheet_name = if args.len() > 4 {
236            let sheet_val = args[4].value()?.into_literal();
237            if let LiteralValue::Error(e) = sheet_val {
238                return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
239            }
240            match sheet_val {
241                LiteralValue::Text(s) => Some(s),
242                _ => None,
243            }
244        } else {
245            None
246        };
247
248        // Build the address
249        let address = if a1_style {
250            // A1 notation
251            let col_letters = column_to_letters(col as u32);
252            let (col_abs, row_abs) = match abs_num {
253                1 => (true, true),   // $A$1
254                2 => (false, true),  // A$1
255                3 => (true, false),  // $A1
256                4 => (false, false), // A1
257                _ => (true, true),
258            };
259
260            let col_str = if col_abs {
261                format!("${col_letters}")
262            } else {
263                col_letters
264            };
265            let row_str = if row_abs {
266                format!("${row}")
267            } else {
268                row.to_string()
269            };
270            format!("{col_str}{row_str}")
271        } else {
272            // R1C1 notation
273            let (col_abs, row_abs) = match abs_num {
274                1 => (true, true),
275                2 => (false, true),
276                3 => (true, false),
277                4 => (false, false),
278                _ => (true, true),
279            };
280
281            let row_str = if row_abs {
282                format!("R{row}")
283            } else {
284                format!("R[{row}]")
285            };
286            let col_str = if col_abs {
287                format!("C{col}")
288            } else {
289                format!("C[{col}]")
290            };
291            format!("{row_str}{col_str}")
292        };
293
294        // Add sheet name if provided
295        let final_address = if let Some(sheet) = sheet_name {
296            // Quote sheet name if it contains spaces or special characters
297            if sheet.contains(' ') || sheet.contains('!') || sheet.contains('\'') {
298                format!("'{}'!{address}", sheet.replace('\'', "''"))
299            } else {
300                format!("{sheet}!{address}")
301            }
302        } else {
303            address
304        };
305
306        Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(
307            final_address,
308        )))
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315    use crate::test_workbook::TestWorkbook;
316    use formualizer_parse::parser::{ASTNode, ASTNodeType};
317    use std::sync::Arc;
318
319    fn lit(v: LiteralValue) -> ASTNode {
320        ASTNode::new(ASTNodeType::Literal(v), None)
321    }
322
323    #[test]
324    fn test_column_to_letters() {
325        assert_eq!(column_to_letters(1), "A");
326        assert_eq!(column_to_letters(26), "Z");
327        assert_eq!(column_to_letters(27), "AA");
328        assert_eq!(column_to_letters(52), "AZ");
329        assert_eq!(column_to_letters(53), "BA");
330        assert_eq!(column_to_letters(702), "ZZ");
331        assert_eq!(column_to_letters(703), "AAA");
332    }
333
334    #[test]
335    fn address_basic() {
336        let wb = TestWorkbook::new().with_function(Arc::new(AddressFn));
337        let ctx = wb.interpreter();
338        let f = ctx.context.get_function("", "ADDRESS").unwrap();
339
340        // ADDRESS(2, 3) -> "$C$2" (default absolute)
341        let two = lit(LiteralValue::Int(2));
342        let three = lit(LiteralValue::Int(3));
343
344        let args = vec![
345            ArgumentHandle::new(&two, &ctx),
346            ArgumentHandle::new(&three, &ctx),
347        ];
348
349        let result = f
350            .dispatch(&args, &ctx.function_context(None))
351            .unwrap()
352            .into_literal();
353        assert_eq!(result, LiteralValue::Text("$C$2".into()));
354    }
355
356    #[test]
357    fn address_abs_variations() {
358        let wb = TestWorkbook::new().with_function(Arc::new(AddressFn));
359        let ctx = wb.interpreter();
360        let f = ctx.context.get_function("", "ADDRESS").unwrap();
361
362        let row = lit(LiteralValue::Int(5));
363        let col = lit(LiteralValue::Int(4)); // Column D
364
365        // abs_num = 1: $D$5
366        let abs1 = lit(LiteralValue::Int(1));
367        let args1 = vec![
368            ArgumentHandle::new(&row, &ctx),
369            ArgumentHandle::new(&col, &ctx),
370            ArgumentHandle::new(&abs1, &ctx),
371        ];
372        assert_eq!(
373            f.dispatch(&args1, &ctx.function_context(None)).unwrap(),
374            LiteralValue::Text("$D$5".into())
375        );
376
377        // abs_num = 2: D$5
378        let abs2 = lit(LiteralValue::Int(2));
379        let args2 = vec![
380            ArgumentHandle::new(&row, &ctx),
381            ArgumentHandle::new(&col, &ctx),
382            ArgumentHandle::new(&abs2, &ctx),
383        ];
384        assert_eq!(
385            f.dispatch(&args2, &ctx.function_context(None)).unwrap(),
386            LiteralValue::Text("D$5".into())
387        );
388
389        // abs_num = 3: $D5
390        let abs3 = lit(LiteralValue::Int(3));
391        let args3 = vec![
392            ArgumentHandle::new(&row, &ctx),
393            ArgumentHandle::new(&col, &ctx),
394            ArgumentHandle::new(&abs3, &ctx),
395        ];
396        assert_eq!(
397            f.dispatch(&args3, &ctx.function_context(None)).unwrap(),
398            LiteralValue::Text("$D5".into())
399        );
400
401        // abs_num = 4: D5
402        let abs4 = lit(LiteralValue::Int(4));
403        let args4 = vec![
404            ArgumentHandle::new(&row, &ctx),
405            ArgumentHandle::new(&col, &ctx),
406            ArgumentHandle::new(&abs4, &ctx),
407        ];
408        assert_eq!(
409            f.dispatch(&args4, &ctx.function_context(None)).unwrap(),
410            LiteralValue::Text("D5".into())
411        );
412    }
413
414    #[test]
415    fn address_with_sheet() {
416        let wb = TestWorkbook::new().with_function(Arc::new(AddressFn));
417        let ctx = wb.interpreter();
418        let f = ctx.context.get_function("", "ADDRESS").unwrap();
419
420        let row = lit(LiteralValue::Int(1));
421        let col = lit(LiteralValue::Int(1));
422        let abs_num = lit(LiteralValue::Int(1));
423        let a1_style = lit(LiteralValue::Boolean(true));
424
425        // Simple sheet name
426        let sheet1 = lit(LiteralValue::Text("Sheet1".into()));
427        let args1 = vec![
428            ArgumentHandle::new(&row, &ctx),
429            ArgumentHandle::new(&col, &ctx),
430            ArgumentHandle::new(&abs_num, &ctx),
431            ArgumentHandle::new(&a1_style, &ctx),
432            ArgumentHandle::new(&sheet1, &ctx),
433        ];
434        assert_eq!(
435            f.dispatch(&args1, &ctx.function_context(None)).unwrap(),
436            LiteralValue::Text("Sheet1!$A$1".into())
437        );
438
439        // Sheet name with spaces (needs quoting)
440        let sheet2 = lit(LiteralValue::Text("My Sheet".into()));
441        let args2 = vec![
442            ArgumentHandle::new(&row, &ctx),
443            ArgumentHandle::new(&col, &ctx),
444            ArgumentHandle::new(&abs_num, &ctx),
445            ArgumentHandle::new(&a1_style, &ctx),
446            ArgumentHandle::new(&sheet2, &ctx),
447        ];
448        assert_eq!(
449            f.dispatch(&args2, &ctx.function_context(None)).unwrap(),
450            LiteralValue::Text("'My Sheet'!$A$1".into())
451        );
452    }
453
454    #[test]
455    fn address_r1c1_style() {
456        let wb = TestWorkbook::new().with_function(Arc::new(AddressFn));
457        let ctx = wb.interpreter();
458        let f = ctx.context.get_function("", "ADDRESS").unwrap();
459
460        let row = lit(LiteralValue::Int(5));
461        let col = lit(LiteralValue::Int(3));
462        let abs1 = lit(LiteralValue::Int(1));
463        let r1c1 = lit(LiteralValue::Boolean(false));
464
465        // R1C1 absolute
466        let args = vec![
467            ArgumentHandle::new(&row, &ctx),
468            ArgumentHandle::new(&col, &ctx),
469            ArgumentHandle::new(&abs1, &ctx),
470            ArgumentHandle::new(&r1c1, &ctx),
471        ];
472        assert_eq!(
473            f.dispatch(&args, &ctx.function_context(None))
474                .unwrap()
475                .into_literal(),
476            LiteralValue::Text("R5C3".into())
477        );
478
479        // R1C1 relative
480        let abs4 = lit(LiteralValue::Int(4));
481        let args2 = vec![
482            ArgumentHandle::new(&row, &ctx),
483            ArgumentHandle::new(&col, &ctx),
484            ArgumentHandle::new(&abs4, &ctx),
485            ArgumentHandle::new(&r1c1, &ctx),
486        ];
487        assert_eq!(
488            f.dispatch(&args2, &ctx.function_context(None)).unwrap(),
489            LiteralValue::Text("R[5]C[3]".into())
490        );
491    }
492
493    #[test]
494    fn address_edge_cases() {
495        let wb = TestWorkbook::new().with_function(Arc::new(AddressFn));
496        let ctx = wb.interpreter();
497        let f = ctx.context.get_function("", "ADDRESS").unwrap();
498
499        // Row too large
500        let big_row = lit(LiteralValue::Int(1_048_577));
501        let col = lit(LiteralValue::Int(1));
502        let args = vec![
503            ArgumentHandle::new(&big_row, &ctx),
504            ArgumentHandle::new(&col, &ctx),
505        ];
506        let result = f
507            .dispatch(&args, &ctx.function_context(None))
508            .unwrap()
509            .into_literal();
510        assert!(matches!(result, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Value));
511
512        // Column too large
513        let row = lit(LiteralValue::Int(1));
514        let big_col = lit(LiteralValue::Int(16385));
515        let args2 = vec![
516            ArgumentHandle::new(&row, &ctx),
517            ArgumentHandle::new(&big_col, &ctx),
518        ];
519        let result2 = f
520            .dispatch(&args2, &ctx.function_context(None))
521            .unwrap()
522            .into_literal();
523        assert!(matches!(result2, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Value));
524
525        // Invalid abs_num
526        let abs5 = lit(LiteralValue::Int(5));
527        let normal_col = lit(LiteralValue::Int(1));
528        let args3 = vec![
529            ArgumentHandle::new(&row, &ctx),
530            ArgumentHandle::new(&normal_col, &ctx),
531            ArgumentHandle::new(&abs5, &ctx),
532        ];
533        let result3 = f
534            .dispatch(&args3, &ctx.function_context(None))
535            .unwrap()
536            .into_literal();
537        assert!(matches!(result3, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Value));
538    }
539}