tdf#123355 DOCX import: fix cell range ABOVE, BELOW,
LEFT and RIGHT by converting them to Writer cell ranges.
These ranges start at the first cell above/below/right/left
(including empty cells or cells with text content!) and end
at the end of the first value cell range. If there is no
value cell range, they end at the table border.
Note: In MSO, table functions COUNT(), AVERAGE() etc. use
non-value cells, as zero with ABOVE, BELOW, LEFT and RIGHT,
but skip them using A1:A10 like range names.
Change-Id: I906f27ecccaa8ded5460cd861ef8808000722860
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/102320
Tested-by: Jenkins
Reviewed-by: László Németh <nemeth@numbertext.org>
diff --git a/sw/qa/extras/ooxmlexport/data/tdf123355.docx b/sw/qa/extras/ooxmlexport/data/tdf123355.docx
new file mode 100644
index 0000000..da36db2
--- /dev/null
+++ b/sw/qa/extras/ooxmlexport/data/tdf123355.docx
Binary files differ
diff --git a/sw/qa/extras/ooxmlexport/ooxmlexport15.cxx b/sw/qa/extras/ooxmlexport/ooxmlexport15.cxx
index b3c3d79..104e867 100644
--- a/sw/qa/extras/ooxmlexport/ooxmlexport15.cxx
+++ b/sw/qa/extras/ooxmlexport/ooxmlexport15.cxx
@@ -255,6 +255,53 @@ DECLARE_OOXMLEXPORT_TEST(testTdf123354, "tdf123354.fodt")
CPPUNIT_ASSERT_EQUAL(OUString("0"), xEnumerationAccess3->getPresentation(false).trim());
}
DECLARE_OOXMLEXPORT_TEST(testTdf123355, "tdf123355.docx")
{
uno::Reference<text::XTextFieldsSupplier> xTextFieldsSupplier(mxComponent, uno::UNO_QUERY);
uno::Reference<container::XEnumerationAccess> xFieldsAccess(xTextFieldsSupplier->getTextFields());
uno::Reference<container::XEnumeration> xFields(xFieldsAccess->createEnumeration());
// Tests conversion of range IDs ABOVE, BELOW, LEFT and RIGHT
uno::Reference<text::XTextField> xEnumerationAccess1(xFields->nextElement(), uno::UNO_QUERY);
// Note: range ends at B4 here, which is a cell with text content
CPPUNIT_ASSERT_EQUAL(OUString("average( <B2:B3> )"), xEnumerationAccess1->getPresentation(true).trim());
CPPUNIT_ASSERT_EQUAL(OUString("5,5"), xEnumerationAccess1->getPresentation(false).trim());
// range ends at the end of the empty cells
uno::Reference<text::XTextField> xEnumerationAccess6(xFields->nextElement(), uno::UNO_QUERY);
CPPUNIT_ASSERT_EQUAL(OUString("SUM(<C6:A6>)"), xEnumerationAccess6->getPresentation(true).trim());
CPPUNIT_ASSERT_EQUAL(OUString("0"), xEnumerationAccess6->getPresentation(false).trim());
// range starts at the first cell above D5
uno::Reference<text::XTextField> xEnumerationAccess2(xFields->nextElement(), uno::UNO_QUERY);
CPPUNIT_ASSERT_EQUAL(OUString("AVERAGE(<D4:D1>)"), xEnumerationAccess2->getPresentation(true).trim());
CPPUNIT_ASSERT_EQUAL(OUString("5,33"), xEnumerationAccess2->getPresentation(false).trim());
uno::Reference<text::XTextField> xEnumerationAccess3(xFields->nextElement(), uno::UNO_QUERY);
CPPUNIT_ASSERT_EQUAL(OUString("AVERAGE(<C2:C1>)"), xEnumerationAccess3->getPresentation(true).trim());
CPPUNIT_ASSERT_EQUAL(OUString("3,5"), xEnumerationAccess3->getPresentation(false).trim());
uno::Reference<text::XTextField> xEnumerationAccess4(xFields->nextElement(), uno::UNO_QUERY);
CPPUNIT_ASSERT_EQUAL(OUString("AVERAGE(<D2:D2>)"), xEnumerationAccess4->getPresentation(true).trim());
CPPUNIT_ASSERT_EQUAL(OUString("5"), xEnumerationAccess4->getPresentation(false).trim());
uno::Reference<text::XTextField> xEnumerationAccess5(xFields->nextElement(), uno::UNO_QUERY);
CPPUNIT_ASSERT_EQUAL(OUString("AVERAGE(<A2:A2>)"), xEnumerationAccess5->getPresentation(true).trim());
CPPUNIT_ASSERT_EQUAL(OUString("4"), xEnumerationAccess5->getPresentation(false).trim());
xmlDocUniquePtr pXmlDoc = parseExport();
if (!pXmlDoc)
return;
// keep original formula IDs
assertXPathContent(pXmlDoc, "/w:document/w:body/w:tbl/w:tr[1]/w:tc[2]/w:p/w:r[2]/w:instrText", " =average( below )");
assertXPathContent(pXmlDoc, "/w:document/w:body/w:tbl/w:tr[2]/w:tc[2]/w:p/w:r[2]/w:instrText", " =AVERAGE(LEFT)");
assertXPathContent(pXmlDoc, "/w:document/w:body/w:tbl/w:tr[2]/w:tc[3]/w:p/w:r[2]/w:instrText", " =AVERAGE(RIGHT)");
assertXPathContent(pXmlDoc, "/w:document/w:body/w:tbl/w:tr[3]/w:tc[3]/w:p/w:r[2]/w:instrText", " =AVERAGE(ABOVE)");
assertXPathContent(pXmlDoc, "/w:document/w:body/w:tbl/w:tr[5]/w:tc[4]/w:p/w:r[2]/w:instrText", " =AVERAGE(ABOVE)");
assertXPathContent(pXmlDoc, "/w:document/w:body/w:tbl/w:tr[6]/w:tc[4]/w:p/w:r[2]/w:instrText", " =SUM(LEFT)");
}
DECLARE_OOXMLEXPORT_TEST(testTdf98000_changePageStyle, "tdf98000_changePageStyle.odt")
{
uno::Reference<frame::XModel> xModel(mxComponent, uno::UNO_QUERY);
diff --git a/writerfilter/source/dmapper/DomainMapperTableHandler.cxx b/writerfilter/source/dmapper/DomainMapperTableHandler.cxx
index 8d8724f..896950e 100644
--- a/writerfilter/source/dmapper/DomainMapperTableHandler.cxx
+++ b/writerfilter/source/dmapper/DomainMapperTableHandler.cxx
@@ -27,6 +27,7 @@
#include <com/sun/star/table/XCellRange.hpp>
#include <com/sun/star/text/HoriOrientation.hpp>
#include <com/sun/star/text/SizeType.hpp>
#include <com/sun/star/text/XTextField.hpp>
#include <com/sun/star/text/XTextRangeCompare.hpp>
#include <com/sun/star/beans/XPropertySet.hpp>
#include <com/sun/star/beans/XPropertyState.hpp>
@@ -41,6 +42,8 @@
#include <tools/diagnose_ex.h>
#include <comphelper/sequence.hxx>
#include <comphelper/propertyvalue.hxx>
#include <com/sun/star/lang/IndexOutOfBoundsException.hpp>
#include <boost/lexical_cast.hpp>
#ifdef DBG_UTIL
#include "PropertyMapHelper.hxx"
@@ -1213,6 +1216,147 @@ void DomainMapperTableHandler::ApplyParagraphPropertiesFromTableStyle(TableParag
}
}
// convert formula range identifier ABOVE, BELOW, LEFT and RIGHT
static void lcl_convertFormulaRanges(const uno::Reference<text::XTextTable> & xTable)
{
uno::Reference<table::XCellRange> xCellRange(xTable, uno::UNO_QUERY_THROW);
uno::Reference<container::XIndexAccess> xTableRows(xTable->getRows(), uno::UNO_QUERY_THROW);
sal_Int32 nRows = xTableRows->getCount();
for (sal_Int32 nRow = 0; nRow < nRows; ++nRow)
{
sal_Int16 nCol = 0;
while (++nCol)
{
try
{
uno::Reference<beans::XPropertySet> xCellProperties(xCellRange->getCellByPosition(nCol, nRow), uno::UNO_QUERY_THROW);
uno::Sequence<beans::PropertyValue> aCellGrabBag;
xCellProperties->getPropertyValue("CellInteropGrabBag") >>= aCellGrabBag;
OUString sFormula;
bool bReplace = false;
for (const auto& rProp : std::as_const(aCellGrabBag))
{
if ( rProp.Name == "CellFormulaConverted" )
{
rProp.Value >>= sFormula;
struct RangeDirection
{
OUString m_sName;
sal_Int16 m_nCol;
sal_Int16 m_nRow;
};
static const RangeDirection pDirections[] =
{
{ OUString(" LEFT "), -1, 0},
{ OUString(" RIGHT "), 1, 0},
{ OUString(" ABOVE "), 0, -1},
{ OUString(" BELOW "), 0, 1 }
};
for (const RangeDirection& rRange : pDirections)
{
if ( sFormula.indexOf(rRange.m_sName) > -1 )
{
// range starts at the first cell above/below/left/right, but ends at the
// table border or at the first non-value cell after a value cell
bool bFoundFirst = false;
OUString sNextCell;
OUString sLastCell;
OUString sLastValueCell;
// walk through the cells of the range
try
{
sal_Int32 nCell = 0;
while (++nCell)
{
uno::Reference<beans::XPropertySet> xCell(
xCellRange->getCellByPosition(nCol + nCell * rRange.m_nCol, nRow + nCell * rRange.m_nRow),
uno::UNO_QUERY_THROW);
// empty cell or cell with text content is end of the range
uno::Reference<text::XText> xText(xCell, uno::UNO_QUERY_THROW);
sLastCell = xCell->getPropertyValue("CellName").get<OUString>();
if (sNextCell.isEmpty())
sNextCell = sLastCell;
try
{
// accept numbers with comma and percent
OUString sCellText = xText->getString().replace(',', '.');
if (sCellText.endsWith("%"))
sCellText = sCellText.copy(0, sCellText.getLength()-1);
boost::lexical_cast<double>(sCellText);
}
catch( boost::bad_lexical_cast const& )
{
if ( !bFoundFirst )
{
// still search value cells
continue;
}
else
{
// end of range
break;
}
}
sLastValueCell = sLastCell;
bFoundFirst = true;
}
}
catch ( const lang::IndexOutOfBoundsException & )
{
}
if ( !sNextCell.isEmpty() )
{
OUString sRange = "<" + sNextCell + ":" +
( sLastValueCell.isEmpty() ? sLastCell : sLastValueCell ) + ">";
sFormula = sFormula.replaceAll(rRange.m_sName, sRange);
bReplace = true;
}
}
}
// update formula field
if (bReplace)
{
uno::Reference<text::XText> xCell(xCellRange->getCellByPosition(nCol, nRow), uno::UNO_QUERY);
uno::Reference<container::XEnumerationAccess> xParaEnumAccess(xCell, uno::UNO_QUERY);
uno::Reference<container::XEnumeration> xParaEnum = xParaEnumAccess->createEnumeration();
uno::Reference<container::XEnumerationAccess> xRunEnumAccess(xParaEnum->nextElement(), uno::UNO_QUERY);
uno::Reference<container::XEnumeration> xRunEnum = xRunEnumAccess->createEnumeration();
while ( xRunEnum->hasMoreElements() )
{
uno::Reference<text::XTextRange> xRun(xRunEnum->nextElement(), uno::UNO_QUERY);
uno::Reference< beans::XPropertySet > xRunProperties( xRun, uno::UNO_QUERY_THROW );
if ( xRunProperties->getPropertyValue("TextPortionType") == uno::makeAny(OUString("TextField")) )
{
uno::Reference<text::XTextField> const xField(xRunProperties->getPropertyValue("TextField").get<uno::Reference<text::XTextField>>());
uno::Reference< beans::XPropertySet > xFieldProperties( xField, uno::UNO_QUERY_THROW );
// cell can contain multiple text fields, but only one is handled now (~formula cell)
if ( rProp.Value != xFieldProperties->getPropertyValue("Content") )
continue;
xFieldProperties->setPropertyValue("Content", uno::makeAny(sFormula));
// update grab bag
auto aGrabBag = comphelper::sequenceToContainer< std::vector<beans::PropertyValue> >(aCellGrabBag);
beans::PropertyValue aValue;
aValue.Name = "CellFormulaConverted";
aValue.Value <<= sFormula;
aGrabBag.push_back(aValue);
xCellProperties->setPropertyValue("CellInteropGrabBag", uno::makeAny(comphelper::containerToSequence(aGrabBag)));
}
}
}
}
}
}
catch ( const lang::IndexOutOfBoundsException & )
{
// jump to next table row
break;
}
}
}
}
void DomainMapperTableHandler::endTable(unsigned int nestedTableLevel, bool bTableStartsAtCellStart)
{
#ifdef DBG_UTIL
@@ -1367,6 +1511,9 @@ void DomainMapperTableHandler::endTable(unsigned int nestedTableLevel, bool bTab
}
}
}
// convert special range IDs ABOVE, BELOW, LEFT and RIGHT
lcl_convertFormulaRanges(xTable);
}
}
}
diff --git a/writerfilter/source/dmapper/DomainMapper_Impl.cxx b/writerfilter/source/dmapper/DomainMapper_Impl.cxx
index e7ecfff..6387d22 100644
--- a/writerfilter/source/dmapper/DomainMapper_Impl.cxx
+++ b/writerfilter/source/dmapper/DomainMapper_Impl.cxx
@@ -4276,7 +4276,7 @@ OUString DomainMapper_Impl::convertFieldFormula(const OUString& input) {
usInput = rmatch2.replaceAll(icu::UnicodeString("<$1>"), status);
/* Cell references must be upper case */
icu::RegexMatcher rmatch3("<[a-z]{1,3}[0-9]+>", usInput, rMatcherFlags, status);
icu::RegexMatcher rmatch3("(<[a-z]{1,3}[0-9]+>|\\b(above|below|left|right)\\b)", usInput, rMatcherFlags, status);
icu::UnicodeString replacedCellRefs;
while (rmatch3.find(status) && status.isSuccess()) {
rmatch3.appendReplacement(replacedCellRefs, rmatch3.group(status).toUpper(), status);
@@ -4291,6 +4291,10 @@ OUString DomainMapper_Impl::convertFieldFormula(const OUString& input) {
icu::RegexMatcher rmatch5("\\bDEFINED\\s*\\(<([A-Z]+[0-9]+)>\\)", usInput, rMatcherFlags, status);
usInput = rmatch5.replaceAll(icu::UnicodeString("DEFINED($1)"), status);
/* Prepare replace of ABOVE/BELOW/LEFT/RIGHT by adding spaces around them */
icu::RegexMatcher rmatch6("\\b(ABOVE|BELOW|LEFT|RIGHT)\\b", usInput, rMatcherFlags, status);
usInput = rmatch6.replaceAll(icu::UnicodeString(" $1 "), status);
return OUString(usInput.getTerminatedBuffer());
}