From f69a0ee82e239bcab655df398097a8f2fc33f447 Mon Sep 17 00:00:00 2001 From: Brian Hung Date: Tue, 15 Jul 2025 12:02:46 -0700 Subject: [PATCH 1/4] Implement ADDRESS function --- .../src/expressions/parser/static_analysis.rs | 2 + base/src/functions/lookup_and_reference.rs | 87 +++++++++++++++++++ base/src/functions/mod.rs | 7 +- base/src/test/mod.rs | 1 + base/src/test/test_fn_address.rs | 28 ++++++ 5 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 base/src/test/test_fn_address.rs diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index 80f194360..99a7ee298 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -650,6 +650,7 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec args_signature_hlookup(arg_count), Function::Index => args_signature_index(arg_count), Function::Indirect => args_signature_scalars(arg_count, 1, 0), + Function::Address => args_signature_scalars(arg_count, 2, 3), Function::Lookup => args_signature_lookup(arg_count), Function::Match => args_signature_match(arg_count), Function::Offset => args_signature_offset(arg_count), @@ -854,6 +855,7 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult { Function::Hlookup => not_implemented(args), Function::Index => static_analysis_index(args), Function::Indirect => static_analysis_indirect(args), + Function::Address => not_implemented(args), Function::Lookup => not_implemented(args), Function::Match => not_implemented(args), Function::Offset => static_analysis_offset(args), diff --git a/base/src/functions/lookup_and_reference.rs b/base/src/functions/lookup_and_reference.rs index f8f9735f5..a5d7d4c05 100644 --- a/base/src/functions/lookup_and_reference.rs +++ b/base/src/functions/lookup_and_reference.rs @@ -725,6 +725,93 @@ impl Model { } } + // ADDRESS(row_num, col_num, [abs_num], [a1], [sheet]) + // Returns a reference as text to a single cell + pub(crate) fn fn_address(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() < 2 || args.len() > 5 { + return CalcResult::new_args_number_error(cell); + } + let row = match self.get_number(&args[0], cell) { + Ok(r) => r as i32, + Err(e) => return e, + }; + let column = match self.get_number(&args[1], cell) { + Ok(c) => c as i32, + Err(e) => return e, + }; + if row < 1 || row > LAST_ROW || column < 1 || column > LAST_COLUMN { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Invalid row or column".to_string(), + }; + } + let abs_num = if args.len() >= 3 { + match self.get_number(&args[2], cell) { + Ok(v) => v as i32, + Err(e) => return e, + } + } else { + 1 + }; + if abs_num < 1 || abs_num > 4 { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Invalid abs_num".to_string(), + }; + } + let a1 = if args.len() >= 4 { + match self.get_boolean(&args[3], cell) { + Ok(v) => v, + Err(e) => return e, + } + } else { + true + }; + let sheet = if args.len() == 5 { + match self.get_string(&args[4], cell) { + Ok(s) => Some(s), + Err(e) => return e, + } + } else { + None + }; + + let result = if a1 { + let col = crate::expressions::utils::number_to_column(column).unwrap(); + let col_str = match abs_num { + 1 | 3 => format!("${}", col), + _ => col, + }; + let row_str = match abs_num { + 1 | 2 => format!("${}", row), + _ => format!("{}", row), + }; + let addr = format!("{}{}", col_str, row_str); + match sheet { + Some(s) => format!("{}!{}", crate::expressions::utils::quote_name(&s), addr), + None => addr, + } + } else { + let row_str = match abs_num { + 1 | 2 => format!("R{}", row), + _ => format!("R[{}]", row), + }; + let col_str = match abs_num { + 1 | 3 => format!("C{}", column), + _ => format!("C[{}]", column), + }; + let addr = format!("{}{}", row_str, col_str); + match sheet { + Some(s) => format!("{}!{}", crate::expressions::utils::quote_name(&s), addr), + None => addr, + } + }; + + CalcResult::String(result) + } + // OFFSET(reference, rows, cols, [height], [width]) // Returns a reference to a range that is a specified number of rows and columns from a cell or range of cells. // The reference that is returned can be a single cell or a range of cells. diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs index 21c8f72da..0a6616a9e 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -97,6 +97,7 @@ pub enum Function { Type, // Lookup and reference + Address, Hlookup, Index, Indirect, @@ -253,7 +254,7 @@ pub enum Function { } impl Function { - pub fn into_iter() -> IntoIter { + pub fn into_iter() -> IntoIter { [ Function::And, Function::False, @@ -303,6 +304,7 @@ impl Function { Function::Columns, Function::Index, Function::Indirect, + Function::Address, Function::Hlookup, Function::Lookup, Function::Match, @@ -562,6 +564,7 @@ impl Function { "COLUMNS" => Some(Function::Columns), "INDEX" => Some(Function::Index), "INDIRECT" => Some(Function::Indirect), + "ADDRESS" => Some(Function::Address), "HLOOKUP" => Some(Function::Hlookup), "LOOKUP" => Some(Function::Lookup), "MATCH" => Some(Function::Match), @@ -781,6 +784,7 @@ impl fmt::Display for Function { Function::Columns => write!(f, "COLUMNS"), Function::Index => write!(f, "INDEX"), Function::Indirect => write!(f, "INDIRECT"), + Function::Address => write!(f, "ADDRESS"), Function::Hlookup => write!(f, "HLOOKUP"), Function::Lookup => write!(f, "LOOKUP"), Function::Match => write!(f, "MATCH"), @@ -1019,6 +1023,7 @@ impl Model { Function::Columns => self.fn_columns(args, cell), Function::Index => self.fn_index(args, cell), Function::Indirect => self.fn_indirect(args, cell), + Function::Address => self.fn_address(args, cell), Function::Hlookup => self.fn_hlookup(args, cell), Function::Lookup => self.fn_lookup(args, cell), Function::Match => self.fn_match(args, cell), diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index a0a0d69d6..4125c2b53 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -48,6 +48,7 @@ mod test_worksheet; pub(crate) mod util; mod engineering; +mod test_fn_address; mod test_fn_offset; mod test_number_format; diff --git a/base/src/test/test_fn_address.rs b/base/src/test/test_fn_address.rs new file mode 100644 index 000000000..008fde63a --- /dev/null +++ b/base/src/test/test_fn_address.rs @@ -0,0 +1,28 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn basic_address() { + let mut model = new_empty_model(); + model._set("A1", "=ADDRESS(1,1)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"$A$1"); +} + +#[test] +fn address_with_sheet_and_r1c1() { + let mut model = new_empty_model(); + model.new_sheet(); + model._set("A1", "=ADDRESS(4,3,2,FALSE,\"Sheet2\")"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"Sheet2!R4C[3]"); +} + +#[test] +fn address_invalid() { + let mut model = new_empty_model(); + model._set("A1", "=ADDRESS(0,1)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"#VALUE!"); +} From 4e918981e9af6b494a2867a4e4c20a6a237db7bc Mon Sep 17 00:00:00 2001 From: Brian Hung Date: Tue, 15 Jul 2025 13:01:40 -0700 Subject: [PATCH 2/4] fix build --- base/src/functions/lookup_and_reference.rs | 33 ++++++++++++++-------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/base/src/functions/lookup_and_reference.rs b/base/src/functions/lookup_and_reference.rs index a5d7d4c05..feae7883b 100644 --- a/base/src/functions/lookup_and_reference.rs +++ b/base/src/functions/lookup_and_reference.rs @@ -739,7 +739,7 @@ impl Model { Ok(c) => c as i32, Err(e) => return e, }; - if row < 1 || row > LAST_ROW || column < 1 || column > LAST_COLUMN { + if !(1..=LAST_ROW).contains(&row) || !(1..=LAST_COLUMN).contains(&column) { return CalcResult::Error { error: Error::VALUE, origin: cell, @@ -754,7 +754,7 @@ impl Model { } else { 1 }; - if abs_num < 1 || abs_num > 4 { + if !(1..=4).contains(&abs_num) { return CalcResult::Error { error: Error::VALUE, origin: cell, @@ -779,30 +779,39 @@ impl Model { }; let result = if a1 { - let col = crate::expressions::utils::number_to_column(column).unwrap(); + let col = match crate::expressions::utils::number_to_column(column) { + Some(c) => c, + None => { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Invalid column number".to_string(), + }; + } + }; let col_str = match abs_num { - 1 | 3 => format!("${}", col), + 1 | 3 => format!("${col}"), _ => col, }; let row_str = match abs_num { - 1 | 2 => format!("${}", row), - _ => format!("{}", row), + 1 | 2 => format!("${row}"), + _ => format!("{row}"), }; - let addr = format!("{}{}", col_str, row_str); + let addr = format!("{col_str}{row_str}"); match sheet { Some(s) => format!("{}!{}", crate::expressions::utils::quote_name(&s), addr), None => addr, } } else { let row_str = match abs_num { - 1 | 2 => format!("R{}", row), - _ => format!("R[{}]", row), + 1 | 2 => format!("R{row}"), + _ => format!("R[{row}]"), }; let col_str = match abs_num { - 1 | 3 => format!("C{}", column), - _ => format!("C[{}]", column), + 1 | 3 => format!("C{column}"), + _ => format!("C[{column}]"), }; - let addr = format!("{}{}", row_str, col_str); + let addr = format!("{row_str}{col_str}"); match sheet { Some(s) => format!("{}!{}", crate::expressions::utils::quote_name(&s), addr), None => addr, From 0d0bd635848eff9ada9da364a5d51a3130cbe938 Mon Sep 17 00:00:00 2001 From: BrianHung Date: Sun, 20 Jul 2025 15:31:36 -0700 Subject: [PATCH 3/4] improve test coverage --- base/src/test/test_fn_address.rs | 110 +++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/base/src/test/test_fn_address.rs b/base/src/test/test_fn_address.rs index 008fde63a..d84ba81fe 100644 --- a/base/src/test/test_fn_address.rs +++ b/base/src/test/test_fn_address.rs @@ -26,3 +26,113 @@ fn address_invalid() { model.evaluate(); assert_eq!(model._get_text("A1"), *"#VALUE!"); } + +// Test all abs_num values (1-4) with A1 style +#[test] +fn address_abs_num_a1_style() { + let mut model = new_empty_model(); + model._set("A1", "=ADDRESS(5,3,1)"); // Both absolute + model._set("A2", "=ADDRESS(5,3,2)"); // Row absolute + model._set("A3", "=ADDRESS(5,3,3)"); // Column absolute + model._set("A4", "=ADDRESS(5,3,4)"); // Both relative + model.evaluate(); + assert_eq!(model._get_text("A1"), *"$C$5"); + assert_eq!(model._get_text("A2"), *"C$5"); + assert_eq!(model._get_text("A3"), *"$C5"); + assert_eq!(model._get_text("A4"), *"C5"); +} + +// Test all abs_num values (1-4) with R1C1 style +#[test] +fn address_abs_num_r1c1_style() { + let mut model = new_empty_model(); + model._set("A1", "=ADDRESS(5,3,1,FALSE)"); // Both absolute + model._set("A2", "=ADDRESS(5,3,2,FALSE)"); // Row absolute + model._set("A3", "=ADDRESS(5,3,3,FALSE)"); // Column absolute + model._set("A4", "=ADDRESS(5,3,4,FALSE)"); // Both relative + model.evaluate(); + assert_eq!(model._get_text("A1"), *"R5C3"); + assert_eq!(model._get_text("A2"), *"R5C[3]"); + assert_eq!(model._get_text("A3"), *"R[5]C3"); + assert_eq!(model._get_text("A4"), *"R[5]C[3]"); +} + +// Test with sheet names +#[test] +fn address_with_sheet_names() { + let mut model = new_empty_model(); + model._set("A1", "=ADDRESS(10,5,1,TRUE,\"MySheet\")"); // A1 style + model._set("A2", "=ADDRESS(10,5,1,FALSE,\"MySheet\")"); // R1C1 style + model._set("A3", "=ADDRESS(2,1,1,TRUE,\"My Sheet\")"); // Sheet with spaces + model.evaluate(); + assert_eq!(model._get_text("A1"), *"MySheet!$E$10"); + assert_eq!(model._get_text("A2"), *"MySheet!R10C5"); + assert_eq!(model._get_text("A3"), *"'My Sheet'!$A$2"); +} + +// Test edge cases for row/column limits +#[test] +fn address_boundary_values() { + let mut model = new_empty_model(); + model._set("A1", "=ADDRESS(1,1)"); // Min values + model._set("A2", "=ADDRESS(1048576,16384)"); // Max values + model._set("A3", "=ADDRESS(1,26)"); // Column Z + model._set("A4", "=ADDRESS(1,27)"); // Column AA + model.evaluate(); + assert_eq!(model._get_text("A1"), *"$A$1"); + assert_eq!(model._get_text("A2"), *"$XFD$1048576"); + assert_eq!(model._get_text("A3"), *"$Z$1"); + assert_eq!(model._get_text("A4"), *"$AA$1"); +} + +// Test invalid inputs +#[test] +fn address_invalid_inputs() { + let mut model = new_empty_model(); + model._set("A1", "=ADDRESS(0,1)"); // Invalid row + model._set("A2", "=ADDRESS(1,0)"); // Invalid column + model._set("A3", "=ADDRESS(1048577,1)"); // Row too large + model._set("A4", "=ADDRESS(1,16385)"); // Column too large + model._set("A5", "=ADDRESS(1,1,0)"); // Invalid abs_num + model._set("A6", "=ADDRESS(1,1,5)"); // Invalid abs_num + model.evaluate(); + assert_eq!(model._get_text("A1"), *"#VALUE!"); + assert_eq!(model._get_text("A2"), *"#VALUE!"); + assert_eq!(model._get_text("A3"), *"#VALUE!"); + assert_eq!(model._get_text("A4"), *"#VALUE!"); + assert_eq!(model._get_text("A5"), *"#VALUE!"); + assert_eq!(model._get_text("A6"), *"#VALUE!"); +} + +// Test argument count and type errors +#[test] +fn address_too_few_arguments() { + let mut model = new_empty_model(); + model._set("A1", "=ADDRESS(1)"); // Too few arguments + model.evaluate(); + assert_eq!(model._get_text("A1"), *"#ERROR!"); +} + +#[test] +fn address_too_many_arguments() { + let mut model = new_empty_model(); + model._set("A1", "=ADDRESS(1,1,1,TRUE,\"Sheet\",\"Extra\")"); // Too many arguments + model.evaluate(); + assert_eq!(model._get_text("A1"), *"#ERROR!"); +} + +#[test] +fn address_string_row() { + let mut model = new_empty_model(); + model._set("A1", "=ADDRESS(\"text\",1)"); // String row + model.evaluate(); + assert_eq!(model._get_text("A1"), *"#VALUE!"); +} + +#[test] +fn address_string_column() { + let mut model = new_empty_model(); + model._set("A1", "=ADDRESS(1,\"text\")"); // String column + model.evaluate(); + assert_eq!(model._get_text("A1"), *"#VALUE!"); +} From 6ae1397c4193af2c4b20a31b73bf1b788ba340e5 Mon Sep 17 00:00:00 2001 From: BrianHung Date: Sun, 20 Jul 2025 15:40:38 -0700 Subject: [PATCH 4/4] fix docs --- docs/src/functions/lookup_and_reference/address.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/src/functions/lookup_and_reference/address.md b/docs/src/functions/lookup_and_reference/address.md index c51c1dd6d..55ee90844 100644 --- a/docs/src/functions/lookup_and_reference/address.md +++ b/docs/src/functions/lookup_and_reference/address.md @@ -7,6 +7,5 @@ lang: en-US # ADDRESS ::: warning -🚧 This function is not yet available in IronCalc. -[Follow development here](https://github.com/ironcalc/IronCalc/labels/Functions) +🚧 This function is implemented but currently lacks detailed documentation. For guidance, you may refer to the equivalent functionality in [Microsoft Excel documentation](https://support.microsoft.com/en-us/office/excel-functions-by-category-5f91f4e9-7b42-46d2-9bd1-63f26a86c0eb). ::: \ No newline at end of file