formula/source/core/api/token.cxx                 |   35 ++++++++++++++++++++--
 include/formula/tokenarray.hxx                    |    1 
 sc/qa/unit/data/ods/tdf170565_empty_functions.ods |binary
 sc/qa/unit/subsequent_export_test4.cxx            |   24 +++++++++++++++
 4 files changed, 58 insertions(+), 2 deletions(-)

New commits:
commit 3cbf4957d33c7414b5cd02481136679b5c6a834e
Author:     Aron Budea <[email protected]>
AuthorDate: Mon Feb 2 22:46:23 2026 +1030
Commit:     Aron Budea <[email protected]>
CommitDate: Thu Feb 5 07:22:05 2026 +0100

    tdf#170565 save empty SUM/MAX/MIN functions with parameters to XLSX
    
    Excel won't allow them with no parameters.
    
    In Calc:
    - SUM() is 0,
    - MAX()/MIN() gives Err:511.
    
    Save them to XLSX as SUM(0), MAX(#VALUE!) and MIN(#VALUE!).
    
    Change-Id: I0f0d05290b67684df294611bd0612090abf1a672
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/198543
    Reviewed-by: Aron Budea <[email protected]>
    Tested-by: Jenkins

diff --git a/formula/source/core/api/token.cxx 
b/formula/source/core/api/token.cxx
index dffcb1b3ff26..9c41f2986c9f 100644
--- a/formula/source/core/api/token.cxx
+++ b/formula/source/core/api/token.cxx
@@ -937,6 +937,11 @@ FormulaToken* FormulaTokenArray::AddStringXML( const 
OUString& rStr )
     return Add( new FormulaStringOpToken( ocStringXML, svl::SharedString( rStr 
) ) );   // string not interned
 }
 
+FormulaToken* FormulaTokenArray::AddError( FormulaError nErr )
+{
+    return Add( new FormulaErrorToken( nErr ) );
+}
+
 FormulaToken* FormulaTokenArray::AddStringName( const OUString& rStr )
 {
     return Add( new FormulaStringOpToken( ocStringName, svl::SharedString( 
rStr ) ) );   // string not interned
@@ -969,7 +974,6 @@ void FormulaTokenArray::AddRecalcMode( ScRecalcMode nBits )
     SetCombinedBitsRecalcMode( nBits );
 }
 
-
 bool FormulaTokenArray::HasMatrixDoubleRefOps() const
 {
     if ( !pRPN || !nRPN )
@@ -1074,6 +1078,9 @@ inline bool MissingConventionOOXML::isRewriteNeeded( 
OpCode eOp )
     switch (eOp)
     {
         case ocIf:
+        case ocMax:
+        case ocMin:
+        case ocSum:
 
         case ocExternal:
         case ocEuroConvert:
@@ -1112,8 +1119,9 @@ class FormulaMissingContext
     public:
             const FormulaToken* mpFunc;
             int                 mnCurArg;
+            bool                mbEmpty;    // no parameters: ()
 
-                    void    Clear() { mpFunc = nullptr; mnCurArg = 0; }
+                    void    Clear() { mpFunc = nullptr; mnCurArg = 0; mbEmpty 
= false; }
             inline  bool    AddDefaultArg( FormulaTokenArray* pNewArr, int 
nArg, double f ) const;
                     bool    AddMissingExternal( FormulaTokenArray* pNewArr ) 
const;
                     bool    AddMissing( FormulaTokenArray *pNewArr, const 
MissingConvention & rConv  ) const;
@@ -1195,6 +1203,24 @@ void FormulaMissingContext::AddMoreArgs( 
FormulaTokenArray *pNewArr, const Missi
                         }
                         break;
 
+                    case ocMax:
+                    case ocMin:
+                        if ( mnCurArg == 0 && mbEmpty )
+                        {
+                            // Excel needs at least one parameter in MAX/MIN 
functions
+                            // In Calc empty MAX/MIN are Err:511
+                            pNewArr->AddError( FormulaError::NoValue );
+                        }
+                        break;
+
+                    case ocSum:
+                        if ( mnCurArg == 0 && mbEmpty )
+                        {
+                            // Excel needs at least one parameter in SUM 
function
+                            pNewArr->AddDouble( 0.0 );      // SUM() = 0
+                        }
+                        break;
+
                     case ocEuroConvert:
                         if ( mnCurArg == 2 )
                         {
@@ -1477,6 +1503,11 @@ FormulaTokenArray * FormulaTokenArray::RewriteMissing( 
const MissingConvention &
                     ++nFn;      // all following operations on _that_ function
                     pCtx[ nFn ].mpFunc = aIter.PeekPrevNoSpaces();
                     pCtx[ nFn ].mnCurArg = 0;
+                    FormulaToken* pNext = aIter.PeekNextNoSpaces();
+                    if (pNext != nullptr && pNext->GetOpCode() == ocClose)
+                        pCtx[ nFn ].mbEmpty = true;
+                    else
+                        pCtx[ nFn ].mbEmpty = false;
                     if (rConv.isPODF() && pCtx[ nFn ].mpFunc && pCtx[ nFn 
].mpFunc->GetOpCode() == ocAddress)
                         pOcas[ nOcas++ ] = nFn;     // entering ADDRESS() if 
PODF
                     else if ((rConv.isODFF() || rConv.isOOXML()) && pCtx[ nFn 
].mpFunc)
diff --git a/include/formula/tokenarray.hxx b/include/formula/tokenarray.hxx
index a623059d7288..e4e5f8d6d182 100644
--- a/include/formula/tokenarray.hxx
+++ b/include/formula/tokenarray.hxx
@@ -488,6 +488,7 @@ public:
     FormulaToken* AddBad( const OUString& rStr );          /// ocBad with 
OUString
     FormulaToken* AddStringXML( const OUString& rStr );    /// ocStringXML 
with OUString, temporary during import
     FormulaToken* AddStringName( const OUString& rStr );   /// ocStringName 
with OUString - Lambda functions
+    FormulaToken* AddError( FormulaError nErr );
 
     virtual FormulaToken* MergeArray( );
 
diff --git a/sc/qa/unit/data/ods/tdf170565_empty_functions.ods 
b/sc/qa/unit/data/ods/tdf170565_empty_functions.ods
new file mode 100644
index 000000000000..2297a76f9e62
Binary files /dev/null and b/sc/qa/unit/data/ods/tdf170565_empty_functions.ods 
differ
diff --git a/sc/qa/unit/subsequent_export_test4.cxx 
b/sc/qa/unit/subsequent_export_test4.cxx
index cc3d419d09c4..14e368e0255c 100644
--- a/sc/qa/unit/subsequent_export_test4.cxx
+++ b/sc/qa/unit/subsequent_export_test4.cxx
@@ -2476,6 +2476,30 @@ CPPUNIT_TEST_FIXTURE(ScExportTest4, 
testTdf165733_leap_day_BCE)
     }
 }
 
+CPPUNIT_TEST_FIXTURE(ScExportTest4, testTdf170565_empty_functions)
+{
+    createScDoc("ods/tdf170565_empty_functions.ods");
+
+    save(TestFilter::XLSX);
+    xmlDocUniquePtr pSheet = parseExport(u"xl/worksheets/sheet1.xml"_ustr);
+    CPPUNIT_ASSERT(pSheet);
+
+    // Without the fix these would've been exported as SUM(), MIN(), MAX(), 
which aren't valid in Excel
+    assertXPathContent(pSheet, "/x:worksheet/x:sheetData/x:row[1]/x:c[1]/x:f", 
u"SUM(0)");
+    assertXPathContent(pSheet, "/x:worksheet/x:sheetData/x:row[2]/x:c[1]/x:f", 
u"MIN(#VALUE!)");
+    assertXPathContent(pSheet, "/x:worksheet/x:sheetData/x:row[3]/x:c[1]/x:f", 
u"MAX(#VALUE!)");
+    // Further checks to ensure there are no regressions
+    assertXPathContent(pSheet, "/x:worksheet/x:sheetData/x:row[1]/x:c[2]/x:f", 
u"SUM(,)");
+    assertXPathContent(pSheet, "/x:worksheet/x:sheetData/x:row[1]/x:c[3]/x:f", 
u"SUM(100)");
+    assertXPathContent(pSheet, "/x:worksheet/x:sheetData/x:row[1]/x:c[4]/x:f", 
u"SUM(C1:C3)");
+    assertXPathContent(pSheet, "/x:worksheet/x:sheetData/x:row[2]/x:c[2]/x:f", 
u"MIN(,)");
+    assertXPathContent(pSheet, "/x:worksheet/x:sheetData/x:row[2]/x:c[3]/x:f", 
u"MIN(200)");
+    assertXPathContent(pSheet, "/x:worksheet/x:sheetData/x:row[2]/x:c[4]/x:f", 
u"MIN(C1:C3)");
+    assertXPathContent(pSheet, "/x:worksheet/x:sheetData/x:row[3]/x:c[2]/x:f", 
u"MAX(,)");
+    assertXPathContent(pSheet, "/x:worksheet/x:sheetData/x:row[3]/x:c[3]/x:f", 
u"MAX(-100)");
+    assertXPathContent(pSheet, "/x:worksheet/x:sheetData/x:row[3]/x:c[4]/x:f", 
u"MAX(C1:C3)");
+}
+
 CPPUNIT_PLUGIN_IMPLEMENT();
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */

Reply via email to