diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index 80f194360..f0ded9441 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -713,6 +713,12 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec args_signature_scalars(arg_count, 3, 2), Function::Npv => args_signature_npv(arg_count), Function::Pduration => args_signature_scalars(arg_count, 3, 0), + Function::Coupdaybs => args_signature_scalars(arg_count, 3, 1), + Function::Coupdays => args_signature_scalars(arg_count, 3, 1), + Function::Coupdaysnc => args_signature_scalars(arg_count, 3, 1), + Function::Coupncd => args_signature_scalars(arg_count, 3, 1), + Function::Coupnum => args_signature_scalars(arg_count, 3, 1), + Function::Couppcd => args_signature_scalars(arg_count, 3, 1), Function::Pmt => args_signature_scalars(arg_count, 3, 2), Function::Ppmt => args_signature_scalars(arg_count, 4, 2), Function::Pv => args_signature_scalars(arg_count, 3, 2), @@ -918,6 +924,12 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult { Function::Nper => not_implemented(args), Function::Npv => not_implemented(args), Function::Pduration => not_implemented(args), + Function::Coupdaybs => not_implemented(args), + Function::Coupdays => not_implemented(args), + Function::Coupdaysnc => not_implemented(args), + Function::Coupncd => not_implemented(args), + Function::Coupnum => not_implemented(args), + Function::Couppcd => not_implemented(args), Function::Pmt => not_implemented(args), Function::Ppmt => not_implemented(args), Function::Pv => not_implemented(args), diff --git a/base/src/functions/financial.rs b/base/src/functions/financial.rs index 8e11ee485..15990cda8 100644 --- a/base/src/functions/financial.rs +++ b/base/src/functions/financial.rs @@ -41,6 +41,78 @@ fn is_less_than_one_year(start_date: i64, end_date: i64) -> Result Ok(end_day <= start_day) } +fn is_leap_year(year: i32) -> bool { + (year % 4 == 0) && (year % 100 != 0 || year % 400 == 0) +} + +fn is_last_day_of_feb(date: chrono::NaiveDate) -> bool { + date.month() == 2 && date.day() == if is_leap_year(date.year()) { 29 } else { 28 } +} + +fn days360_us(start: chrono::NaiveDate, end: chrono::NaiveDate) -> i32 { + let mut d1 = start.day() as i32; + let mut d2 = end.day() as i32; + let m1 = start.month() as i32; + let m2 = end.month() as i32; + let y1 = start.year(); + let y2 = end.year(); + + if d1 == 31 || is_last_day_of_feb(start) { + d1 = 30; + } + if d2 == 31 && (d1 == 30 || d1 == 31) { + d2 = 30; + } + + 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1) +} + +fn days360_eu(start: chrono::NaiveDate, end: chrono::NaiveDate) -> i32 { + let mut d1 = start.day() as i32; + let mut d2 = end.day() as i32; + let m1 = start.month() as i32; + let m2 = end.month() as i32; + let y1 = start.year(); + let y2 = end.year(); + + if d1 == 31 { + d1 = 30; + } + if d2 == 31 { + d2 = 30; + } + + 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1) +} + +fn days_between(start: chrono::NaiveDate, end: chrono::NaiveDate, basis: i32) -> i32 { + match basis { + 0 => days360_us(start, end), + 1 | 2 => (end - start).num_days() as i32, + 3 => (end - start).num_days() as i32, + 4 => days360_eu(start, end), + _ => (end - start).num_days() as i32, + } +} + +fn coupon_dates( + settlement: chrono::NaiveDate, + maturity: chrono::NaiveDate, + freq: i32, +) -> (chrono::NaiveDate, chrono::NaiveDate) { + let months = 12 / freq; + let step = chrono::Months::new(months as u32); + let mut ncd = maturity; + while let Some(prev) = ncd.checked_sub_months(step) { + if settlement >= prev { + return (prev, ncd); + } + ncd = prev; + } + // Fallback if we somehow exit the loop (shouldn't happen in practice) + (settlement, maturity) +} + fn compute_payment( rate: f64, nper: f64, @@ -1515,6 +1587,316 @@ impl Model { CalcResult::Number(result) } + // COUPDAYBS(settlement, maturity, frequency, [basis]) + pub(crate) fn fn_coupdaybs(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() < 3 || args.len() > 4 { + return CalcResult::new_args_number_error(cell); + } + let settlement = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f.trunc() as i64, + Err(s) => return s, + }; + let maturity = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f.trunc() as i64, + Err(s) => return s, + }; + let frequency = match self.get_number_no_bools(&args[2], cell) { + Ok(f) => f.trunc() as i32, + Err(s) => return s, + }; + let basis = if args.len() > 3 { + match self.get_number_no_bools(&args[3], cell) { + Ok(f) => f.trunc() as i32, + Err(s) => return s, + } + } else { + 0 + }; + + if ![1, 2, 4].contains(&frequency) || !(0..=4).contains(&basis) { + return CalcResult::new_error(Error::NUM, cell, "invalid arguments".to_string()); + } + if settlement >= maturity { + return CalcResult::new_error(Error::NUM, cell, "settlement < maturity".to_string()); + } + + let settlement_date = match from_excel_date(settlement) { + Ok(d) => d, + Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), + }; + let maturity_date = match from_excel_date(maturity) { + Ok(d) => d, + Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), + }; + + let (pcd, _) = coupon_dates(settlement_date, maturity_date, frequency); + let days = days_between(pcd, settlement_date, basis); + CalcResult::Number(days as f64) + } + + // COUPDAYS(settlement, maturity, frequency, [basis]) + pub(crate) fn fn_coupdays(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() < 3 || args.len() > 4 { + return CalcResult::new_args_number_error(cell); + } + let settlement = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f.trunc() as i64, + Err(s) => return s, + }; + let maturity = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f.trunc() as i64, + Err(s) => return s, + }; + let frequency = match self.get_number_no_bools(&args[2], cell) { + Ok(f) => f.trunc() as i32, + Err(s) => return s, + }; + let basis = if args.len() > 3 { + match self.get_number_no_bools(&args[3], cell) { + Ok(f) => f.trunc() as i32, + Err(s) => return s, + } + } else { + 0 + }; + + if ![1, 2, 4].contains(&frequency) || !(0..=4).contains(&basis) { + return CalcResult::new_error(Error::NUM, cell, "invalid arguments".to_string()); + } + if settlement >= maturity { + return CalcResult::new_error(Error::NUM, cell, "settlement < maturity".to_string()); + } + + let settlement_date = match from_excel_date(settlement) { + Ok(d) => d, + Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), + }; + let maturity_date = match from_excel_date(maturity) { + Ok(d) => d, + Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), + }; + + let (pcd, ncd) = coupon_dates(settlement_date, maturity_date, frequency); + let days = match basis { + 0 | 4 => 360 / frequency, // 30/360 conventions + _ => days_between(pcd, ncd, basis), // Actual day counts + }; + CalcResult::Number(days as f64) + } + + // COUPDAYSNC(settlement, maturity, frequency, [basis]) + pub(crate) fn fn_coupdaysnc(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() < 3 || args.len() > 4 { + return CalcResult::new_args_number_error(cell); + } + let settlement = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f.trunc() as i64, + Err(s) => return s, + }; + let maturity = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f.trunc() as i64, + Err(s) => return s, + }; + let frequency = match self.get_number_no_bools(&args[2], cell) { + Ok(f) => f.trunc() as i32, + Err(s) => return s, + }; + let basis = if args.len() > 3 { + match self.get_number_no_bools(&args[3], cell) { + Ok(f) => f.trunc() as i32, + Err(s) => return s, + } + } else { + 0 + }; + + if ![1, 2, 4].contains(&frequency) || !(0..=4).contains(&basis) { + return CalcResult::new_error(Error::NUM, cell, "invalid arguments".to_string()); + } + if settlement >= maturity { + return CalcResult::new_error(Error::NUM, cell, "settlement < maturity".to_string()); + } + + let settlement_date = match from_excel_date(settlement) { + Ok(d) => d, + Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), + }; + let maturity_date = match from_excel_date(maturity) { + Ok(d) => d, + Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), + }; + + let (_, ncd) = coupon_dates(settlement_date, maturity_date, frequency); + let days = days_between(settlement_date, ncd, basis); + CalcResult::Number(days as f64) + } + + // COUPNCD(settlement, maturity, frequency, [basis]) + pub(crate) fn fn_coupncd(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() < 3 || args.len() > 4 { + return CalcResult::new_args_number_error(cell); + } + let settlement = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f.trunc() as i64, + Err(s) => return s, + }; + let maturity = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f.trunc() as i64, + Err(s) => return s, + }; + let frequency = match self.get_number_no_bools(&args[2], cell) { + Ok(f) => f.trunc() as i32, + Err(s) => return s, + }; + let basis = if args.len() > 3 { + match self.get_number_no_bools(&args[3], cell) { + Ok(f) => f.trunc() as i32, + Err(s) => return s, + } + } else { + 0 + }; + + if ![1, 2, 4].contains(&frequency) || !(0..=4).contains(&basis) { + return CalcResult::new_error(Error::NUM, cell, "invalid arguments".to_string()); + } + if settlement >= maturity { + return CalcResult::new_error(Error::NUM, cell, "settlement < maturity".to_string()); + } + + let settlement_date = match from_excel_date(settlement) { + Ok(d) => d, + Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), + }; + let maturity_date = match from_excel_date(maturity) { + Ok(d) => d, + Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), + }; + + let (_, ncd) = coupon_dates(settlement_date, maturity_date, frequency); + match crate::formatter::dates::date_to_serial_number(ncd.day(), ncd.month(), ncd.year()) { + Ok(n) => { + if !(MINIMUM_DATE_SERIAL_NUMBER..=MAXIMUM_DATE_SERIAL_NUMBER).contains(&n) { + CalcResult::new_error(Error::NUM, cell, "date out of range".to_string()) + } else { + CalcResult::Number(n as f64) + } + } + Err(msg) => CalcResult::new_error(Error::NUM, cell, msg), + } + } + + // COUPNUM(settlement, maturity, frequency, [basis]) + pub(crate) fn fn_coupnum(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() < 3 || args.len() > 4 { + return CalcResult::new_args_number_error(cell); + } + let settlement = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f.trunc() as i64, + Err(s) => return s, + }; + let maturity = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f.trunc() as i64, + Err(s) => return s, + }; + let frequency = match self.get_number_no_bools(&args[2], cell) { + Ok(f) => f.trunc() as i32, + Err(s) => return s, + }; + let basis = if args.len() > 3 { + match self.get_number_no_bools(&args[3], cell) { + Ok(f) => f.trunc() as i32, + Err(s) => return s, + } + } else { + 0 + }; + + if ![1, 2, 4].contains(&frequency) || !(0..=4).contains(&basis) { + return CalcResult::new_error(Error::NUM, cell, "invalid arguments".to_string()); + } + if settlement >= maturity { + return CalcResult::new_error(Error::NUM, cell, "settlement < maturity".to_string()); + } + + let settlement_date = match from_excel_date(settlement) { + Ok(d) => d, + Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), + }; + let maturity_date = match from_excel_date(maturity) { + Ok(d) => d, + Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), + }; + + let months = 12 / frequency; + let step = chrono::Months::new(months as u32); + let mut date = maturity_date; + let mut count = 0; + while settlement_date < date { + count += 1; + date = match date.checked_sub_months(step) { + Some(new_date) => new_date, + None => break, // Safety check to avoid infinite loop + }; + } + CalcResult::Number(count as f64) + } + + // COUPPCD(settlement, maturity, frequency, [basis]) + pub(crate) fn fn_couppcd(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() < 3 || args.len() > 4 { + return CalcResult::new_args_number_error(cell); + } + let settlement = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f.trunc() as i64, + Err(s) => return s, + }; + let maturity = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f.trunc() as i64, + Err(s) => return s, + }; + let frequency = match self.get_number_no_bools(&args[2], cell) { + Ok(f) => f.trunc() as i32, + Err(s) => return s, + }; + let basis = if args.len() > 3 { + match self.get_number_no_bools(&args[3], cell) { + Ok(f) => f.trunc() as i32, + Err(s) => return s, + } + } else { + 0 + }; + + if ![1, 2, 4].contains(&frequency) || !(0..=4).contains(&basis) { + return CalcResult::new_error(Error::NUM, cell, "invalid arguments".to_string()); + } + if settlement >= maturity { + return CalcResult::new_error(Error::NUM, cell, "settlement < maturity".to_string()); + } + + let settlement_date = match from_excel_date(settlement) { + Ok(d) => d, + Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), + }; + let maturity_date = match from_excel_date(maturity) { + Ok(d) => d, + Err(_) => return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()), + }; + + let (pcd, _) = coupon_dates(settlement_date, maturity_date, frequency); + match crate::formatter::dates::date_to_serial_number(pcd.day(), pcd.month(), pcd.year()) { + Ok(n) => { + if !(MINIMUM_DATE_SERIAL_NUMBER..=MAXIMUM_DATE_SERIAL_NUMBER).contains(&n) { + CalcResult::new_error(Error::NUM, cell, "date out of range".to_string()) + } else { + CalcResult::Number(n as f64) + } + } + Err(msg) => CalcResult::new_error(Error::NUM, cell, msg), + } + } + // DOLLARDE(fractional_dollar, fraction) pub(crate) fn fn_dollarde(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { if args.len() != 2 { diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs index 21c8f72da..80b9b2139 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -157,6 +157,12 @@ pub enum Function { Year, // Financial + Coupdaybs, + Coupdays, + Coupdaysnc, + Coupncd, + Coupnum, + Couppcd, Cumipmt, Cumprinc, Db, @@ -253,7 +259,7 @@ pub enum Function { } impl Function { - pub fn into_iter() -> IntoIter { + pub fn into_iter() -> IntoIter { [ Function::And, Function::False, @@ -389,6 +395,12 @@ impl Function { Function::Nominal, Function::Effect, Function::Pduration, + Function::Coupdaybs, + Function::Coupdays, + Function::Coupdaysnc, + Function::Coupncd, + Function::Coupnum, + Function::Couppcd, Function::Tbillyield, Function::Tbillprice, Function::Tbilleq, @@ -656,6 +668,13 @@ impl Function { "EFFECT" => Some(Function::Effect), "PDURATION" | "_XLFN.PDURATION" => Some(Function::Pduration), + "COUPDAYBS" => Some(Function::Coupdaybs), + "COUPDAYS" => Some(Function::Coupdays), + "COUPDAYSNC" => Some(Function::Coupdaysnc), + "COUPNCD" => Some(Function::Coupncd), + "COUPNUM" => Some(Function::Coupnum), + "COUPPCD" => Some(Function::Couppcd), + "TBILLYIELD" => Some(Function::Tbillyield), "TBILLPRICE" => Some(Function::Tbillprice), "TBILLEQ" => Some(Function::Tbilleq), @@ -868,6 +887,12 @@ impl fmt::Display for Function { Function::Nominal => write!(f, "NOMINAL"), Function::Effect => write!(f, "EFFECT"), Function::Pduration => write!(f, "PDURATION"), + Function::Coupdaybs => write!(f, "COUPDAYBS"), + Function::Coupdays => write!(f, "COUPDAYS"), + Function::Coupdaysnc => write!(f, "COUPDAYSNC"), + Function::Coupncd => write!(f, "COUPNCD"), + Function::Coupnum => write!(f, "COUPNUM"), + Function::Couppcd => write!(f, "COUPPCD"), Function::Tbillyield => write!(f, "TBILLYIELD"), Function::Tbillprice => write!(f, "TBILLPRICE"), Function::Tbilleq => write!(f, "TBILLEQ"), @@ -1110,6 +1135,12 @@ impl Model { Function::Nominal => self.fn_nominal(args, cell), Function::Effect => self.fn_effect(args, cell), Function::Pduration => self.fn_pduration(args, cell), + Function::Coupdaybs => self.fn_coupdaybs(args, cell), + Function::Coupdays => self.fn_coupdays(args, cell), + Function::Coupdaysnc => self.fn_coupdaysnc(args, cell), + Function::Coupncd => self.fn_coupncd(args, cell), + Function::Coupnum => self.fn_coupnum(args, cell), + Function::Couppcd => self.fn_couppcd(args, cell), Function::Tbillyield => self.fn_tbillyield(args, cell), Function::Tbillprice => self.fn_tbillprice(args, cell), Function::Tbilleq => self.fn_tbilleq(args, cell), diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index a0a0d69d6..3ee6413c4 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -13,6 +13,7 @@ mod test_fn_averageifs; mod test_fn_choose; mod test_fn_concatenate; mod test_fn_count; +mod test_fn_coupon; mod test_fn_day; mod test_fn_exact; mod test_fn_financial; diff --git a/base/src/test/test_fn_coupon.rs b/base/src/test/test_fn_coupon.rs new file mode 100644 index 000000000..26740dd67 --- /dev/null +++ b/base/src/test/test_fn_coupon.rs @@ -0,0 +1,100 @@ +#![allow(clippy::unwrap_used)] + +use crate::{cell::CellValue, test::util::new_empty_model}; + +#[test] +fn fn_coupon_functions() { + let mut model = new_empty_model(); + + // Test with basis 1 (original test) + model._set("A1", "=DATE(2001,1,25)"); + model._set("A2", "=DATE(2001,11,15)"); + model._set("B1", "=COUPDAYBS(A1,A2,2,1)"); + model._set("B2", "=COUPDAYS(A1,A2,2,1)"); + model._set("B3", "=COUPDAYSNC(A1,A2,2,1)"); + model._set("B4", "=COUPNCD(A1,A2,2,1)"); + model._set("B5", "=COUPNUM(A1,A2,2,1)"); + model._set("B6", "=COUPPCD(A1,A2,2,1)"); + + // Test with basis 3 for better coverage + model._set("C1", "=COUPDAYBS(DATE(2001,1,25),DATE(2001,11,15),2,3)"); + model._set("C2", "=COUPDAYS(DATE(2001,1,25),DATE(2001,11,15),2,3)"); + model._set("C3", "=COUPDAYSNC(DATE(2001,1,25),DATE(2001,11,15),2,3)"); + model._set("C4", "=COUPNCD(DATE(2001,1,25),DATE(2001,11,15),2,3)"); + model._set("C5", "=COUPNUM(DATE(2007,1,25),DATE(2008,11,15),2,1)"); + model._set("C6", "=COUPPCD(DATE(2001,1,25),DATE(2001,11,15),2,3)"); + + model.evaluate(); + + // Test basis 1 + assert_eq!(model._get_text("B1"), "71"); + assert_eq!(model._get_text("B2"), "181"); + assert_eq!(model._get_text("B3"), "110"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!B4"), + Ok(CellValue::Number(37026.0)) + ); + assert_eq!(model._get_text("B5"), "2"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!B6"), + Ok(CellValue::Number(36845.0)) + ); + + // Test basis 3 (more comprehensive coverage) + assert_eq!(model._get_text("C1"), "71"); + assert_eq!(model._get_text("C2"), "181"); // Fixed: actual days + assert_eq!(model._get_text("C3"), "110"); + assert_eq!(model._get_text("C4"), "37026"); + assert_eq!(model._get_text("C5"), "4"); + assert_eq!(model._get_text("C6"), "36845"); +} + +#[test] +fn fn_coupon_functions_error_cases() { + let mut model = new_empty_model(); + + // Test invalid frequency + model._set("E1", "=COUPDAYBS(DATE(2001,1,25),DATE(2001,11,15),3,1)"); + // Test invalid basis + model._set("E2", "=COUPDAYS(DATE(2001,1,25),DATE(2001,11,15),2,5)"); + // Test settlement >= maturity + model._set("E3", "=COUPDAYSNC(DATE(2001,11,15),DATE(2001,1,25),2,1)"); + // Test too few arguments + model._set("E4", "=COUPNCD(DATE(2001,1,25),DATE(2001,11,15))"); + // Test too many arguments + model._set("E5", "=COUPNUM(DATE(2001,1,25),DATE(2001,11,15),2,1,1)"); + + model.evaluate(); + + // All should return errors + assert_eq!(model._get_text("E1"), "#NUM!"); + assert_eq!(model._get_text("E2"), "#NUM!"); + assert_eq!(model._get_text("E3"), "#NUM!"); + assert_eq!(model._get_text("E4"), *"#ERROR!"); + assert_eq!(model._get_text("E5"), *"#ERROR!"); +} + +#[test] +fn fn_coupdays_actual_day_count_fix() { + // Verify COUPDAYS correctly distinguishes between fixed vs actual day count methods + // Bug: basis 2&3 were incorrectly using fixed calculations like basis 0&4 + let mut model = new_empty_model(); + + model._set("A1", "=DATE(2023,1,15)"); + model._set("A2", "=DATE(2023,7,15)"); + + model._set("B1", "=COUPDAYS(A1,A2,2,0)"); // 30/360: uses 360/freq + model._set("B2", "=COUPDAYS(A1,A2,2,2)"); // Actual/360: uses actual days + model._set("B3", "=COUPDAYS(A1,A2,2,3)"); // Actual/365: uses actual days + model._set("B4", "=COUPDAYS(A1,A2,2,4)"); // 30/360 European: uses 360/freq + + model.evaluate(); + + // Basis 0&4: theoretical 360/2 = 180 days + assert_eq!(model._get_text("B1"), "180"); + assert_eq!(model._get_text("B4"), "180"); + + // Basis 2&3: actual days between Jan 15 and Jul 15 = 181 days + assert_eq!(model._get_text("B2"), "181"); + assert_eq!(model._get_text("B3"), "181"); +} diff --git a/docs/src/functions/financial.md b/docs/src/functions/financial.md index e8593cdf2..14c8b2048 100644 --- a/docs/src/functions/financial.md +++ b/docs/src/functions/financial.md @@ -15,12 +15,12 @@ You can track the progress in this [GitHub issue](https://github.com/ironcalc/Ir | ACCRINTM | | – | | AMORDEGRC | | – | | AMORLINC | | – | -| COUPDAYBS | | – | -| COUPDAYS | | – | -| COUPDAYSNC | | – | -| COUPNCD | | – | -| COUPNUM | | – | -| COUPPCD | | – | +| COUPDAYBS | | – | +| COUPDAYS | | – | +| COUPDAYSNC | | – | +| COUPNCD | | – | +| COUPNUM | | – | +| COUPPCD | | – | | CUMIPMT | | – | | CUMPRINC | | – | | DB | | – | diff --git a/docs/src/functions/financial/coupdaybs.md b/docs/src/functions/financial/coupdaybs.md index 710809b68..287bf6cd8 100644 --- a/docs/src/functions/financial/coupdaybs.md +++ b/docs/src/functions/financial/coupdaybs.md @@ -7,6 +7,5 @@ lang: en-US # COUPDAYBS ::: 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/financial/coupdays.md b/docs/src/functions/financial/coupdays.md index b47d8e8c5..a9c53692c 100644 --- a/docs/src/functions/financial/coupdays.md +++ b/docs/src/functions/financial/coupdays.md @@ -7,6 +7,5 @@ lang: en-US # COUPDAYS ::: 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/financial/coupdaysnc.md b/docs/src/functions/financial/coupdaysnc.md index 1aabcf855..1b8b961b9 100644 --- a/docs/src/functions/financial/coupdaysnc.md +++ b/docs/src/functions/financial/coupdaysnc.md @@ -7,6 +7,5 @@ lang: en-US # COUPDAYSNC ::: 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/financial/coupncd.md b/docs/src/functions/financial/coupncd.md index 4c3eeb24a..6715a8dd7 100644 --- a/docs/src/functions/financial/coupncd.md +++ b/docs/src/functions/financial/coupncd.md @@ -7,6 +7,5 @@ lang: en-US # COUPNCD ::: 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/financial/coupnum.md b/docs/src/functions/financial/coupnum.md index 7bd4538e9..74cd68332 100644 --- a/docs/src/functions/financial/coupnum.md +++ b/docs/src/functions/financial/coupnum.md @@ -7,6 +7,5 @@ lang: en-US # COUPNUM ::: 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/financial/couppcd.md b/docs/src/functions/financial/couppcd.md index 933c0efd2..16d3fe1c3 100644 --- a/docs/src/functions/financial/couppcd.md +++ b/docs/src/functions/financial/couppcd.md @@ -7,6 +7,5 @@ lang: en-US # COUPPCD ::: 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