From a2e7d7e7554894d4809c6b4d543fba0e63b98333 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Tue, 6 Oct 2026 22:06:20 +0200 Subject: [PATCH] feat: support modern statistical formula names --- CHANGELOG.md | 3 +++ .../formula/formula_functions_math.cpp | 21 ++++++++----------- .../formula/formula_functions_math_test.cpp | 17 +++++++++++++++ 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30646ac68..a6d27e9fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ The release run heads these entries with the version and opens a fresh ## Unreleased +- Spreadsheet recalculation recognizes STDEV.S, STDEV.P, VAR.S, and VAR.P, + including their Excel and LibreOffice compatibility prefixes. + - PDF streams support TIFF predictor samples packed at 1, 2, or 4 bits per component. LZW decoding rejects invalid width parameters and dictionary growth beyond the format's limit. diff --git a/src/odr/internal/formula/formula_functions_math.cpp b/src/odr/internal/formula/formula_functions_math.cpp index 27300d5c8..459f08da6 100644 --- a/src/odr/internal/formula/formula_functions_math.cpp +++ b/src/odr/internal/formula/formula_functions_math.cpp @@ -306,7 +306,7 @@ template Value kth(const Call &call) { /// The variance of the numbers, of a sample where @p sample. LibreOffice /// takes the mean with `KahanSum` and each deviation with `approxSub`. -Value variance_of(const Call &call, const bool sample, const bool root) { +template Value variance_of(const Call &call) { const Collected collected = collect(call); if (collected.error.has_value()) { return Value{*collected.error}; @@ -662,23 +662,20 @@ constexpr std::array entries{ FunctionEntry{"SINH", unary}, FunctionEntry{"SMALL", kth}, FunctionEntry{"SQRT", unary}, - FunctionEntry{ - "STDEV", - [](const Call &call) { return variance_of(call, true, true); }}, - FunctionEntry{ - "STDEVP", - [](const Call &call) { return variance_of(call, false, true); }}, + FunctionEntry{"STDEV", variance_of}, + FunctionEntry{"STDEV.P", variance_of}, + FunctionEntry{"STDEV.S", variance_of}, + FunctionEntry{"STDEVP", variance_of}, FunctionEntry{"SUM", sum}, FunctionEntry{"SUMPRODUCT", sum_product}, FunctionEntry{"SUMSQ", sum_of_squares}, FunctionEntry{"TAN", unary}, FunctionEntry{"TANH", unary}, FunctionEntry{"TRUNC", binary}, - FunctionEntry{ - "VAR", [](const Call &call) { return variance_of(call, true, false); }}, - FunctionEntry{ - "VARP", - [](const Call &call) { return variance_of(call, false, false); }}, + FunctionEntry{"VAR", variance_of}, + FunctionEntry{"VAR.P", variance_of}, + FunctionEntry{"VAR.S", variance_of}, + FunctionEntry{"VARP", variance_of}, }; } // namespace diff --git a/test/src/internal/formula/formula_functions_math_test.cpp b/test/src/internal/formula/formula_functions_math_test.cpp index 39a1a4a5e..f615861db 100644 --- a/test/src/internal/formula/formula_functions_math_test.cpp +++ b/test/src/internal/formula/formula_functions_math_test.cpp @@ -2,11 +2,13 @@ #include +#include #include #include #include #include #include +#include using namespace odr::internal::formula; using namespace odr::test::formula; @@ -231,3 +233,18 @@ TEST(FormulaFunctionsMath, the_blank_cells_of_a_whole_column_have_no_answer) { // the grid counts the cells past the extent of the sheet EXPECT_EQ(xlsx("=COUNTBLANK(A:A)"), std::nullopt); } + +TEST(FormulaFunctionsMath, modern_statistics_use_the_existing_implementations) { + const std::array names{std::pair{"STDEV.S", "STDEV"}, + std::pair{"STDEV.P", "STDEVP"}, + std::pair{"VAR.S", "VAR"}, std::pair{"VAR.P", "VARP"}}; + for (const auto &[modern, legacy] : names) { + SCOPED_TRACE(modern); + EXPECT_EQ(xlsx(std::string("=") + modern + "(2,4,6)"), + xlsx(std::string("=") + legacy + "(2,4,6)")); + EXPECT_EQ(xlsx(std::string("=_xlfn.") + modern + "(A1:A6)"), + xlsx(std::string("=") + legacy + "(A1:A6)")); + EXPECT_EQ(ods(std::string("=COM.MICROSOFT.") + modern + "(2;4;6)"), + ods(std::string("=") + legacy + "(2;4;6)")); + } +}