From 49e9da81faec2194dc042a27d5ca44cfba45f64b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=C5=BE=20Vajngerl?= Date: Fri, 26 Jun 2026 13:45:32 +0900 Subject: [PATCH] sc: import @-wrapped XLSX cells as scalar formulas, not CSE arrays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit XLSX tags a cell holding =@(expr) with t="array" ref=, because the OOXML writer evaluates the @ in array context. The import path honoured the tag and routed such cells through setMatrixCells, so GetFormula wrapped the imported text in {} braces. A user-typed =@(F3:F5+0) came back as {=@(F3:F5+0)}, reading as a CSE array even though @ keeps the cell scalar. When the t="array" range is a single cell and the parse array starts with @, construct a plain ScFormulaCell instead of calling setMatrixCells. The @ collapses the operand, so there is nothing to spill. testIntersectionOperatorXlsxRoundTrip loads the fixture and asserts the @-wrapped cells read back as plain formulas, both on import and after a round trip. Signed-off-by: Tomaž Vajngerl Change-Id: I05020fa2810a8f2d4f0c9374a9a8afc39e2d9e5e Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/5536 Tested-by: Tomaž Vajngerl Reviewed-by: Mike Kaganski (cherry picked from commit 75eda52e374810561261485ed038e70d9e2edab1) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/207869 Tested-by: Xisco Fauli Reviewed-by: Xisco Fauli --- .../xlsx/IntersectionOperatorFixture.xlsx | Bin 0 -> 9128 bytes sc/qa/unit/subsequent_filters_test.cxx | 31 ++++++++++++++++++ sc/source/filter/oox/formulabuffer.cxx | 19 +++++++++++ 3 files changed, 50 insertions(+) create mode 100644 sc/qa/unit/data/xlsx/IntersectionOperatorFixture.xlsx diff --git a/sc/qa/unit/data/xlsx/IntersectionOperatorFixture.xlsx b/sc/qa/unit/data/xlsx/IntersectionOperatorFixture.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..7458b80d5c67b815ce470670f8652553637c8a3f GIT binary patch literal 9128 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 literal 0 HcmV?d00001 diff --git a/sc/qa/unit/subsequent_filters_test.cxx b/sc/qa/unit/subsequent_filters_test.cxx index 53b50d7ac149..95f5b0c8fafc 100644 --- a/sc/qa/unit/subsequent_filters_test.cxx +++ b/sc/qa/unit/subsequent_filters_test.cxx @@ -1680,6 +1680,37 @@ CPPUNIT_TEST_FIXTURE(ScFiltersTest, testCmRoundTripXLSX) CPPUNIT_ASSERT(pMaster); CPPUNIT_ASSERT(pMaster->IsDynamicArrayMaster()); } +CPPUNIT_TEST_FIXTURE(ScFiltersTest, testIntersectionOperatorXlsxRoundTrip) +{ + // The fixture has A1:A4 = {42, 69, 13, 666} and three cells + // that read back the upper-left value 42 through implicit + // intersection. All three import as plain formulas, so + // GetFormula returns them without {} wrapping. The same shape + // survives an XLSX round trip. + + auto assertFormulas = [](ScDocument* pDoc) { + // C1: plain =A1:A4 range reference. No t="array" tag, so + // the cell collapses through implicit intersection at row 1. + CPPUNIT_ASSERT_EQUAL(u"=A1:A4"_ustr, pDoc->GetFormula(2, 0, 0)); + CPPUNIT_ASSERT_EQUAL(42.0, pDoc->GetValue(ScAddress(2, 0, 0))); + + // D1: t="array" wrapper around (A1:A4+0). The outer paren + // pair the writer adds is stripped on import. + CPPUNIT_ASSERT_EQUAL(u"=@(A1:A4+0)"_ustr, pDoc->GetFormula(3, 0, 0)); + CPPUNIT_ASSERT_EQUAL(42.0, pDoc->GetValue(ScAddress(3, 0, 0))); + + // E1: t="array" wrapper around TRANSPOSE(A1:A4). The + // argument is a function call, so no outer paren strip. + CPPUNIT_ASSERT_EQUAL(u"=@(TRANSPOSE(A1:A4))"_ustr, pDoc->GetFormula(4, 0, 0)); + CPPUNIT_ASSERT_EQUAL(42.0, pDoc->GetValue(ScAddress(4, 0, 0))); + }; + + createScDoc("xlsx/IntersectionOperatorFixture.xlsx"); + assertFormulas(getScDoc()); + + saveAndReload(TestFilter::XLSX); + assertFormulas(getScDoc()); +} CPPUNIT_TEST_FIXTURE(ScFiltersTest, testArrayFormulaSpillXLSX) { diff --git a/sc/source/filter/oox/formulabuffer.cxx b/sc/source/filter/oox/formulabuffer.cxx index 0b820a5cc4c9..192327eeb65a 100644 --- a/sc/source/filter/oox/formulabuffer.cxx +++ b/sc/source/filter/oox/formulabuffer.cxx @@ -381,6 +381,25 @@ void applyArrayFormulas( if (pArray) { lcl_stripRedundantParentheses(*pArray, {ocSingleValue}); + + // A single-cell t="array" that starts with @ imports as + // a plain ScFormulaCell, not a CSE array. The @ collapses + // the operand to a scalar, so the cell reads back as + // =@(expr) instead of {=@(expr)}. + const sal_uInt16 nParseLength = pArray->GetLen(); + const formula::FormulaToken* pFirst = nParseLength ? pArray->GetArray()[0] : nullptr; + const bool bSingleCellRange + = rAddressItem.maRange.aStart == rAddressItem.maRange.aEnd; + if (bSingleCellRange && pFirst + && pFirst->GetOpCode() == ocSingleValue) + { + aComp.CompileTokenArray(); + ScFormulaCell* pCell = new ScFormulaCell(rDoc.getDoc(), aPos, + std::move(pArray)); + rDoc.setFormulaCell(aPos, pCell); + continue; + } + rDoc.setMatrixCells(rAddressItem.maRange, *pArray, formula::FormulaGrammar::GRAM_OOXML, rAddressItem.mbCachedSpill);