From 6f1fb25a946d2de3973b18218d2d676d15f9000b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=C5=BE=20Vajngerl?= Date: Wed, 3 Jun 2026 01:20:47 +0900 Subject: [PATCH] sc: add @ prefix operator for opting out of array spill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The @ operator collapses an array operand to its upper-left value. It is the implicit-intersection prefix used to opt a formula out of the dynamic-array spill behaviour. =@SEQUENCE(4) returns 1 instead of the four-row spill. The new ocSingleValue opcode joins the unary-operator range so the existing compile and interpret machinery treats it like any other unary prefix. The @ character stops being part of name syntax in the OOXML grammar, so sheet names that contain @ have to be quoted from now on. In-session only for now. The operator is parsed and interpreted but not yet serialised into XLSX or ODS. Signed-off-by: Tomaž Vajngerl Change-Id: I18cf56e34f933ebb7f6e43c157618c9fe28dbe0f Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/4172 Tested-by: Tomaž Vajngerl Reviewed-by: Miklos Vajna (cherry picked from commit fbd04d7c394ad7445a667bf63820f88551b176e1) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/206673 Tested-by: Jenkins Reviewed-by: Xisco Fauli --- formula/inc/core_resource.hrc | 6 ++++++ include/formula/opcode.hxx | 5 ++++- sc/qa/unit/ucalc_formula2.cxx | 17 +++++++++++++++++ sc/source/core/inc/interpre.hxx | 1 + sc/source/core/tool/compiler.cxx | 6 ++++-- sc/source/core/tool/interpr1.cxx | 23 +++++++++++++++++++++++ sc/source/core/tool/interpr4.cxx | 1 + 7 files changed, 56 insertions(+), 3 deletions(-) diff --git a/formula/inc/core_resource.hrc b/formula/inc/core_resource.hrc index 4376966f1840..77cf58e0a41d 100644 --- a/formula/inc/core_resource.hrc +++ b/formula/inc/core_resource.hrc @@ -68,6 +68,7 @@ const std::pair RID_STRLIST_FUNCTION_NAMES_ENGLISH_ODFF[] = { "NOT" , ocNot }, { "NEG" , ocNeg }, { "-" , ocNegSub }, + { "@" , ocSingleValue }, { "PI" , ocPi }, { "RAND" , ocRandom }, { "TRUE" , ocTrue }, @@ -540,6 +541,7 @@ const std::pair RID_STRLIST_FUNCTION_NAMES_ENGLISH_OOXML[] = { "NOT" , ocNot }, { "NEG" , ocNeg }, { "-" , ocNegSub }, + { "@" , ocSingleValue }, { "PI" , ocPi }, { "RAND" , ocRandom }, { "TRUE" , ocTrue }, @@ -1017,6 +1019,7 @@ const std::pair RID_STRLIST_FUNCTION_NAMES_ENGLISH_PODF[] = { "NOT" , ocNot }, { "NEG" , ocNeg }, { "-" , ocNegSub }, + { "@" , ocSingleValue }, { "PI" , ocPi }, { "RAND" , ocRandom }, { "TRUE" , ocTrue }, @@ -1492,6 +1495,7 @@ const std::pair RID_STRLIST_FUNCTION_NAMES_ENGLISH_API[] = { "NOT" , ocNot }, { "NEG" , ocNeg }, { "-" , ocNegSub }, + { "@" , ocSingleValue }, { "PI" , ocPi }, { "RAND" , ocRandom }, { "TRUE" , ocTrue }, @@ -1965,6 +1969,7 @@ const std::pair RID_STRLIST_FUNCTION_NAMES_ENGLISH[] = { "NOT" , ocNot }, { "NEG" , ocNeg }, { "-" , ocNegSub }, + { "@" , ocSingleValue }, { "PI" , ocPi }, { "RAND" , ocRandom }, { "TRUE" , ocTrue }, @@ -2892,6 +2897,7 @@ const std::pair RID_STRLIST_FUNCTION_NAMES_SYMBOLS[] = { "~" , ocUnion }, { ":" , ocRange }, { "-" , ocNegSub }, + { "@" , ocSingleValue }, { nullptr, -1 } }; diff --git a/include/formula/opcode.hxx b/include/formula/opcode.hxx index 46b923584a94..a93d89a4549f 100644 --- a/include/formula/opcode.hxx +++ b/include/formula/opcode.hxx @@ -113,7 +113,9 @@ enum OpCode : sal_uInt16 // Unary operators ocStartUnaryOperators = 70, ocNegSub = 70, - ocStopUnaryOperators = 71, + // The @ single-value prefix operator (implicit intersection). + ocSingleValue = 71, + ocStopUnaryOperators = 72, ocStartFunction = 75, @@ -646,6 +648,7 @@ inline std::string OpCodeEnumToString(OpCode eCode) case ocNot: return "Not"; case ocNeg: return "Neg"; case ocNegSub: return "NegSub"; + case ocSingleValue: return "SingleValue"; case ocPi: return "Pi"; case ocRandom: return "Random"; case ocRandomNV: return "RandomNV"; diff --git a/sc/qa/unit/ucalc_formula2.cxx b/sc/qa/unit/ucalc_formula2.cxx index 816243758d80..02d793ccf4c2 100644 --- a/sc/qa/unit/ucalc_formula2.cxx +++ b/sc/qa/unit/ucalc_formula2.cxx @@ -5767,6 +5767,23 @@ CPPUNIT_TEST_FIXTURE(TestFormula2, testDynamicArrayFlagCopy) m_pDoc->DeleteTab(0); } +CPPUNIT_TEST_FIXTURE(TestFormula2, testSingleValueOperator) +{ + // The @ prefix collapses an array-returning function to its upper-left + // value, opting the formula out of auto-spill. + + sc::AutoCalcSwitch aACSwitch(*m_pDoc, true); + m_pDoc->InsertTab(0, u"Sheet1"_ustr); + + m_pDoc->SetFormula(ScAddress(0, 0, 0), u"=@SEQUENCE(4)"_ustr, + formula::FormulaGrammar::GRAM_NATIVE); + CPPUNIT_ASSERT_EQUAL(1.0, m_pDoc->GetValue(ScAddress(0, 0, 0))); + // No spill into the row below. + CPPUNIT_ASSERT_EQUAL(CELLTYPE_NONE, m_pDoc->GetCellType(ScAddress(0, 1, 0))); + + m_pDoc->DeleteTab(0); +} + CPPUNIT_TEST_FIXTURE(TestFormula2, testDynamicArrayResizeDuringCopyToClip) { // A dynamic-array resize queued during CopyToClip must run after the clipboard clone is made. diff --git a/sc/source/core/inc/interpre.hxx b/sc/source/core/inc/interpre.hxx index fe5718587abc..5fc463fae4ca 100644 --- a/sc/source/core/inc/interpre.hxx +++ b/sc/source/core/inc/interpre.hxx @@ -610,6 +610,7 @@ private: void ScXor(); void ScNot(); void ScNeg(); + void ScSingleValue(); void ScPercentSign(); void ScIntersect(); void ScRangeFunc(); diff --git a/sc/source/core/tool/compiler.cxx b/sc/source/core/tool/compiler.cxx index 281f10a7c567..d55dae6557fc 100644 --- a/sc/source/core/tool/compiler.cxx +++ b/sc/source/core/tool/compiler.cxx @@ -141,7 +141,8 @@ constexpr std::array makeCommonCharTable() a['='] = ScCharFlags::Char | ScCharFlags::Bool | ScCharFlags::WordSep | ScCharFlags::ValueSep; a['>'] = ScCharFlags::CharBool | ScCharFlags::Bool | ScCharFlags::WordSep | ScCharFlags::ValueSep; a['?'] = ScCharFlags::CharWord | ScCharFlags::Word | ScCharFlags::Name; - /* @ */ // FREE + // @ is the prefix operator that opts a formula out of auto-spill. + a['@'] = ScCharFlags::Char | ScCharFlags::WordSep | ScCharFlags::ValueSep; for (int i = 'A'; i <= 'Z'; i++) a[i] = ScCharFlags::CharWord | ScCharFlags::Word | ScCharFlags::CharIdent | ScCharFlags::Ident | ScCharFlags::CharName | ScCharFlags::Name; @@ -211,7 +212,8 @@ constexpr std::array makeCharTable_XL() a['='] |= ScCharFlags::Word; a['>'] |= ScCharFlags::Word; /* ? */ // question really is not permitted in sheet name - a['@'] |= ScCharFlags::Word; + // @ stays an operator in the XL grammar too. Sheet names that contain + // @ have to be quoted. a['['] = ScCharFlags::Word; a[']'] = ScCharFlags::Word; a['{'] |= ScCharFlags::Word; diff --git a/sc/source/core/tool/interpr1.cxx b/sc/source/core/tool/interpr1.cxx index 831f9fbfbd91..24d5a748edd4 100644 --- a/sc/source/core/tool/interpr1.cxx +++ b/sc/source/core/tool/interpr1.cxx @@ -1543,6 +1543,29 @@ void ScInterpreter::ScNeg() } } +void ScInterpreter::ScSingleValue() +{ + // The @ operator collapses an array operand to a single value. It is + // the implicit-intersection prefix that opts a formula out of the + // dynamic-array spill behaviour. + nFuncFmtType = nCurFmtType; + if (GetStackType() == svMatrix) + { + ScMatrixRef pMat = GetMatrix(); + if (!pMat) + { + PushIllegalParameter(); + return; + } + ScMatrixValue aVal = pMat->Get(0, 0); + if (aVal.nType == ScMatValType::String || aVal.nType == ScMatValType::Empty) + PushString(aVal.aStr); + else + PushDouble(aVal.fVal); + } + // Non-matrix operand passes through unchanged. +} + void ScInterpreter::ScPercentSign() { nFuncFmtType = SvNumFormatType::PERCENT; diff --git a/sc/source/core/tool/interpr4.cxx b/sc/source/core/tool/interpr4.cxx index 491438cd04c3..3c98cf2956cb 100644 --- a/sc/source/core/tool/interpr4.cxx +++ b/sc/source/core/tool/interpr4.cxx @@ -4154,6 +4154,7 @@ StackVar ScInterpreter::Interpret() case ocNot : ScNot(); break; case ocNegSub : case ocNeg : ScNeg(); break; + case ocSingleValue : ScSingleValue(); break; case ocPercentSign : ScPercentSign(); break; case ocPi : ScPi(); break; case ocRandom : ScRandom(); break;