use super::*;
fn eval1(source: &str) -> ResultData {
let sheet = Sheet::new(SheetInit::default());
sheet.eval(source, None).unwrap().0
}
fn assert_float_close(result: &ResultData, expected: f64, tol: f64) {
match result {
ResultData::Float(f) => assert!((f - expected).abs() < tol, "expected {expected}, got {f}"),
ResultData::Integer(i) => assert!(
(*i as f64 - expected).abs() < tol,
"expected {expected}, got {i}"
),
other => panic!("expected numeric result close to {expected}, got {other:?}"),
}
}
#[test]
fn test_date_serial_matches_real_excel_reference_values() {
assert_float_close(&eval1("=DATE(1900,1,1)"), 1.0, 1e-9);
assert_float_close(&eval1("=DATE(1970,1,1)"), 25569.0, 1e-9);
assert_float_close(&eval1("=DATE(2000,1,1)"), 36526.0, 1e-9);
assert_float_close(&eval1("=DATE(2007,11,21)"), 39407.0, 1e-9);
assert_float_close(&eval1("=DATE(2021,1,1)"), 44197.0, 1e-9);
}
#[test]
fn test_choose_basic() {
assert_float_close(&eval1("=CHOOSE(1, 10, 20, 30)"), 10.0, 1e-9);
assert_float_close(&eval1("=CHOOSE(3, 10, 20, 30)"), 30.0, 1e-9);
assert_eq!(
eval1("=CHOOSE(2, \"a\", \"b\", \"c\")").to_string(),
"b".to_string()
);
}
#[test]
fn test_choose_out_of_range_errors() {
assert!(
matches!(eval1("=CHOOSE(0, 10, 20)"), ResultData::Error(ref e) if e.contains("#VALUE!"))
);
assert!(
matches!(eval1("=CHOOSE(5, 10, 20)"), ResultData::Error(ref e) if e.contains("#VALUE!"))
);
}
#[test]
fn test_choose_lazy_evaluation_skips_unselected_branch_errors() {
let result = eval1("=CHOOSE(1, 42, NA())");
assert_float_close(&result, 42.0, 1e-9);
}
#[test]
fn test_yearfrac_basis1_uses_actual_actual_year_average() {
assert_float_close(
&eval1("=YEARFRAC(DATE(1998,8,8),DATE(1998,9,8),1)"),
0.08493150684931507,
1e-9,
);
assert_float_close(
&eval1("=YEARFRAC(DATE(2016,1,1),DATE(2016,6,1),1)"),
0.41530054644808745,
1e-9,
);
assert_float_close(
&eval1("=YEARFRAC(DATE(2017,6,1),DATE(2020,9,1),1)"),
3.252566735112936,
1e-6,
);
}
#[test]
fn test_coupon_date_functions_match_microsoft_docs_example() {
assert_float_close(
&eval1("=COUPDAYBS(DATE(2011,1,25), DATE(2011,11,15), 2, 1)"),
71.0,
1e-9,
);
assert_float_close(
&eval1("=COUPDAYS(DATE(2011,1,25), DATE(2011,11,15), 2, 1)"),
181.0,
1e-9,
);
assert_float_close(
&eval1("=COUPNUM(DATE(2011,1,25), DATE(2011,11,15), 2)"),
2.0,
1e-9,
);
assert_float_close(
&eval1("=COUPPCD(DATE(2011,1,25), DATE(2011,11,15), 2)"),
40497.0,
1e-9,
);
}
#[test]
fn test_coupncd_handles_day_of_month_clamping_correctly() {
assert_float_close(
&eval1("=COUPNCD(DATE(2007,11,21), EDATE(DATE(2007,11,21),30), 4)"),
39499.0,
1e-9,
);
}
#[test]
fn test_coupdaysnc_uses_real_calendar_days_not_coupdays_minus_coupdaybs() {
assert_float_close(
&eval1("=COUPDAYSNC(DATE(2030,7,25), EDATE(DATE(2030,7,25),48), 2, 3)"),
184.0,
1e-9,
);
}
#[test]
fn test_price_and_yield_are_inverses_and_match_real_excel() {
assert_float_close(
&eval1("=PRICE(DATE(1997,5,13), EDATE(DATE(1997,5,13),42), 0.0606, 0.0885, 100, 2, 3)"),
91.75707270015924,
1e-6,
);
assert_float_close(
&eval1("=PRICE(DATE(2015,3,15), EDATE(DATE(2015,3,15),72), 0.0248, 0.0816, 100, 1, 2)"),
73.86901031390656,
1e-6,
);
assert_float_close(
&eval1("=YIELD(DATE(2032,10,16), EDATE(DATE(2032,10,16),51), 0.0846, 103.71, 105, 4, 3)"),
0.0840390355112947,
1e-9,
);
}
#[test]
fn test_duration_matches_real_excel() {
assert_float_close(
&eval1("=DURATION(DATE(2008,1,1), DATE(2016,1,1), 0.08, 0.09, 2, 1)"),
5.993774955545186,
1e-6,
);
}
#[test]
fn test_disc_pricedisc_yielddisc_match_real_excel() {
assert_float_close(
&eval1("=DISC(DATE(1998,4,12), EDATE(DATE(1998,4,12),4), 93.41, 100, 1)"),
0.197159836065574,
1e-9,
);
assert_float_close(
&eval1("=PRICEDISC(DATE(1996,2,2), EDATE(DATE(1996,2,2),14), 0.044, 100, 1)"),
94.88372093023256,
1e-6,
);
}
#[test]
fn test_disc_basis1_year_length_has_two_regimes() {
assert_float_close(
&eval1("=DISC(DATE(2016,6,1), DATE(2016,9,1), 93, 100, 1)"),
0.278478260869565,
1e-9,
);
assert_float_close(
&eval1("=PRICEDISC(DATE(2027,12,26), EDATE(DATE(2027,12,26),3), 0.0899, 100, 1)"),
97.76478142076503,
1e-6,
);
assert_float_close(
&eval1("=DISC(DATE(2017,6,1), DATE(2020,9,1), 70, 100, 1)"),
0.0922348484848485,
1e-9,
);
assert_float_close(
&eval1("=PRICEDISC(DATE(2023,8,5), EDATE(DATE(2023,8,5),5), 0.0226, 100, 1)"),
99.05265753424658,
1e-9,
);
assert_float_close(
&eval1("=YEARFRAC(DATE(2015,2,15), DATE(2016,2,13), 1)"),
0.994520547945205,
1e-12,
);
}
#[test]
fn test_pricemat_yieldmat_basis1_uses_issue_to_settlement_span() {
assert_float_close(
&eval1(
"=YIELDMAT(EDATE(DATE(2012,9,3),2), EDATE(DATE(2012,9,3),25), DATE(2012,9,3), 0.079, 119.55, 1)",
),
-0.019331059183910253,
1e-9,
);
assert_float_close(
&eval1(
"=YIELDMAT(EDATE(DATE(1995,9,4),6), EDATE(DATE(1995,9,4),11), DATE(1995,9,4), 0.0226, 95.27, 1)",
),
0.14082750571984862,
1e-9,
);
}
#[test]
fn test_received_and_intrate_match_real_excel() {
assert_float_close(
&eval1("=RECEIVED(DATE(2008,2,15), DATE(2008,5,15), 1000000, 0.0575, 2)"),
1014584.6544071021,
1e-3,
);
assert_float_close(
&eval1("=INTRATE(DATE(2020,9,4), EDATE(DATE(2020,9,4),13), 24974.48, 5441.6, 1)"),
-0.7237025672261941,
1e-9,
);
}
#[test]
fn test_tbill_functions_match_real_excel() {
assert_float_close(
&eval1("=TBILLPRICE(DATE(2008,3,31), DATE(2008,6,1), 0.09)"),
98.45,
1e-9,
);
assert_float_close(
&eval1("=TBILLYIELD(DATE(2008,3,31), DATE(2008,6,1), 98.45)"),
0.09141696292534264,
1e-9,
);
assert_float_close(
&eval1("=TBILLEQ(DATE(2008,3,31), DATE(2008,6,1), 0.0914)"),
0.09415149356594302,
1e-9,
);
}
#[test]
fn test_accrint_totals_from_issue_regardless_of_calc_method() {
assert_float_close(
&eval1(
"=ACCRINT(DATE(2017,6,7), DATE(2018,6,7), DATE(2018,7,7), 0.0547, 14735.46, 1, 0, TRUE)",
),
873.1988004999998,
1e-6,
);
assert_float_close(
&eval1(
"=ACCRINT(DATE(2017,6,7), DATE(2018,6,7), DATE(2018,7,7), 0.0547, 14735.46, 1, 0, FALSE)",
),
873.1988004999998,
1e-6,
);
}
#[test]
fn test_accrintm_matches_real_excel() {
assert_float_close(
&eval1("=ACCRINTM(DATE(1998,8,8), EDATE(DATE(1998,8,8),1), 0.096, 37328.54, 1)"),
304.3554384657534,
1e-6,
);
}
#[test]
fn test_amorlinc_amordegrc_reject_basis_2() {
assert!(matches!(
eval1("=AMORLINC(17737.01, DATE(2026,5,16), EDATE(DATE(2026,5,16),9), 5082.98, 1, 0.5, 2)"),
ResultData::Error(ref e) if e.contains("#NUM!")
));
assert!(matches!(
eval1("=AMORDEGRC(8000.89, DATE(1995,11,26), EDATE(DATE(1995,11,26),9), 1484.45, 12, 0.05, 2)"),
ResultData::Error(ref e) if e.contains("#NUM!")
));
}
#[test]
fn test_amordegrc_rejects_life_at_or_below_two_years() {
assert!(matches!(
eval1("=AMORDEGRC(9832.03, DATE(2024,9,7), EDATE(DATE(2024,9,7),5), 2414.1, 0, 0.5, 3)"),
ResultData::Error(ref e) if e.contains("#NUM!")
));
assert_float_close(
&eval1("=AMORDEGRC(9832.03, DATE(2024,9,7), EDATE(DATE(2024,9,7),5), 2414.1, 0, 0.499, 3)"),
3085.0,
1e-6,
);
}
#[test]
fn test_amordegrc_period_sequence_matches_real_excel() {
let expected = [
5699.0, 8515.0, 6742.0, 5338.0, 4226.0, 3346.0, 2649.0, 2098.0, 1661.0, 1315.0,
];
for (period, exp) in expected.iter().enumerate() {
let f = format!(
"=AMORDEGRC(46587.76, DATE(2028,7,27), EDATE(DATE(2028,7,27),7), 3292.26, {period}, 0.0833, 1)"
);
assert_float_close(&eval1(&f), *exp, 1e-6);
}
}
#[test]
fn test_oddfprice_oddfyield_match_real_excel() {
assert_float_close(
&eval1(
"=ODDFPRICE((DATE(2012,7,19)+26), EDATE((DATE(2012,7,19)+60),24), DATE(2012,7,19), (DATE(2012,7,19)+60), 0.082, 0.0923, 100, 4, 3)",
),
98.06021551292406,
1e-6,
);
assert_float_close(
&eval1(
"=ODDFPRICE(DATE(2012,7,19)+26, EDATE(DATE(2012,7,19)+60,24), DATE(2012,7,19), DATE(2012,7,19)+60, 0.082, 0.0923, 100, 4, 2)",
),
98.05901871412289,
1e-6,
);
assert_float_close(
&eval1(
"=ODDFYIELD((DATE(2008,2,16)+20), EDATE((DATE(2008,2,16)+25),12), DATE(2008,2,16), (DATE(2008,2,16)+25), 0.0923, 117.26, 100, 4, 1)",
),
-0.07045544328557858,
1e-6,
);
}
#[test]
fn test_oddlprice_oddlyield_match_real_excel() {
assert_float_close(
&eval1(
"=ODDLYIELD((DATE(2029,6,23)+28), (DATE(2029,6,23)+44), DATE(2029,6,23), 0.0404, 115.8, 100, 4, 2)",
),
-3.0950656625987194,
1e-6,
);
assert_float_close(
&eval1(
"=ODDLYIELD((DATE(2002,7,10)+45), (DATE(2002,7,10)+114), DATE(2002,7,10), 0.0775, 96.4, 100, 1, 2)",
),
0.2752128427867764,
1e-6,
);
assert_float_close(
&eval1(
"=ODDLYIELD((DATE(2004,7,25)+51), (DATE(2004,7,25)+57), DATE(2004,7,25), 0.0682, 80.65, 100, 2, 3)",
),
14.62856320740453,
1e-6,
);
assert_float_close(
&eval1(
"=ODDLYIELD((DATE(2001,3,3)+10), (DATE(2001,3,3)+123), DATE(2001,3,3), 0.0546, 93.74, 100, 4, 1)",
),
0.27529020678767524,
1e-6,
);
assert_float_close(
&eval1(
"=ODDLPRICE(DATE(2019,6,21)+65, DATE(2019,6,21)+90, DATE(2019,6,21), 0.0478, 0.0376, 100, 2, 2)",
),
100.06731898218347,
1e-6,
);
}
#[test]
fn test_euroconvert_direct_and_reverse() {
assert_float_close(&eval1("=EUROCONVERT(100, \"EUR\", \"DEM\")"), 195.58, 1e-9);
assert_float_close(&eval1("=EUROCONVERT(100, \"DEM\", \"EUR\")"), 51.13, 1e-9);
}
#[test]
fn test_euroconvert_triangulates_through_eur() {
assert_float_close(&eval1("=EUROCONVERT(1, \"DEM\", \"FRF\")"), 3.35, 1e-9);
assert_float_close(
&eval1("=EUROCONVERT(1, \"DEM\", \"FRF\", TRUE)"),
3.353854885138279,
1e-9,
);
}
#[test]
fn test_euroconvert_same_currency_is_a_no_op() {
assert_float_close(&eval1("=EUROCONVERT(42, \"EUR\", \"EUR\")"), 42.0, 1e-9);
}
#[test]
fn test_euroconvert_rounds_zero_decimal_currencies_to_whole_units() {
assert_float_close(
&eval1("=EUROCONVERT(100, \"EUR\", \"ITL\")"),
193627.0,
1e-9,
);
}
#[test]
fn test_euroconvert_rejects_unknown_currency_code() {
assert!(matches!(
eval1("=EUROCONVERT(100, \"EUR\", \"USD\")"),
ResultData::Error(ref e) if e.contains("#VALUE!")
));
}
#[test]
fn test_euroconvert_rejects_triangulation_precision_below_3() {
assert!(matches!(
eval1("=EUROCONVERT(1, \"DEM\", \"FRF\", FALSE, 2)"),
ResultData::Error(ref e) if e.contains("#NUM!")
));
}
#[test]
fn test_odd_period_functions_reject_settlement_at_or_before_anchor() {
assert!(matches!(
eval1("=ODDFPRICE(DATE(2030,4,13)+0, EDATE(DATE(2030,4,13)+44,84), DATE(2030,4,13), DATE(2030,4,13)+44, 0.073, 0.0368, 100, 1, 3)"),
ResultData::Error(ref e) if e.contains("#NUM!")
));
assert!(matches!(
eval1("=ODDFYIELD(DATE(2023,11,11)+0, EDATE(DATE(2023,11,11)+20,12), DATE(2023,11,11), DATE(2023,11,11)+20, 0.0562, 89.35, 105, 4, 3)"),
ResultData::Error(ref e) if e.contains("#NUM!")
));
assert!(matches!(
eval1("=ODDLPRICE(DATE(2005,6,28)+0, DATE(2005,6,28)+58, DATE(2005,6,28), 0.0353, 0.0938, 105, 4, 4)"),
ResultData::Error(ref e) if e.contains("#NUM!")
));
assert!(matches!(
eval1("=ODDLYIELD(DATE(2010,7,11)+0, DATE(2010,7,11)+15, DATE(2010,7,11), 0.028, 110.37, 100, 2, 1)"),
ResultData::Error(ref e) if e.contains("#NUM!")
));
}
fn database_test_sheet() -> Sheet {
let grid: [[&str; 12]; 7] = [
[
"Tree",
"Height",
"Age",
"Yield",
"Profit",
"",
"Tree",
"Height",
"",
"Tree",
"Tree",
"=DSUM(A1:E7, \"Profit\", G1:H2)",
],
[
"Apple",
"18",
"20",
"14",
"105",
"",
"Apple",
">12",
"",
"Pear",
"Cherry",
"=DAVERAGE(A1:E7, \"Yield\", G1:H2)",
],
[
"Pear",
"12",
"12",
"10",
"96",
"",
"",
"",
"",
"Cherry",
"Tree",
"=DCOUNT(A1:E7, \"Age\", G1:H2)",
],
[
"Cherry",
"13",
"14",
"9",
"105",
"",
"",
"",
"",
"",
"",
"=DCOUNTA(A1:E7, \"Tree\", G1:H2)",
],
[
"Apple",
"14",
"15",
"10",
"75",
"",
"Tree",
"",
"",
"",
"",
"=DMAX(A1:E7, \"Profit\", G1:H2)",
],
[
"Pear",
"9",
"8",
"8",
"76.8",
"",
"Pear",
"",
"",
"",
"",
"=DMIN(A1:E7, \"Profit\", G1:H2)",
],
[
"Apple",
"8",
"9",
"6",
"45",
"",
"Cherry",
"",
"",
"",
"",
"=DPRODUCT(A1:E7, \"Yield\", G1:H2)",
],
];
let mut sheet = create_sheet(&grid);
sheet.commit(None).unwrap();
sheet
}
#[test]
fn test_database_functions_and_criteria_match_real_excel() {
let sheet = database_test_sheet();
assert_float_close(&sheet.get_result_data(&CellRef::new(0, 11)), 180.0, 1e-9); assert_float_close(&sheet.get_result_data(&CellRef::new(1, 11)), 12.0, 1e-9); assert_float_close(&sheet.get_result_data(&CellRef::new(2, 11)), 2.0, 1e-9); assert_float_close(&sheet.get_result_data(&CellRef::new(3, 11)), 2.0, 1e-9); assert_float_close(&sheet.get_result_data(&CellRef::new(4, 11)), 105.0, 1e-9); assert_float_close(&sheet.get_result_data(&CellRef::new(5, 11)), 75.0, 1e-9); assert_float_close(&sheet.get_result_data(&CellRef::new(6, 11)), 140.0, 1e-9); }
#[test]
fn test_dget_unique_match_or_error() {
let mut sheet = database_test_sheet();
sheet.set_cell_src(0, 11, "=DGET(A1:E7, \"Profit\", K1:K2)".to_string()); sheet.set_cell_src(1, 11, "=DGET(A1:E7, \"Profit\", J1:J2)".to_string()); sheet.set_cell_src(2, 10, "Tree".to_string());
sheet.set_cell_src(3, 10, "Mango".to_string());
sheet.set_cell_src(2, 11, "=DGET(A1:E7, \"Profit\", K3:K4)".to_string()); sheet.commit(None).unwrap();
assert_float_close(&sheet.get_result_data(&CellRef::new(0, 11)), 105.0, 1e-9);
assert!(matches!(
sheet.get_result_data(&CellRef::new(1, 11)),
ResultData::Error(ref e) if e.contains("#NUM!")
));
assert!(matches!(
sheet.get_result_data(&CellRef::new(2, 11)),
ResultData::Error(ref e) if e.contains("#VALUE!")
));
}
#[test]
fn test_database_or_across_criteria_rows_and_field_by_index() {
let mut sheet = database_test_sheet();
sheet.set_cell_src(0, 11, "=DSUM(A1:E7, 5, J1:J3)".to_string());
sheet.commit(None).unwrap();
assert_float_close(&sheet.get_result_data(&CellRef::new(0, 11)), 277.8, 1e-6);
}
#[test]
fn test_database_blank_criteria_row_matches_every_record() {
let mut sheet = database_test_sheet();
sheet.set_cell_src(0, 11, "=DSTDEV(A1:E7, \"Profit\", K3:K4)".to_string());
sheet.set_cell_src(1, 11, "=DVARP(A1:E7, \"Profit\", K3:K4)".to_string());
sheet.commit(None).unwrap();
assert_float_close(
&sheet.get_result_data(&CellRef::new(0, 11)),
23.149946004256645,
1e-6,
);
assert_float_close(
&sheet.get_result_data(&CellRef::new(1, 11)),
446.59999999999934,
1e-6,
);
}
#[test]
fn test_database_aggregation_ignores_blank_and_boolean_range_values() {
let grid: [[&str; 4]; 4] = [
["Key", "Val", "=DSUM(A1:B4, \"Val\", D1:D2)", "Key"],
["x", "10", "", "x"],
["x", "", "=DCOUNT(A1:B4, \"Val\", D1:D2)", ""],
["x", "TRUE", "=DPRODUCT(A1:B4, \"Val\", D1:D2)", ""],
];
let mut sheet = create_sheet(&grid);
sheet.commit(None).unwrap();
assert_float_close(&sheet.get_result_data(&CellRef::new(0, 2)), 10.0, 1e-9);
assert_float_close(&sheet.get_result_data(&CellRef::new(2, 2)), 1.0, 1e-9);
assert_float_close(&sheet.get_result_data(&CellRef::new(3, 2)), 10.0, 1e-9);
}
#[test]
fn test_database_numeric_criteria_excludes_non_numeric_cells() {
let grid: [[&str; 4]; 6] = [
["Key", "Val", "=DCOUNT(A1:B6, \"Val\", D1:D2)", "Val"],
["x", "1", "", "<1000"],
["x", "", "", ""], ["x", "text", "", ""], ["x", "TRUE", "", ""], ["x", "-5", "", ""],
];
let mut sheet = create_sheet(&grid);
sheet.commit(None).unwrap();
assert_float_close(&sheet.get_result_data(&CellRef::new(0, 2)), 2.0, 1e-9);
}
#[test]
fn test_lambda_bare_is_uncallable() {
assert!(matches!(
eval1("=LAMBDA(x, x*2)"),
ResultData::Error(ref e) if e.contains("#CALC!")
));
}
#[test]
fn test_isomitted_best_effort() {
assert!(matches!(
eval1("=INDEX(MAP(1, LAMBDA(x, ISOMITTED(x))), 1)"),
ResultData::Boolean(false)
));
assert!(matches!(
eval1("=ISOMITTED(some_undeclared_name)"),
ResultData::Boolean(true)
));
}
#[test]
fn test_map_single_and_multiple_arrays_match_real_excel() {
let grid: [[&str; 3]; 3] = [
["10", "1", "=INDEX(MAP(A1:A3, LAMBDA(x, x*2)), 2)"],
["20", "2", "=INDEX(MAP(A1:A3, B1:B3, LAMBDA(x,y, x+y)), 3)"],
["30", "3", ""],
];
let mut sheet = create_sheet(&grid);
sheet.commit(None).unwrap();
assert_float_close(&sheet.get_result_data(&CellRef::new(0, 2)), 40.0, 1e-9); assert_float_close(&sheet.get_result_data(&CellRef::new(1, 2)), 33.0, 1e-9); }
#[test]
fn test_reduce_and_scan_match_real_excel() {
let grid: [[&str; 2]; 3] = [
["1", "=REDUCE(0, A1:A3, LAMBDA(acc,v, acc+v))"],
["2", "=INDEX(SCAN(0, A1:A3, LAMBDA(acc,v, acc+v)), 3)"],
["3", ""],
];
let mut sheet = create_sheet(&grid);
sheet.commit(None).unwrap();
assert_float_close(&sheet.get_result_data(&CellRef::new(0, 1)), 6.0, 1e-9);
assert_float_close(&sheet.get_result_data(&CellRef::new(1, 1)), 6.0, 1e-9);
}
#[test]
fn test_reduce_two_arg_form_seeds_from_first_element() {
let grid: [[&str; 2]; 3] = [
["1", "=REDUCE(A1:A3, LAMBDA(acc,v, acc+v))"],
["2", ""],
["3", ""],
];
let mut sheet = create_sheet(&grid);
sheet.commit(None).unwrap();
assert_float_close(&sheet.get_result_data(&CellRef::new(0, 1)), 6.0, 1e-9);
}
#[test]
fn test_byrow_and_bycol_sum_hand_verified() {
let grid: [[&str; 5]; 3] = [
["1", "2", "3", "", ""],
["4", "5", "6", "", ""],
["7", "8", "9", "", ""],
];
let mut sheet = create_sheet(&grid);
for i in 0..3 {
sheet.set_cell_src(
i,
3,
format!("=INDEX(BYROW(A1:C3, LAMBDA(zz, SUM(zz))), {})", i + 1),
);
sheet.set_cell_src(
i,
4,
format!("=INDEX(BYCOL(A1:C3, LAMBDA(zz, SUM(zz))), {})", i + 1),
);
}
sheet.commit(None).unwrap();
for (i, expected_row) in [6.0, 15.0, 24.0].iter().enumerate() {
assert_float_close(
&sheet.get_result_data(&CellRef::new(i, 3)),
*expected_row,
1e-9,
);
}
for (i, expected_col) in [12.0, 15.0, 18.0].iter().enumerate() {
assert_float_close(
&sheet.get_result_data(&CellRef::new(i, 4)),
*expected_col,
1e-9,
);
}
}
#[test]
fn test_makearray_builds_row_major_flat_array_hand_verified() {
let grid: [[&str; 1]; 6] = [
["=INDEX(MAKEARRAY(2, 3, LAMBDA(rr,cc, rr*10+cc)), 1)"],
["=INDEX(MAKEARRAY(2, 3, LAMBDA(rr,cc, rr*10+cc)), 2)"],
["=INDEX(MAKEARRAY(2, 3, LAMBDA(rr,cc, rr*10+cc)), 3)"],
["=INDEX(MAKEARRAY(2, 3, LAMBDA(rr,cc, rr*10+cc)), 4)"],
["=INDEX(MAKEARRAY(2, 3, LAMBDA(rr,cc, rr*10+cc)), 5)"],
["=INDEX(MAKEARRAY(2, 3, LAMBDA(rr,cc, rr*10+cc)), 6)"],
];
let mut sheet = create_sheet(&grid);
sheet.commit(None).unwrap();
for (i, expected) in [11.0, 12.0, 13.0, 21.0, 22.0, 23.0].iter().enumerate() {
assert_float_close(&sheet.get_result_data(&CellRef::new(i, 0)), *expected, 1e-9);
}
}
#[test]
fn test_index_two_arg_form_is_one_based() {
let grid: [[&str; 2]; 3] = [
["10", "=INDEX(A1:A3, 1)"],
["20", "=INDEX(A1:A3, 2)"],
["30", "=INDEX(A1:A3, 3)"],
];
let mut sheet = create_sheet(&grid);
sheet.commit(None).unwrap();
assert_float_close(&sheet.get_result_data(&CellRef::new(0, 1)), 10.0, 1e-9);
assert_float_close(&sheet.get_result_data(&CellRef::new(1, 1)), 20.0, 1e-9);
assert_float_close(&sheet.get_result_data(&CellRef::new(2, 1)), 30.0, 1e-9);
}
#[test]
fn test_row_and_column_return_array_for_multi_row_or_col_range() {
let grid: [[&str; 2]; 5] = [
["10", "=SUM(ROW(A1:A5))"],
["20", "=INDEX(ROW(A1:A5), 3)"],
["30", "=COLUMN(A1)"],
["40", ""],
["50", ""],
];
let mut sheet = create_sheet(&grid);
sheet.commit(None).unwrap();
assert_float_close(&sheet.get_result_data(&CellRef::new(0, 1)), 15.0, 1e-9);
assert_float_close(&sheet.get_result_data(&CellRef::new(1, 1)), 3.0, 1e-9);
assert_float_close(&sheet.get_result_data(&CellRef::new(2, 1)), 1.0, 1e-9);
}
#[test]
fn test_rows_columns_areas_match_real_excel() {
let grid: [[&str; 5]; 3] = [
["1", "2", "3", "=ROWS(A1:C3)", "=COLUMNS(A1:C3)"],
["4", "5", "6", "=AREAS(A1:C3)", ""],
["7", "8", "9", "", ""],
];
let mut sheet = create_sheet(&grid);
sheet.commit(None).unwrap();
assert_float_close(&sheet.get_result_data(&CellRef::new(0, 3)), 3.0, 1e-9);
assert_float_close(&sheet.get_result_data(&CellRef::new(0, 4)), 3.0, 1e-9);
assert_float_close(&sheet.get_result_data(&CellRef::new(1, 3)), 1.0, 1e-9);
}
#[test]
fn test_isref_distinguishes_references_from_values() {
assert!(matches!(eval1("=ISREF(A1)"), ResultData::Boolean(true)));
assert!(matches!(eval1("=ISREF(5)"), ResultData::Boolean(false)));
}
#[test]
fn test_formulatext_and_isformula() {
let grid: [[&str; 3]; 1] = [["10", "=A1*2", "=FORMULATEXT(B1)"]];
let mut sheet = create_sheet(&grid);
sheet.set_cell_src(0, 2, "=ISFORMULA(B1)".to_string());
sheet.commit(None).unwrap();
assert!(matches!(
sheet.get_result_data(&CellRef::new(0, 2)),
ResultData::Boolean(true)
));
let grid2: [[&str; 2]; 1] = [["10", "=FORMULATEXT(A1)"]];
let mut sheet2 = create_sheet(&grid2);
sheet2.commit(None).unwrap();
assert!(matches!(
sheet2.get_result_data(&CellRef::new(0, 1)),
ResultData::Error(ref e) if e.contains("#N/A")
));
}
#[test]
fn test_hyperlink_returns_friendly_name_or_link() {
assert_eq!(
eval1("=HYPERLINK(\"https://example.com\")").to_string(),
"https://example.com"
);
assert_eq!(
eval1("=HYPERLINK(\"https://example.com\", \"Click\")").to_string(),
"Click"
);
}
#[test]
fn test_sheets_counts_context_sheets() {
assert!(matches!(eval1("=SHEETS()"), ResultData::Float(f) if f == 1.0));
}
#[test]
fn test_sheet_reports_real_workbook_ordinal() {
let table1 = Sheet::new(SheetInit {
id: None,
name: Some("table_1".to_string()),
rows: 2,
cols: 2,
});
let table2 = Sheet::new(SheetInit {
id: None,
name: Some("table_2".to_string()),
rows: 2,
cols: 2,
});
let table3 = Sheet::new(SheetInit {
id: None,
name: Some("table_3".to_string()),
rows: 2,
cols: 2,
});
let sheets = [table1, table2, table3];
let mut context = Context::new();
for sheet in &sheets {
context.add_table(sheet.name.clone(), sheet);
}
context.sheet_order = sheets.iter().map(|s| s.name.clone()).collect();
let (r1, _) = sheets[0].eval("=SHEET()", Some(&context)).unwrap();
assert_float_close(&r1, 1.0, 1e-9);
let (r2, _) = sheets[1].eval("=SHEET()", Some(&context)).unwrap();
assert_float_close(&r2, 2.0, 1e-9);
let (r3, _) = sheets[2].eval("=SHEET()", Some(&context)).unwrap();
assert_float_close(&r3, 3.0, 1e-9);
let (r4, _) = sheets[0]
.eval("=SHEET(table_3!A1)", Some(&context))
.unwrap();
assert_float_close(&r4, 3.0, 1e-9);
let (r5, _) = sheets[0]
.eval("=SHEET(\"table_2\")", Some(&context))
.unwrap();
assert_float_close(&r5, 2.0, 1e-9);
assert!(matches!(eval1("=SHEET()"), ResultData::Float(f) if f == 1.0));
}
#[test]
fn test_indirect_resolves_cell_and_range_text() {
let grid: [[&str; 2]; 3] = [
["10", "=INDIRECT(\"A1\")"],
["20", "=SUM(INDIRECT(\"A1:A3\"))"],
["30", ""],
];
let mut sheet = create_sheet(&grid);
sheet.commit(None).unwrap();
assert_float_close(&sheet.get_result_data(&CellRef::new(0, 1)), 10.0, 1e-9);
assert_float_close(&sheet.get_result_data(&CellRef::new(1, 1)), 60.0, 1e-9);
}
#[test]
fn test_offset_shifts_and_resizes_reference() {
let grid: [[&str; 2]; 3] = [
["10", "=OFFSET(A1, 1, 0)"],
["20", "=SUM(OFFSET(A1, 0, 0, 3, 1))"],
["30", ""],
];
let mut sheet = create_sheet(&grid);
sheet.commit(None).unwrap();
assert_float_close(&sheet.get_result_data(&CellRef::new(0, 1)), 20.0, 1e-9);
assert_float_close(&sheet.get_result_data(&CellRef::new(1, 1)), 60.0, 1e-9);
}
#[test]
fn test_cell_info_subset() {
let grid: [[&str; 2]; 5] = [
["10", "=CELL(\"row\", A3)"],
["20", "=CELL(\"col\", A3)"],
["30", "=CELL(\"address\", A3)"],
["40", "=CELL(\"contents\", A1)"],
["50", ""],
];
let mut sheet = create_sheet(&grid);
sheet.commit(None).unwrap();
assert_float_close(&sheet.get_result_data(&CellRef::new(0, 1)), 3.0, 1e-9);
assert_float_close(&sheet.get_result_data(&CellRef::new(1, 1)), 1.0, 1e-9);
assert_eq!(
sheet.get_result_data(&CellRef::new(2, 1)).to_string(),
"$A$3"
);
assert_float_close(&sheet.get_result_data(&CellRef::new(3, 1)), 10.0, 1e-9);
}
#[test]
fn test_transpose_swaps_rows_and_cols() {
assert_float_close(&eval1("=INDEX(TRANSPOSE(SEQUENCE(2,3)),3,1)"), 3.0, 1e-9);
assert_float_close(&eval1("=INDEX(TRANSPOSE(SEQUENCE(2,3)),1,2)"), 4.0, 1e-9);
assert_float_close(&eval1("=SUM(TRANSPOSE(SEQUENCE(2,3)))"), 21.0, 1e-9);
}
#[test]
fn test_index_recovers_shape_of_nested_reshape_function() {
let grid: [[&str; 3]; 2] = [
["1", "2", "=INDEX(EXPAND(A1:B2,3,3,0),3,3)"],
["4", "5", ""],
];
let mut sheet = create_sheet(&grid);
sheet.commit(None).unwrap();
assert_float_close(&sheet.get_result_data(&CellRef::new(0, 2)), 0.0, 1e-9);
}
#[test]
fn test_hstack_vstack_combine_arrays() {
assert_float_close(
&eval1("=INDEX(HSTACK(SEQUENCE(2,1),SEQUENCE(2,1)),2,1)"),
2.0,
1e-9,
);
assert_float_close(
&eval1("=INDEX(HSTACK(SEQUENCE(2,1),SEQUENCE(2,1)),2,2)"),
2.0,
1e-9,
);
assert_float_close(
&eval1("=INDEX(VSTACK(SEQUENCE(1,2),SEQUENCE(1,2)),2,2)"),
2.0,
1e-9,
);
assert_float_close(
&eval1("=SUM(HSTACK(SEQUENCE(2,1),SEQUENCE(2,1)))"),
6.0,
1e-9,
);
}
#[test]
fn test_chooserows_chosecols_select_by_index() {
assert_float_close(&eval1("=INDEX(CHOOSEROWS(SEQUENCE(3,3),2),1,1)"), 4.0, 1e-9);
assert_float_close(
&eval1("=INDEX(CHOOSEROWS(SEQUENCE(3,3),-1),1,1)"),
7.0,
1e-9,
);
assert_float_close(&eval1("=INDEX(CHOOSECOLS(SEQUENCE(3,3),2),1,1)"), 2.0, 1e-9);
}
#[test]
fn test_drop_take_slice_from_either_end() {
assert_float_close(&eval1("=SUM(DROP(SEQUENCE(3,3),1))"), 39.0, 1e-9);
assert_float_close(&eval1("=SUM(DROP(SEQUENCE(3,3),-1))"), 21.0, 1e-9);
assert_float_close(&eval1("=SUM(TAKE(SEQUENCE(3,3),2))"), 21.0, 1e-9);
assert_float_close(&eval1("=SUM(TAKE(SEQUENCE(3,3),-1))"), 24.0, 1e-9);
}
#[test]
fn test_expand_pads_with_given_value() {
assert_float_close(&eval1("=INDEX(EXPAND(SEQUENCE(2,2),3,3,0),3,3)"), 0.0, 1e-9);
assert_float_close(&eval1("=INDEX(EXPAND(SEQUENCE(2,2),3,3,0),1,1)"), 1.0, 1e-9);
}
#[test]
fn test_tocol_torow_flatten() {
assert_float_close(&eval1("=SUM(TOCOL(SEQUENCE(3,3)))"), 45.0, 1e-9);
assert_float_close(&eval1("=SUM(TOROW(SEQUENCE(3,3)))"), 45.0, 1e-9);
assert_float_close(&eval1("=INDEX(TOCOL(SEQUENCE(2,2)),3)"), 3.0, 1e-9);
}
#[test]
fn test_wraprows_wrapcols_reshape_flat_sequence() {
assert_float_close(&eval1("=INDEX(WRAPROWS(SEQUENCE(7),3,0),3,1)"), 7.0, 1e-9);
assert_float_close(&eval1("=SUM(WRAPROWS(SEQUENCE(7),3,0))"), 28.0, 1e-9);
assert_float_close(&eval1("=INDEX(WRAPCOLS(SEQUENCE(7),3,0),1,3)"), 7.0, 1e-9);
assert_float_close(&eval1("=SUM(WRAPCOLS(SEQUENCE(7),3,0))"), 28.0, 1e-9);
}
#[test]
fn test_unique_sort_sortby_filter_trimrange() {
let grid_u: [[&str; 3]; 5] = [
["10", "", "=SUM(UNIQUE(A1:A5))"],
["20", "", ""],
["5", "", ""],
["20", "", ""],
["30", "", ""],
];
let mut sheet_u = create_sheet(&grid_u);
sheet_u.commit(None).unwrap();
assert_float_close(&sheet_u.get_result_data(&CellRef::new(0, 2)), 65.0, 1e-9);
let grid_s: [[&str; 3]; 5] = [
["10", "", "=INDEX(SORT(A1:A5,1,-1),1)"],
["20", "", ""],
["5", "", ""],
["20", "", ""],
["30", "", ""],
];
let mut sheet_s = create_sheet(&grid_s);
sheet_s.commit(None).unwrap();
assert_float_close(&sheet_s.get_result_data(&CellRef::new(0, 2)), 30.0, 1e-9);
let grid_sb: [[&str; 3]; 5] = [
["10", "50", "=INDEX(SORTBY(A1:A5,B1:B5,-1),1)"],
["20", "0", ""],
["5", "1", ""],
["20", "1", ""],
["30", "1", ""],
];
let mut sheet_sb = create_sheet(&grid_sb);
sheet_sb.commit(None).unwrap();
assert_float_close(&sheet_sb.get_result_data(&CellRef::new(0, 2)), 10.0, 1e-9);
let grid_f: [[&str; 3]; 5] = [
["10", "0", "=SUM(FILTER(A1:A5,B1:B5))"],
["20", "1", ""],
["5", "0", ""],
["20", "1", ""],
["30", "1", ""],
];
let mut sheet_f = create_sheet(&grid_f);
sheet_f.commit(None).unwrap();
assert_float_close(&sheet_f.get_result_data(&CellRef::new(0, 2)), 70.0, 1e-9);
assert_float_close(&eval1("=SUM(TRIMRANGE(SEQUENCE(3,3)))"), 45.0, 1e-9);
}
#[test]
fn test_sort_and_sortby_always_place_blanks_last() {
let grid: [[&str; 4]; 5] = [
["-215.8", "10", "", "=INDEX(SORT(A1:A5,1,-1),1)"],
["", "20", "4", "=INDEX(SORTBY(B1:B5,C1:C5,-1),1)"],
["-100", "30", "3", ""],
["-240.97", "40", "2", ""],
["-88", "50", "1", ""],
];
let mut sheet = create_sheet(&grid);
sheet.commit(None).unwrap();
assert_float_close(&sheet.get_result_data(&CellRef::new(0, 3)), -88.0, 1e-9);
assert_float_close(&sheet.get_result_data(&CellRef::new(1, 3)), 20.0, 1e-9);
}
#[test]
fn test_lookup_vector_form_on_sorted_data() {
let grid: [[&str; 2]; 5] = [
["5", "50"],
["10", "100"],
["20", "200"],
["20", "201"],
["30", "300"],
];
let mut sheet = create_sheet(&grid);
sheet.commit(None).unwrap();
let cases: [(&str, f64); 3] = [
("=LOOKUP(20,A1:A5,B1:B5)", 201.0),
("=LOOKUP(15,A1:A5,B1:B5)", 100.0),
("=LOOKUP(100,A1:A5,B1:B5)", 300.0),
];
for (formula, expected) in cases {
let (result, _) = sheet.eval(formula, None).unwrap();
assert_float_close(&result, expected, 1e-9);
}
assert!(matches!(
sheet.eval("=LOOKUP(1,A1:A5,B1:B5)", None).unwrap().0,
ResultData::Error(ref e) if e.contains("#N/A")
));
}
#[test]
fn test_xmatch_supports_next_smaller_and_larger_modes() {
let grid: [[&str; 1]; 5] = [["10"], ["20"], ["5"], ["20"], ["30"]];
let mut sheet = create_sheet(&grid);
sheet.commit(None).unwrap();
let (exact, _) = sheet.eval("=XMATCH(20,A1:A5)", None).unwrap();
assert_float_close(&exact, 2.0, 1e-9);
let (next_smaller, _) = sheet.eval("=XMATCH(25,A1:A5,-1)", None).unwrap();
assert_float_close(&next_smaller, 2.0, 1e-9);
}
#[test]
fn test_bare_row_and_column_report_current_cell_position() {
let grid: [[&str; 3]; 2] = [["=ROW()", "=COLUMN()", "x"], ["x", "x", "=ROW()+COLUMN()"]];
let mut sheet = create_sheet(&grid);
sheet.commit(None).unwrap();
assert_float_close(&sheet.get_result_data(&CellRef::new(0, 0)), 1.0, 1e-9);
assert_float_close(&sheet.get_result_data(&CellRef::new(0, 1)), 2.0, 1e-9);
assert_float_close(&sheet.get_result_data(&CellRef::new(1, 2)), 5.0, 1e-9);
}
#[test]
fn test_let_binds_names_in_sequence_and_rejects_duplicate_names() {
assert_float_close(&eval1("=LET(x, 5, x * 2)"), 10.0, 1e-9);
assert_float_close(&eval1("=LET(x, 5, y, x + 1, x + y)"), 11.0, 1e-9);
assert!(matches!(
eval1("=LET(x, 1, x, 2, x)"),
ResultData::Error(ref e) if e == "#VALUE!"
));
}
#[test]
fn test_randarray_respects_shape_bounds_and_whole_number_flag() {
match eval1("=RANDARRAY(2,3,10,20,TRUE)") {
ResultData::List(rows) => {
assert_eq!(rows.len(), 2);
for row in &rows {
match row {
ResultData::List(cells) => {
assert_eq!(cells.len(), 3);
for cell in cells {
let v = match cell {
ResultData::Float(f) => *f,
ResultData::Integer(i) => *i as f64,
other => panic!("expected numeric cell, got {other:?}"),
};
assert!((10.0..=20.0).contains(&v), "{v} out of [10,20]");
assert_eq!(v.fract(), 0.0, "expected a whole number, got {v}");
}
}
other => panic!("expected a row List, got {other:?}"),
}
}
}
other => panic!("expected a List of rows, got {other:?}"),
}
}
#[test]
fn test_hlookup_exact_and_approximate_match_on_text_header_row() {
let grid: [[&str; 3]; 2] = [["Jan", "Feb", "Mar"], ["10", "20", "30"]];
let mut sheet = create_sheet(&grid);
sheet.commit(None).unwrap();
let (exact, _) = sheet.eval("=HLOOKUP(\"Feb\",A1:C2,2,FALSE)", None).unwrap();
assert_float_close(&exact, 20.0, 1e-9);
assert!(matches!(
sheet.eval("=HLOOKUP(\"Nope\",A1:C2,2,FALSE)", None).unwrap().0,
ResultData::Error(ref e) if e == "#N/A"
));
let numeric_grid: [[&str; 3]; 2] = [["10", "20", "30"], ["a", "b", "c"]];
let mut numeric_sheet = create_sheet(&numeric_grid);
numeric_sheet.commit(None).unwrap();
let (approx, _) = numeric_sheet.eval("=HLOOKUP(25,A1:C2,2)", None).unwrap();
assert_eq!(approx.to_string(), "b");
}
#[test]
fn test_date_functions_match_documented_excel_examples() {
assert_float_close(&eval1("=WORKDAY(DATE(2024,1,1),5)"), 45299.0, 1e-9);
assert_float_close(
&eval1("=NETWORKDAYS(DATE(2024,1,1),DATE(2024,1,8))"),
6.0,
1e-9,
);
assert_float_close(&eval1("=DAY(EOMONTH(DATE(2024,1,15),0))"), 31.0, 1e-9);
assert_float_close(&eval1("=EOMONTH(DATE(2024,1,15),1)"), 45351.0, 1e-9);
assert_float_close(&eval1("=WEEKNUM(DATE(2024,1,1))"), 1.0, 1e-9);
assert_float_close(&eval1("=WEEKNUM(DATE(2024,1,7))"), 2.0, 1e-9);
assert_float_close(
&eval1("=DAYS360(DATE(2011,1,30),DATE(2011,2,1))"),
1.0,
1e-9,
);
}
#[test]
fn test_besselk_bessely_match_known_reference_values() {
assert_float_close(&eval1("=BESSELK(1,0)"), 0.4210244382, 1e-8);
assert_float_close(&eval1("=BESSELK(1,1)"), 0.6019072301, 1e-8);
assert_float_close(&eval1("=BESSELY(1,0)"), 0.0882569642, 1e-8);
assert_float_close(&eval1("=BESSELY(1,1)"), -0.7812128213, 1e-8);
assert_float_close(&eval1("=BESSELI(1,0)"), 1.2660658778, 1e-8);
assert_float_close(&eval1("=BESSELJ(1,0)"), 0.7651976866, 1e-8);
}
#[test]
fn test_complex_number_functions_round_trip() {
assert_eq!(eval1("=COMPLEX(3,4)").to_string(), "3+4i");
assert_float_close(&eval1("=IMABS(\"3+4i\")"), 5.0, 1e-9);
assert_float_close(&eval1("=IMREAL(\"3+4i\")"), 3.0, 1e-9);
assert_float_close(&eval1("=IMAGINARY(\"3+4i\")"), 4.0, 1e-9);
assert_eq!(eval1("=IMCONJUGATE(\"3+4i\")").to_string(), "3-4i");
assert_eq!(eval1("=IMSUM(\"3+4i\",\"1-2i\")").to_string(), "4+2i");
assert_eq!(eval1("=IMSUB(\"3+4i\",\"1-2i\")").to_string(), "2+6i");
assert_eq!(eval1("=IMPRODUCT(\"3+4i\",\"1-2i\")").to_string(), "11-2i");
assert_eq!(eval1("=IMDIV(\"3+4i\",\"1-2i\")").to_string(), "-1+2i");
}
#[test]
fn test_cube_webservice_image_report_unavailable_connections_not_echo_stub_args() {
for formula in [
"=CUBEKPIMEMBER(\"conn\",\"kpi\",1)",
"=CUBEMEMBER(\"conn\",\"member\")",
"=CUBEMEMBERPROPERTY(\"conn\",\"member\",\"prop\")",
"=CUBERANKEDMEMBER(\"conn\",\"set\",1)",
"=CUBESET(\"conn\",\"set\")",
"=CUBESETCOUNT(\"set\")",
"=CUBEVALUE(\"conn\",\"member\")",
] {
assert!(
matches!(eval1(formula), ResultData::Error(ref e) if e == "#N/A"),
"expected #N/A for {formula}"
);
}
assert!(matches!(
eval1("=WEBSERVICE(\"https://example.com\")"),
ResultData::Error(ref e) if e == "#VALUE!"
));
assert!(matches!(
eval1("=IMAGE(\"https://example.com/pic.png\")"),
ResultData::Error(ref e) if e == "#VALUE!"
));
}
#[test]
fn test_stockhistory_and_rtd_report_unavailable_data_source() {
assert!(matches!(
eval1("=STOCKHISTORY(\"MSFT\",\"2024-01-01\",\"2024-01-31\")"),
ResultData::Error(ref e) if e == "#N/A"
));
assert!(matches!(
eval1("=RTD(\"prog.id\",\"server\",\"topic\")"),
ResultData::Error(ref e) if e == "#N/A"
));
}
#[test]
fn test_coupon_schedule_is_end_of_month_when_maturity_is() {
let settlement = "DATE(2024,2,28)";
let maturity = "EDATE(DATE(2024,2,28),180)"; assert_float_close(
&eval1(&format!("=COUPPCD({settlement}, {maturity}, 1)")),
44985.0,
1e-9,
);
assert_float_close(
&eval1(&format!("=COUPNCD({settlement}, {maturity}, 1)")),
45351.0,
1e-9,
);
assert_float_close(
&eval1(&format!("=COUPNUM({settlement}, {maturity}, 1)")),
16.0,
1e-9,
);
assert_float_close(
&eval1(&format!("=COUPPCD({settlement}, {maturity}, 2)")),
45169.0,
1e-9,
);
assert_float_close(
&eval1("=COUPPCD(DATE(2024,5,15), EDATE(DATE(2024,5,15),181), 2)"),
45275.0, 1e-9,
);
assert_float_close(
&eval1("=COUPNCD(DATE(2024,5,15), EDATE(DATE(2024,5,15),181), 2)"),
45458.0, 1e-9,
);
}
#[test]
fn test_amordegrc_keeps_full_precision_in_the_running_balance() {
let call = |period: i32| {
format!(
"=AMORDEGRC(27370.88, DATE(1998,10,17), EDATE(DATE(1998,10,17),2), 6352.4, {period}, 0.0909, 0)"
)
};
for (period, want) in [(0, 1037.0), (1, 5984.0), (2, 4624.0), (3, 3574.0)] {
assert_float_close(&eval1(&call(period)), want, 1e-9);
}
}