diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index 80f194360..058d0a475 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -785,6 +785,7 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec args_signature_scalars(arg_count, 1, 0), Function::Unicode => args_signature_scalars(arg_count, 1, 0), Function::Geomean => vec![Signature::Vector; arg_count], + Function::Intercept | Function::Slope => vec![Signature::Vector; arg_count], } } @@ -990,5 +991,6 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult { Function::Eomonth => scalar_arguments(args), Function::Formulatext => not_implemented(args), Function::Geomean => not_implemented(args), + Function::Intercept | Function::Slope => scalar_arguments(args), } } diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs index 21c8f72da..578145a60 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -145,6 +145,8 @@ pub enum Function { Maxifs, Minifs, Geomean, + Intercept, + Slope, // Date and time Date, @@ -253,7 +255,7 @@ pub enum Function { } impl Function { - pub fn into_iter() -> IntoIter { + pub fn into_iter() -> IntoIter { [ Function::And, Function::False, @@ -357,6 +359,8 @@ impl Function { Function::Maxifs, Function::Minifs, Function::Geomean, + Function::Intercept, + Function::Slope, Function::Year, Function::Day, Function::Month, @@ -625,6 +629,8 @@ impl Function { "MAXIFS" | "_XLFN.MAXIFS" => Some(Function::Maxifs), "MINIFS" | "_XLFN.MINIFS" => Some(Function::Minifs), "GEOMEAN" => Some(Function::Geomean), + "INTERCEPT" => Some(Function::Intercept), + "SLOPE" => Some(Function::Slope), // Date and Time "YEAR" => Some(Function::Year), "DAY" => Some(Function::Day), @@ -836,6 +842,8 @@ impl fmt::Display for Function { Function::Maxifs => write!(f, "MAXIFS"), Function::Minifs => write!(f, "MINIFS"), Function::Geomean => write!(f, "GEOMEAN"), + Function::Intercept => write!(f, "INTERCEPT"), + Function::Slope => write!(f, "SLOPE"), Function::Year => write!(f, "YEAR"), Function::Day => write!(f, "DAY"), Function::Month => write!(f, "MONTH"), @@ -1076,6 +1084,8 @@ impl Model { Function::Maxifs => self.fn_maxifs(args, cell), Function::Minifs => self.fn_minifs(args, cell), Function::Geomean => self.fn_geomean(args, cell), + Function::Intercept => self.fn_intercept(args, cell), + Function::Slope => self.fn_slope(args, cell), // Date and Time Function::Year => self.fn_year(args, cell), Function::Day => self.fn_day(args, cell), diff --git a/base/src/functions/statistical.rs b/base/src/functions/statistical.rs index cdb936406..311cf3d3d 100644 --- a/base/src/functions/statistical.rs +++ b/base/src/functions/statistical.rs @@ -7,7 +7,7 @@ use crate::{ model::Model, }; -use super::util::build_criteria; +use super::util::{build_criteria, collect_series}; impl Model { pub(crate) fn fn_average(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { @@ -730,4 +730,107 @@ impl Model { } CalcResult::Number(product.powf(1.0 / count)) } + + // collect_series method moved to functions::util::collect_series + + pub(crate) fn fn_slope(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() != 2 { + return CalcResult::new_args_number_error(cell); + } + let ys = match collect_series(self, &args[0], cell) { + Ok(v) => v, + Err(e) => return e, + }; + let xs = match collect_series(self, &args[1], cell) { + Ok(v) => v, + Err(e) => return e, + }; + if ys.len() != xs.len() { + return CalcResult::new_error( + Error::NA, + cell, + "Ranges have different lengths".to_string(), + ); + } + let mut pairs = Vec::new(); + let mut sum_x = 0.0; + let mut sum_y = 0.0; + let mut n = 0.0; + for (y, x) in ys.iter().zip(xs.iter()) { + if let (Some(yy), Some(xx)) = (y, x) { + pairs.push((*yy, *xx)); + sum_x += xx; + sum_y += yy; + n += 1.0; + } + } + if n == 0.0 { + return CalcResult::new_error(Error::DIV, cell, "Division by Zero".to_string()); + } + let mean_x = sum_x / n; + let mean_y = sum_y / n; + let mut numerator = 0.0; + let mut denominator = 0.0; + for (yy, xx) in pairs { + let dx = xx - mean_x; + let dy = yy - mean_y; + numerator += dx * dy; + denominator += dx * dx; + } + if denominator == 0.0 { + return CalcResult::new_error(Error::DIV, cell, "Division by Zero".to_string()); + } + CalcResult::Number(numerator / denominator) + } + + pub(crate) fn fn_intercept(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() != 2 { + return CalcResult::new_args_number_error(cell); + } + let ys = match collect_series(self, &args[0], cell) { + Ok(v) => v, + Err(e) => return e, + }; + let xs = match collect_series(self, &args[1], cell) { + Ok(v) => v, + Err(e) => return e, + }; + if ys.len() != xs.len() { + return CalcResult::new_error( + Error::NA, + cell, + "Ranges have different lengths".to_string(), + ); + } + let mut pairs = Vec::new(); + let mut sum_x = 0.0; + let mut sum_y = 0.0; + let mut n = 0.0; + for (y, x) in ys.iter().zip(xs.iter()) { + if let (Some(yy), Some(xx)) = (y, x) { + pairs.push((*yy, *xx)); + sum_x += xx; + sum_y += yy; + n += 1.0; + } + } + if n == 0.0 { + return CalcResult::new_error(Error::DIV, cell, "Division by Zero".to_string()); + } + let mean_x = sum_x / n; + let mean_y = sum_y / n; + let mut numerator = 0.0; + let mut denominator = 0.0; + for (yy, xx) in pairs { + let dx = xx - mean_x; + let dy = yy - mean_y; + numerator += dx * dy; + denominator += dx * dx; + } + if denominator == 0.0 { + return CalcResult::new_error(Error::DIV, cell, "Division by Zero".to_string()); + } + let slope = numerator / denominator; + CalcResult::Number(mean_y - slope * mean_x) + } } diff --git a/base/src/functions/util.rs b/base/src/functions/util.rs index dea96e843..161f7a437 100644 --- a/base/src/functions/util.rs +++ b/base/src/functions/util.rs @@ -1,6 +1,10 @@ #[cfg(feature = "use_regex_lite")] use regex_lite as regex; +use crate::expressions::parser::{ArrayNode, Node}; +use crate::expressions::token::Error; +use crate::expressions::types::CellReferenceIndex; +use crate::model::Model; use crate::{calc_result::CalcResult, expressions::token::is_english_error_string}; /// This test for exact match (modulo case). @@ -398,3 +402,109 @@ pub(crate) fn build_criteria<'a>(value: &'a CalcResult) -> Box Box::new(result_is_equal_to_empty), } } + +/// Collect a numeric series preserving positional information. +/// +/// Given a single argument (range, reference, literal, or array), returns a +/// vector with the same length as the flattened input. Each position contains +/// `Some(f64)` when the corresponding element is numeric and `None` when it is +/// non-numeric or empty. Errors are propagated immediately. +/// +/// Behaviour mirrors Excel's rules used by paired-data statistical functions +/// (SLOPE, INTERCEPT, CORREL, etc.): +/// - Booleans/string literals are coerced to numbers, literals coming from +/// references are ignored. +/// - Non-numeric cells become `None`, keeping the alignment between two series. +/// - Ranges crossing sheets cause a `#VALUE!` error. +pub(crate) fn collect_series( + model: &mut Model, + node: &Node, + cell: CellReferenceIndex, +) -> Result>, CalcResult> { + let is_reference = matches!( + node, + Node::ReferenceKind { .. } | Node::RangeKind { .. } | Node::OpRangeKind { .. } + ); + + match model.evaluate_node_in_context(node, cell) { + CalcResult::Number(v) => Ok(vec![Some(v)]), + CalcResult::Boolean(b) => { + if is_reference { + Ok(vec![None]) + } else { + Ok(vec![Some(if b { 1.0 } else { 0.0 })]) + } + } + CalcResult::String(s) => { + if is_reference { + Ok(vec![None]) + } else if let Ok(v) = s.parse::() { + Ok(vec![Some(v)]) + } else { + Err(CalcResult::new_error( + Error::VALUE, + cell, + "Argument cannot be cast into number".to_string(), + )) + } + } + CalcResult::Range { left, right } => { + if left.sheet != right.sheet { + return Err(CalcResult::new_error( + Error::VALUE, + cell, + "Ranges are in different sheets".to_string(), + )); + } + let mut values = Vec::new(); + for row in left.row..=right.row { + for column in left.column..=right.column { + let cell_result = model.evaluate_cell(CellReferenceIndex { + sheet: left.sheet, + row, + column, + }); + match cell_result { + CalcResult::Number(n) => values.push(Some(n)), + error @ CalcResult::Error { .. } => { + return Err(error); + } + _ => values.push(None), + } + } + } + Ok(values) + } + CalcResult::Array(arr) => { + let mut values = Vec::new(); + for row in arr { + for val in row { + match val { + ArrayNode::Number(n) => values.push(Some(n)), + ArrayNode::Boolean(b) => values.push(Some(if b { 1.0 } else { 0.0 })), + ArrayNode::String(s) => match s.parse::() { + Ok(v) => values.push(Some(v)), + Err(_) => { + return Err(CalcResult::new_error( + Error::VALUE, + cell, + "Argument cannot be cast into number".to_string(), + )) + } + }, + ArrayNode::Error(e) => { + return Err(CalcResult::Error { + error: e, + origin: cell, + message: "Error in array".to_string(), + }) + } + } + } + } + Ok(values) + } + CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(vec![None]), + error @ CalcResult::Error { .. } => Err(error), + } +} diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index a0a0d69d6..ca7417c90 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -55,6 +55,7 @@ mod test_arrays; mod test_escape_quotes; mod test_extend; mod test_fn_fv; +mod test_fn_slope_intercept; mod test_fn_type; mod test_frozen_rows_and_columns; mod test_geomean; diff --git a/base/src/test/test_fn_slope_intercept.rs b/base/src/test/test_fn_slope_intercept.rs new file mode 100644 index 000000000..1f106b9b9 --- /dev/null +++ b/base/src/test/test_fn_slope_intercept.rs @@ -0,0 +1,369 @@ +#![allow(clippy::unwrap_used)] +#![allow(clippy::panic)] + +use crate::test::util::new_empty_model; + +// ============================================================================= +// TEST CONSTANTS +// ============================================================================= + +const EXACT_TOLERANCE: f64 = 1e-10; +const STANDARD_TOLERANCE: f64 = 1e-9; +const HIGH_PRECISION_TOLERANCE: f64 = 1e-15; +const STABILITY_TOLERANCE: f64 = 1e-6; + +// ============================================================================= +// TEST HELPER FUNCTIONS +// ============================================================================= + +fn assert_approx_eq(actual: &str, expected: f64, tolerance: f64) { + let actual_val: f64 = actual + .parse() + .unwrap_or_else(|_| panic!("Failed to parse result as number: {actual}")); + assert!( + (actual_val - expected).abs() < tolerance, + "Expected ~{expected}, got {actual}" + ); +} + +fn assert_slope_intercept_eq( + model: &crate::Model, + slope_cell: &str, + intercept_cell: &str, + expected_slope: f64, + expected_intercept: f64, + tolerance: f64, +) { + assert_approx_eq(&model._get_text(slope_cell), expected_slope, tolerance); + assert_approx_eq( + &model._get_text(intercept_cell), + expected_intercept, + tolerance, + ); +} + +fn assert_slope_intercept_error( + model: &crate::Model, + slope_cell: &str, + intercept_cell: &str, + expected_error: &str, +) { + assert_eq!(model._get_text(slope_cell), *expected_error); + assert_eq!(model._get_text(intercept_cell), *expected_error); +} + +fn set_linear_data(model: &mut crate::Model, slope: f64, intercept: f64, x_values: &[f64]) { + for (i, &x) in x_values.iter().enumerate() { + let y = slope * x + intercept; + model._set(&format!("B{}", i + 1), &y.to_string()); + model._set(&format!("C{}", i + 1), &x.to_string()); + } +} + +// ============================================================================= +// ARGUMENT VALIDATION TESTS +// ============================================================================= + +#[test] +fn test_slope_intercept_invalid_args() { + let mut model = new_empty_model(); + + // Wrong argument counts + model._set("A1", "=SLOPE()"); + model._set("A2", "=SLOPE(B1:B3)"); + model._set("A3", "=INTERCEPT()"); + model._set("A4", "=INTERCEPT(B1:B3)"); + model._set("A5", "=SLOPE(B1:B3, C1:C3, D1:D3)"); + model._set("A6", "=INTERCEPT(B1:B3, C1:C3, D1:D3)"); + + // Mismatched range sizes + model._set("B1", "1"); + model._set("B2", "2"); + model._set("B3", "3"); + model._set("C1", "10"); + model._set("C2", "20"); + model._set("A7", "=SLOPE(B1:B3, C1:C2)"); + model._set("A8", "=INTERCEPT(B1:B3, C1:C2)"); + + // Direct invalid types + model._set("A9", "=SLOPE(1;TRUE;3, 2;FALSE;6)"); + model._set("A10", "=INTERCEPT(\"1\";\"2\";\"3\", \"2\";\"4\";\"6\")"); + + model.evaluate(); + + // All should error appropriately + for cell in ["A1", "A2", "A3", "A4", "A5", "A6", "A9", "A10"] { + assert_eq!(model._get_text(cell), "#ERROR!"); + } + assert_slope_intercept_error(&model, "A7", "A8", "#N/A"); +} + +// ============================================================================= +// CORE MATHEMATICAL FUNCTIONALITY TESTS +// ============================================================================= + +#[test] +fn test_slope_intercept_perfect_lines() { + let mut model = new_empty_model(); + + // Test 1: Positive slope through origin (y = 3x) + set_linear_data(&mut model, 3.0, 0.0, &[1.0, 2.0, 3.0, 4.0]); + model._set("A1", "=SLOPE(B1:B4, C1:C4)"); + model._set("A2", "=INTERCEPT(B1:B4, C1:C4)"); + + // Test 2: Negative slope with intercept (y = -2x + 10) + model._set("B5", "8"); + model._set("B6", "6"); + model._set("B7", "4"); + model._set("B8", "2"); + model._set("C5", "1"); + model._set("C6", "2"); + model._set("C7", "3"); + model._set("C8", "4"); + model._set("A3", "=SLOPE(B5:B8, C5:C8)"); + model._set("A4", "=INTERCEPT(B5:B8, C5:C8)"); + + // Test 3: Zero slope (y = 7) + model._set("B9", "7"); + model._set("B10", "7"); + model._set("B11", "7"); + model._set("C9", "10"); + model._set("C10", "20"); + model._set("C11", "30"); + model._set("A5", "=SLOPE(B9:B11, C9:C11)"); + model._set("A6", "=INTERCEPT(B9:B11, C9:C11)"); + + model.evaluate(); + + assert_slope_intercept_eq(&model, "A1", "A2", 3.0, 0.0, EXACT_TOLERANCE); + assert_slope_intercept_eq(&model, "A3", "A4", -2.0, 10.0, EXACT_TOLERANCE); + assert_slope_intercept_eq(&model, "A5", "A6", 0.0, 7.0, EXACT_TOLERANCE); +} + +#[test] +fn test_slope_intercept_regression() { + let mut model = new_empty_model(); + + // Non-perfect data: (1,1), (2,2), (5,3) + // Manual calculation: slope = 6/13, intercept = 10/13 + model._set("B1", "1"); + model._set("B2", "2"); + model._set("B3", "3"); + model._set("C1", "1"); + model._set("C2", "2"); + model._set("C3", "5"); + model._set("A1", "=SLOPE(B1:B3, C1:C3)"); + model._set("A2", "=INTERCEPT(B1:B3, C1:C3)"); + + model.evaluate(); + + assert_slope_intercept_eq( + &model, + "A1", + "A2", + 0.461538462, + 0.769230769, + STANDARD_TOLERANCE, + ); +} + +// ============================================================================= +// DEGENERATE CASES +// ============================================================================= + +#[test] +fn test_slope_intercept_insufficient_data() { + let mut model = new_empty_model(); + + // Single data point + model._set("B1", "5"); + model._set("C1", "10"); + model._set("A1", "=SLOPE(B1, C1)"); + model._set("A2", "=INTERCEPT(B1, C1)"); + + // Empty ranges + model._set("A3", "=SLOPE(B5:B7, C5:C7)"); + model._set("A4", "=INTERCEPT(B5:B7, C5:C7)"); + + // Identical x values (vertical line) + model._set("B8", "1"); + model._set("B9", "2"); + model._set("B10", "3"); + model._set("C8", "5"); + model._set("C9", "5"); + model._set("C10", "5"); + model._set("A5", "=SLOPE(B8:B10, C8:C10)"); + model._set("A6", "=INTERCEPT(B8:B10, C8:C10)"); + + model.evaluate(); + + assert_slope_intercept_error(&model, "A1", "A2", "#DIV/0!"); + assert_slope_intercept_error(&model, "A3", "A4", "#DIV/0!"); + assert_slope_intercept_error(&model, "A5", "A6", "#DIV/0!"); +} + +// ============================================================================= +// DATA FILTERING AND ERROR PROPAGATION +// ============================================================================= + +#[test] +fn test_slope_intercept_data_filtering() { + let mut model = new_empty_model(); + + // Mixed data types - only numeric pairs used: (1,1), (5,2), (9,3) -> y = 4x - 3 + model._set("B1", "1"); // Valid + model._set("B2", ""); // Empty - ignored + model._set("B3", "text"); // Text - ignored + model._set("B4", "5"); // Valid + model._set("B5", "TRUE"); // Boolean - ignored + model._set("B6", "9"); // Valid + model._set("C1", "1"); // Valid + model._set("C2", ""); // Empty - ignored + model._set("C3", "text"); // Text - ignored + model._set("C4", "2"); // Valid + model._set("C5", "FALSE"); // Boolean - ignored + model._set("C6", "3"); // Valid + + model._set("A1", "=SLOPE(B1:B6, C1:C6)"); + model._set("A2", "=INTERCEPT(B1:B6, C1:C6)"); + + model.evaluate(); + + assert_slope_intercept_eq(&model, "A1", "A2", 4.0, -3.0, EXACT_TOLERANCE); +} + +#[test] +fn test_slope_intercept_error_propagation() { + let mut model = new_empty_model(); + + // Error in y values + model._set("B1", "1"); + model._set("B2", "=1/0"); // Division by zero + model._set("B3", "3"); + model._set("C1", "1"); + model._set("C2", "2"); + model._set("C3", "3"); + model._set("A1", "=SLOPE(B1:B3, C1:C3)"); + model._set("A2", "=INTERCEPT(B1:B3, C1:C3)"); + + // Error in x values + model._set("B4", "1"); + model._set("B5", "2"); + model._set("B6", "3"); + model._set("C4", "1"); + model._set("C5", "=SQRT(-1)"); // NaN error + model._set("C6", "3"); + model._set("A3", "=SLOPE(B4:B6, C4:C6)"); + model._set("A4", "=INTERCEPT(B4:B6, C4:C6)"); + + model.evaluate(); + + assert_slope_intercept_error(&model, "A1", "A2", "#DIV/0!"); + assert_slope_intercept_error(&model, "A3", "A4", "#NUM!"); +} + +// ============================================================================= +// NUMERICAL PRECISION AND EXTREMES +// ============================================================================= + +#[test] +fn test_slope_intercept_numeric_precision() { + let mut model = new_empty_model(); + + // Very small slope near machine epsilon + model._set("B1", "5.0001"); + model._set("B2", "5.0002"); + model._set("B3", "5.0003"); + model._set("C1", "1"); + model._set("C2", "2"); + model._set("C3", "3"); + model._set("A1", "=SLOPE(B1:B3, C1:C3)"); + model._set("A2", "=INTERCEPT(B1:B3, C1:C3)"); + + // Large numbers with stability concerns + model._set("B4", "1000000"); + model._set("B5", "3000000"); + model._set("B6", "5000000"); + model._set("C4", "1000"); + model._set("C5", "2000"); + model._set("C6", "3000"); + model._set("A3", "=SLOPE(B4:B6, C4:C6)"); + model._set("A4", "=INTERCEPT(B4:B6, C4:C6)"); + + model.evaluate(); + + assert_slope_intercept_eq(&model, "A1", "A2", 0.0001, 5.0, HIGH_PRECISION_TOLERANCE); + assert_slope_intercept_eq(&model, "A3", "A4", 2000.0, -1000000.0, STABILITY_TOLERANCE); +} + +// ============================================================================= +// RANGE ORIENTATIONS AND PERFORMANCE +// ============================================================================= + +#[test] +fn test_slope_intercept_range_orientations() { + let mut model = new_empty_model(); + + // Row-wise ranges: y = 3x - 1 + model._set("B1", "2"); // (1,2) + model._set("C1", "5"); // (2,5) + model._set("D1", "8"); // (3,8) + model._set("B2", "1"); + model._set("C2", "2"); + model._set("D2", "3"); + model._set("A1", "=SLOPE(B1:D1, B2:D2)"); + model._set("A2", "=INTERCEPT(B1:D1, B2:D2)"); + + model.evaluate(); + + assert_slope_intercept_eq(&model, "A1", "A2", 3.0, -1.0, EXACT_TOLERANCE); +} + +#[test] +fn test_slope_intercept_large_dataset() { + let mut model = new_empty_model(); + + // Test with 20 points: y = 0.1x + 100 + for i in 1..=20 { + let y = 0.1 * i as f64 + 100.0; + model._set(&format!("B{i}"), &y.to_string()); + model._set(&format!("C{i}"), &i.to_string()); + } + + model._set("A1", "=SLOPE(B1:B20, C1:C20)"); + model._set("A2", "=INTERCEPT(B1:B20, C1:C20)"); + + model.evaluate(); + + assert_slope_intercept_eq(&model, "A1", "A2", 0.1, 100.0, EXACT_TOLERANCE); +} + +// ============================================================================= +// REAL-WORLD EDGE CASES +// ============================================================================= + +#[test] +fn test_slope_intercept_statistical_outliers() { + let mut model = new_empty_model(); + + // Most points follow y = 2x + 1, with one outlier: (1,3), (2,5), (3,7), (4,9), (5,100) + model._set("B1", "3"); + model._set("B2", "5"); + model._set("B3", "7"); + model._set("B4", "9"); + model._set("B5", "100"); // Statistical outlier + model._set("C1", "1"); + model._set("C2", "2"); + model._set("C3", "3"); + model._set("C4", "4"); + model._set("C5", "5"); + + model._set("A1", "=SLOPE(B1:B5, C1:C5)"); + model._set("A2", "=INTERCEPT(B1:B5, C1:C5)"); + + model.evaluate(); + + // With outlier: mathematically correct results + assert_approx_eq(&model._get_text("A1"), 19.8, STANDARD_TOLERANCE); + assert_approx_eq(&model._get_text("A2"), -34.6, STANDARD_TOLERANCE); +} diff --git a/docs/src/functions/statistical.md b/docs/src/functions/statistical.md index 6842212c3..df463266f 100644 --- a/docs/src/functions/statistical.md +++ b/docs/src/functions/statistical.md @@ -62,7 +62,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir | GROWTH | | – | | HARMEAN | | – | | HYPGEOM.DIST | | – | -| INTERCEPT | | – | +| INTERCEPT | | – | | KURT | | – | | LARGE | | – | | LINEST | | – | @@ -97,7 +97,7 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir | RSQ | | – | | SKEW | | – | | SKEW.P | | – | -| SLOPE | | – | +| SLOPE | | – | | SMALL | | – | | STANDARDIZE | | – | | STDEV.P | | – | diff --git a/docs/src/functions/statistical/intercept.md b/docs/src/functions/statistical/intercept.md index 4d8208842..280583b2d 100644 --- a/docs/src/functions/statistical/intercept.md +++ b/docs/src/functions/statistical/intercept.md @@ -7,6 +7,5 @@ lang: en-US # INTERCEPT ::: 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 diff --git a/docs/src/functions/statistical/slope.md b/docs/src/functions/statistical/slope.md index fc6d59db3..185314ef9 100644 --- a/docs/src/functions/statistical/slope.md +++ b/docs/src/functions/statistical/slope.md @@ -7,6 +7,5 @@ lang: en-US # SLOPE ::: 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