Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions base/src/expressions/parser/static_analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,7 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Hlookup => 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),
Expand Down Expand Up @@ -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),
Expand Down
96 changes: 96 additions & 0 deletions base/src/functions/lookup_and_reference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,102 @@ 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 !(1..=LAST_ROW).contains(&row) || !(1..=LAST_COLUMN).contains(&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 !(1..=4).contains(&abs_num) {
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 = 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}"),
_ => 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.
Expand Down
7 changes: 6 additions & 1 deletion base/src/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ pub enum Function {
Type,

// Lookup and reference
Address,
Hlookup,
Index,
Indirect,
Expand Down Expand Up @@ -253,7 +254,7 @@ pub enum Function {
}

impl Function {
pub fn into_iter() -> IntoIter<Function, 198> {
pub fn into_iter() -> IntoIter<Function, 199> {
[
Function::And,
Function::False,
Expand Down Expand Up @@ -303,6 +304,7 @@ impl Function {
Function::Columns,
Function::Index,
Function::Indirect,
Function::Address,
Function::Hlookup,
Function::Lookup,
Function::Match,
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions base/src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
138 changes: 138 additions & 0 deletions base/src/test/test_fn_address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#![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!");
}

// 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!");
}
3 changes: 1 addition & 2 deletions docs/src/functions/lookup_and_reference/address.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
:::