sc: add the # postfix operator for spilled-range references

A1# is the postfix counterpart of the @ prefix. When A1 is
the origin of a dynamic-array formula spilled into A1:A4, A1#
refers to the whole spill range, so SUM(A1#) totals the four
values and ROWS(A1#) returns 4.

The new opcode ocSpill sits in the unary-operator range. The
tokenizer produces it from a bare # after a reference or
value. Otherwise # stays the start of a #REF! or #NAME!
token. The compiler routes it through PostOpLine so it
follows its operand in the RPN.

ScSpilledRange pushes the spill range as an ScMatrixRangeToken
so aggregating functions see the whole range, and ISREF
reports it as a reference. A master yields its matrix. A
plain number, string, or non-master matrix slot yields the
the cell value.

A freshly typed =A1# counts as an array result, so it
promotes to a dynamic-array master and spills again. A cell
that does not promote collapses the range by implicit
intersection to the row-aligned value, or #VALUE! outside the
range, keeping that cell's number format.

Signed-off-by: Tomaž Vajngerl <tomaz.vajngerl@collabora.co.uk>
Change-Id: Idf2752af3f7575f6bc95b940344aa365415e4a2f
Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/5785
Reviewed-by: Mike Kaganski <mike.kaganski@collabora.com>
Tested-by: Tomaž Vajngerl <tomaz.vajngerl@collabora.com>
(cherry picked from commit 9b97b0fc91bce4b8726ea210347e8ea84270e148)
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/207904
Tested-by: Xisco Fauli <xiscofauli@libreoffice.org>
Reviewed-by: Xisco Fauli <xiscofauli@libreoffice.org>
Tested-by: Jenkins
This commit is contained in:
Tomaž Vajngerl
2026-07-10 01:08:46 +02:00
committed by Xisco Fauli
parent f0b1496469
commit 6612d4f785
11 changed files with 530 additions and 11 deletions
+6
View File
@@ -69,6 +69,7 @@ const std::pair<const char *, int> RID_STRLIST_FUNCTION_NAMES_ENGLISH_ODFF[] =
{ "NEG" , ocNeg },
{ "-" , ocNegSub },
{ "@" , ocSingleValue },
{ "#" , ocSpill },
{ "PI" , ocPi },
{ "RAND" , ocRandom },
{ "TRUE" , ocTrue },
@@ -551,6 +552,7 @@ const std::pair<const char *, int> RID_STRLIST_FUNCTION_NAMES_ENGLISH_OOXML[] =
{ "NEG" , ocNeg },
{ "-" , ocNegSub },
{ "@" , ocSingleValue },
{ "#" , ocSpill },
{ "PI" , ocPi },
{ "RAND" , ocRandom },
{ "TRUE" , ocTrue },
@@ -1037,6 +1039,7 @@ const std::pair<const char *, int> RID_STRLIST_FUNCTION_NAMES_ENGLISH_PODF[] =
{ "NEG" , ocNeg },
{ "-" , ocNegSub },
{ "@" , ocSingleValue },
{ "#" , ocSpill },
{ "PI" , ocPi },
{ "RAND" , ocRandom },
{ "TRUE" , ocTrue },
@@ -1521,6 +1524,7 @@ const std::pair<const char *, int> RID_STRLIST_FUNCTION_NAMES_ENGLISH_API[] =
{ "NEG" , ocNeg },
{ "-" , ocNegSub },
{ "@" , ocSingleValue },
{ "#" , ocSpill },
{ "PI" , ocPi },
{ "RAND" , ocRandom },
{ "TRUE" , ocTrue },
@@ -2003,6 +2007,7 @@ const std::pair<const char *, int> RID_STRLIST_FUNCTION_NAMES_ENGLISH[] =
{ "NEG" , ocNeg },
{ "-" , ocNegSub },
{ "@" , ocSingleValue },
{ "#" , ocSpill },
{ "PI" , ocPi },
{ "RAND" , ocRandom },
{ "TRUE" , ocTrue },
@@ -2947,6 +2952,7 @@ const std::pair<const char *, int> RID_STRLIST_FUNCTION_NAMES_SYMBOLS[] =
{ ":" , ocRange },
{ "-" , ocNegSub },
{ "@" , ocSingleValue },
{ "#" , ocSpill },
{ nullptr, -1 }
};
+14 -5
View File
@@ -57,6 +57,15 @@ class FormulaCompilerRecursionGuard
~FormulaCompilerRecursionGuard() { --rRecursion; }
};
// True when the opcode needs an operand after it: the prefix unary
// and binary operators. The postfix # is excluded, as its operand
// comes before it, so a binary operator may follow it.
bool isOperatorExpectingOperand(OpCode eOp)
{
return ocStartBinaryOperators <= eOp && eOp < ocStopUnaryOperators
&& eOp != ocSpill;
}
SvNumFormatType lcl_GetRetFormat( OpCode eOpCode )
{
switch (eOpCode)
@@ -2439,8 +2448,8 @@ void FormulaCompiler::UnaryLine()
void FormulaCompiler::PostOpLine()
{
UnaryLine();
while ( mpToken->GetOpCode() == ocPercentSign )
{ // this operator _follows_ its operand
while (mpToken->GetOpCode() == ocPercentSign || mpToken->GetOpCode() == ocSpill)
{ // these operators _follow_ their operand
if (mbComputeII)
{
FormulaToken** pArg = mpCode - 1;
@@ -3214,11 +3223,11 @@ OpCode FormulaCompiler::NextToken()
// There must be an operator before a push
if ( (eOp == ocPush || eOp == ocColRowNameAuto) &&
!( (meLastOp == ocOpen) || (meLastOp == ocSep) ||
(ocStartBinaryOperators <= meLastOp && meLastOp < ocStopUnaryOperators)) )
isOperatorExpectingOperand(meLastOp)) )
SetError( FormulaError::OperatorExpected);
// Operator and Plus => operator
if (eOp == ocAdd && (meLastOp == ocOpen || meLastOp == ocSep ||
(ocStartBinaryOperators <= meLastOp && meLastOp < ocStopUnaryOperators)))
isOperatorExpectingOperand(meLastOp)))
{
FormulaCompilerRecursionGuard aRecursionGuard( mnRecursion );
eOp = NextToken();
@@ -3230,7 +3239,7 @@ OpCode FormulaCompiler::NextToken()
if ( eOp != ocAnd && eOp != ocOr &&
(ocStartBinaryOperators <= eOp && eOp < ocStopBinaryOperators )
&& (meLastOp == ocOpen || meLastOp == ocSep ||
(ocStartBinaryOperators <= meLastOp && meLastOp < ocStopUnaryOperators)))
isOperatorExpectingOperand(meLastOp)))
{
SetError( FormulaError::VariableExpected);
if ( mbAutoCorrect && !mpStack )
+5 -1
View File
@@ -117,7 +117,10 @@ enum OpCode : sal_uInt16
ocNegSub = 70,
// The @ single-value prefix operator (implicit intersection).
ocSingleValue = 71,
ocStopUnaryOperators = 72,
// The # postfix spilled-range operator: A1# expands to the
// spill range of the dynamic-array formula at A1.
ocSpill = 72,
ocStopUnaryOperators = 73,
ocStartFunction = 75,
@@ -656,6 +659,7 @@ inline std::string OpCodeEnumToString(OpCode eCode)
case ocUnion: return "Union";
case ocRange: return "Range";
case ocCall: return "Call";
case ocSpill: return "SpilledRange";
case ocNot: return "Not";
case ocNeg: return "Neg";
case ocNegSub: return "NegSub";
+14
View File
@@ -0,0 +1,14 @@
# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*-
#*************************************************************************
#
# This file is part of the LibreOffice project.
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
#*************************************************************************
$(eval $(call sc_ucalc_test,_spilled_range))
# vim: set noet sw=4 ts=4:
+1
View File
@@ -51,6 +51,7 @@ $(eval $(call gb_Module_add_check_targets,sc,\
CppunitTest_sc_ucalc_tablestyles \
CppunitTest_sc_ucalc_formula \
CppunitTest_sc_ucalc_formula2 \
CppunitTest_sc_ucalc_spilled_range \
CppunitTest_sc_ucalc_parallelism \
CppunitTest_sc_ucalc_pivottable \
CppunitTest_sc_ucalc_rangelst \
+367
View File
@@ -0,0 +1,367 @@
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
* Copyright the LibreOffice contributors.
*
* SPDX-License-Identifier: MPL-2.0
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include "helper/qahelper.hxx"
#include <compiler.hxx>
#include <formulacell.hxx>
#include <patattr.hxx>
#include <scitems.hxx>
#include <scopetools.hxx>
#include <tokenarray.hxx>
#include <formula/errorcodes.hxx>
#include <i18nlangtag/lang.h>
#include <svl/intitem.hxx>
#include <svl/numformat.hxx>
#include <svl/zforlist.hxx>
#include <memory>
using namespace formula;
class TestSpilledRange : public ScUcalcTestBase
{
};
CPPUNIT_TEST_FIXTURE(TestSpilledRange, testSpilledRangeOperator)
{
// The # postfix operator on the origin of a 1x4 matrix formula
// expands to the spill range, so SUM(A1#) totals all four values.
sc::AutoCalcSwitch aACSwitch(*m_pDoc, true);
m_pDoc->InsertTab(0, u"Sheet1"_ustr);
m_pDoc->SetValue(ScAddress(1, 0, 0), 10.0);
m_pDoc->SetValue(ScAddress(1, 1, 0), 20.0);
m_pDoc->SetValue(ScAddress(1, 2, 0), 30.0);
m_pDoc->SetValue(ScAddress(1, 3, 0), 40.0);
ScMarkData aMark(m_pDoc->GetSheetLimits());
aMark.SelectOneTable(0);
m_pDoc->InsertMatrixFormula(0, 0, 0, 3, aMark, u"=B1:B4"_ustr);
ScFormulaCell* pFC = m_pDoc->GetFormulaCell(ScAddress(0, 0, 0));
CPPUNIT_ASSERT(pFC);
CPPUNIT_ASSERT_EQUAL(ScMatrixMode::Formula, pFC->GetMatrixFlag());
SCCOL nCols = 0;
SCROW nRows = 0;
pFC->GetMatColsRows(nCols, nRows);
CPPUNIT_ASSERT_EQUAL(static_cast<SCCOL>(1), nCols);
CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(4), nRows);
m_pDoc->SetString(ScAddress(2, 0, 0), u"=SUM(A1#)"_ustr);
CPPUNIT_ASSERT_EQUAL(100.0, m_pDoc->GetValue(ScAddress(2, 0, 0)));
m_pDoc->SetString(ScAddress(2, 1, 0), u"=COUNT(A1#)"_ustr);
CPPUNIT_ASSERT_EQUAL(4.0, m_pDoc->GetValue(ScAddress(2, 1, 0)));
CPPUNIT_ASSERT_EQUAL(u"=SUM(A1#)"_ustr, m_pDoc->GetFormula(2, 0, 0));
m_pDoc->DeleteTab(0);
}
CPPUNIT_TEST_FIXTURE(TestSpilledRange, testSpilledRangeOperatorOnNumberCell)
{
// # on a plain value cell reports #REF!. The operator requires
// its operand to be the master cell of a dynamic-array formula.
sc::AutoCalcSwitch aACSwitch(*m_pDoc, true);
m_pDoc->InsertTab(0, u"Sheet1"_ustr);
m_pDoc->SetValue(ScAddress(0, 0, 0), 42.0);
m_pDoc->SetString(ScAddress(1, 0, 0), u"=A1#"_ustr);
CPPUNIT_ASSERT_EQUAL(FormulaError::NoRef, m_pDoc->GetErrCode(ScAddress(1, 0, 0)));
m_pDoc->DeleteTab(0);
}
CPPUNIT_TEST_FIXTURE(TestSpilledRange, testSpilledRangeOperatorOnStringCell)
{
// # on a plain string cell reports #REF!.
sc::AutoCalcSwitch aACSwitch(*m_pDoc, true);
m_pDoc->InsertTab(0, u"Sheet1"_ustr);
m_pDoc->SetString(ScAddress(0, 0, 0), u"hello"_ustr);
m_pDoc->SetString(ScAddress(1, 0, 0), u"=A1#"_ustr);
CPPUNIT_ASSERT_EQUAL(FormulaError::NoRef, m_pDoc->GetErrCode(ScAddress(1, 0, 0)));
m_pDoc->DeleteTab(0);
}
CPPUNIT_TEST_FIXTURE(TestSpilledRange, testSpilledRangeOperatorOnNonMasterFormula)
{
// # on a plain formula cell that is not a dynamic-array master
// falls back to that cell's value. Only a master triggers the
// spill expansion.
sc::AutoCalcSwitch aACSwitch(*m_pDoc, true);
m_pDoc->InsertTab(0, u"Sheet1"_ustr);
m_pDoc->SetString(ScAddress(0, 0, 0), u"=1+2"_ustr);
m_pDoc->SetString(ScAddress(1, 0, 0), u"=A1#"_ustr);
CPPUNIT_ASSERT_EQUAL(3.0, m_pDoc->GetValue(ScAddress(1, 0, 0)));
m_pDoc->DeleteTab(0);
}
CPPUNIT_TEST_FIXTURE(TestSpilledRange, testSpilledRangeOperatorImplicitIntersection)
{
// A # reference in a cell that does not promote to a dynamic-array
// master reduces by implicit intersection on the spill range. The
// cell takes the slot of the spill range aligned with the formula
// position, or #VALUE! when the formula sits outside the spill
// range. Cells set through SetString are not eligible for
// promotion, so they take this path.
sc::AutoCalcSwitch aACSwitch(*m_pDoc, true);
m_pDoc->InsertTab(0, u"Sheet1"_ustr);
// Master at A1 spills =B1:B3 with 10, 20 and 30 into A1:A3.
m_pDoc->SetValue(ScAddress(1, 0, 0), 10.0);
m_pDoc->SetValue(ScAddress(1, 1, 0), 20.0);
m_pDoc->SetValue(ScAddress(1, 2, 0), 30.0);
ScMarkData aMark(m_pDoc->GetSheetLimits());
aMark.SelectOneTable(0);
m_pDoc->InsertMatrixFormula(0, 0, 0, 2, aMark, u"=B1:B3"_ustr);
// The reference cells E1..E3 sit in rows aligned with the spill
// range. Each picks the slot of A1# matching its own row. E4
// sits one row past the spill range, so the cell reports
// #VALUE! through implicit intersection failure.
m_pDoc->SetString(ScAddress(4, 0, 0), u"=A1#"_ustr);
m_pDoc->SetString(ScAddress(4, 1, 0), u"=A1#"_ustr);
m_pDoc->SetString(ScAddress(4, 2, 0), u"=A1#"_ustr);
m_pDoc->SetString(ScAddress(4, 3, 0), u"=A1#"_ustr);
CPPUNIT_ASSERT_EQUAL(10.0, m_pDoc->GetValue(ScAddress(4, 0, 0)));
CPPUNIT_ASSERT_EQUAL(20.0, m_pDoc->GetValue(ScAddress(4, 1, 0)));
CPPUNIT_ASSERT_EQUAL(30.0, m_pDoc->GetValue(ScAddress(4, 2, 0)));
CPPUNIT_ASSERT_EQUAL(FormulaError::NoValue, m_pDoc->GetErrCode(ScAddress(4, 3, 0)));
m_pDoc->DeleteTab(0);
}
CPPUNIT_TEST_FIXTURE(TestSpilledRange, testSpilledRangeOperatorRespills)
{
// A freshly typed =A1# intends an array result, so it promotes to
// a dynamic-array master and spills the referenced range again
// instead of collapsing to the row-aligned value.
m_pDoc->SetAutoCalc(false);
m_pDoc->InsertTab(0, u"Sheet1"_ustr);
m_pDoc->SetValue(ScAddress(1, 0, 0), 10.0);
m_pDoc->SetValue(ScAddress(1, 1, 0), 20.0);
m_pDoc->SetValue(ScAddress(1, 2, 0), 30.0);
// Master at A1 spills =B1:B3 into A1:A3.
ScMarkData aMark(m_pDoc->GetSheetLimits());
aMark.SelectOneTable(0);
m_pDoc->InsertMatrixFormula(0, 0, 0, 2, aMark, u"=B1:B3"_ustr);
// Enter =A1# at E1 the way the UI does: eligible for promotion.
ScAddress aRefPos(4, 0, 0);
ScCompiler aComp(*m_pDoc, aRefPos, m_pDoc->GetGrammar(), false, false);
std::unique_ptr<ScTokenArray> pCode = aComp.CompileString(u"=A1#"_ustr);
auto pRef = new ScFormulaCell(*m_pDoc, aRefPos, std::move(pCode));
pRef->SetAutoDynamicArrayEligible(true);
m_pDoc->SetFormulaCell(aRefPos, pRef);
m_pDoc->SetAutoCalc(true);
m_pDoc->CalcAll();
// E1 became a master covering three rows and spilled its values
// into E1:E3.
CPPUNIT_ASSERT(pRef->IsDynamicArrayMaster());
SCCOL nCols = 0;
SCROW nRows = 0;
pRef->GetMatColsRows(nCols, nRows);
CPPUNIT_ASSERT_EQUAL(static_cast<SCCOL>(1), nCols);
CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(3), nRows);
CPPUNIT_ASSERT_EQUAL(10.0, m_pDoc->GetValue(ScAddress(4, 0, 0)));
CPPUNIT_ASSERT_EQUAL(20.0, m_pDoc->GetValue(ScAddress(4, 1, 0)));
CPPUNIT_ASSERT_EQUAL(30.0, m_pDoc->GetValue(ScAddress(4, 2, 0)));
m_pDoc->DeleteTab(0);
}
CPPUNIT_TEST_FIXTURE(TestSpilledRange, testSpilledRangeOperatorIsRef)
{
// A # spilled-range reference answers ISREF as a reference, even
// though it evaluates to a matrix. A plain computed matrix is not
// a reference.
sc::AutoCalcSwitch aACSwitch(*m_pDoc, true);
m_pDoc->InsertTab(0, u"Sheet1"_ustr);
m_pDoc->SetValue(ScAddress(1, 0, 0), 10.0);
m_pDoc->SetValue(ScAddress(1, 1, 0), 20.0);
// Master at A1 spills =B1:B2 into A1:A2.
ScMarkData aMark(m_pDoc->GetSheetLimits());
aMark.SelectOneTable(0);
m_pDoc->InsertMatrixFormula(0, 0, 0, 1, aMark, u"=B1:B2"_ustr);
m_pDoc->SetString(ScAddress(4, 0, 0), u"=ISREF(A1#)"_ustr);
CPPUNIT_ASSERT_EQUAL(1.0, m_pDoc->GetValue(ScAddress(4, 0, 0)));
// A matrix that carries no source range stays a non-reference.
m_pDoc->SetString(ScAddress(4, 1, 0), u"=ISREF(MUNIT(2))"_ustr);
CPPUNIT_ASSERT_EQUAL(0.0, m_pDoc->GetValue(ScAddress(4, 1, 0)));
m_pDoc->DeleteTab(0);
}
CPPUNIT_TEST_FIXTURE(TestSpilledRange, testSpilledRangeOperatorInheritsFormat)
{
// A # reference that collapses to a single value keeps the number
// format of the spill-range cell it resolves to, the same as a
// plain reference to that cell would. A fractional value also
// survives without being truncated to a whole number.
sc::AutoCalcSwitch aACSwitch(*m_pDoc, true);
m_pDoc->InsertTab(0, u"Sheet1"_ustr);
m_pDoc->SetValue(ScAddress(1, 0, 0), 43831.5);
m_pDoc->SetValue(ScAddress(1, 1, 0), 43832.5);
m_pDoc->SetValue(ScAddress(1, 2, 0), 43833.5);
// Master at A1 spills =B1:B3 into A1:A3.
ScMarkData aMark(m_pDoc->GetSheetLimits());
aMark.SelectOneTable(0);
m_pDoc->InsertMatrixFormula(0, 0, 0, 2, aMark, u"=B1:B3"_ustr);
// Give the spill cells a date format.
SvNumberFormatter* pFormatter = m_pDoc->GetFormatTable();
sal_uInt32 nDateFormat = pFormatter->GetFormatIndex(NF_DATE_SYS_DDMMYYYY, LANGUAGE_ENGLISH_US);
ScPatternAttr aFormatAttr(m_pDoc->getCellAttributeHelper());
aFormatAttr.ItemSetPut(SfxUInt32Item(ATTR_VALUE_FORMAT, nDateFormat));
m_pDoc->ApplyPatternAreaTab(0, 0, 0, 2, 0, aFormatAttr);
// E2 collapses A1# to the row-aligned slot, keeping its value and
// date format.
m_pDoc->SetString(ScAddress(4, 1, 0), u"=A1#"_ustr);
CPPUNIT_ASSERT_EQUAL(43832.5, m_pDoc->GetValue(ScAddress(4, 1, 0)));
CPPUNIT_ASSERT_EQUAL(SvNumFormatType::DATE,
pFormatter->GetType(m_pDoc->GetNumberFormat(4, 1, 0)));
m_pDoc->DeleteTab(0);
}
CPPUNIT_TEST_FIXTURE(TestSpilledRange, testSpilledRangeOperatorMultiColumn)
{
// # on the origin of a 2x3 matrix expands to all six cells.
sc::AutoCalcSwitch aACSwitch(*m_pDoc, true);
m_pDoc->InsertTab(0, u"Sheet1"_ustr);
m_pDoc->SetValue(ScAddress(2, 0, 0), 1.0);
m_pDoc->SetValue(ScAddress(3, 0, 0), 2.0);
m_pDoc->SetValue(ScAddress(2, 1, 0), 3.0);
m_pDoc->SetValue(ScAddress(3, 1, 0), 4.0);
m_pDoc->SetValue(ScAddress(2, 2, 0), 5.0);
m_pDoc->SetValue(ScAddress(3, 2, 0), 6.0);
ScMarkData aMark(m_pDoc->GetSheetLimits());
aMark.SelectOneTable(0);
m_pDoc->InsertMatrixFormula(0, 0, 1, 2, aMark, u"=C1:D3"_ustr);
m_pDoc->SetString(ScAddress(4, 0, 0), u"=SUM(A1#)"_ustr);
CPPUNIT_ASSERT_EQUAL(21.0, m_pDoc->GetValue(ScAddress(4, 0, 0)));
m_pDoc->SetString(ScAddress(4, 1, 0), u"=ROWS(A1#)"_ustr);
CPPUNIT_ASSERT_EQUAL(3.0, m_pDoc->GetValue(ScAddress(4, 1, 0)));
m_pDoc->SetString(ScAddress(4, 2, 0), u"=COLUMNS(A1#)"_ustr);
CPPUNIT_ASSERT_EQUAL(2.0, m_pDoc->GetValue(ScAddress(4, 2, 0)));
m_pDoc->DeleteTab(0);
}
CPPUNIT_TEST_FIXTURE(TestSpilledRange, testSpilledRangeOperatorOnNonMasterMatrixCell)
{
// # on a non-master slot of a spilled matrix reports #REF!. Only
// the master cell of the dynamic-array formula is a valid operand
// for the spill expansion.
sc::AutoCalcSwitch aACSwitch(*m_pDoc, true);
m_pDoc->InsertTab(0, u"Sheet1"_ustr);
m_pDoc->SetValue(ScAddress(1, 0, 0), 10.0);
m_pDoc->SetValue(ScAddress(1, 1, 0), 20.0);
m_pDoc->SetValue(ScAddress(1, 2, 0), 30.0);
ScMarkData aMark(m_pDoc->GetSheetLimits());
aMark.SelectOneTable(0);
m_pDoc->InsertMatrixFormula(0, 0, 0, 2, aMark, u"=B1:B3"_ustr);
// A2 is a non-master slot of the A1:A3 spill range, so =A2# is
// an illegal reference.
m_pDoc->SetString(ScAddress(2, 0, 0), u"=A2#"_ustr);
CPPUNIT_ASSERT_EQUAL(FormulaError::NoRef, m_pDoc->GetErrCode(ScAddress(2, 0, 0)));
m_pDoc->DeleteTab(0);
}
CPPUNIT_TEST_FIXTURE(TestSpilledRange, testSpilledRangeOperatorEmptyCell)
{
// # on an empty cell reports #REF!. An empty cell is not a
// dynamic-array master, so the operator has no spill range
// to expand.
sc::AutoCalcSwitch aACSwitch(*m_pDoc, true);
m_pDoc->InsertTab(0, u"Sheet1"_ustr);
m_pDoc->SetString(ScAddress(1, 0, 0), u"=A1#"_ustr);
CPPUNIT_ASSERT_EQUAL(FormulaError::NoRef, m_pDoc->GetErrCode(ScAddress(1, 0, 0)));
m_pDoc->DeleteTab(0);
}
CPPUNIT_TEST_FIXTURE(TestSpilledRange, testSpilledRangeOperatorInExpression)
{
// # composes with arithmetic and aggregating functions.
sc::AutoCalcSwitch aACSwitch(*m_pDoc, true);
m_pDoc->InsertTab(0, u"Sheet1"_ustr);
m_pDoc->SetValue(ScAddress(1, 0, 0), 5.0);
m_pDoc->SetValue(ScAddress(1, 1, 0), 15.0);
ScMarkData aMark(m_pDoc->GetSheetLimits());
aMark.SelectOneTable(0);
m_pDoc->InsertMatrixFormula(0, 0, 0, 1, aMark, u"=B1:B2"_ustr);
m_pDoc->SetString(ScAddress(2, 0, 0), u"=SUM(A1#)+100"_ustr);
CPPUNIT_ASSERT_EQUAL(120.0, m_pDoc->GetValue(ScAddress(2, 0, 0)));
CPPUNIT_ASSERT_EQUAL(u"=SUM(A1#)+100"_ustr, m_pDoc->GetFormula(2, 0, 0));
m_pDoc->DeleteTab(0);
}
CPPUNIT_TEST_FIXTURE(TestSpilledRange, testSpilledRangeOperatorFormulaStringRoundtrip)
{
// The # operator round-trips through GetFormula across several
// common shapes: standalone, inside SUM/AVERAGE, and on both
// operands of a binary expression.
sc::AutoCalcSwitch aACSwitch(*m_pDoc, true);
m_pDoc->InsertTab(0, u"Sheet1"_ustr);
m_pDoc->SetString(ScAddress(0, 0, 0), u"=A2#"_ustr);
CPPUNIT_ASSERT_EQUAL(u"=A2#"_ustr, m_pDoc->GetFormula(0, 0, 0));
m_pDoc->SetString(ScAddress(0, 1, 0), u"=SUM(B1#)"_ustr);
CPPUNIT_ASSERT_EQUAL(u"=SUM(B1#)"_ustr, m_pDoc->GetFormula(0, 1, 0));
m_pDoc->SetString(ScAddress(0, 2, 0), u"=AVERAGE(C5#)"_ustr);
CPPUNIT_ASSERT_EQUAL(u"=AVERAGE(C5#)"_ustr, m_pDoc->GetFormula(0, 2, 0));
m_pDoc->SetString(ScAddress(0, 3, 0), u"=MAX(D1#)+MIN(D1#)"_ustr);
CPPUNIT_ASSERT_EQUAL(u"=MAX(D1#)+MIN(D1#)"_ustr, m_pDoc->GetFormula(0, 3, 0));
m_pDoc->DeleteTab(0);
}
CPPUNIT_PLUGIN_IMPLEMENT();
/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */
+5
View File
@@ -1654,6 +1654,11 @@ bool intendsArrayResultInRange(formula::FormulaToken* const* pRpn,
// left scalar from its operand. The result is scalar even
// when the operand was an array.
bResultArray = false;
else if (eOp == ocSpill)
// The # spilled-range operator expands its operand to the
// whole spill range. The result is an array whatever the
// operand shape was.
bResultArray = true;
else if (formula::FormulaCompiler::IsMatrixFunction(eOp) || p->IsInForceArray())
bResultArray = true;
else if (eOp == ocRange || eOp == ocUnion || eOp == ocIntersect)
+1
View File
@@ -626,6 +626,7 @@ private:
void ScNeg();
void ScSingleValue();
void ScPercentSign();
void ScSpilledRange();
void ScIntersect();
void ScRangeFunc();
void ScUnionFunc();
+12 -1
View File
@@ -4789,7 +4789,18 @@ bool ScCompiler::NextNewToken( bool bInArray )
if ( (cSymbol[0] == '#' || cSymbol[0] == '$') && cSymbol[1] == 0 &&
!mbAutoCorrect )
{ // special case to speed up broken [$]#REF documents
{
// A bare # right after a reference or value is the spilled-range
// operator. It expands the preceding reference to the dynamic-
// array spill range whose origin lives at that cell.
if (cSymbol[0] == '#' && (meLastOp == ocColRowName || meLastOp == ocPush
|| meLastOp == ocClose || meLastOp == ocMatRef
|| meLastOp == ocPercentSign || meLastOp == ocSpill))
{
maRawToken.SetOpCode(ocSpill);
return true;
}
// special case to speed up broken [$]#REF documents
/* FIXME: ISERROR(#REF!) would be valid and true and the formula to
* be processed as usual. That would need some special treatment,
* also in NextSymbol() because of possible combinations of
+81
View File
@@ -1637,6 +1637,77 @@ void ScInterpreter::ScPercentSign()
cPar = nSavePar;
}
void ScInterpreter::ScSpilledRange()
{
// The # postfix operator expands a master cell's spill range
// into a matrix. A plain formula cell falls back to its scalar
// value. An empty cell, a number, a string, or a non-master
// spill slot yields #REF!, since none of them is a dynamic-array
// master.
ScAddress aAddress;
PopSingleRef(aAddress);
if (nGlobalError != FormulaError::NONE)
{
PushError(nGlobalError);
return;
}
ScFormulaCell* pFormulaCell = mrDoc.GetFormulaCell(aAddress);
if (!pFormulaCell)
{
PushError(FormulaError::NoRef);
return;
}
if (pFormulaCell->GetMatrixFlag() == ScMatrixMode::Reference)
{
PushError(FormulaError::NoRef);
return;
}
if (pFormulaCell->GetMatrixFlag() != ScMatrixMode::Formula)
{
PushCellResultToken(false, aAddress, nullptr, nullptr);
return;
}
// Force the master to interpret if it has not yet, so that its
// matrix result is materialized before we read it.
pFormulaCell->MaybeInterpret();
SCCOL nCols = 0;
SCROW nRows = 0;
pFormulaCell->GetMatColsRows(nCols, nRows);
if (nCols <= 1 && nRows <= 1)
{
PushCellResultToken(false, aAddress, nullptr, nullptr);
return;
}
// Push the spill range as a matrix so binary operators and
// aggregating functions see all values. Pushing a double reference
// would let arithmetic operators fold it to a single value through
// implicit intersection. The source range travels with the matrix
// through ScMatrixRangeToken so a plain cell receiving the spill
// range as its final result can reduce it via implicit
// intersection at result-storage time.
ScMatrixRef pMatrix = CreateMatrixFromDoubleRef(nullptr,
aAddress.Col(), aAddress.Row(), aAddress.Tab(),
aAddress.Col() + nCols - 1, aAddress.Row() + nRows - 1, aAddress.Tab());
if (pMatrix)
{
sc::RangeMatrix aRangeMat;
aRangeMat.mpMat = std::move(pMatrix);
aRangeMat.mnCol1 = aAddress.Col();
aRangeMat.mnRow1 = aAddress.Row();
aRangeMat.mnTab1 = aAddress.Tab();
aRangeMat.mnCol2 = aAddress.Col() + nCols - 1;
aRangeMat.mnRow2 = aAddress.Row() + nRows - 1;
aRangeMat.mnTab2 = aAddress.Tab();
PushMatrix(aRangeMat);
}
else
PushIllegalArgument();
}
void ScInterpreter::ScNot()
{
nFuncFmtType = SvNumFormatType::LOGICAL;
@@ -2787,6 +2858,16 @@ void ScInterpreter::ScIsRef()
bRes = true;
}
break;
case svMatrix:
{
// A spilled-range reference travels as a matrix that
// carries its source range. It answers as a reference,
// while a plain computed matrix does not.
bRes = sp > 0
&& static_cast<const ScMatrixToken*>(pStack[sp - 1])->IsMatrixRangeToken();
Pop();
}
break;
default:
Pop();
}
+24 -4
View File
@@ -3699,17 +3699,23 @@ void ScInterpreter::MergeCalcConfig()
namespace {
double applyImplicitIntersection(const sc::RangeMatrix& rMat, const ScAddress& rPos)
// Reduce a range-carrying matrix to the single value aligned with the
// formula position. On a hit rSrc receives the spill-range cell the
// value came from, so the caller can inherit its number format. On a
// miss the result is NaN and rSrc is left untouched.
double applyImplicitIntersection(const sc::RangeMatrix& rMat, const ScAddress& rPos, ScAddress& rSrc)
{
if (rMat.mnRow1 <= rPos.Row() && rPos.Row() <= rMat.mnRow2 && rMat.mnCol1 == rMat.mnCol2)
{
SCROW nOffset = rPos.Row() - rMat.mnRow1;
rSrc = ScAddress(rMat.mnCol1, rPos.Row(), rMat.mnTab1);
return rMat.mpMat->GetDouble(0, nOffset);
}
if (rMat.mnCol1 <= rPos.Col() && rPos.Col() <= rMat.mnCol2 && rMat.mnRow1 == rMat.mnRow2)
{
SCROW nOffset = rPos.Col() - rMat.mnCol1;
rSrc = ScAddress(rPos.Col(), rMat.mnRow1, rMat.mnTab1);
return rMat.mpMat->GetDouble(nOffset, 0);
}
@@ -3790,6 +3796,7 @@ void ScInterpreter::DispatchOpCode( OpCode eOp )
case ocNegSub :
case ocNeg : ScNeg(); break;
case ocSingleValue : ScSingleValue(); break;
case ocSpill : ScSpilledRange(); break;
case ocPercentSign : ScPercentSign(); break;
case ocPi : ScPi(); break;
case ocRandom : ScRandom(); break;
@@ -4557,14 +4564,27 @@ StackVar ScInterpreter::Interpret()
case svMatrix :
{
sc::RangeMatrix aMat = PopRangeMatrix();
if (aMat.isRangeValid())
// A matrix that carries a source range reduces
// by implicit intersection when the cell
// expects a single value. A matrix-formula
// context (CSE array master) wants the full
// matrix and routes through QueryMatrixType so
// the master can hold and spill the range.
if (aMat.isRangeValid() && !bMatrixFormula)
{
// This matrix represents a range reference. Apply implicit intersection.
double fVal = applyImplicitIntersection(aMat, aPos);
ScAddress aSrc;
double fVal = applyImplicitIntersection(aMat, aPos, aSrc);
if (std::isnan(fVal))
PushNoValue();
else
PushInt(fVal);
{
PushDouble(fVal);
// Keep the number format of the spill-range
// cell the value came from, so a spilled date
// or currency value still shows its formatting.
mrDoc.GetNumberFormatInfo(mrContext, nRetTypeExpr, nRetIndexExpr, aSrc);
}
}
else
// This is a normal matrix.