From 23814ec18c967e7c4a19e67747e0aff177c2dedb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Hatcher?= Date: Wed, 4 Dec 2024 19:41:03 +0100 Subject: [PATCH] FIX: Several fixes on the FV function (1+x)^(1+y) was stringifyfied incorrectly We still need work on this FV now returns currency FV(-1,-2,1) should return #DIV/0! not #NUM! --- base/src/expressions/parser/mod.rs | 13 +--- base/src/expressions/parser/stringify.rs | 64 ++++++++++++++++-- base/src/expressions/parser/tests/mod.rs | 6 ++ .../parser/{test.rs => tests/test_general.rs} | 14 ++-- .../parser/{ => tests}/test_issue_155.rs | 6 +- .../parser/{ => tests}/test_move_formula.rs | 6 +- .../parser/{ => tests}/test_ranges.rs | 6 +- .../parser/tests/test_stringify.rs | 34 ++++++++++ .../parser/{ => tests}/test_tables.rs | 4 +- base/src/functions/financial.rs | 3 + base/src/test/mod.rs | 1 + base/src/test/test_fn_formulatext.rs | 3 - base/src/test/test_fn_fv.rs | 36 ++++++++++ base/src/units.rs | 1 + xlsx/tests/calc_tests/FV.xlsx | Bin 10681 -> 11542 bytes 15 files changed, 155 insertions(+), 42 deletions(-) create mode 100644 base/src/expressions/parser/tests/mod.rs rename base/src/expressions/parser/{test.rs => tests/test_general.rs} (98%) rename base/src/expressions/parser/{ => tests}/test_issue_155.rs (93%) rename base/src/expressions/parser/{ => tests}/test_move_formula.rs (99%) rename base/src/expressions/parser/{ => tests}/test_ranges.rs (94%) create mode 100644 base/src/expressions/parser/tests/test_stringify.rs rename base/src/expressions/parser/{ => tests}/test_tables.rs (97%) create mode 100644 base/src/test/test_fn_fv.rs diff --git a/base/src/expressions/parser/mod.rs b/base/src/expressions/parser/mod.rs index f396a75..d8de57d 100644 --- a/base/src/expressions/parser/mod.rs +++ b/base/src/expressions/parser/mod.rs @@ -49,18 +49,7 @@ pub mod stringify; pub mod walk; #[cfg(test)] -mod test; - -#[cfg(test)] -mod test_ranges; - -#[cfg(test)] -mod test_move_formula; -#[cfg(test)] -mod test_tables; - -#[cfg(test)] -mod test_issue_155; +mod tests; pub(crate) fn parse_range(formula: &str) -> Result<(i32, i32, i32, i32), String> { let mut lexer = lexer::Lexer::new( diff --git a/base/src/expressions/parser/stringify.rs b/base/src/expressions/parser/stringify.rs index 352b285..f9ed35c 100644 --- a/base/src/expressions/parser/stringify.rs +++ b/base/src/expressions/parser/stringify.rs @@ -456,11 +456,65 @@ fn stringify( }; format!("{}{}{}", x, kind, y) } - OpPowerKind { left, right } => format!( - "{}^{}", - stringify(left, context, displace_data, use_original_name), - stringify(right, context, displace_data, use_original_name) - ), + OpPowerKind { left, right } => { + let x = match **left { + BooleanKind(_) + | NumberKind(_) + | StringKind(_) + | ReferenceKind { .. } + | RangeKind { .. } + | WrongReferenceKind { .. } + | VariableKind(_) + | WrongRangeKind { .. } => { + stringify(left, context, displace_data, use_original_name) + } + OpRangeKind { .. } + | OpConcatenateKind { .. } + | OpProductKind { .. } + | OpPowerKind { .. } + | FunctionKind { .. } + | InvalidFunctionKind { .. } + | ArrayKind(_) + | UnaryKind { .. } + | ErrorKind(_) + | ParseErrorKind { .. } + | OpSumKind { .. } + | CompareKind { .. } + | EmptyArgKind => format!( + "({})", + stringify(left, context, displace_data, use_original_name) + ), + }; + let y = match **right { + BooleanKind(_) + | NumberKind(_) + | StringKind(_) + | ReferenceKind { .. } + | RangeKind { .. } + | WrongReferenceKind { .. } + | VariableKind(_) + | WrongRangeKind { .. } => { + stringify(right, context, displace_data, use_original_name) + } + OpRangeKind { .. } + | OpConcatenateKind { .. } + | OpProductKind { .. } + | OpPowerKind { .. } + | FunctionKind { .. } + | InvalidFunctionKind { .. } + | ArrayKind(_) + | UnaryKind { .. } + | ErrorKind(_) + | ParseErrorKind { .. } + | OpSumKind { .. } + | CompareKind { .. } + | EmptyArgKind => format!( + "({})", + stringify(right, context, displace_data, use_original_name) + ), + }; + format!("{}^{}", x, y) + } InvalidFunctionKind { name, args } => { format_function(name, args, context, displace_data, use_original_name) } diff --git a/base/src/expressions/parser/tests/mod.rs b/base/src/expressions/parser/tests/mod.rs new file mode 100644 index 0000000..7661338 --- /dev/null +++ b/base/src/expressions/parser/tests/mod.rs @@ -0,0 +1,6 @@ +mod test_general; +mod test_issue_155; +mod test_move_formula; +mod test_ranges; +mod test_stringify; +mod test_tables; diff --git a/base/src/expressions/parser/test.rs b/base/src/expressions/parser/tests/test_general.rs similarity index 98% rename from base/src/expressions/parser/test.rs rename to base/src/expressions/parser/tests/test_general.rs index aff3df4..2882b34 100644 --- a/base/src/expressions/parser/test.rs +++ b/base/src/expressions/parser/tests/test_general.rs @@ -3,17 +3,11 @@ use std::collections::HashMap; use crate::expressions::lexer::LexerMode; -use crate::expressions::parser::stringify::DisplaceData; - -use super::super::types::CellReferenceRC; -use super::Parser; -use super::{ - super::parser::{ - stringify::{to_rc_format, to_string}, - Node, - }, - stringify::to_string_displaced, +use crate::expressions::parser::stringify::{ + to_rc_format, to_string, to_string_displaced, DisplaceData, }; +use crate::expressions::parser::{Node, Parser}; +use crate::expressions::types::CellReferenceRC; struct Formula<'a> { initial: &'a str, diff --git a/base/src/expressions/parser/test_issue_155.rs b/base/src/expressions/parser/tests/test_issue_155.rs similarity index 93% rename from base/src/expressions/parser/test_issue_155.rs rename to base/src/expressions/parser/tests/test_issue_155.rs index 7144cc6..5c0191d 100644 --- a/base/src/expressions/parser/test_issue_155.rs +++ b/base/src/expressions/parser/tests/test_issue_155.rs @@ -2,9 +2,9 @@ use std::collections::HashMap; -use super::super::parser::stringify::to_string; -use super::super::types::CellReferenceRC; -use super::Parser; +use crate::expressions::parser::stringify::to_string; +use crate::expressions::parser::Parser; +use crate::expressions::types::CellReferenceRC; #[test] fn issue_155_parser() { diff --git a/base/src/expressions/parser/test_move_formula.rs b/base/src/expressions/parser/tests/test_move_formula.rs similarity index 99% rename from base/src/expressions/parser/test_move_formula.rs rename to base/src/expressions/parser/tests/test_move_formula.rs index 0b84811..63196f0 100644 --- a/base/src/expressions/parser/test_move_formula.rs +++ b/base/src/expressions/parser/tests/test_move_formula.rs @@ -1,10 +1,8 @@ use std::collections::HashMap; use crate::expressions::parser::move_formula::{move_formula, MoveContext}; -use crate::expressions::types::Area; - -use super::super::types::CellReferenceRC; -use super::Parser; +use crate::expressions::parser::Parser; +use crate::expressions::types::{Area, CellReferenceRC}; #[test] fn test_move_formula() { diff --git a/base/src/expressions/parser/test_ranges.rs b/base/src/expressions/parser/tests/test_ranges.rs similarity index 94% rename from base/src/expressions/parser/test_ranges.rs rename to base/src/expressions/parser/tests/test_ranges.rs index 2e876dc..adc0f5e 100644 --- a/base/src/expressions/parser/test_ranges.rs +++ b/base/src/expressions/parser/tests/test_ranges.rs @@ -2,9 +2,9 @@ use std::collections::HashMap; use crate::expressions::lexer::LexerMode; -use super::super::parser::stringify::{to_rc_format, to_string}; -use super::super::types::CellReferenceRC; -use super::Parser; +use crate::expressions::parser::stringify::{to_rc_format, to_string}; +use crate::expressions::parser::Parser; +use crate::expressions::types::CellReferenceRC; struct Formula<'a> { formula_a1: &'a str, diff --git a/base/src/expressions/parser/tests/test_stringify.rs b/base/src/expressions/parser/tests/test_stringify.rs new file mode 100644 index 0000000..3d6d50d --- /dev/null +++ b/base/src/expressions/parser/tests/test_stringify.rs @@ -0,0 +1,34 @@ +#![allow(clippy::panic)] + +use std::collections::HashMap; + +use crate::expressions::parser::stringify::to_string; +use crate::expressions::parser::Parser; +use crate::expressions::types::CellReferenceRC; + +#[test] +fn exp_order() { + let worksheets = vec!["Sheet1".to_string()]; + let mut parser = Parser::new(worksheets, HashMap::new()); + + // Reference cell is Sheet1!A1 + let cell_reference = CellReferenceRC { + sheet: "Sheet1".to_string(), + row: 1, + column: 1, + }; + let t = parser.parse("(1 + 2)^3 + 4", &Some(cell_reference.clone())); + assert_eq!(to_string(&t, &cell_reference), "(1+2)^3+4"); + + let t = parser.parse("(C5 + 3)^R4", &Some(cell_reference.clone())); + assert_eq!(to_string(&t, &cell_reference), "(C5+3)^R4"); + + let t = parser.parse("(C5 + 3)^(R4*6)", &Some(cell_reference.clone())); + assert_eq!(to_string(&t, &cell_reference), "(C5+3)^(R4*6)"); + + let t = parser.parse("(C5)^(R4)", &Some(cell_reference.clone())); + assert_eq!(to_string(&t, &cell_reference), "C5^R4"); + + let t = parser.parse("(5)^(4)", &Some(cell_reference.clone())); + assert_eq!(to_string(&t, &cell_reference), "5^4"); +} diff --git a/base/src/expressions/parser/test_tables.rs b/base/src/expressions/parser/tests/test_tables.rs similarity index 97% rename from base/src/expressions/parser/test_tables.rs rename to base/src/expressions/parser/tests/test_tables.rs index 89fd880..ebb177a 100644 --- a/base/src/expressions/parser/test_tables.rs +++ b/base/src/expressions/parser/tests/test_tables.rs @@ -6,8 +6,8 @@ use crate::expressions::parser::stringify::to_string; use crate::expressions::utils::{number_to_column, parse_reference_a1}; use crate::types::{Table, TableColumn, TableStyleInfo}; -use super::super::types::CellReferenceRC; -use super::Parser; +use crate::expressions::parser::Parser; +use crate::expressions::types::CellReferenceRC; fn create_test_table( table_name: &str, diff --git a/base/src/functions/financial.rs b/base/src/functions/financial.rs index 9a6f11c..766eb7d 100644 --- a/base/src/functions/financial.rs +++ b/base/src/functions/financial.rs @@ -89,6 +89,9 @@ fn compute_future_value( if rate == 0.0 { return Ok(-pv - pmt * nper); } + if rate == -1.0 && nper < 0.0 { + return Err((Error::DIV, "Divide by zero".to_string())); + } let rate_nper = (1.0 + rate).powf(nper); let fv = if period_start { diff --git a/base/src/test/mod.rs b/base/src/test/mod.rs index f53465d..fcacb71 100644 --- a/base/src/test/mod.rs +++ b/base/src/test/mod.rs @@ -51,6 +51,7 @@ mod test_number_format; mod test_escape_quotes; mod test_extend; +mod test_fn_fv; mod test_fn_type; mod test_frozen_rows_and_columns; mod test_geomean; diff --git a/base/src/test/test_fn_formulatext.rs b/base/src/test/test_fn_formulatext.rs index 4825990..bfe338d 100644 --- a/base/src/test/test_fn_formulatext.rs +++ b/base/src/test/test_fn_formulatext.rs @@ -2,9 +2,6 @@ use crate::test::util::new_empty_model; -#[test] -fn simple_cases() {} - #[test] fn wrong_number_of_arguments() { let mut model = new_empty_model(); diff --git a/base/src/test/test_fn_fv.rs b/base/src/test/test_fn_fv.rs new file mode 100644 index 0000000..69b72d0 --- /dev/null +++ b/base/src/test/test_fn_fv.rs @@ -0,0 +1,36 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn computation() { + let i2 = "=-C2*(1+D2)^E2-F2*((D2+1)*((1+D2)^E2-1))/D2"; + + let mut model = new_empty_model(); + model._set("C2", "1"); + model._set("D2", "2"); + model._set("E2", "3"); + model._set("F2", "4"); + + model._set("I2", i2); + + model.evaluate(); + + assert_eq!(model._get_text("I2"), "-183"); + assert_eq!(model._get_formula("I2"), i2); +} + +#[test] +fn format_as_currency() { + let mut model = new_empty_model(); + model._set("C2", "1"); + model._set("D2", "2"); + model._set("E2", "3"); + model._set("F2", "4"); + + model._set("I2", "=FV(D2,E2,F2,C2,1)"); + + model.evaluate(); + + assert_eq!(model._get_text("I2"), "-$183.00"); +} diff --git a/base/src/units.rs b/base/src/units.rs index 9b9ae68..3c82e14 100644 --- a/base/src/units.rs +++ b/base/src/units.rs @@ -309,6 +309,7 @@ impl Model { Function::Sum => self.units_fn_sum_like(args, cell), Function::Average => self.units_fn_sum_like(args, cell), Function::Pmt => self.units_fn_currency(args, cell), + Function::Fv => self.units_fn_currency(args, cell), Function::Nper => self.units_fn_currency(args, cell), Function::Npv => self.units_fn_currency(args, cell), Function::Irr => self.units_fn_percentage(args, cell), diff --git a/xlsx/tests/calc_tests/FV.xlsx b/xlsx/tests/calc_tests/FV.xlsx index 9df63db324be6c523d054c6b7aee5cbe06dd998c..8bb97266ed4a0beb890de188ace40e60afb572a0 100644 GIT binary patch delta 6269 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 SeFy?!-`}YBBd*#0c>6!CA87&r delta 5435 zcmZu#1yEeeww;0CgKKbtyC*n=fm|$Ta2s3(g1bzB1cJlN;2sD;gADGLV8PuzxQ7rd zd~$!)z4hMz=Tx2Q)4g|B_wLoZ_FBnCjXIQ?s_2i%0GI%5006)Q$mVf!fdT;lqAJQK zXs|M5r+T*lDEwHd1)Rj~h;oy|E?=*K-g+iV-Ece-6-c&rQP)pI|t9jg}uI%2AM zlgr}9WL@K`!(TwwqzvYZP3#p4W-8as`e3yNz;?RWqXy5Xp|O5WqO+c7*h#agsnUl# z=OHX_o+l3xhLr2~zrB2+N*sxFDZk!=Gtl7ZgK%i$}N%`~RmR+Qi{)pfub=zj?&=NQEz!iBz>O?^_X$Xs*xU1lREG|}#p zjg){=KpV?qH}NhEU1MaB7_Ju2^tBa8+jO8@Gtx)_oHPcBJPt#@ojh7g=<4%($t&ba ztlqSJTkJyF=~sY5Z~}XCOze#E(2ytk5I&LMEqc?RSa`oq3Jz%J(4~J@pP1 zuTAE2uXp4C`E#wz_I?{d3A)^O_Mv9CW&aKl3@yEi%kd#Hbq`o2e1NO36Tgp(JJiy} z1{O;Z_BvmTEXds99(bxA+nMkvDUR_?owj7jxLWhgt42taZik^&`m3qi8xurA z4PuIGVUD9JHb`7%d)aFHcCfHGmXb}9Z+^7LuFj$IDJ6zWj{Nz85UQ|njB}o%h62Wm zH>5wEX3iIUe;CNSd7e)+f(fWGwbxTAM|<1UVu^n^+!Iw>sf|0Qi4lK>h$q_4-9PA(^P zmR>pooD@lyY6x_ER}e4Sw+tDXRf7GxKIwicGTcDQL+3noViKi}+ymu~CI3hac4Cf^ zEr57rhD{`3!w=r=Qy&>B-y_OyrITTR?w|bm7M0VhNdWkzvWUybkRboCsu`-e+zY?S zDClGC=U2|uqKB`HT)#w(W_IKopa@B;&A&)jSdcGJ-LN&yE?m8!#^81^FNLvE`Mr&h zTgeg6j(cRKP-|KJF=|M$v?D^&w0*A;7+{9u+E4|7U784q8LaY;;_?omU(YUS>OO8Q z1Cs{r%Sk!<@yQAJZ~x`*^!t8QDIJ=I?fJUVsaFXTmp+!)-%6@san=Mrch zTKcH*dwp1nc$xwX_VqBeQ-}4j)X~DP^xz3SQ6G5f4h)etsS3W?iN}mDuAlSSSCp7W zihA0U6m5@h$7UDL>xlNBkL`>7 zbQ-Ovr#SM9%Y1se$by)cYuMtGww#)5a<$qh_I8S zFkR=boGfZu(_Upy|G*jp1sLyyY%lMqEE^d<2JXxuL`P(Pa~xhVY zvX9E3KSsy7e5&HXg9QMLB2GZuu!Q3t2)43jpJTJ~e5>xAl%y@To|BsMww3N7>==-7 zpt8$kgj4xgYD6;3eB@Z&2m`B9J-v|el;a6dqzhR+N3ppb?a^Aw(D1=Y8i9_3H z9C#}rOWo`t`i3xJv>|kIxnBIE`ebe+O&SIDt@h*6#$NsAkDqHh24$4FVFMpH^3w;y zL2nnAy99JuF?eEUq4u|cs@#U&Yg1lhpF#&WJav!eh59^!CdHl?T)R2`95^+74E3>9 zf%%okI+^aonLK8o<4vB4-U?(5*|eC>6)}8Q^W5u3SR&uVL}})35u@_y4F{n( zy%B3ce%2$`8C^&oS6@&j%oZmv+Rr7|oFwjONa`f;oQ@2ekb?w~d%pcO)wKSE7)<=c zv^^K>zP1IVm*ZryvpAl#$Git9J{{*xb2~8@j zo0Zs4dYxc_uX#O{*Q|f6J4mrSYHo^Zq$`PkHBKi}HH^7m&s%c6CEEhxlsG0_y}eiAy*M8>HbCC%%gAuL_xv--XOk zbpk>KNv4oU0~+n0zA($4ezm$=?%b(ojQrWs0l!an&m-EUVLx`Ry3MN_Yq?ZkW3lS! z98JZW-{0L)E}OUiKG{2cwmZpw+SSv3do`HSet#-)QMtZI&fgYvcD9S4TnW0^)`+K@ zU!bK+JzeJ%MtH*7aa}up~1|I8TY!pDi6CBXbrw% zv`Do>=b9K!8(HGgDqd7!ELz2QQ~5}&mZT4!oVaGIEg{YuBby^f6QM_ZDrJDnZQ)$~ zb?v#>N}KQaQ6!OtVx8kU?N#6yy3%Iw`>h#ip>?&&$R9^U6=CluSi$ZPR&Bl-u;u%~ zE*r+T^JhcL&ajH^BIuNKpf$6_!$Jgt-xqIv9kl1uI?xtZ5EP|ri?-era))*P#rBz> zMZrIc^ZeS1=dJ2@iHHm8=k2(l4T2kb)1IerRrj6o#%L^VH_DKWAreivi9rpg(W|*$3B_^$vfzmUyGTLTd zs&2M+C{kEb;=DL%zL`qLGv+H*K)sBI!Kj7s;CADf{u4+N-GNFOci*t?MJ5Dj122J2 zbGoNsbK6{=BAqMkT--u^DL5jDR|>AE6_!*?#kMkE`Deuuyj&GU+hW*^p5UCp*b#s? zw=3_*!mthPQv4~70!>}HgI!R)5c6f}!?s6{105eT{;dBwM5F}`m9hxO#syI>g}x{M zZK9A>YUFf;%$Uu1N{CMOLa&t+u^IbiB(j?Q)@Oj;2k3!`5pR5YgOteNu-R4!LE&67 z@Id|TvllyD;bk7ve!3oJQ@`$SN^B?0WGZZiVbHX5qpue~FP=N5wFHI?upOW6WNWlk zZ3#0Wj|MTmG|T%0J!%^8mNlgp<6*$Bd^t^G!6_<*J4VvS%jJ{% zu;86nK87in9CH&?7HYOMT3%FI<^=L&W_EAQ=F$wtJPpP)_fqnVUbt=H3OJLK`)>VQ zRczggcs~C;+?E!(ZAJd6CaoprGwyKO3x9-gaT-w5l3$oH-cx6;$<5Ev%b}!Iv4EF3 z#uF7QFfI^ExyB!en$UJFG`9Y__`>}HN>t8+=$&V?kBNUJlCJ6NbOu@v|0AvrZIWH^; zUu2B-gEP1O8zLE_jdVF~h730%Psu)Cp-yIp@Fgm(Y;}f^HJ#6{ze~QX(3P>w(Zae~ z8DisG(IvK@5f>O3@N;9uMZSp+bKe^h49Tb@Pu#llS%(bR&$L91izDNhC89OGKcre` zcZo)R{x+t;%|N9wm)@DT&nJ~yg8s8rW9b>g)PM)3!*{#FFI;<*o`~HD$xZyyGw@_J z*T>g)>b&lsp?r-5BvXDWEo+lO(de*F0p6T%LN5(l)VrflD6NGWVIFLd#^lpq&R#FX zq)$Jy%l2h`GxCz-*;mQ>bv>}yGG&Y96C&mB3KRRJy`w4blETb!JxR_m zue6A>^WFz@I$|sE!v4*g=yyz*Ok8WrqUa{t2Ej3;v!syBx;!uOzgw1fhHEC zirx+ty)%4A8(X|X?}($L)y%(9)~;Qzx0%cZx*ZcUX5G{dBQe8VEFBo)b`dYZf3J4S9R z$QEaibsON)vAOR4ffL@}qXQ6j%mg@HW`@`a4;?fpC3Z$24F1w(;SWbgzh(d}(TF>1 zfkMhGmGpm9S_0wTcIt&zdDSfM%MuS}H0s7C=e*}*8Tid)J`^1hWO4heS%+2hxNAkB zwc^xX+^9$$)APwjhf)RN^ZAzb5Gyb4Mr$}CH3iu)+CxyFQglaSU%2X81E4?RBN z6r&G2%n!mwMW$DS1YGF+ll5)SAF5!eZcBs@(!)?^ZDG!TkXlg}h+-g^+=h z&d61TQ(hJy!Ivg#Q3__ z8J0%)S95pxsZSkx=Eq&M`qeZ@_QTdPBHybkb+Q*+RSp4&Xu;^b9LEC6*uPKxE6Z;@ zAey$rde`|lS?X@vPMv?bxie&D#+c5qn#%NGU*QCsuE^r+S`}ZLy2*KQifvJ(&eR1b zm^}NY=<^LLgZETz|B_0ioq&5qg=nVNL*b^8bu>a~gOu?DQGA?DTT;sHcW;!uSF){N zrmbNAvw~a)b-c4__Ftkay>$HZI&t?z(fCYA>l%Y^?^oV5_1Kvg{6_l|Qyk-~@O{z; z%vU3VnW zTNaU}@?o{w$n@8yi?HYnw=<@ox%l(dvhc^s_;LlK~oio#tZm=lsp7Emo~5fVb4WR`=9dCgX2F>KPF;@ zi-+^S6_$U-0r)uop|>EO^N>9dKqAn;76l*#KMxxsgPw@u-_Cgazui1M{hVwNay;a; z|8PZm0D$UWyN6aVF@l|&i}t@6#=kbJR0tF|E7w2i??DM7`gio9WzGlySi4why1Te~ z@LRaL{#hjL-?#ArIWA}b0P%mAvoa$pc$jJbf!6<-uYeWd#zRJif%(S`@8NBGm}86W H&)@$5?g{#<