sc: wrap the whole @ operand in _xlfn.SINGLE, or none of it
The @ operand walk on OOXML save took the token behind the @ as the whole operand, so any operand longer than one token was cut apart by the wrapper: a leading sign, a structured table reference, a range built with the range operator, and a nested @ or # inside an argument list all left parts of themselves outside. The @ now writes no text of its own. The main token loop notes where the wrapper has to go and which parenthesis scope the operand lives in, and splices _xlfn.SINGLE(...) in once the operand ends. The # splice already worked that way, so there is one walk with one set of special cases and no recursion. Prefix signs and whitespace go inside the wrapper with the operand, and a sign moves the factor start past itself, so a # wraps the reference it follows and leaves the sign outside. Table reference brackets scope like parentheses, which keeps a multi-item reference whole. AND and OR are written as a name with an argument list, so such a name does not end the operand either and the whole call stays inside. An operand that writes no text leaves the wrapper out. One that is exactly one parenthesised group lends its parentheses to the wrapper, so a saved formula keeps its shape over round trips. Text that never parsed takes no wrapper and drops a # behind it, the way an ODF save leaves the @ out. The error strings for tokens the writer cannot spell out sit in AppendTokenOrError, which tests the cheap conditions first. The whitespace opcode test moved to opcode.hxx as isWhitespaceOpCode, replacing the private copies, and the operator range tests join it there as isUnaryOperatorOpCode and isBinaryOperatorOpCode. Unit tests pin the saved output for each operand shape. Signed-off-by: Tomaž Vajngerl <tomaz.vajngerl@collabora.co.uk> Change-Id: I9b6c12bbc8d0ddb9efa043a2364d13c4699e7bd5 Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/9290 Tested-by: Tomaž Vajngerl <tomaz.vajngerl@collabora.com> Reviewed-by: Mike Kaganski <mike.kaganski@collabora.com> (cherry picked from commit 2b8d9c58e8f7b5f138d44f41d756421c3f7932cd) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/209393 Tested-by: Jenkins Reviewed-by: Xisco Fauli <xiscofauli@libreoffice.org>
This commit is contained in:
committed by
Xisco Fauli
parent
c3dffb8998
commit
44c66f0b84
@@ -40,6 +40,7 @@
|
||||
#include <com/sun/star/sheet/FormulaMapGroupSpecialOffset.hpp>
|
||||
#include <algorithm>
|
||||
#include <mutex>
|
||||
#include <ranges>
|
||||
|
||||
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<PendingSingleValue>& 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<PendingSingleValue>& 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<PendingSingleValue>& 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<const FormulaErrorToken*>(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<sal_Int32> aFactorStarts;
|
||||
aFactorStarts.push_back(rBuffer.getLength());
|
||||
// The @ markers whose _xlfn.SINGLE wrapper is still to be spliced in.
|
||||
std::vector<PendingSingleValue> 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<const FormulaErrorToken*>(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<sal_Int32>(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())
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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<ScTokenArray>
|
||||
compileInNativeGrammar(ScDocument& rDocument, const OUString& rFormula, const ScAddress& rPosition)
|
||||
{
|
||||
ScCompiler aCompiler(rDocument, rPosition, formula::FormulaGrammar::GRAM_NATIVE, true, false);
|
||||
std::unique_ptr<ScTokenArray> 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<ScTokenArray> 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<ScTokenArray> 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<ScDBData> 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: */
|
||||
|
||||
Reference in New Issue
Block a user