From 68a33a5f87dfcdd9a98f00445866a058b46e7e55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Hatcher=20Andr=C3=A9s?= Date: Tue, 4 Nov 2025 22:16:16 +0100 Subject: [PATCH] UPDATE: Adds COMBIN, COMBINA and SUMSQ (#511) --- .../src/expressions/parser/static_analysis.rs | 6 + base/src/functions/mathematical.rs | 158 ++++++++++++++++++ base/src/functions/mod.rs | 18 +- xlsx/tests/calc_tests/COMBIN_COMBINA.xlsx | Bin 0 -> 9602 bytes xlsx/tests/calc_tests/SUMSQ.xlsx | Bin 0 -> 8634 bytes 5 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 xlsx/tests/calc_tests/COMBIN_COMBINA.xlsx create mode 100644 xlsx/tests/calc_tests/SUMSQ.xlsx diff --git a/base/src/expressions/parser/static_analysis.rs b/base/src/expressions/parser/static_analysis.rs index e7402f4..e4f64e2 100644 --- a/base/src/expressions/parser/static_analysis.rs +++ b/base/src/expressions/parser/static_analysis.rs @@ -868,6 +868,9 @@ fn get_function_args_signature(kind: &Function, arg_count: usize) -> Vec args_signature_scalars(arg_count, 2, 0), Function::Roman => args_signature_scalars(arg_count, 1, 1), Function::Arabic => args_signature_scalars(arg_count, 1, 0), + Function::Combin => args_signature_scalars(arg_count, 2, 0), + Function::Combina => args_signature_scalars(arg_count, 2, 0), + Function::Sumsq => vec![Signature::Vector; arg_count], } } @@ -1124,5 +1127,8 @@ fn static_analysis_on_function(kind: &Function, args: &[Node]) -> StaticResult { Function::Decimal => scalar_arguments(args), Function::Roman => scalar_arguments(args), Function::Arabic => scalar_arguments(args), + Function::Combin => scalar_arguments(args), + Function::Combina => scalar_arguments(args), + Function::Sumsq => StaticResult::Scalar, } } diff --git a/base/src/functions/mathematical.rs b/base/src/functions/mathematical.rs index 1333a11..22a16e5 100644 --- a/base/src/functions/mathematical.rs +++ b/base/src/functions/mathematical.rs @@ -633,6 +633,101 @@ impl Model { CalcResult::Number(result) } + pub(crate) fn fn_sumsq(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.is_empty() { + return CalcResult::new_args_number_error(cell); + } + + let mut result = 0.0; + for arg in args { + match self.evaluate_node_in_context(arg, cell) { + CalcResult::Number(value) => result += value * value, + CalcResult::Range { left, right } => { + if left.sheet != right.sheet { + return CalcResult::new_error( + Error::VALUE, + cell, + "Ranges are in different sheets".to_string(), + ); + } + // TODO: We should do this for all functions that run through ranges + // Running cargo test for the ironcalc takes around .8 seconds with this speedup + // and ~ 3.5 seconds without it. Note that once properly in place sheet.dimension should be almost a noop + let row1 = left.row; + let mut row2 = right.row; + let column1 = left.column; + let mut column2 = right.column; + if row1 == 1 && row2 == LAST_ROW { + row2 = match self.workbook.worksheet(left.sheet) { + Ok(s) => s.dimension().max_row, + Err(_) => { + return CalcResult::new_error( + Error::ERROR, + cell, + format!("Invalid worksheet index: '{}'", left.sheet), + ); + } + }; + } + if column1 == 1 && column2 == LAST_COLUMN { + column2 = match self.workbook.worksheet(left.sheet) { + Ok(s) => s.dimension().max_column, + Err(_) => { + return CalcResult::new_error( + Error::ERROR, + cell, + format!("Invalid worksheet index: '{}'", left.sheet), + ); + } + }; + } + for row in row1..row2 + 1 { + for column in column1..(column2 + 1) { + match self.evaluate_cell(CellReferenceIndex { + sheet: left.sheet, + row, + column, + }) { + CalcResult::Number(value) => { + result += value * value; + } + error @ CalcResult::Error { .. } => return error, + _ => { + // We ignore booleans and strings + } + } + } + } + } + CalcResult::Array(array) => { + for row in array { + for value in row { + match value { + ArrayNode::Number(value) => { + result += value * value; + } + ArrayNode::Error(error) => { + return CalcResult::Error { + error, + origin: cell, + message: "Error in array".to_string(), + } + } + _ => { + // We ignore booleans and strings + } + } + } + } + } + error @ CalcResult::Error { .. } => return error, + _ => { + // We ignore booleans and strings + } + }; + } + CalcResult::Number(result) + } pub(crate) fn fn_product(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { if args.is_empty() { return CalcResult::new_args_number_error(cell); @@ -1465,4 +1560,67 @@ impl Model { }, } } + + pub(crate) fn fn_combin(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() != 2 { + return CalcResult::new_args_number_error(cell); + } + let n = match self.get_number(&args[0], cell) { + Ok(f) => f.floor(), + Err(s) => return s, + }; + let k = match self.get_number(&args[1], cell) { + Ok(f) => f.floor(), + Err(s) => return s, + }; + if n < 0.0 || k < 0.0 { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Arguments must be non-negative integers".to_string(), + }; + } + if k > n { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "k cannot be greater than n".to_string(), + }; + } + let k = k as usize; + let mut result = 1.0; + for i in 0..k { + let t = i as f64; + result *= (n - t) / (t + 1.0); + } + CalcResult::Number(result) + } + + pub(crate) fn fn_combina(&mut self, args: &[Node], cell: CellReferenceIndex) -> CalcResult { + if args.len() != 2 { + return CalcResult::new_args_number_error(cell); + } + let n = match self.get_number(&args[0], cell) { + Ok(f) => f.floor(), + Err(s) => return s, + }; + let k = match self.get_number(&args[1], cell) { + Ok(f) => f.floor(), + Err(s) => return s, + }; + if n < 0.0 || k < 0.0 || (n == 0.0 && k > 0.0) { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Arguments must be non-negative integers".to_string(), + }; + } + let k = k as usize; + let mut result = 1.0; + for i in 0..k { + let t = i as f64; + result *= (n + t) / (t + 1.0); + } + CalcResult::Number(result) + } } diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs index 3b4bc50..5e994e0 100644 --- a/base/src/functions/mod.rs +++ b/base/src/functions/mod.rs @@ -111,6 +111,9 @@ pub enum Function { Decimal, Roman, Arabic, + Combin, + Combina, + Sumsq, // Information ErrorType, @@ -305,7 +308,7 @@ pub enum Function { } impl Function { - pub fn into_iter() -> IntoIter { + pub fn into_iter() -> IntoIter { [ Function::And, Function::False, @@ -556,6 +559,9 @@ impl Function { Function::Subtotal, Function::Roman, Function::Arabic, + Function::Combin, + Function::Combina, + Function::Sumsq, ] .into_iter() } @@ -607,6 +613,7 @@ impl Function { Function::Base => "_xlfn.BASE".to_string(), Function::Decimal => "_xlfn.DECIMAL".to_string(), Function::Arabic => "_xlfn.ARABIC".to_string(), + Function::Combina => "_xlfn.COMBINA".to_string(), _ => self.to_string(), } @@ -696,6 +703,9 @@ impl Function { "SUM" => Some(Function::Sum), "SUMIF" => Some(Function::Sumif), "SUMIFS" => Some(Function::Sumifs), + "COMBIN" => Some(Function::Combin), + "COMBINA" | "_XLFN.COMBINA" => Some(Function::Combina), + "SUMSQ" => Some(Function::Sumsq), // Lookup and Reference "CHOOSE" => Some(Function::Choose), @@ -1141,6 +1151,9 @@ impl fmt::Display for Function { Function::Decimal => write!(f, "DECIMAL"), Function::Roman => write!(f, "ROMAN"), Function::Arabic => write!(f, "ARABIC"), + Function::Combin => write!(f, "COMBIN"), + Function::Combina => write!(f, "COMBINA"), + Function::Sumsq => write!(f, "SUMSQ"), } } } @@ -1418,6 +1431,9 @@ impl Model { Function::Decimal => self.fn_decimal(args, cell), Function::Roman => self.fn_roman(args, cell), Function::Arabic => self.fn_arabic(args, cell), + Function::Combin => self.fn_combin(args, cell), + Function::Combina => self.fn_combina(args, cell), + Function::Sumsq => self.fn_sumsq(args, cell), } } } diff --git a/xlsx/tests/calc_tests/COMBIN_COMBINA.xlsx b/xlsx/tests/calc_tests/COMBIN_COMBINA.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..b02cc1241b173d0bd94a82558b849ecd7070845e GIT binary patch literal 9602 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 literal 0 HcmV?d00001 diff --git a/xlsx/tests/calc_tests/SUMSQ.xlsx b/xlsx/tests/calc_tests/SUMSQ.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..36d077f349c7cfcdbcc743f56e2edcd70aee5083 GIT binary patch literal 8634 zcmeHMgGoq*~{@!&7@mRx7bO8eQb5`^v#K;yGqSt2uu;Lk$j*O(XMEqiMEBX{M|GdPR zE*%DN%w2+fU=LdOMeC9z)Be3rTKTF$adLy|y~!#&7SgjKj09{T{&~Zl=R~PA&-2V_ zi%fTq@f0f=leFIKzc52yc#XrMdaogI5$U4AJ(7VO$Bw}aoX9?nPr=&kZQu)TVH=Ff zbo{nG!!%@622{*x$CtFR18CazeoiUl+grLI%#M6HjGj;W=yI`@rl`CDgASFOOpw7f zMV|fBdEz_UsykP5r((O5IFX;Ry<(fTEs4cag@4HN&42VVa-fMa1eneMzch!jMHx}E zT94PfliVTm9eZ*ha~&7mf}q~b4GKX0FQkNMb3HmnSei0|b9WJ>GzMANIdgLSoc~A0 z|6&II>93b2sHj1?utWFdFT-GGHR8o=k2{ zaV#G`6~9yJUVJ1w3V&LOKggj=j(NX;GL-nqn*d;^ir#|h_j06hsb{-op&#o6a<&o& zGyEn~3culqhYKn1jJzR&Ih($mF87An(VYAs*M4RtWc|@J3nWbIX>4xy<>+Zf%l#{F zcGXOnCN1B+?>vKYUGzC8e!v<&c<)=!b^%Q22iQS(U=&sH!atHkd-KWgF@hkG=l}p7 zVrM+;INcpUwx$jawm+j*sn#op87};5G3y(e!!tfNdFlu(BAN)D_gX|2yQl9#_fd!y zD1{2uwXgO?(2*^a?{5(Yygf3^^4jZg_k=KmRzteeqpIRyXtH-$pS^~I7UUL-BEHe& z>D2UDaVSp6Rg}u=22~hyOiFQNhdd_-s89IRsFz@|eNZJS%{d7vevX;`CLT?jx~E=S zH(|Kch^sJiu8MmPi1w9!h8FdZ5}P{%o+LwSNK~q&kKYX{@%L?7(w@&H`X=OvmfWjt zQJ7l{Sb4Ky(8QkuV;Wfk8(}GAJ7aiB8<;&>o(vIh{Ww*|sQ&g7iwBDgyX6*gP8s{R z#P6837($Az>wM@NG(sunoCcGQgNknA-6f4(TMxfrT)*T)E*n2q1ax1}r8jO^=Cyu} z-qI#c?IwXoYc|YbR#qVXM(x z^&$;;8b?!7*QU~}jQawH6vOls($V(=2%;f2e%%c z5x z!8S^^B{rbKQlO{M`UYW<_-<{79Jk!xdYBCzuhZ>2au7Yi$wxlB*i2aLyb)!`yAM%$UC$51Ie3rpoPylMY*OyB zjBAa!U_Ntgv!p3|7gLLbv>0bDJ$ctjPC!HI-YAV2KlI~vnthVX+`|aj$_z@_*1^l{ z&x%>&-u=Zug|u%H4X;s}7#c^fmhHSgei$8A#$tFqaIv@DLbf1%EF>~2=qWhr=pvF8 zJ6O{cQKS~vUEc|HBE=c3qf%vnW{-ZC+ip8IlPW7*0>7FWvzawNxcg60WK03G>JUMU zDg;T$0T@UKg8VCx{Fx^IA5@SK86JXW|J|cBVNkK13%_$ea36o&ezcsHHe*kc;FRSA zNlN%tTcJXCwBPw@Xrw+#%E?IY7XhgEBByk1V?}Eep7NThx;ha(1~x-2$1&DIdTItb ze!FnZOqV|v1Krg4r0%Tlar)6cJ%I!mM*!jTb-0Ei4>7um3?=uP0?BBOQ14i;Mn=${ zhJDYWQDMtN^=ywwpAXc#v-8pmJwQbYGJ-#3PuL`h+#0V_vdv^`vrfcszj(A)auSUcgU zI@cMDKQM8P2@lcY*#R=q1M`nYo_CR89W8YFaKI*(GnOVrU^S~Gia@NFP3%%6-FEvw zc&=zDcsOG7doL^6+I`kvuxH#AeU~uH))DlGRj3s#(DpF)+4@S z`!%`5_hKOwAmc}GUVx9`*~bta5MXa84@Xh7b}AT2a-q3yR2U*woWn`2-}utuND*59 zZGB+%`1zE}N@VrH&K`~704UA}SD}Cv9Kr)mx)OK=5^z8S(+c8l5LE}Q47iD4Od!Y>9Ex?Fzcw)~W9S|RRVGez_%Q{NC~#bDr`VA2s=;0Rju=y>nZ(OSQeLQvvtk zfM}<{C-G_>NL%NXNz;-L7R#aN_6I4;gjwvQYVpDP&7}iE4X90+WucNor3~Tu&8fMl zV`4xy7`E5_f{~Uw4Y6Fu48?ckmPy&n<5LMkQ$IXaXxf3;OlKVUyXQlLnTDzd$;VG3 zgM-lx#fSWSPZs7`gkd4YbC2{BD7IS+I7rg6!y43@GwnriWKBd$UY*zik{ds1=wH>I z^8xxTxip~#rPaibJ9oLdQY6+#3L3R590Zdj)~lMK^%M5EZLpCF$vafLu}*VjI26Q2 zLD$juM>A9(X^LE2u2Oy;B|9D9?@+d*dgDVsvrNT0a0iS%0zk*^;h zqYZzI;TZ1-EC3mo(r=QjngN)oWfJnU3|@K!SmJjyDf2AjM8KGU7e=9)3^_s=K~Cyn zH(Mm%SYJ80_-iNt;m8c8MGA0udvv0*+ZB4wIH#o1)$wRAiFmU&{4CYj8-Cc~zCfVY zIZ|A;hO5!Q)_ZkXS8;umy6AOty<8#gcO@ralmxG6xq@!{T^^;5LA6+w;ah5pE!Xf1 zUBBx?<3;b?p@mf|V5pjWV2FDlXHyqj0FuqKsY`g$qO$3|N2Urnq%I;_aprbNY7^lN zjy`r72h3q?tH=w+PpUsD;5|NWPymRMi?6 zA8klCIN`0+854}E#>s85U=M)AJ*(mF!zI zs*c)+O0W-0wwG)N`O)M~XtURXdf0aq^gkhCJ^HYqAb(!CvVrO}z|jj)E4yRio}??@ zfXw4`v}~Ilr*ra&s70-*nzMq^=mY3<`5N6eg9cboM4FN~-|nhEGnc}DedA+Y?ZB}} z%vXoj|4MQdjCR3vKfZCbeFxtL&R{$drSbKSyIu@sIR_#fgkZ%LKA@y16ql zc2y0{&^m*WyK|pdNx&^;Og<=*yHqC&u=rAJrSEDwKud>>&v2uu&w|kw8lQOR@1MuOxB?#t6%TLpsCAB0?6YimtE=tvGO)`5 z2&Wpe_L$1O!ir>HO6{_XzIr{R%57qiIMh6XmKDDIN-%EVu=#=$ZBE5!p4q@=_*vmp zUudB0hbwOHvIH1pPGS^PnDgEp(^)iFVZ)0z$@L0thXQJmAbEgsvKO*>57*c-A%pCO z<30VV6z=Qrm=4zo?rVkc_PIQ|Z|63bvyM6MRcS*F&BzM%AcEL6RDbKz|Eg3PsXbR|2kF&SMPO(t!5vCwWI z^W3))9+9szbe>pUn{+B>+Vms(xwmzmJFquc^?vBLt}PjN9qci&923T|RcakCCo;*% ze8lv&-=w^|DgI@-xsf$Axo#zY+wUPCc$nG_n?`e-_#y8wp6yH33>p-2*A29irOsPcKZ&iJKVRBJotnF%1!v9f#4lkwq z)?44|k@shv$yQSiy23%0Ej)I(Kq%aC$jZHQ&}UyND9^|T|FPVzVl_XhR<32vW}Qf~ z4q<3h@wjbV?!wXB6&ENm$XVb22VaHP4I&l&d*G*rNY!T`5>=@?007Rv0>871r=5lK z&+If`OWz@bi_kZ_=H|tZx8zo^1dQVJLiyxc8%+`G3f#2zbWWHXPnGqJPnX2KT-DjJ z=A~6A+`ZXZ#R14XlttDH>2Y)+GHus-&sxq;c714o${SspSCT8mnzD>i{hh(@h6A!k zJvExb9R`q^#&&pXOB_4}>ZOC|Hke>tkIl$$E!pt5^M?-ioXcs(3t|^N)hu_6`xyWA z#$Vp1c{e?0tm<8CMg7v=kckOhZwFM?T6z+>xOO-=r8uULLAFdt_*)IIyXtyy%1b&4-v^pW@m$T}O^+*{rsG3T=Th-Dtjm(Ci^oL1=A;GDs_o*{ z6e05wFn9uO-r<1M>E$(xG_48_}?X8+mJ?~By`ci8=by_g3UH*A`^#EMuhrCA^b!xhl1Be z$wh(VxZ&i@)90h#?ljx(Qfi5)v8oCHHI}7?nn_VHip-@%f7ssujbGF~))di`KU23L zUGHREW|E=~np!;}i4&LIu3W^9nmNBmg)djbDW5&}$oLpr{2Wd=G=hQ(-2dcga23Gy zLT6V9?OlccQQido#u^%rMrHb5u$S0X-+|tBJ4}U5^YmZkId__61wL>5SDcl%7rU(oJ)$%rdc&lEPXQ* zG&VF|RFUBCE)y0WE zh)IXAS=~mN_{VLO0^7&>7vpLT7|Fm>DslVp0PmHp^CsIUf#_~<9kGo@1o8t9vw1gm zU%R~?Rt>t5fz1gKgQzDAPV<>QdXmMha+eQ%zQ_|lYgOeK$DddUVH(q9@Mu*nLSv;& z929d)C+#f5CO0}E{#sSSVG))e!@!OX<>`wxt>WX-4>h!tLqWNxzcZwfVDoNGQi-(j z*cu9>WEN4n_#&dR`p}C4wpQw^=0<$#)3Q0(#qHrspZ#!;s!LmGJG2xaQ_Z8ESU{YS zEGqgV*{$VCGNqLDe zbe-`^nx%e7h4-puwMr*dE|%&}zejkw);> z?Is0PhGEn`LOGF$r}%%iUK2;h{~0f0WBwSK@xLqeOlObDs z0!0+t#;(1v@1r@bSzbV>T56}Gr8!mOLDZ#Rt41^dU>jsNi1)lWhkZV*!(4F>q8T98GgHd{!TFu-S3Kxcf~XcYs15ar8%=I@i1hs_P9mD z%7e^f((^>fbDPTYnZZp6& zd7AqCOjeQ;<>-!OAyqbaF}ejj0%s?Kn6Of%dZXjZE_nKZq=IA?Hyr~C=RMM}-NHK3 zowcFr5*)TW2F%+Eww1B8iitWa>2uFwpO^GUr98eruoA9_-C#Q#6bk`_E|l0U%>^8u zpcEfyTtSJvozTu(gg^h=+L4gi0spB}{)ck^efx*Lxw_I{1^l(E_irTPOEO~b{?y~U z4ZPjy_zi79YCVX4Y?b_gPNjTX5Uc}!jg}0^LuGsvR z(uDg<%I*5iZ2`A4zuy9CiGK