sc: parenthesize a reference union on OOXML save
OOXML writes the union operator as the same comma that separates function arguments, so a union with no parentheses of its own fell apart on save: SUM(A1~B2) became SUM(A1,B2), which reads as two arguments, and INDEX(A1:B3~D1:E3;1;1;2) changed its meaning even on Calc's own reload. A union already inside a parenthesised group was safe, since the group's tokens are kept. The walk now records where the union-level expression of each parenthesis scope starts. The first union operator opens a list there, and the list takes one pair of parentheses when the expression ends: at a looser operator, an argument separator, the parenthesis that closes the scope, or the end of the formula. A list that fills a grouping parenthesis exactly keeps that pair, so a formula holds its shape over repeated round trips. Two kinds of parenthesis cannot be reused that way: the one behind AND, OR or the call operator, which opens an argument list rather than a group, and one that an @ wrapper beginning at the same place has already taken. The group check behind the @ wrapper refuses a group with a comma directly inside it, passing over the commas an inline matrix or a bracketed reference carries. Text that no parse could read takes no parentheses at all, so a list reaching such text drops its pair. Unit tests pin the written string for each union shape, and the positions the inserted parenthesis shifts. A round-trip test checks the saved XML and the reloaded formulas and values. Signed-off-by: Tomaž Vajngerl <tomaz.vajngerl@collabora.co.uk> Change-Id: I970f3b4ecea680f6e70d146a2f1608c4adff9c1f Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/9308 Tested-by: Tomaž Vajngerl <tomaz.vajngerl@collabora.com> Reviewed-by: Mike Kaganski <mike.kaganski@collabora.com> (cherry picked from commit dd8104342d7147f07006bebb1dbeec60b48250f6) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/209411 Reviewed-by: Xisco Fauli <xiscofauli@libreoffice.org> Tested-by: Jenkins
This commit is contained in:
committed by
Xisco Fauli
parent
1a18114fce
commit
cccd6f9f1a
@@ -2884,6 +2884,9 @@ bool lclIsOneParenthesizedGroup(const OUStringBuffer& rBuffer, sal_Int32 nStart)
|
||||
if (nEnd - nStart < 2 || rBuffer[nStart] != '(' || rBuffer[nEnd - 1] != ')')
|
||||
return false;
|
||||
sal_Int32 nGroupDepth = 0;
|
||||
// The braces of an inline matrix and the brackets of a table or external
|
||||
// reference, which carry separators of their own.
|
||||
sal_Int32 nBracketDepth = 0;
|
||||
sal_Unicode cQuote = 0;
|
||||
for (sal_Int32 nIndex = nStart; nIndex < nEnd; ++nIndex)
|
||||
{
|
||||
@@ -2895,6 +2898,10 @@ bool lclIsOneParenthesizedGroup(const OUStringBuffer& rBuffer, sal_Int32 nStart)
|
||||
}
|
||||
else if (cChar == '"' || cChar == '\'')
|
||||
cQuote = cChar;
|
||||
else if (cChar == '{' || cChar == '[')
|
||||
++nBracketDepth;
|
||||
else if (cChar == '}' || cChar == ']')
|
||||
--nBracketDepth;
|
||||
else if (cChar == '(')
|
||||
++nGroupDepth;
|
||||
else if (cChar == ')')
|
||||
@@ -2903,6 +2910,13 @@ bool lclIsOneParenthesizedGroup(const OUStringBuffer& rBuffer, sal_Int32 nStart)
|
||||
if (nGroupDepth == 0)
|
||||
return nIndex == nEnd - 1;
|
||||
}
|
||||
else if (cChar == ',' && nGroupDepth == 1 && nBracketDepth == 0)
|
||||
{
|
||||
// A comma directly inside the group makes it a union list, whose
|
||||
// parentheses keep it one argument, so they cannot double as the
|
||||
// wrapper's own.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -2935,6 +2949,59 @@ void lclCancelSingleValues(std::vector<PendingSingleValue>& rPending, sal_Int32
|
||||
while (!rPending.empty() && rPending.back().mnScope >= nScope)
|
||||
rPending.pop_back();
|
||||
}
|
||||
|
||||
// Buffer positions for one open parenthesis scope. Parentheses are spliced in
|
||||
// once the text they enclose stands in the buffer, so the walk records where
|
||||
// each of them would go.
|
||||
struct UnionScope
|
||||
{
|
||||
// Where the scope's own text begins.
|
||||
sal_Int32 mnContentStart;
|
||||
// Where the union-level expression being written began.
|
||||
sal_Int32 mnExpressionStart;
|
||||
// Where an open union list began. -1 while the scope has none.
|
||||
sal_Int32 mnListStart;
|
||||
// The scope is a grouping parenthesis rather than a call's argument list.
|
||||
bool mbGroupParenthesis;
|
||||
};
|
||||
|
||||
// Whether a wrapper still to be spliced in begins where the scope's text does, so
|
||||
// the scope's own parentheses are already spoken for.
|
||||
bool lclHasWrapperAtStart(const std::vector<PendingSingleValue>& rPending, sal_Int32 nScope,
|
||||
sal_Int32 nContentStart)
|
||||
{
|
||||
for (const PendingSingleValue& rSingle : std::views::reverse(rPending))
|
||||
{
|
||||
if (rSingle.mnScope < nScope)
|
||||
break;
|
||||
if (rSingle.mnWrapStart == nContentStart)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wrap the scope's open union list in parentheses. A list that exactly fills a
|
||||
// grouping parenthesis keeps that pair when bMayTakeScopeParenthesis allows it.
|
||||
void lclCloseUnionList(OUStringBuffer& rBuffer, UnionScope& rScope,
|
||||
std::vector<PendingSingleValue>& rPending, bool bMayTakeScopeParenthesis)
|
||||
{
|
||||
if (rScope.mnListStart < 0)
|
||||
return;
|
||||
if (!(bMayTakeScopeParenthesis && rScope.mbGroupParenthesis
|
||||
&& rScope.mnListStart == rScope.mnContentStart))
|
||||
{
|
||||
rBuffer.insert(rScope.mnListStart, u"(");
|
||||
rBuffer.append(u")");
|
||||
// The opening parenthesis moves the text behind it along, so a wrapper
|
||||
// waiting inside the list follows its own operand.
|
||||
for (PendingSingleValue& rSingle : rPending)
|
||||
{
|
||||
if (rSingle.mnWrapStart > rScope.mnListStart)
|
||||
++rSingle.mnWrapStart;
|
||||
}
|
||||
}
|
||||
rScope.mnListStart = -1;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool FormulaCompiler::AppendTokenOrError(OUStringBuffer& rBuffer, const FormulaToken*& rpToken)
|
||||
@@ -3021,6 +3088,9 @@ void FormulaCompiler::CreateStringFromTokenArray( OUStringBuffer& rBuffer )
|
||||
aFactorStarts.push_back(rBuffer.getLength());
|
||||
// The @ markers whose _xlfn.SINGLE wrapper is still to be spliced in.
|
||||
std::vector<PendingSingleValue> aPendingSingles;
|
||||
// One entry per parenthesis scope, tracking the scope's union list.
|
||||
std::vector<UnionScope> aUnionScopes;
|
||||
aUnionScopes.push_back({ rBuffer.getLength(), rBuffer.getLength(), -1, false });
|
||||
// Whether the next token stands where an operand starts, so a plus or a
|
||||
// minus there is the unary form.
|
||||
bool bOperandPosition = true;
|
||||
@@ -3051,6 +3121,8 @@ void FormulaCompiler::CreateStringFromTokenArray( OUStringBuffer& rBuffer )
|
||||
// The consumed parenthesis opens a scope like a written one.
|
||||
lclMarkPendingOperand(aPendingSingles);
|
||||
aFactorStarts.push_back(rBuffer.getLength());
|
||||
aUnionScopes.push_back(
|
||||
{ rBuffer.getLength(), rBuffer.getLength(), -1, false });
|
||||
rBuffer.append(GetNativeSymbol(ocErrRef));
|
||||
bOperandPosition = false;
|
||||
bBadFactor = false;
|
||||
@@ -3102,7 +3174,12 @@ void FormulaCompiler::CreateStringFromTokenArray( OUStringBuffer& rBuffer )
|
||||
// behind it.
|
||||
const bool bPrefix = bOperandPosition && lclIsPrefixOperator(eOpHere);
|
||||
const bool bCallName = !bPrefix && lclIsCallBeforeArguments(eOpHere, maArrIterator);
|
||||
const bool bSeparator = !bPrefix && lclIsOoxmlFactorSeparator(eOpHere);
|
||||
const bool bSeparator
|
||||
= !bPrefix && !bCallName && lclIsOoxmlFactorSeparator(eOpHere);
|
||||
// The union list and the @ operand cover the same stretch of the
|
||||
// grammar, so the same tokens end both of them.
|
||||
const bool bEndsOperand
|
||||
= !bPrefix && !bCallName && lclEndsSingleValueOperand(eOpHere);
|
||||
|
||||
// A structured table reference walks through here token by token,
|
||||
// so its brackets scope like parentheses and the separators inside
|
||||
@@ -3118,6 +3195,10 @@ void FormulaCompiler::CreateStringFromTokenArray( OUStringBuffer& rBuffer )
|
||||
}
|
||||
lclMarkPendingOperand(aPendingSingles);
|
||||
aFactorStarts.push_back(rBuffer.getLength());
|
||||
// A grouping parenthesis stands where an operand starts; a call's
|
||||
// follows its function name.
|
||||
aUnionScopes.push_back({ rBuffer.getLength(), rBuffer.getLength(), -1,
|
||||
eOpHere == ocOpen && bOperandPosition });
|
||||
bOperandPosition = true;
|
||||
bBadFactor = false;
|
||||
continue;
|
||||
@@ -3126,9 +3207,14 @@ void FormulaCompiler::CreateStringFromTokenArray( OUStringBuffer& rBuffer )
|
||||
{
|
||||
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()));
|
||||
// The union list closes before the @ wrappers, so its
|
||||
// parentheses sit inside theirs.
|
||||
const sal_Int32 nScope = sal_Int32(aFactorStarts.size());
|
||||
const bool bMayTakeScopeParenthesis = !lclHasWrapperAtStart(
|
||||
aPendingSingles, nScope, aUnionScopes.back().mnContentStart);
|
||||
lclCloseUnionList(rBuffer, aUnionScopes.back(), aPendingSingles,
|
||||
bMayTakeScopeParenthesis);
|
||||
lclCloseSingleValues(rBuffer, aPendingSingles, nScope);
|
||||
}
|
||||
if (!AppendTokenOrError(rBuffer, t))
|
||||
{
|
||||
@@ -3137,21 +3223,27 @@ void FormulaCompiler::CreateStringFromTokenArray( OUStringBuffer& rBuffer )
|
||||
}
|
||||
if (aFactorStarts.size() > 1)
|
||||
aFactorStarts.pop_back();
|
||||
if (aUnionScopes.size() > 1)
|
||||
aUnionScopes.pop_back();
|
||||
lclMarkPendingOperand(aPendingSingles);
|
||||
bOperandPosition = false;
|
||||
bBadFactor = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// An operator the grammar puts above the @ ends its operand, so the
|
||||
// wrapper closes in front of it.
|
||||
if (!bPrefix && !bCallName && lclEndsSingleValueOperand(eOpHere))
|
||||
// An operator the grammar puts above them ends the union list and the
|
||||
// @ operand, so their wrappers close in front of it.
|
||||
if (bEndsOperand)
|
||||
{
|
||||
lclCloseUnionList(rBuffer, aUnionScopes.back(), aPendingSingles, false);
|
||||
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.
|
||||
// the wrappers waiting on it are left out.
|
||||
aUnionScopes.back().mnListStart = -1;
|
||||
lclCancelSingleValues(aPendingSingles, sal_Int32(aFactorStarts.size()));
|
||||
bBadFactor = true;
|
||||
}
|
||||
@@ -3170,6 +3262,18 @@ void FormulaCompiler::CreateStringFromTokenArray( OUStringBuffer& rBuffer )
|
||||
// outside the # wrapper of the operand it applies to.
|
||||
aFactorStarts.back() = rBuffer.getLength();
|
||||
}
|
||||
if (eOpHere == ocUnion && aUnionScopes.back().mnListStart < 0)
|
||||
{
|
||||
// The first union operator of the expression opens the list; the
|
||||
// ones after it keep extending the same list.
|
||||
aUnionScopes.back().mnListStart = aUnionScopes.back().mnExpressionStart;
|
||||
}
|
||||
if (bPrefix || (bEndsOperand && bSeparator))
|
||||
{
|
||||
// The next expression begins behind a prefix sign or an infix
|
||||
// operator. A postfix one follows its operand instead.
|
||||
aUnionScopes.back().mnExpressionStart = rBuffer.getLength();
|
||||
}
|
||||
if (bSeparator)
|
||||
bBadFactor = false;
|
||||
if (!bWhitespace)
|
||||
@@ -3182,9 +3286,14 @@ void FormulaCompiler::CreateStringFromTokenArray( OUStringBuffer& rBuffer )
|
||||
}
|
||||
|
||||
// Whatever is still pending wraps up to the end of the formula, unless the
|
||||
// formula came out as a single error string.
|
||||
// formula came out as a single error string. The union lists close first, so
|
||||
// their parentheses sit inside the @ wrappers.
|
||||
if (FormulaGrammar::isOOXML(meGrammar) && !bUnwritable)
|
||||
{
|
||||
for (UnionScope& rScope : std::views::reverse(aUnionScopes))
|
||||
lclCloseUnionList(rBuffer, rScope, aPendingSingles, false);
|
||||
lclCloseSingleValues(rBuffer, aPendingSingles, 1);
|
||||
}
|
||||
|
||||
if (pSaveArr != mpArr)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
||||
xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0"
|
||||
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
|
||||
xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2"
|
||||
office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.spreadsheet">
|
||||
<office:body>
|
||||
<office:spreadsheet>
|
||||
<table:table table:name="Sheet1">
|
||||
<table:table-column table:number-columns-repeated="6"/>
|
||||
<table:table-row>
|
||||
<table:table-cell office:value-type="float" office:value="1"><text:p>1</text:p></table:table-cell>
|
||||
<table:table-cell office:value-type="float" office:value="2"><text:p>2</text:p></table:table-cell>
|
||||
<table:table-cell/>
|
||||
<table:table-cell office:value-type="float" office:value="10"><text:p>10</text:p></table:table-cell>
|
||||
<table:table-cell office:value-type="float" office:value="20"><text:p>20</text:p></table:table-cell>
|
||||
<table:table-cell table:formula="of:=SUM([.A1]~[.B2])" office:value-type="float" office:value="5"/>
|
||||
</table:table-row>
|
||||
<table:table-row>
|
||||
<table:table-cell office:value-type="float" office:value="3"><text:p>3</text:p></table:table-cell>
|
||||
<table:table-cell office:value-type="float" office:value="4"><text:p>4</text:p></table:table-cell>
|
||||
<table:table-cell/>
|
||||
<table:table-cell office:value-type="float" office:value="11"><text:p>11</text:p></table:table-cell>
|
||||
<table:table-cell office:value-type="float" office:value="21"><text:p>21</text:p></table:table-cell>
|
||||
<table:table-cell table:formula="of:=AREAS([.A1:.A2]~[.B1:.B2])" office:value-type="float" office:value="2"/>
|
||||
</table:table-row>
|
||||
<table:table-row>
|
||||
<table:table-cell table:number-columns-repeated="3"/>
|
||||
<table:table-cell office:value-type="float" office:value="12"><text:p>12</text:p></table:table-cell>
|
||||
<table:table-cell office:value-type="float" office:value="22"><text:p>22</text:p></table:table-cell>
|
||||
<table:table-cell table:formula="of:=INDEX([.A1:.B3]~[.D1:.E3];1;1;2)" office:value-type="float" office:value="10"/>
|
||||
</table:table-row>
|
||||
</table:table>
|
||||
</office:spreadsheet>
|
||||
</office:body>
|
||||
</office:document>
|
||||
@@ -1418,6 +1418,43 @@ CPPUNIT_TEST_FIXTURE(ScExportTest4, testSingleOnSpilledRangeXlsxExport)
|
||||
u"_xlfn.SINGLE(_xlfn.ANCHORARRAY(A1))");
|
||||
}
|
||||
|
||||
CPPUNIT_TEST_FIXTURE(ScExportTest4, testReferenceUnionXlsxRoundTrip)
|
||||
{
|
||||
// OOXML spells the union operator with the same comma that separates
|
||||
// arguments, so a union saves as a parenthesized list that stays one
|
||||
// argument and survives the round trip.
|
||||
|
||||
// F1:F3 hold =SUM(A1~B2), =AREAS(A1:A2~B1:B2) and
|
||||
// =INDEX(A1:B3~D1:E3;1;1;2).
|
||||
createScDoc("fods/reference-union.fods");
|
||||
ScDocument* pDoc = getScDoc();
|
||||
CPPUNIT_ASSERT_EQUAL(5.0, pDoc->GetValue(ScAddress(5, 0, 0)));
|
||||
CPPUNIT_ASSERT_EQUAL(2.0, pDoc->GetValue(ScAddress(5, 1, 0)));
|
||||
CPPUNIT_ASSERT_EQUAL(10.0, pDoc->GetValue(ScAddress(5, 2, 0)));
|
||||
|
||||
save(TestFilter::XLSX);
|
||||
|
||||
xmlDocUniquePtr pSheet = parseExport(u"xl/worksheets/sheet1.xml"_ustr);
|
||||
CPPUNIT_ASSERT(pSheet);
|
||||
assertXPathContent(pSheet, "/x:worksheet/x:sheetData/x:row[1]/x:c[@r='F1']/x:f",
|
||||
u"SUM((A1,B2))");
|
||||
assertXPathContent(pSheet, "/x:worksheet/x:sheetData/x:row[2]/x:c[@r='F2']/x:f",
|
||||
u"AREAS((A1:A2,B1:B2))");
|
||||
assertXPathContent(pSheet, "/x:worksheet/x:sheetData/x:row[3]/x:c[@r='F3']/x:f",
|
||||
u"INDEX((A1:B3,D1:E3),1,1,2)");
|
||||
|
||||
// On reload each union is still one argument, now inside the parentheses
|
||||
// the save gave it, and the values are unchanged.
|
||||
saveAndReload(TestFilter::XLSX);
|
||||
pDoc = getScDoc();
|
||||
CPPUNIT_ASSERT_EQUAL(u"=SUM((A1~B2))"_ustr, pDoc->GetFormula(5, 0, 0));
|
||||
CPPUNIT_ASSERT_EQUAL(u"=AREAS((A1:A2~B1:B2))"_ustr, pDoc->GetFormula(5, 1, 0));
|
||||
CPPUNIT_ASSERT_EQUAL(u"=INDEX((A1:B3~D1:E3),1,1,2)"_ustr, pDoc->GetFormula(5, 2, 0));
|
||||
CPPUNIT_ASSERT_EQUAL(5.0, pDoc->GetValue(ScAddress(5, 0, 0)));
|
||||
CPPUNIT_ASSERT_EQUAL(2.0, pDoc->GetValue(ScAddress(5, 1, 0)));
|
||||
CPPUNIT_ASSERT_EQUAL(10.0, pDoc->GetValue(ScAddress(5, 2, 0)));
|
||||
}
|
||||
|
||||
CPPUNIT_PLUGIN_IMPLEMENT();
|
||||
|
||||
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
|
||||
|
||||
@@ -6972,9 +6972,13 @@ CPPUNIT_TEST_FIXTURE(TestFormula2, testImplicitIntersectionTakesOperandWhole)
|
||||
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.
|
||||
// An inline matrix reaches the writer as one push, brackets and all. Its column
|
||||
// separator is a comma, which does not turn a parenthesized matrix into a union
|
||||
// list, so the group's own pair still serves as the wrapper's.
|
||||
CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE({1,2})"_ustr,
|
||||
resolveAndPrintAsOoxml(*m_pDoc, u"{1;2}"_ustr));
|
||||
CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE({1,2})"_ustr,
|
||||
compileAndPrintAsOoxml(*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,
|
||||
@@ -7053,6 +7057,94 @@ CPPUNIT_TEST_FIXTURE(TestFormula2, testSpilledRangeBindsTighterThanSignAndMarker
|
||||
m_pDoc->DeleteTab(0);
|
||||
}
|
||||
|
||||
CPPUNIT_TEST_FIXTURE(TestFormula2, testUnionSavesAsParenthesizedListInOoxml)
|
||||
{
|
||||
// OOXML spells the union operator with the same comma that separates arguments,
|
||||
// so a union expression takes parentheses of its own to stay one argument.
|
||||
m_pDoc->InsertTab(0, u"Sheet1"_ustr);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(u"(A1,B2)"_ustr, compileAndPrintAsOoxml(*m_pDoc, u"A1~B2"_ustr));
|
||||
// One pair encloses the whole list, however many parts it has.
|
||||
CPPUNIT_ASSERT_EQUAL(u"(A1,B2,C3)"_ustr, compileAndPrintAsOoxml(*m_pDoc, u"A1~B2~C3"_ustr));
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(u"SUM((A1,B2))"_ustr, compileAndPrintAsOoxml(*m_pDoc, u"SUM(A1~B2)"_ustr));
|
||||
CPPUNIT_ASSERT_EQUAL(u"INDEX((A1:B3,D1:E3),1,1,2)"_ustr,
|
||||
compileAndPrintAsOoxml(*m_pDoc, u"INDEX(A1:B3~D1:E3;1;1;2)"_ustr));
|
||||
// AND and OR open an argument list too, although their opcodes are operators.
|
||||
CPPUNIT_ASSERT_EQUAL(u"AND((A1,B2))"_ustr, compileAndPrintAsOoxml(*m_pDoc, u"AND(A1~B2)"_ustr));
|
||||
CPPUNIT_ASSERT_EQUAL(u"OR((A1,B2))"_ustr, compileAndPrintAsOoxml(*m_pDoc, u"OR(A1~B2)"_ustr));
|
||||
|
||||
// A sign applies to the whole union, so the list's parentheses sit inside it.
|
||||
CPPUNIT_ASSERT_EQUAL(u"-(A1,B2)"_ustr, compileAndPrintAsOoxml(*m_pDoc, u"-A1~B2"_ustr));
|
||||
// The list ends where an operator of looser binding follows.
|
||||
CPPUNIT_ASSERT_EQUAL(u"(1+(A1,B2))"_ustr, compileAndPrintAsOoxml(*m_pDoc, u"(1+A1~B2)"_ustr));
|
||||
|
||||
// Not doubling the pair is what holds a formula's shape over round trips.
|
||||
CPPUNIT_ASSERT_EQUAL(u"(A1,B2)"_ustr, compileAndPrintAsOoxml(*m_pDoc, u"(A1~B2)"_ustr));
|
||||
CPPUNIT_ASSERT_EQUAL(u"SUM((A1,B2))"_ustr,
|
||||
compileAndPrintAsOoxml(*m_pDoc, u"SUM((A1~B2))"_ustr));
|
||||
|
||||
m_pDoc->DeleteTab(0);
|
||||
}
|
||||
|
||||
CPPUNIT_TEST_FIXTURE(TestFormula2, testUnionStaysInsideImplicitIntersectionWrapper)
|
||||
{
|
||||
// The union binds tighter than the @, so the parenthesized list sits inside the
|
||||
// _xlfn.SINGLE wrapper and keeps the wrapper to one argument.
|
||||
m_pDoc->InsertTab(0, u"Sheet1"_ustr);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE((A1,B2))"_ustr,
|
||||
compileAndPrintAsOoxml(*m_pDoc, u"@A1~B2"_ustr));
|
||||
CPPUNIT_ASSERT_EQUAL(u"_xlfn.SINGLE((A1,B2))"_ustr,
|
||||
compileAndPrintAsOoxml(*m_pDoc, u"@(A1~B2)"_ustr));
|
||||
CPPUNIT_ASSERT_EQUAL(u"SUM(_xlfn.SINGLE((A1,B2)))"_ustr,
|
||||
compileAndPrintAsOoxml(*m_pDoc, u"SUM(@A1~B2)"_ustr));
|
||||
|
||||
// With the @ inside the group, one pair of parentheses cannot serve both, so
|
||||
// the list takes a pair of its own.
|
||||
CPPUNIT_ASSERT_EQUAL(u"(_xlfn.SINGLE((A1,B2)))"_ustr,
|
||||
compileAndPrintAsOoxml(*m_pDoc, u"(@A1~B2)"_ustr));
|
||||
CPPUNIT_ASSERT_EQUAL(u"SUM((_xlfn.SINGLE((A1,B2))))"_ustr,
|
||||
compileAndPrintAsOoxml(*m_pDoc, u"SUM((@A1~B2))"_ustr));
|
||||
|
||||
m_pDoc->DeleteTab(0);
|
||||
}
|
||||
|
||||
CPPUNIT_TEST_FIXTURE(TestFormula2, testUnionParenthesesEncloseExactlyTheirList)
|
||||
{
|
||||
// The list's opening parenthesis moves the text behind it along, and a wrapper
|
||||
// spliced afterwards still lands where its own operand begins.
|
||||
m_pDoc->InsertTab(0, u"Sheet1"_ustr);
|
||||
|
||||
ScSingleRefData aFirst;
|
||||
aFirst.InitAddress(ScAddress(0, 0, 0));
|
||||
ScSingleRefData aSecond;
|
||||
aSecond.InitAddress(ScAddress(1, 1, 0));
|
||||
|
||||
// An @ behind the union operator, a shape no parse of a formula produces.
|
||||
ScTokenArray aMarkerInList(*m_pDoc);
|
||||
aMarkerInList.AddSingleReference(aFirst);
|
||||
aMarkerInList.AddOpCode(ocUnion);
|
||||
aMarkerInList.AddOpCode(ocSingleValue);
|
||||
aMarkerInList.AddSingleReference(aSecond);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(u"($A$1,_xlfn.SINGLE($B$2))"_ustr,
|
||||
printAsOoxml(*m_pDoc, aMarkerInList, ScAddress(1, 0, 0)));
|
||||
|
||||
// A percent sign in front of the union operator, likewise. The sign follows the
|
||||
// first part of the list, so the list still begins in front of that part.
|
||||
ScTokenArray aPercentInList(*m_pDoc);
|
||||
aPercentInList.AddSingleReference(aFirst);
|
||||
aPercentInList.AddOpCode(ocPercentSign);
|
||||
aPercentInList.AddOpCode(ocUnion);
|
||||
aPercentInList.AddSingleReference(aSecond);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(u"($A$1%,$B$2)"_ustr,
|
||||
printAsOoxml(*m_pDoc, aPercentInList, ScAddress(1, 0, 0)));
|
||||
|
||||
m_pDoc->DeleteTab(0);
|
||||
}
|
||||
|
||||
CPPUNIT_TEST_FIXTURE(TestFormula2, testWrappersDroppedFromTextThatDidNotParse)
|
||||
{
|
||||
// Text that no parse could read cannot take parentheses, so every wrapper around
|
||||
@@ -7082,6 +7174,16 @@ CPPUNIT_TEST_FIXTURE(TestFormula2, testWrappersDroppedFromTextThatDidNotParse)
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(u"PW value"_ustr, printAsOoxml(*m_pDoc, aSpilled, ScAddress(1, 0, 0)));
|
||||
|
||||
// A union list that reaches such text goes without its parentheses too.
|
||||
ScTokenArray aUnion(*m_pDoc);
|
||||
ScSingleRefData aReference;
|
||||
aReference.InitAddress(ScAddress(0, 0, 0));
|
||||
aUnion.AddSingleReference(aReference);
|
||||
aUnion.AddOpCode(ocUnion);
|
||||
aUnion.AddBad(u"PW value"_ustr);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(u"$A$1,PW value"_ustr, printAsOoxml(*m_pDoc, aUnion, ScAddress(1, 0, 0)));
|
||||
|
||||
m_pDoc->DeleteTab(0);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user