//! ADDRESS function - creates a cell reference as text
//!
//! Excel semantics:
//! - ADDRESS(row_num, column_num, [abs_num], [a1], [sheet_text])
//! - abs_num: 1 = absolute ($A$1), 2 = abs row (A$1), 3 = abs col ($A1), 4 = relative (A1)
//! - a1: TRUE = A1 style, FALSE = R1C1 style
//! - sheet_text: optional sheet name to include
use crate::args::{ArgSchema, CoercionPolicy, ShapeKind};
use crate::function::Function;
use crate::traits::{ArgumentHandle, FunctionContext};
use formualizer_common::{
ArgKind, ExcelError, ExcelErrorKind, LiteralValue, col_letters_from_1based,
};
use formualizer_macros::func_caps;
/// Convert a column number to Excel letter notation (1 = A, 27 = AA, etc.)
fn column_to_letters(col: u32) -> String {
col_letters_from_1based(col).unwrap_or_default()
}
#[derive(Debug)]
pub struct AddressFn;
/// Returns a cell reference as text from row and column numbers.
///
/// `ADDRESS` can emit either A1 or R1C1 notation and optionally prefix the address with a
/// sheet name.
///
/// # Remarks
/// - `abs_num` defaults to `1` (`$A$1`) and supports values `1..4`.
/// - `a1` defaults to `TRUE`; `FALSE` returns R1C1-style text.
/// - Valid row range is `1..1048576`; valid column range is `1..16384`.
/// - Out-of-range row/column values or invalid `abs_num` return `#VALUE!`.
/// - If `sheet_text` contains spaces or special characters, it is quoted.
///
/// # Examples
/// ```yaml,sandbox
/// title: "Absolute A1 reference"
/// formula: '=ADDRESS(2,3)'
/// expected: "$C$2"
/// ```
///
/// ```yaml,sandbox
/// title: "Relative R1C1 reference with sheet"
/// formula: '=ADDRESS(5,3,4,FALSE,"Data Sheet")'
/// expected: "'Data Sheet'!R[5]C[3]"
/// ```
///
/// ```yaml,docs
/// related:
/// - INDEX
/// - OFFSET
/// - INDIRECT
/// faq:
/// - q: "What happens when abs_num is outside 1..4?"
/// a: "ADDRESS returns #VALUE!; only 1 (absolute), 2 (absolute row), 3 (absolute column), and 4 (relative) are valid."
/// - q: "Why does sheet_text sometimes add single quotes?"
/// a: "Sheet names containing spaces or special characters are quoted and internal apostrophes are escaped to keep a valid reference string."
/// ```
/// [formualizer-docgen:schema:start]
/// Name: ADDRESS
/// Type: AddressFn
/// Min args: 2
/// Max args: 5
/// Variadic: false
/// Signature: ADDRESS(arg1: number@scalar, arg2: number@scalar, arg3?: number@scalar, arg4?: logical@scalar, arg5?: text@scalar)
/// 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}
/// Caps: PURE
/// [formualizer-docgen:schema:end]
impl Function for AddressFn {
fn name(&self) -> &'static str {
"ADDRESS"
}
fn min_args(&self) -> usize {
2
}
func_caps!(PURE);
fn arg_schema(&self) -> &'static [ArgSchema] {
use once_cell::sync::Lazy;
static SCHEMA: Lazy<Vec<ArgSchema>> = Lazy::new(|| {
vec![
// row_num (required, strict number)
ArgSchema {
kinds: smallvec::smallvec![ArgKind::Number],
required: true,
by_ref: false,
shape: ShapeKind::Scalar,
coercion: CoercionPolicy::NumberStrict,
max: None,
repeating: None,
default: None,
},
// column_num (required, strict number)
ArgSchema {
kinds: smallvec::smallvec![ArgKind::Number],
required: true,
by_ref: false,
shape: ShapeKind::Scalar,
coercion: CoercionPolicy::NumberStrict,
max: None,
repeating: None,
default: None,
},
// abs_num (optional, default 1)
ArgSchema {
kinds: smallvec::smallvec![ArgKind::Number],
required: false,
by_ref: false,
shape: ShapeKind::Scalar,
coercion: CoercionPolicy::NumberStrict,
max: None,
repeating: None,
default: Some(LiteralValue::Int(1)),
},
// a1 (optional, default TRUE)
ArgSchema {
kinds: smallvec::smallvec![ArgKind::Logical],
required: false,
by_ref: false,
shape: ShapeKind::Scalar,
coercion: CoercionPolicy::Logical,
max: None,
repeating: None,
default: Some(LiteralValue::Boolean(true)),
},
// sheet_text (optional)
ArgSchema {
kinds: smallvec::smallvec![ArgKind::Text],
required: false,
by_ref: false,
shape: ShapeKind::Scalar,
coercion: CoercionPolicy::None,
max: None,
repeating: None,
default: None,
},
]
});
&SCHEMA
}
fn eval<'a, 'b, 'c>(
&self,
args: &'c [ArgumentHandle<'a, 'b>],
_ctx: &dyn FunctionContext<'b>,
) -> Result<crate::traits::CalcValue<'b>, ExcelError> {
if args.len() < 2 {
return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
ExcelError::new(ExcelErrorKind::Value),
)));
}
// Get row number
let row_val = args[0].value()?.into_literal();
if let LiteralValue::Error(e) = row_val {
return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
}
let row = match row_val {
LiteralValue::Number(n) => n as i64,
LiteralValue::Int(i) => i,
_ => {
return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
ExcelError::new(ExcelErrorKind::Value),
)));
}
};
if !(1..=1_048_576).contains(&row) {
return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
ExcelError::new(ExcelErrorKind::Value),
)));
}
// Get column number
let col_val = args[1].value()?.into_literal();
if let LiteralValue::Error(e) = col_val {
return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
}
let col = match col_val {
LiteralValue::Number(n) => n as i64,
LiteralValue::Int(i) => i,
_ => {
return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
ExcelError::new(ExcelErrorKind::Value),
)));
}
};
if !(1..=16384).contains(&col) {
return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
ExcelError::new(ExcelErrorKind::Value),
)));
}
// Get abs_num (default 1 = absolute)
let abs_num = if args.len() > 2 {
let abs_val = args[2].value()?.into_literal();
if let LiteralValue::Error(e) = abs_val {
return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
}
match abs_val {
LiteralValue::Number(n) => n as i64,
LiteralValue::Int(i) => i,
_ => 1,
}
} else {
1
};
if !(1..=4).contains(&abs_num) {
return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(
ExcelError::new(ExcelErrorKind::Value),
)));
}
// Get a1 (default TRUE = A1 notation)
let a1_style = if args.len() > 3 {
let a1_val = args[3].value()?.into_literal();
if let LiteralValue::Error(e) = a1_val {
return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
}
match a1_val {
LiteralValue::Boolean(b) => b,
_ => true,
}
} else {
true
};
// Get sheet name (optional)
let sheet_name = if args.len() > 4 {
let sheet_val = args[4].value()?.into_literal();
if let LiteralValue::Error(e) = sheet_val {
return Ok(crate::traits::CalcValue::Scalar(LiteralValue::Error(e)));
}
match sheet_val {
LiteralValue::Text(s) => Some(s),
_ => None,
}
} else {
None
};
// Build the address
let address = if a1_style {
// A1 notation
let col_letters = column_to_letters(col as u32);
let (col_abs, row_abs) = match abs_num {
1 => (true, true), // $A$1
2 => (false, true), // A$1
3 => (true, false), // $A1
4 => (false, false), // A1
_ => (true, true),
};
let col_str = if col_abs {
format!("${col_letters}")
} else {
col_letters
};
let row_str = if row_abs {
format!("${row}")
} else {
row.to_string()
};
format!("{col_str}{row_str}")
} else {
// R1C1 notation
let (col_abs, row_abs) = match abs_num {
1 => (true, true),
2 => (false, true),
3 => (true, false),
4 => (false, false),
_ => (true, true),
};
let row_str = if row_abs {
format!("R{row}")
} else {
format!("R[{row}]")
};
let col_str = if col_abs {
format!("C{col}")
} else {
format!("C[{col}]")
};
format!("{row_str}{col_str}")
};
// Add sheet name if provided
let final_address = if let Some(sheet) = sheet_name {
// Quote sheet name if it contains spaces or special characters
if sheet.contains(' ') || sheet.contains('!') || sheet.contains('\'') {
format!("'{}'!{address}", sheet.replace('\'', "''"))
} else {
format!("{sheet}!{address}")
}
} else {
address
};
Ok(crate::traits::CalcValue::Scalar(LiteralValue::Text(
final_address,
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_workbook::TestWorkbook;
use formualizer_parse::parser::{ASTNode, ASTNodeType};
use std::sync::Arc;
fn lit(v: LiteralValue) -> ASTNode {
ASTNode::new(ASTNodeType::Literal(v), None)
}
#[test]
fn test_column_to_letters() {
assert_eq!(column_to_letters(1), "A");
assert_eq!(column_to_letters(26), "Z");
assert_eq!(column_to_letters(27), "AA");
assert_eq!(column_to_letters(52), "AZ");
assert_eq!(column_to_letters(53), "BA");
assert_eq!(column_to_letters(702), "ZZ");
assert_eq!(column_to_letters(703), "AAA");
}
#[test]
fn address_basic() {
let wb = TestWorkbook::new().with_function(Arc::new(AddressFn));
let ctx = wb.interpreter();
let f = ctx.context.get_function("", "ADDRESS").unwrap();
// ADDRESS(2, 3) -> "$C$2" (default absolute)
let two = lit(LiteralValue::Int(2));
let three = lit(LiteralValue::Int(3));
let args = vec![
ArgumentHandle::new(&two, &ctx),
ArgumentHandle::new(&three, &ctx),
];
let result = f
.dispatch(&args, &ctx.function_context(None))
.unwrap()
.into_literal();
assert_eq!(result, LiteralValue::Text("$C$2".into()));
}
#[test]
fn address_abs_variations() {
let wb = TestWorkbook::new().with_function(Arc::new(AddressFn));
let ctx = wb.interpreter();
let f = ctx.context.get_function("", "ADDRESS").unwrap();
let row = lit(LiteralValue::Int(5));
let col = lit(LiteralValue::Int(4)); // Column D
// abs_num = 1: $D$5
let abs1 = lit(LiteralValue::Int(1));
let args1 = vec![
ArgumentHandle::new(&row, &ctx),
ArgumentHandle::new(&col, &ctx),
ArgumentHandle::new(&abs1, &ctx),
];
assert_eq!(
f.dispatch(&args1, &ctx.function_context(None)).unwrap(),
LiteralValue::Text("$D$5".into())
);
// abs_num = 2: D$5
let abs2 = lit(LiteralValue::Int(2));
let args2 = vec![
ArgumentHandle::new(&row, &ctx),
ArgumentHandle::new(&col, &ctx),
ArgumentHandle::new(&abs2, &ctx),
];
assert_eq!(
f.dispatch(&args2, &ctx.function_context(None)).unwrap(),
LiteralValue::Text("D$5".into())
);
// abs_num = 3: $D5
let abs3 = lit(LiteralValue::Int(3));
let args3 = vec![
ArgumentHandle::new(&row, &ctx),
ArgumentHandle::new(&col, &ctx),
ArgumentHandle::new(&abs3, &ctx),
];
assert_eq!(
f.dispatch(&args3, &ctx.function_context(None)).unwrap(),
LiteralValue::Text("$D5".into())
);
// abs_num = 4: D5
let abs4 = lit(LiteralValue::Int(4));
let args4 = vec![
ArgumentHandle::new(&row, &ctx),
ArgumentHandle::new(&col, &ctx),
ArgumentHandle::new(&abs4, &ctx),
];
assert_eq!(
f.dispatch(&args4, &ctx.function_context(None)).unwrap(),
LiteralValue::Text("D5".into())
);
}
#[test]
fn address_with_sheet() {
let wb = TestWorkbook::new().with_function(Arc::new(AddressFn));
let ctx = wb.interpreter();
let f = ctx.context.get_function("", "ADDRESS").unwrap();
let row = lit(LiteralValue::Int(1));
let col = lit(LiteralValue::Int(1));
let abs_num = lit(LiteralValue::Int(1));
let a1_style = lit(LiteralValue::Boolean(true));
// Simple sheet name
let sheet1 = lit(LiteralValue::Text("Sheet1".into()));
let args1 = vec![
ArgumentHandle::new(&row, &ctx),
ArgumentHandle::new(&col, &ctx),
ArgumentHandle::new(&abs_num, &ctx),
ArgumentHandle::new(&a1_style, &ctx),
ArgumentHandle::new(&sheet1, &ctx),
];
assert_eq!(
f.dispatch(&args1, &ctx.function_context(None)).unwrap(),
LiteralValue::Text("Sheet1!$A$1".into())
);
// Sheet name with spaces (needs quoting)
let sheet2 = lit(LiteralValue::Text("My Sheet".into()));
let args2 = vec![
ArgumentHandle::new(&row, &ctx),
ArgumentHandle::new(&col, &ctx),
ArgumentHandle::new(&abs_num, &ctx),
ArgumentHandle::new(&a1_style, &ctx),
ArgumentHandle::new(&sheet2, &ctx),
];
assert_eq!(
f.dispatch(&args2, &ctx.function_context(None)).unwrap(),
LiteralValue::Text("'My Sheet'!$A$1".into())
);
}
#[test]
fn address_r1c1_style() {
let wb = TestWorkbook::new().with_function(Arc::new(AddressFn));
let ctx = wb.interpreter();
let f = ctx.context.get_function("", "ADDRESS").unwrap();
let row = lit(LiteralValue::Int(5));
let col = lit(LiteralValue::Int(3));
let abs1 = lit(LiteralValue::Int(1));
let r1c1 = lit(LiteralValue::Boolean(false));
// R1C1 absolute
let args = vec![
ArgumentHandle::new(&row, &ctx),
ArgumentHandle::new(&col, &ctx),
ArgumentHandle::new(&abs1, &ctx),
ArgumentHandle::new(&r1c1, &ctx),
];
assert_eq!(
f.dispatch(&args, &ctx.function_context(None))
.unwrap()
.into_literal(),
LiteralValue::Text("R5C3".into())
);
// R1C1 relative
let abs4 = lit(LiteralValue::Int(4));
let args2 = vec![
ArgumentHandle::new(&row, &ctx),
ArgumentHandle::new(&col, &ctx),
ArgumentHandle::new(&abs4, &ctx),
ArgumentHandle::new(&r1c1, &ctx),
];
assert_eq!(
f.dispatch(&args2, &ctx.function_context(None)).unwrap(),
LiteralValue::Text("R[5]C[3]".into())
);
}
#[test]
fn address_edge_cases() {
let wb = TestWorkbook::new().with_function(Arc::new(AddressFn));
let ctx = wb.interpreter();
let f = ctx.context.get_function("", "ADDRESS").unwrap();
// Row too large
let big_row = lit(LiteralValue::Int(1_048_577));
let col = lit(LiteralValue::Int(1));
let args = vec![
ArgumentHandle::new(&big_row, &ctx),
ArgumentHandle::new(&col, &ctx),
];
let result = f
.dispatch(&args, &ctx.function_context(None))
.unwrap()
.into_literal();
assert!(matches!(result, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Value));
// Column too large
let row = lit(LiteralValue::Int(1));
let big_col = lit(LiteralValue::Int(16385));
let args2 = vec![
ArgumentHandle::new(&row, &ctx),
ArgumentHandle::new(&big_col, &ctx),
];
let result2 = f
.dispatch(&args2, &ctx.function_context(None))
.unwrap()
.into_literal();
assert!(matches!(result2, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Value));
// Invalid abs_num
let abs5 = lit(LiteralValue::Int(5));
let normal_col = lit(LiteralValue::Int(1));
let args3 = vec![
ArgumentHandle::new(&row, &ctx),
ArgumentHandle::new(&normal_col, &ctx),
ArgumentHandle::new(&abs5, &ctx),
];
let result3 = f
.dispatch(&args3, &ctx.function_context(None))
.unwrap()
.into_literal();
assert!(matches!(result3, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Value));
}
}