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