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
4 changes: 4 additions & 0 deletions base/src/expressions/parser/static_analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,8 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec<Signatu
Function::Len => args_signature_scalars(arg_count, 1, 0),
Function::Lower => args_signature_scalars(arg_count, 1, 0),
Function::Mid => args_signature_scalars(arg_count, 3, 0),
Function::Proper => args_signature_scalars(arg_count, 1, 0),
Function::Replace => args_signature_scalars(arg_count, 4, 0),
Function::Rept => args_signature_scalars(arg_count, 2, 0),
Function::Right => args_signature_scalars(arg_count, 2, 1),
Function::Search => args_signature_scalars(arg_count, 2, 1),
Expand Down Expand Up @@ -870,6 +872,8 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult {
Function::Len => not_implemented(args),
Function::Lower => not_implemented(args),
Function::Mid => not_implemented(args),
Function::Proper => not_implemented(args),
Function::Replace => not_implemented(args),
Function::Rept => not_implemented(args),
Function::Right => not_implemented(args),
Function::Search => not_implemented(args),
Expand Down
12 changes: 11 additions & 1 deletion base/src/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ pub enum Function {
Len,
Lower,
Mid,
Proper,
Replace,
Rept,
Right,
Search,
Expand Down Expand Up @@ -253,7 +255,7 @@ pub enum Function {
}

impl Function {
pub fn into_iter() -> IntoIter<Function, 198> {
pub fn into_iter() -> IntoIter<Function, 200> {
[
Function::And,
Function::False,
Expand Down Expand Up @@ -322,6 +324,8 @@ impl Function {
Function::Len,
Function::Lower,
Function::Mid,
Function::Proper,
Function::Replace,
Function::Right,
Function::Search,
Function::Text,
Expand Down Expand Up @@ -582,6 +586,8 @@ impl Function {
"LEN" => Some(Function::Len),
"LOWER" => Some(Function::Lower),
"MID" => Some(Function::Mid),
"PROPER" => Some(Function::Proper),
"REPLACE" => Some(Function::Replace),
"RIGHT" => Some(Function::Right),
"SEARCH" => Some(Function::Search),
"TEXT" => Some(Function::Text),
Expand Down Expand Up @@ -800,6 +806,8 @@ impl fmt::Display for Function {
Function::Len => write!(f, "LEN"),
Function::Lower => write!(f, "LOWER"),
Function::Mid => write!(f, "MID"),
Function::Proper => write!(f, "PROPER"),
Function::Replace => write!(f, "REPLACE"),
Function::Right => write!(f, "RIGHT"),
Function::Search => write!(f, "SEARCH"),
Function::Text => write!(f, "TEXT"),
Expand Down Expand Up @@ -1039,6 +1047,8 @@ impl Model {
Function::Len => self.fn_len(args, cell),
Function::Lower => self.fn_lower(args, cell),
Function::Mid => self.fn_mid(args, cell),
Function::Proper => self.fn_proper(args, cell),
Function::Replace => self.fn_replace(args, cell),
Function::Right => self.fn_right(args, cell),
Function::Search => self.fn_search(args, cell),
Function::Text => self.fn_text(args, cell),
Expand Down
101 changes: 101 additions & 0 deletions base/src/functions/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,59 @@ impl Model {
CalcResult::new_args_number_error(cell)
}

pub(crate) fn fn_proper(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 1 {
let text = match self.evaluate_node_in_context(&args[0], cell) {
CalcResult::Number(v) => format!("{v}"),
CalcResult::String(v) => v,
CalcResult::Boolean(b) => {
if b {
"TRUE".to_string()
} else {
"FALSE".to_string()
}
}
error @ CalcResult::Error { .. } => return error,
CalcResult::Range { .. } => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Implicit Intersection not implemented".to_string(),
};
}
CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(),
CalcResult::Array(_) => {
return CalcResult::Error {
error: Error::NIMPL,
origin: cell,
message: "Arrays not supported yet".to_string(),
}
}
};
let mut result = String::new();
let mut start_word = true;
for ch in text.chars() {
if ch.is_alphabetic() {
if start_word {
for c in ch.to_uppercase() {
result.push(c);
}
} else {
for c in ch.to_lowercase() {
result.push(c);
}
}
start_word = false;
} else {
result.push(ch);
start_word = true;
}
}
return CalcResult::String(result);
}
CalcResult::new_args_number_error(cell)
}

pub(crate) fn fn_unicode(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() == 1 {
let s = match self.evaluate_node_in_context(&args[0], cell) {
Expand Down Expand Up @@ -752,6 +805,54 @@ impl Model {
CalcResult::String(result)
}

pub(crate) fn fn_replace(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 4 {
return CalcResult::new_args_number_error(cell);
}
let old_text = match self.get_string(&args[0], cell) {
Ok(s) => s,
Err(e) => return e,
};
let start_num = match self.get_number(&args[1], cell) {
Ok(f) => f,
Err(e) => return e,
};
let num_chars = match self.get_number(&args[2], cell) {
Ok(f) => f,
Err(e) => return e,
};
let new_text = match self.get_string(&args[3], cell) {
Ok(s) => s,
Err(e) => return e,
};
if start_num < 1.0 || num_chars < 0.0 {
return CalcResult::Error {
error: Error::VALUE,
origin: cell,
message: "Invalid value".to_string(),
};
}
let start_index = start_num.floor() as usize - 1;
let num = num_chars.floor() as usize;
fn byte_index_from_char(s: &str, idx: usize) -> usize {
for (count, (b, _)) in s.char_indices().enumerate() {
if count == idx {
return b;
}
}
s.len()
}
let start_byte = byte_index_from_char(&old_text, start_index);
let end_byte = byte_index_from_char(&old_text, start_index.saturating_add(num));
let result = format!(
"{}{}{}",
&old_text[..start_byte],
new_text,
&old_text[end_byte..]
);
CalcResult::String(result)
}

// REPT(text, number_times)
pub(crate) fn fn_rept(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult {
if args.len() != 2 {
Expand Down
2 changes: 2 additions & 0 deletions base/src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ mod test_fn_maxifs;
mod test_fn_minifs;
mod test_fn_or_xor;
mod test_fn_product;
mod test_fn_proper;
mod test_fn_replace;
mod test_fn_rept;
mod test_fn_sum;
mod test_fn_sumifs;
Expand Down
45 changes: 45 additions & 0 deletions base/src/test/test_fn_proper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#![allow(clippy::unwrap_used)]

use crate::test::util::new_empty_model;

#[test]
fn fn_proper_args_number() {
let mut model = new_empty_model();

// No arguments
model._set("A1", "=PROPER()");
// Too many arguments
model._set("A2", "=PROPER(\"text\", \"extra\")");

model.evaluate();

assert_eq!(model._get_text("A1"), *"#ERROR!");
assert_eq!(model._get_text("A2"), *"#ERROR!");
}

#[test]
fn fn_proper_basic() {
let mut model = new_empty_model();
model._set("A1", "one TWO");

model._set("B1", "=PROPER(A1)");
model._set("B2", "=PROPER(\"mcdonald\")");

model.evaluate();

assert_eq!(model._get_text("B1"), *"One Two");
assert_eq!(model._get_text("B2"), *"Mcdonald");
}

#[test]
fn fn_proper_punctuation() {
let mut model = new_empty_model();

model._set("A1", "=PROPER(\"o'reilly\")");
model._set("A2", "=PROPER(\"smith-jones\")");

model.evaluate();

assert_eq!(model._get_text("A1"), *"O'Reilly");
assert_eq!(model._get_text("A2"), *"Smith-Jones");
}
61 changes: 61 additions & 0 deletions base/src/test/test_fn_replace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#![allow(clippy::unwrap_used)]

use crate::test::util::new_empty_model;

#[test]
fn fn_replace_args_number() {
let mut model = new_empty_model();
model._set("A1", "abcdef");

// Too few arguments
model._set("B1", "=REPLACE(A1, 2, 3)");
// Too many arguments
model._set("B2", "=REPLACE(A1, 2, 3, \"X\", \"Y\")");

model.evaluate();

assert_eq!(model._get_text("B1"), *"#ERROR!");
assert_eq!(model._get_text("B2"), *"#ERROR!");
}

#[test]
fn fn_replace_basic() {
let mut model = new_empty_model();
model._set("A1", "abcdef");

model._set("B1", "=REPLACE(A1, 2, 3, \"XYZ\")");
model._set("B2", "=REPLACE(\"12345\", 1, 1, \"abc\")");

model.evaluate();

assert_eq!(model._get_text("B1"), *"aXYZef");
assert_eq!(model._get_text("B2"), *"abc2345");
}

#[test]
fn fn_replace_invalid_values() {
let mut model = new_empty_model();
model._set("A1", "abcdef");

// start_num less than 1
model._set("C1", "=REPLACE(A1, 0, 2, \"x\")");
// num_chars negative
model._set("C2", "=REPLACE(A1, 2, -1, \"x\")");

model.evaluate();

assert_eq!(model._get_text("C1"), *"#VALUE!");
assert_eq!(model._get_text("C2"), *"#VALUE!");
}

#[test]
fn fn_replace_start_beyond_length() {
let mut model = new_empty_model();

// start_num is greater than the length of old_text → new_text should be appended
model._set("A1", "=REPLACE(\"abc\", 5, 0, \"X\")");

model.evaluate();

assert_eq!(model._get_text("A1"), *"abcX");
}
4 changes: 2 additions & 2 deletions docs/src/functions/text.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir
| MIDB | <Badge type="info" text="Not implemented yet" /> | – |
| NUMBERVALUE | <Badge type="info" text="Not implemented yet" /> | – |
| PHONETIC | <Badge type="info" text="Not implemented yet" /> | – |
| PROPER | <Badge type="info" text="Not implemented yet" /> | – |
| REPLACE | <Badge type="info" text="Not implemented yet" /> | – |
| PROPER | <Badge type="tip" text="Available" /> | – |
| REPLACE | <Badge type="tip" text="Available" /> | – |
| REPLACEB | <Badge type="info" text="Not implemented yet" /> | – |
| REPT | <Badge type="tip" text="Available" /> | – |
| RIGHT | <Badge type="tip" text="Available" /> | – |
Expand Down
3 changes: 1 addition & 2 deletions docs/src/functions/text/proper.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,5 @@ lang: en-US
# PROPER

::: 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).
:::
3 changes: 1 addition & 2 deletions docs/src/functions/text/replace.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,5 @@ lang: en-US
# REPLACE

::: 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).
:::