diff --git a/formula/source/core/api/FormulaCompiler.cxx b/formula/source/core/api/FormulaCompiler.cxx index 3a3601961414..909073e797ab 100644 --- a/formula/source/core/api/FormulaCompiler.cxx +++ b/formula/source/core/api/FormulaCompiler.cxx @@ -40,6 +40,7 @@ #include #include #include +#include namespace formula { @@ -1560,7 +1561,7 @@ bool FormulaCompiler::GetToken() nWasColRowName = 0; OpCode eTmpOp; mpToken = maArrIterator.Next(); - while (mpToken && ((eTmpOp = mpToken->GetOpCode()) == ocSpaces || eTmpOp == ocWhitespace)) + while (mpToken && isWhitespaceOpCode(eTmpOp = mpToken->GetOpCode())) { if (eTmpOp == ocSpaces) { @@ -2808,65 +2809,172 @@ void FormulaCompiler::CreateStringFromTokenArray( OUString& rFormula ) rFormula = aBuffer.makeStringAndClear(); } -void FormulaCompiler::EmitSingleValueOperandOOXML( - OUStringBuffer& rBuffer, const FormulaToken*& rpToken) +namespace { - // Emit one @ operand: a nested @, a function call, or a primary - // push. - if (!rpToken) - return; - if (rpToken->GetOpCode() == ocSingleValue) +// Operators that can stand in front of an operand. A sign in that position is +// unary, whichever of the two opcodes the parser gave it. The @ and the # get +// wrappers of their own, so they stay out. +bool lclIsPrefixOperator(OpCode eOp) +{ + if (eOp == ocSingleValue || eOp == ocSpill) + return false; + return eOp == ocAdd || eOp == ocSub || isUnaryOperatorOpCode(eOp); +} + +bool lclIsOoxmlFactorSeparator(OpCode eOp) +{ + // Tokens that end one factor and start the next at the same parenthesis + // depth, resetting where a # wrapper would splice. + if (isBinaryOperatorOpCode(eOp)) + return true; + return eOp == ocSep || eOp == ocArrayColSep || eOp == ocArrayRowSep; +} + +// AND, OR and the call operator have operator opcodes but are written as a +// call, so the parenthesis behind the name opens an argument list, not a group. +bool lclIsCallBeforeArguments(OpCode eOp, const FormulaTokenArrayPlainIterator& rIterator) +{ + if (eOp != ocAnd && eOp != ocOr && eOp != ocCall) + return false; + const FormulaToken* pNext = rIterator.PeekNextNoSpaces(); + return pNext && pNext->GetOpCode() == ocOpen; +} + +// Tokens that end the operand of an @ in front of them. Range, intersection, +// union and call bind tighter than the @ and stay inside. Everything above it +// in the grammar ends it. +bool lclEndsSingleValueOperand(OpCode eOp) +{ + if (eOp == ocRange || eOp == ocIntersect || eOp == ocUnion || eOp == ocCall) + return false; + if (isBinaryOperatorOpCode(eOp)) + return true; + return eOp == ocSep || eOp == ocArrayColSep || eOp == ocArrayRowSep + || eOp == ocPercentSign; +} + +// An @ whose wrapper is still to go in: where it goes, the parenthesis scope +// of its operand, and whether the operand has written text yet. An operand +// that stays empty takes no wrapper. +struct PendingSingleValue +{ + sal_Int32 mnScope; + sal_Int32 mnWrapStart; + bool mbHasOperand; +}; + +// Mark the pending wrappers' operands as written. Marks never come off, so +// the walk from the back can stop at the first marked one. +void lclMarkPendingOperand(std::vector& rPending) +{ + for (PendingSingleValue& rSingle : std::views::reverse(rPending)) { - rBuffer.append(u"_xlfn.SINGLE("); - rpToken = maArrIterator.Next(); - EmitSingleValueOperandOOXML(rBuffer, rpToken); - rBuffer.append(u")"); - return; - } - // Capture the splice point so a trailing # postfix can wrap the - // operand text we are about to emit with _xlfn.ANCHORARRAY(...). - sal_Int32 nOperandStart = rBuffer.getLength(); - // Primary push or function name. Emit the current token first. - rpToken = CreateStringFromToken(rBuffer, rpToken, true); - // If the next token is ocOpen, this token was a function name - // and the call's argument list still has to be emitted. - if (rpToken && rpToken->GetOpCode() == ocOpen) - { - sal_uInt16 nDepth = 0; - while (rpToken) - { - OpCode eOp = rpToken->GetOpCode(); - if (eOp == ocOpen) - ++nDepth; - rpToken = CreateStringFromToken(rBuffer, rpToken, true); - if (eOp == ocClose) - { - --nDepth; - if (nDepth == 0) - break; - } - } - } - // A trailing # postfix binds tighter than the surrounding @, so - // wrap the operand text we just emitted with the ANCHORARRAY - // wrapper before the outer SINGLE closes. - if (rpToken && rpToken->GetOpCode() == ocSpill) - { - rBuffer.insert(nOperandStart, u"_xlfn.ANCHORARRAY("); - rBuffer.append(u")"); - rpToken = maArrIterator.Next(); + if (rSingle.mbHasOperand) + break; + rSingle.mbHasOperand = true; } } -static bool lclIsOoxmlFactorSeparator(OpCode eOp) +// Whether the text from nStart to the end is one parenthesised group, so its +// parentheses can double as the wrapper's. Quoted literals are skipped. A +// doubled quote toggles the quote state twice, which cancels out. +bool lclIsOneParenthesizedGroup(const OUStringBuffer& rBuffer, sal_Int32 nStart) { - // Tokens that end one factor and start the next at the current - // parenthesis depth. The # postfix wraps the most recent factor, so the - // tracked position resets after any of these. - if (ocStartBinaryOperators <= eOp && eOp < ocStopBinaryOperators) - return true; - return eOp == ocSep || eOp == ocArrayColSep || eOp == ocArrayRowSep - || eOp == ocRange || eOp == ocUnion || eOp == ocIntersect; + const sal_Int32 nEnd = rBuffer.getLength(); + if (nEnd - nStart < 2 || rBuffer[nStart] != '(' || rBuffer[nEnd - 1] != ')') + return false; + sal_Int32 nGroupDepth = 0; + sal_Unicode cQuote = 0; + for (sal_Int32 nIndex = nStart; nIndex < nEnd; ++nIndex) + { + const sal_Unicode cChar = rBuffer[nIndex]; + if (cQuote != 0) + { + if (cChar == cQuote) + cQuote = 0; + } + else if (cChar == '"' || cChar == '\'') + cQuote = cChar; + else if (cChar == '(') + ++nGroupDepth; + else if (cChar == ')') + { + --nGroupDepth; + if (nGroupDepth == 0) + return nIndex == nEnd - 1; + } + } + return false; +} + +// Splice in the wrappers pending from nScope and deeper, innermost first. +// Each operand runs from its recorded start to the buffer's end. +void lclCloseSingleValues(OUStringBuffer& rBuffer, std::vector& rPending, + sal_Int32 nScope) +{ + while (!rPending.empty() && rPending.back().mnScope >= nScope) + { + const PendingSingleValue& rSingle = rPending.back(); + if (rSingle.mbHasOperand) + { + if (lclIsOneParenthesizedGroup(rBuffer, rSingle.mnWrapStart)) + rBuffer.insert(rSingle.mnWrapStart, u"_xlfn.SINGLE"); + else + { + rBuffer.insert(rSingle.mnWrapStart, u"_xlfn.SINGLE("); + rBuffer.append(u")"); + } + } + rPending.pop_back(); + } +} + +// Drop the wrappers pending from nScope and deeper without splicing them in. +void lclCancelSingleValues(std::vector& rPending, sal_Int32 nScope) +{ + while (!rPending.empty() && rPending.back().mnScope >= nScope) + rPending.pop_back(); +} +} // namespace + +bool FormulaCompiler::AppendTokenOrError(OUStringBuffer& rBuffer, const FormulaToken*& rpToken) +{ + const OpCode eOp = rpToken->GetOpCode(); + if (rpToken->GetType() == svByte) + { + if (eOp == ocNoName && FormulaGrammar::isOOXML(meGrammar)) + { + // A call with no name would come out as "#NAME!()", which no reader + // accepts, so the error string replaces the whole formula. + rBuffer.setLength(0); + rBuffer.append(GetNativeSymbol(ocErrRef)); + return false; + } + if (eOp == ocExternal || eOp == ocPush) + { + // A function token that lost its name. The error string stands in for + // this one token only. + rBuffer.append(GetNativeSymbol(ocErrRef)); + rpToken = maArrIterator.Next(); + return true; + } + } + else if (eOp == ocPush && rpToken->GetType() == svError + && static_cast(rpToken)->GetError() == FormulaError::NoName + && FormulaGrammar::isOOXML(meGrammar)) + { + const FormulaToken* pNext = maArrIterator.PeekNext(); + if (pNext && pNext->IsRef()) + { + // The error would come out as "#NAME?$C7", which no reader accepts, + // so the error string replaces the whole formula. + rBuffer.setLength(0); + rBuffer.append(GetNativeSymbol(ocErrName)); + return false; + } + } + rpToken = CreateStringFromToken(rBuffer, rpToken, true); + return true; } void FormulaCompiler::CreateStringFromTokenArray( OUStringBuffer& rBuffer ) @@ -2911,30 +3019,18 @@ void FormulaCompiler::CreateStringFromTokenArray( OUStringBuffer& rBuffer ) // the optional leading '='. std::vector aFactorStarts; aFactorStarts.push_back(rBuffer.getLength()); + // The @ markers whose _xlfn.SINGLE wrapper is still to be spliced in. + std::vector aPendingSingles; + // Whether the next token stands where an operand starts, so a plus or a + // minus there is the unary form. + bool bOperandPosition = true; + // Whether the current factor is raw text that never parsed, which no + // wrapper's parentheses can go around. + bool bBadFactor = false; + bool bUnwritable = false; const FormulaToken* t = maArrIterator.First(); while( t ) { - // Discard writing unknown functions without a name in OOXML ex: #NAME!() - if (FormulaGrammar::isOOXML(meGrammar) && t->GetOpCode() == ocNoName - && t->GetType() == svByte) - { - rBuffer.setLength(0); - rBuffer.append(GetNativeSymbol(ocErrRef)); - break; - } - // #NAME? followed by a ref produces invalid OOXML like "#NAME?$C7" - if (FormulaGrammar::isOOXML(meGrammar) && t->GetOpCode() == ocPush - && t->GetType() == svError - && static_cast(t)->GetError() == FormulaError::NoName) - { - FormulaToken* pNextToken = maArrIterator.PeekNext(); - if (pNextToken && pNextToken->IsRef()) - { - rBuffer.setLength(0); - rBuffer.append(GetNativeSymbol(ocErrName)); - break; - } - } if (FormulaGrammar::isOOXML(meGrammar) && t->GetOpCode() == ocOffset && t->GetType() == svByte) { @@ -2952,7 +3048,12 @@ void FormulaCompiler::CreateStringFromTokenArray( OUStringBuffer& rBuffer ) { rBuffer.append(GetNativeSymbol(t->GetOpCode())); rBuffer.append(GetNativeSymbol(pNext->GetOpCode())); + // The consumed parenthesis opens a scope like a written one. + lclMarkPendingOperand(aPendingSingles); + aFactorStarts.push_back(rBuffer.getLength()); rBuffer.append(GetNativeSymbol(ocErrRef)); + bOperandPosition = false; + bBadFactor = false; maArrIterator.Jump(aTempIter.GetIndex()); t = maArrIterator.Next(); continue; @@ -2960,37 +3061,32 @@ void FormulaCompiler::CreateStringFromTokenArray( OUStringBuffer& rBuffer ) } } } - if ((t->GetOpCode() == ocExternal || t->GetOpCode() == ocPush) && t->GetType() == svByte) + + // The @ writes no text of its own. Remember where its wrapper goes and + // the scope its operand lives in. The wrapper is spliced in once the + // operand stands in the buffer. + if (FormulaGrammar::isOOXML(meGrammar) && t->GetOpCode() == ocSingleValue) { - rBuffer.append(GetNativeSymbol(ocErrRef)); + aPendingSingles.push_back({ static_cast(aFactorStarts.size()), + rBuffer.getLength(), false }); t = maArrIterator.Next(); continue; } - // Wrap a bare or function-call @ operand in the parentheses - // that _xlfn.SINGLE needs. The symbol-table walk handles the - // parenthesised case on its own. - if (FormulaGrammar::isOOXML(meGrammar) && t->GetOpCode() == ocSingleValue - && maArrIterator.PeekNext() - && maArrIterator.PeekNext()->GetOpCode() != ocOpen) - { - aFactorStarts.back() = rBuffer.getLength(); - rBuffer.append(u"_xlfn.SINGLE("); - t = maArrIterator.Next(); - EmitSingleValueOperandOOXML(rBuffer, t); - rBuffer.append(u")"); - continue; - } - - // XLSX writes the # operator as _xlfn.ANCHORARRAY(operand). - // The operand is already in the buffer, so wrap it by splicing - // the prefix at the factor start and appending the close. + // XLSX writes the # operator as _xlfn.ANCHORARRAY around the operand + // already in the buffer, splicing the prefix at the factor start. Text + // that never parsed takes no parentheses, so a # behind it is dropped. if (FormulaGrammar::isOOXML(meGrammar) && t->GetOpCode() == ocSpill) { - rBuffer.insert(aFactorStarts.back(), u"_xlfn.ANCHORARRAY("); - rBuffer.append(u")"); + if (!bBadFactor) + { + rBuffer.insert(aFactorStarts.back(), u"_xlfn.ANCHORARRAY("); + rBuffer.append(u")"); + lclMarkPendingOperand(aPendingSingles); + aFactorStarts.back() = rBuffer.getLength(); + } t = maArrIterator.Next(); - aFactorStarts.back() = rBuffer.getLength(); + bOperandPosition = false; continue; } @@ -3000,33 +3096,96 @@ void FormulaCompiler::CreateStringFromTokenArray( OUStringBuffer& rBuffer ) // branch. ocOpen pushes a nested scope, ocClose pops it, // and a factor separator starts a fresh factor. const OpCode eOpHere = t->GetOpCode(); - const bool bSeparator - = lclIsOoxmlFactorSeparator(eOpHere); - if (eOpHere == ocOpen) + const bool bWhitespace = isWhitespaceOpCode(eOpHere); + // A sign where an operand starts can only be the unary form, + // whichever opcode the parser gave it, and belongs to the operand + // behind it. + const bool bPrefix = bOperandPosition && lclIsPrefixOperator(eOpHere); + const bool bCallName = !bPrefix && lclIsCallBeforeArguments(eOpHere, maArrIterator); + const bool bSeparator = !bPrefix && lclIsOoxmlFactorSeparator(eOpHere); + + // A structured table reference walks through here token by token, + // so its brackets scope like parentheses and the separators inside + // them stay inside the reference. + if (eOpHere == ocOpen || eOpHere == ocTableRefOpen) { - // Leave the outer factor start alone: a grouping '(' is - // already marked by the preceding separator, and a - // function call must be wrapped from its name. - t = CreateStringFromToken(rBuffer, t, true); + // Leave the outer factor start alone: a separator already + // marked a grouping '(', and a call is wrapped from its name. + if (!AppendTokenOrError(rBuffer, t)) + { + bUnwritable = true; + break; + } + lclMarkPendingOperand(aPendingSingles); aFactorStarts.push_back(rBuffer.getLength()); + bOperandPosition = true; + bBadFactor = false; continue; } - if (eOpHere == ocClose) + if (eOpHere == ocClose || eOpHere == ocTableRefClose) { - t = CreateStringFromToken(rBuffer, t, true); + if (aFactorStarts.size() > 1) + { + // A wrapper whose operand lives in this scope closes inside + // the parenthesis that ends the scope. + lclCloseSingleValues(rBuffer, aPendingSingles, sal_Int32(aFactorStarts.size())); + } + if (!AppendTokenOrError(rBuffer, t)) + { + bUnwritable = true; + break; + } if (aFactorStarts.size() > 1) aFactorStarts.pop_back(); + lclMarkPendingOperand(aPendingSingles); + bOperandPosition = false; + bBadFactor = false; continue; } - t = CreateStringFromToken(rBuffer, t, true); - if (bSeparator) + + // An operator the grammar puts above the @ ends its operand, so the + // wrapper closes in front of it. + if (!bPrefix && !bCallName && lclEndsSingleValueOperand(eOpHere)) + lclCloseSingleValues(rBuffer, aPendingSingles, sal_Int32(aFactorStarts.size())); + + if (eOpHere == ocBad) + { + // Raw text that no parse could read cannot take parentheses, so + // the @ markers waiting on it are left out. + lclCancelSingleValues(aPendingSingles, sal_Int32(aFactorStarts.size())); + bBadFactor = true; + } + + const sal_Int32 nLengthBefore = rBuffer.getLength(); + if (!AppendTokenOrError(rBuffer, t)) + { + bUnwritable = true; + break; + } + if (!bPrefix && !bWhitespace && rBuffer.getLength() > nLengthBefore) + lclMarkPendingOperand(aPendingSingles); + if (bSeparator || bPrefix) + { + // A separator starts a fresh factor, and a prefix sign stays + // outside the # wrapper of the operand it applies to. aFactorStarts.back() = rBuffer.getLength(); + } + if (bSeparator) + bBadFactor = false; + if (!bWhitespace) + bOperandPosition = bPrefix || bSeparator; continue; } - t = CreateStringFromToken(rBuffer, t, true); + if (!AppendTokenOrError(rBuffer, t)) + break; } + // Whatever is still pending wraps up to the end of the formula, unless the + // formula came out as a single error string. + if (FormulaGrammar::isOOXML(meGrammar) && !bUnwritable) + lclCloseSingleValues(rBuffer, aPendingSingles, 1); + if (pSaveArr != mpArr) { delete mpArr; @@ -3065,7 +3224,7 @@ const FormulaToken* FormulaCompiler::CreateStringFromToken( OUStringBuffer& rBuf if( bSpaces ) rBuffer.append( ' '); - if (eOp == ocSpaces || eOp == ocWhitespace) + if (isWhitespaceOpCode(eOp)) { bool bWriteSpaces = true; if (eOp == ocSpaces && mxSymbols->isODFF()) diff --git a/formula/source/core/api/token.cxx b/formula/source/core/api/token.cxx index ce57b304c28d..e611436fe420 100644 --- a/formula/source/core/api/token.cxx +++ b/formula/source/core/api/token.cxx @@ -1525,10 +1525,6 @@ FormulaTokenArray * FormulaTokenArray::RewriteMissing( const MissingConvention & return pNewArr; } -namespace { -inline bool isWhitespace( OpCode eOp ) { return eOp == ocSpaces || eOp == ocWhitespace; } -} - bool FormulaTokenArray::MayReferenceFollow() { if ( !pCode || nLen <= 0 ) @@ -1536,11 +1532,11 @@ bool FormulaTokenArray::MayReferenceFollow() // ignore trailing spaces sal_uInt16 i = nLen - 1; - while (i > 0 && isWhitespace( pCode[i]->GetOpCode())) + while (i > 0 && isWhitespaceOpCode( pCode[i]->GetOpCode())) { --i; } - if (i > 0 || !isWhitespace( pCode[i]->GetOpCode())) + if (i > 0 || !isWhitespaceOpCode( pCode[i]->GetOpCode())) { OpCode eOp = pCode[i]->GetOpCode(); if ( (ocStartBinaryOperators <= eOp && eOp < ocStopBinaryOperators ) || @@ -1865,8 +1861,10 @@ FormulaToken* FormulaTokenArrayPlainIterator::NextNoSpaces() { if( mpFTA->GetArray() ) { - while ((mnIndex < mpFTA->GetLen()) && isWhitespace( mpFTA->GetArray()[ mnIndex ]->GetOpCode())) + while ((mnIndex < mpFTA->GetLen()) && isWhitespaceOpCode(mpFTA->GetArray()[mnIndex]->GetOpCode())) + { ++mnIndex; + } if( mnIndex < mpFTA->GetLen() ) return mpFTA->GetArray()[ mnIndex++ ]; } @@ -1902,7 +1900,7 @@ FormulaToken* FormulaTokenArrayPlainIterator::PeekNextNoSpaces() const if( mpFTA->GetArray() && mnIndex < mpFTA->GetLen() ) { sal_uInt16 j = mnIndex; - while (j < mpFTA->GetLen() && isWhitespace( mpFTA->GetArray()[j]->GetOpCode())) + while (j < mpFTA->GetLen() && isWhitespaceOpCode( mpFTA->GetArray()[j]->GetOpCode())) j++; if ( j < mpFTA->GetLen() ) return mpFTA->GetArray()[ j ]; @@ -1918,9 +1916,9 @@ FormulaToken* FormulaTokenArrayPlainIterator::PeekPrevNoSpaces() const if( mpFTA->GetArray() && mnIndex > 1 ) { sal_uInt16 j = mnIndex - 2; - while (isWhitespace( mpFTA->GetArray()[j]->GetOpCode()) && j > 0 ) + while (isWhitespaceOpCode( mpFTA->GetArray()[j]->GetOpCode()) && j > 0 ) j--; - if (j > 0 || !isWhitespace( mpFTA->GetArray()[j]->GetOpCode())) + if (j > 0 || !isWhitespaceOpCode( mpFTA->GetArray()[j]->GetOpCode())) return mpFTA->GetArray()[ j ]; else return nullptr; diff --git a/include/formula/FormulaCompiler.hxx b/include/formula/FormulaCompiler.hxx index e812047bdb02..7e1185fbcd2c 100644 --- a/include/formula/FormulaCompiler.hxx +++ b/include/formula/FormulaCompiler.hxx @@ -298,10 +298,10 @@ public: bool bAllowArrAdvance = false ); private: - // Emit a non-parenthesised @ operand on OOXML save. The caller - // supplies the wrapper's parentheses. Advances maArrIterator past - // the operand and leaves rpToken at the next token. - void EmitSingleValueOperandOOXML(OUStringBuffer& rBuffer, const FormulaToken*& rpToken); + // Write one token, or the error string that stands in for it, and + // advance rpToken. When this returns false, the buffer holds the + // error string that replaces the whole formula. + bool AppendTokenOrError(OUStringBuffer& rBuffer, const FormulaToken*& rpToken); public: void AppendBoolean( OUStringBuffer& rBuffer, bool bVal ) const; diff --git a/include/formula/opcode.hxx b/include/formula/opcode.hxx index 6fb3aebaa84e..fa158cb6e62c 100644 --- a/include/formula/opcode.hxx +++ b/include/formula/opcode.hxx @@ -585,6 +585,21 @@ enum OpCode : sal_uInt16 ocNone = 0xFFFF }; +// Whether the opcode is one of the two whitespace token forms. +constexpr bool isWhitespaceOpCode(OpCode eOp) { return eOp == ocSpaces || eOp == ocWhitespace; } + +// Whether the opcode is a unary operator. +constexpr bool isUnaryOperatorOpCode(OpCode eOp) +{ + return ocStartUnaryOperators <= eOp && eOp < ocStopUnaryOperators; +} + +// Whether the opcode is a binary operator. +constexpr bool isBinaryOperatorOpCode(OpCode eOp) +{ + return ocStartBinaryOperators <= eOp && eOp < ocStopBinaryOperators; +} + // Only to be used for debugging output. No guarantee of stability of the // return value. diff --git a/sc/qa/unit/ucalc_formula2.cxx b/sc/qa/unit/ucalc_formula2.cxx index 17185b726368..cb4cf4907ec3 100644 --- a/sc/qa/unit/ucalc_formula2.cxx +++ b/sc/qa/unit/ucalc_formula2.cxx @@ -6846,6 +6846,337 @@ CPPUNIT_TEST_FIXTURE(TestFormula2, testUndoDeleteTabRestoresCrossSheetFormula) m_pDoc->DeleteTab(0); } +namespace +{ +// Read a token array back as an OOXML formula string, the way an XLSX save does. +OUString printAsOoxml(ScDocument& rDocument, ScTokenArray& rCode, const ScAddress& rPosition) +{ + sc::CompileFormulaContext aContext(rDocument, formula::FormulaGrammar::GRAM_OOXML); + ScCompiler aCompiler(aContext, rPosition, rCode); + OUStringBuffer aBuffer; + aCompiler.CreateStringFromTokenArray(aBuffer); + return aBuffer.makeStringAndClear(); +} + +// Add a call whose function name is gone, the shape an xls with an unknown function +// leaves behind. +void addCallWithoutName(ScTokenArray& rCode) +{ + rCode.AddOpCode(ocNoName); + rCode.AddOpCode(ocOpen); + ScSingleRefData aReference; + aReference.InitAddress(ScAddress(0, 0, 0)); + rCode.AddSingleReference(aReference); + rCode.AddOpCode(ocClose); +} + +std::unique_ptr +compileInNativeGrammar(ScDocument& rDocument, const OUString& rFormula, const ScAddress& rPosition) +{ + ScCompiler aCompiler(rDocument, rPosition, formula::FormulaGrammar::GRAM_NATIVE, true, false); + std::unique_ptr pCode = aCompiler.CompileString(rFormula); + CPPUNIT_ASSERT(pCode); + return pCode; +} + +// Compile a formula in native grammar and print it as OOXML. +OUString compileAndPrintAsOoxml(ScDocument& rDocument, const OUString& rFormula) +{ + const ScAddress aPosition(1, 0, 0); + std::unique_ptr pCode = compileInNativeGrammar(rDocument, rFormula, aPosition); + return printAsOoxml(rDocument, *pCode, aPosition); +} + +// The same, with the @ marker an xls import adds put in first. +OUString resolveAndPrintAsOoxml(ScDocument& rDocument, const OUString& rFormula) +{ + const ScAddress aPosition(1, 0, 0); + std::unique_ptr pCode = compileInNativeGrammar(rDocument, rFormula, aPosition); + ScFormulaCell::ResolveImplicitIntersection(*pCode, rDocument, aPosition); + return printAsOoxml(rDocument, *pCode, aPosition); +} + +} // end anonymous namespace + +CPPUNIT_TEST_FIXTURE(TestFormula2, testCallWithoutNameSavesAsErrorInOoxml) +{ + // A call with no function name has no spelling in OOXML, so the whole formula + // saves as a plain error string. + m_pDoc->InsertTab(0, u"Sheet1"_ustr); + + ScTokenArray aCode(*m_pDoc); + addCallWithoutName(aCode); + + CPPUNIT_ASSERT_EQUAL(u"#REF!"_ustr, printAsOoxml(*m_pDoc, aCode, ScAddress(1, 0, 0))); + + // The error stands for the whole formula, so an @ in front of the call goes + // with it. + ScTokenArray aMarkedCode(*m_pDoc); + aMarkedCode.AddOpCode(ocSingleValue); + addCallWithoutName(aMarkedCode); + + CPPUNIT_ASSERT_EQUAL(u"#REF!"_ustr, printAsOoxml(*m_pDoc, aMarkedCode, ScAddress(1, 0, 0))); + + m_pDoc->DeleteTab(0); +} + +CPPUNIT_TEST_FIXTURE(TestFormula2, testImplicitIntersectionWrapsPrefixOperatorWithItsOperand) +{ + // The sign belongs inside the wrapper, however many signs there are and + // whatever they apply to. + m_pDoc->InsertTab(0, u"Sheet1"_ustr); + + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(-A1:A3)"_ustr, + resolveAndPrintAsOoxml(*m_pDoc, u"-A1:A3"_ustr)); + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(+A1:A3)"_ustr, + resolveAndPrintAsOoxml(*m_pDoc, u"+A1:A3"_ustr)); + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(--A1:A3)"_ustr, + resolveAndPrintAsOoxml(*m_pDoc, u"--A1:A3"_ustr)); + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(-TRANSPOSE(A1:A3))"_ustr, + resolveAndPrintAsOoxml(*m_pDoc, u"-TRANSPOSE(A1:A3)"_ustr)); + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(-(A1:A3))"_ustr, + resolveAndPrintAsOoxml(*m_pDoc, u"-(A1:A3)"_ustr)); + + // A sign parsed with the @ already in front of it gets the binary opcode rather + // than the negation one, and still belongs inside the wrapper. + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(-A1:A3)"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"@-A1:A3"_ustr)); + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(--A1:A3)"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"@--A1:A3"_ustr)); + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(-SUM(A1:A3))"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"@-SUM(A1:A3)"_ustr)); + + // A sign between two @ markers applies to what the inner one produces, so it + // sits between the two wrappers. + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(-_xlfn.SINGLE(A1:A3))"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"@-@A1:A3"_ustr)); + + // A sign in front of an @ the formula already carries applies to the value the + // @ produces, so it stays outside the wrapper. + CPPUNIT_ASSERT_EQUAL(u"-_xlfn.SINGLE(A1:A3)"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"-@A1:A3"_ustr)); + + m_pDoc->DeleteTab(0); +} + +CPPUNIT_TEST_FIXTURE(TestFormula2, testImplicitIntersectionTakesOperandWhole) +{ + // The wrapper encloses the whole operand, whatever form the operand takes. + m_pDoc->InsertTab(0, u"Sheet1"_ustr); + m_pDoc->GetRangeName()->insert( + new ScRangeData(*m_pDoc, u"myrange"_ustr, u"$Sheet1.$A$1:$A$3"_ustr, ScAddress(1, 0, 0))); + + // A name reaches the writer as its own token rather than as a push. + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(myrange)"_ustr, + resolveAndPrintAsOoxml(*m_pDoc, u"myrange"_ustr)); + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(-myrange)"_ustr, + resolveAndPrintAsOoxml(*m_pDoc, u"-myrange"_ustr)); + + // An inline matrix reaches the writer as one push, brackets and all. + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE({1,2})"_ustr, + resolveAndPrintAsOoxml(*m_pDoc, u"{1;2}"_ustr)); + + // Whitespace between the @ and its operand goes inside the wrapper. + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE( A1:A3)"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"@ A1:A3"_ustr)); + + // An operand built with the range operator keeps all of itself inside. + CPPUNIT_ASSERT_EQUAL(u"A1:INDEX(B1:B10,5)"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"A1:INDEX(B1:B10;5)"_ustr)); + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(A1:INDEX(B1:B10,5))"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"@A1:INDEX(B1:B10;5)"_ustr)); + // The @ the resolve step adds to such a formula wraps the same way. + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(A1:INDEX(B1:B10,5))"_ustr, + resolveAndPrintAsOoxml(*m_pDoc, u"A1:INDEX(B1:B10;5)"_ustr)); + + // Whitespace between a function name and its parenthesis is dropped on save, + // and the call stays inside the wrapper as if it had never been there. + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(SUM(A1:A3))"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"@SUM (A1:A3)"_ustr)); + + // AND and OR carry operator opcodes although they are written as calls, so + // neither name ends the operand in front of it. + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(AND(A1:A3))"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"@AND(A1:A3)"_ustr)); + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(OR(A1:A3))"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"@OR(A1:A3)"_ustr)); + + m_pDoc->DeleteTab(0); +} + +CPPUNIT_TEST_FIXTURE(TestFormula2, testImplicitIntersectionMarkersWrapOneAnother) +{ + // Each @ takes a wrapper of its own around the one behind it. + m_pDoc->InsertTab(0, u"Sheet1"_ustr); + + ScTokenArray aCode(*m_pDoc); + aCode.AddOpCode(ocSingleValue); + aCode.AddOpCode(ocSingleValue); + ScSingleRefData aReference; + aReference.InitAddress(ScAddress(0, 0, 0)); + aCode.AddSingleReference(aReference); + + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(_xlfn.SINGLE($A$1))"_ustr, + printAsOoxml(*m_pDoc, aCode, ScAddress(1, 0, 0))); + + // An @ on an argument inside a wrapped call takes a wrapper of its own too. + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(SUM(_xlfn.SINGLE(A1:A3)))"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"@SUM(@A1:A3)"_ustr)); + + m_pDoc->DeleteTab(0); +} + +CPPUNIT_TEST_FIXTURE(TestFormula2, testSpilledRangeBindsTighterThanSignAndMarker) +{ + // The # postfix reads as _xlfn.ANCHORARRAY around the operand it follows, and + // binds tighter than the sign or the @ in front of that operand. + m_pDoc->InsertTab(0, u"Sheet1"_ustr); + + // The sign stays outside the wrapper. + CPPUNIT_ASSERT_EQUAL(u"-_xlfn.ANCHORARRAY(A1)"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"-A1#"_ustr)); + + // The @ wrapper goes around the # one, whatever form the operand takes. + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(_xlfn.ANCHORARRAY(A1))"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"@A1#"_ustr)); + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(_xlfn.ANCHORARRAY((A1:A3)))"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"@(A1:A3)#"_ustr)); + + // Whitespace between the operand and the # stays with the operand. + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(_xlfn.ANCHORARRAY(A1 ))"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"@A1 #"_ustr)); + + // A # on an argument inside a wrapped call wraps just that argument. + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(SUM(_xlfn.ANCHORARRAY(A1)))"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"@SUM(A1#)"_ustr)); + + m_pDoc->DeleteTab(0); +} + +CPPUNIT_TEST_FIXTURE(TestFormula2, testWrappersDroppedFromTextThatDidNotParse) +{ + // Text that no parse could read cannot take parentheses, so every wrapper around + // it is left out, however many markers stand in front of it or follow it. + m_pDoc->InsertTab(0, u"Sheet1"_ustr); + + // A prefix sign is kept while the @ is left out. + ScTokenArray aSigned(*m_pDoc); + aSigned.AddOpCode(ocSingleValue); + aSigned.AddOpCode(ocAdd); + aSigned.AddBad(u"PW value"_ustr); + + CPPUNIT_ASSERT_EQUAL(u"+PW value"_ustr, printAsOoxml(*m_pDoc, aSigned, ScAddress(1, 0, 0))); + + ScTokenArray aNestedMarkers(*m_pDoc); + aNestedMarkers.AddOpCode(ocSingleValue); + aNestedMarkers.AddOpCode(ocSingleValue); + aNestedMarkers.AddBad(u"PW value"_ustr); + + CPPUNIT_ASSERT_EQUAL(u"PW value"_ustr, + printAsOoxml(*m_pDoc, aNestedMarkers, ScAddress(1, 0, 0))); + + ScTokenArray aSpilled(*m_pDoc); + aSpilled.AddOpCode(ocSingleValue); + aSpilled.AddBad(u"PW value"_ustr); + aSpilled.AddOpCode(ocSpill); + + CPPUNIT_ASSERT_EQUAL(u"PW value"_ustr, printAsOoxml(*m_pDoc, aSpilled, ScAddress(1, 0, 0))); + + m_pDoc->DeleteTab(0); +} + +CPPUNIT_TEST_FIXTURE(TestFormula2, testImplicitIntersectionDroppedWhenItHasNoOperand) +{ + // An @ with nothing behind it to enclose is left out of the saved formula. + m_pDoc->InsertTab(0, u"Sheet1"_ustr); + + ScTokenArray aMarkerOnly(*m_pDoc); + aMarkerOnly.AddOpCode(ocSingleValue); + + CPPUNIT_ASSERT_EQUAL(OUString(), printAsOoxml(*m_pDoc, aMarkerOnly, ScAddress(1, 0, 0))); + + // The same holds when the operand behind the @ is another @ with nothing of its + // own behind it. + ScTokenArray aMarkersOnly(*m_pDoc); + aMarkersOnly.AddOpCode(ocSingleValue); + aMarkersOnly.AddOpCode(ocSingleValue); + + CPPUNIT_ASSERT_EQUAL(OUString(), printAsOoxml(*m_pDoc, aMarkersOnly, ScAddress(1, 0, 0))); + + // A missing argument writes nothing, so it gives the wrapper nothing to enclose + // either. + ScTokenArray aMissingOperand(*m_pDoc); + aMissingOperand.AddOpCode(ocSingleValue); + aMissingOperand.AddOpCode(ocMissing); + + CPPUNIT_ASSERT_EQUAL(OUString(), printAsOoxml(*m_pDoc, aMissingOperand, ScAddress(1, 0, 0))); + + m_pDoc->DeleteTab(0); +} + +CPPUNIT_TEST_FIXTURE(TestFormula2, testImplicitIntersectionWrapsWholeTableReference) +{ + // A structured table reference is more than one token, and the wrapper goes around + // all of them. + m_pDoc->InsertTab(0, u"Sheet1"_ustr); + + // A two-column table over A1:B4 whose header row names the columns. + std::unique_ptr pData(new ScDBData(u"table"_ustr, 0, 0, 0, 1, 3)); + CPPUNIT_ASSERT(m_pDoc->GetDBCollection()->getNamedDBs().insert(std::move(pData))); + m_pDoc->SetString(ScAddress(0, 0, 0), u"Header1"_ustr); + m_pDoc->SetString(ScAddress(1, 0, 0), u"Header2"_ustr); + + CPPUNIT_ASSERT_EQUAL(u"table[[Header1]]"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"table[[Header1]]"_ustr)); + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(table[[Header1]])"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"@table[[Header1]]"_ustr)); + + // A reference of several bracketed items has a separator of its own inside, and + // still goes into the wrapper whole. + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(table[[#Data],[Header1]])"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"@table[[#Data];[Header1]]"_ustr)); + + m_pDoc->DeleteTab(0); +} + +CPPUNIT_TEST_FIXTURE(TestFormula2, testImplicitIntersectionKeepsOffsetParameterErrorRewrite) +{ + // An OFFSET call whose first argument is not a reference saves that argument as a + // reference error, with or without an @ in front of the call. + m_pDoc->InsertTab(0, u"Sheet1"_ustr); + + CPPUNIT_ASSERT_EQUAL(u"OFFSET(#REF!,0,2)"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"OFFSET(\"x\";0;2)"_ustr)); + CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE(OFFSET(#REF!,0,2))"_ustr, + compileAndPrintAsOoxml(*m_pDoc, u"@OFFSET(\"x\";0;2)"_ustr)); + + m_pDoc->DeleteTab(0); +} + +CPPUNIT_TEST_FIXTURE(TestFormula2, testLongImplicitIntersectionChainSavesCompletely) +{ + // A formula may carry as many @ markers as the token array holds, and the writer + // gets through all of them. + m_pDoc->InsertTab(0, u"Sheet1"_ustr); + + ScTokenArray aCode(*m_pDoc); + // 30000 pairs stay under the 65535 token limit while going deeper than any stack + // of nested calls can follow. + for (sal_Int32 nPair = 0; nPair < 30000; ++nPair) + { + aCode.AddOpCode(ocSingleValue); + aCode.AddOpCode(ocAdd); + } + ScSingleRefData aReference; + aReference.InitAddress(ScAddress(0, 0, 0)); + aCode.AddSingleReference(aReference); + + const OUString aFormula = printAsOoxml(*m_pDoc, aCode, ScAddress(1, 0, 0)); + CPPUNIT_ASSERT(!aFormula.isEmpty()); + + m_pDoc->DeleteTab(0); +} + CPPUNIT_PLUGIN_IMPLEMENT(); /* vim:set shiftwidth=4 softtabstop=4 expandtab: */