/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * 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/. * * This file incorporates work covered by the following license notice: * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed * with this work for additional information regarding copyright * ownership. The ASF licenses this file to you under the Apache * License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of * the License at http://www.apache.org/licenses/LICENSE-2.0 . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace ::com::sun::star; namespace naturalsort { using namespace ::com::sun::star::i18n; /** Splits a given string into three parts: the prefix, number string, and the suffix. @param sWhole Original string to be split into pieces @param sPrefix Prefix string that consists of the part before the first number token. If no number was found, sPrefix is unchanged. @param sSuffix String after the last number token. This may still contain number strings. If no number was found, sSuffix is unchanged. @param fNum Number converted from the middle number string If no number was found, fNum is unchanged. @return Returns TRUE if a numeral element is found in a given string, or FALSE if no numeral element is found. */ static bool SplitString( const OUString &sWhole, OUString &sPrefix, OUString &sSuffix, double &fNum ) { // Get prefix element, search for any digit and stop. sal_Int32 nPos = 0; while (nPos < sWhole.getLength()) { const sal_uInt16 nType = ScGlobal::getCharClass().getCharacterType( sWhole, nPos); if (nType & KCharacterType::DIGIT) break; sWhole.iterateCodePoints( &nPos ); } // Return FALSE if no numeral element is found if ( nPos == sWhole.getLength() ) return false; // Get numeral element const OUString& sUser = ScGlobal::getLocaleData().getNumDecimalSep(); ParseResult aPRNum = ScGlobal::getCharClass().parsePredefinedToken( KParseType::ANY_NUMBER, sWhole, nPos, KParseTokens::ANY_NUMBER, "", KParseTokens::ANY_NUMBER, sUser ); if ( aPRNum.EndPos == nPos ) { SAL_WARN("sc.core","naturalsort::SplitString - digit found but no number parsed, pos " << nPos << " : " << sWhole); return false; } sPrefix = sWhole.copy( 0, nPos ); fNum = aPRNum.Value; sSuffix = sWhole.copy( aPRNum.EndPos ); return true; } /** Naturally compares two given strings. This is the main function that should be called externally. It returns either 1, 0, or -1 depending on the comparison result of given two strings. @param sInput1 Input string 1 @param sInput2 Input string 2 @param bCaseSens Boolean value for case sensitivity @param pData Pointer to user defined sort list @param pCW Pointer to collator wrapper for normal string comparison @return Returns 1 if sInput1 is greater, 0 if sInput1 == sInput2, and -1 if sInput2 is greater. */ static short Compare( const OUString &sInput1, const OUString &sInput2, const bool bCaseSens, const ScUserListData* pData, const CollatorWrapper *pCW ) { OUString sStr1( sInput1 ), sStr2( sInput2 ), sPre1, sSuf1, sPre2, sSuf2; do { double nNum1, nNum2; bool bNumFound1 = SplitString( sStr1, sPre1, sSuf1, nNum1 ); bool bNumFound2 = SplitString( sStr2, sPre2, sSuf2, nNum2 ); short nPreRes; // Prefix comparison result if ( pData ) { if ( bCaseSens ) { if ( !bNumFound1 || !bNumFound2 ) return static_cast(pData->Compare( sStr1, sStr2 )); else nPreRes = pData->Compare( sPre1, sPre2 ); } else { if ( !bNumFound1 || !bNumFound2 ) return static_cast(pData->ICompare( sStr1, sStr2 )); else nPreRes = pData->ICompare( sPre1, sPre2 ); } } else { if ( !bNumFound1 || !bNumFound2 ) return static_cast(pCW->compareString( sStr1, sStr2 )); else nPreRes = static_cast(pCW->compareString( sPre1, sPre2 )); } // Prefix strings differ. Return immediately. if ( nPreRes != 0 ) return nPreRes; if ( nNum1 != nNum2 ) { if ( nNum1 < nNum2 ) return -1; return (nNum1 > nNum2) ? 1 : 0; } // The prefix and the first numerical elements are equal, but the suffix // strings may still differ. Stay in the loop. sStr1 = sSuf1; sStr2 = sSuf2; } while (true); return 0; } } namespace { struct ScSortInfo final { ScRefCellValue maCell; SCCOLROW nOrg; }; } class ScSortInfoArray { public: struct Cell { ScRefCellValue maCell; const sc::CellTextAttr* mpAttr; const ScPostIt* mpNote; std::vector maDrawObjects; const ScPatternAttr* mpPattern; Cell() : mpAttr(nullptr), mpNote(nullptr), mpPattern(nullptr) {} }; struct Row { std::vector maCells; bool mbHidden:1; bool mbFiltered:1; explicit Row( size_t nColSize ) : maCells(nColSize, Cell()), mbHidden(false), mbFiltered(false) {} }; typedef std::vector RowsType; private: std::unique_ptr mpRows; /// row-wise data table for sort by row operation. std::vector> mvppInfo; SCCOLROW nStart; SCCOLROW mnLastIndex; /// index of last non-empty cell position. std::vector maOrderIndices; bool mbKeepQuery; bool mbUpdateRefs; public: ScSortInfoArray(const ScSortInfoArray&) = delete; const ScSortInfoArray& operator=(const ScSortInfoArray&) = delete; ScSortInfoArray( sal_uInt16 nSorts, SCCOLROW nInd1, SCCOLROW nInd2 ) : mvppInfo(nSorts), nStart( nInd1 ), mnLastIndex(nInd2), mbKeepQuery(false), mbUpdateRefs(false) { SCSIZE nCount( nInd2 - nInd1 + 1 ); if (nSorts) { for ( sal_uInt16 nSort = 0; nSort < nSorts; nSort++ ) { mvppInfo[nSort].reset(new ScSortInfo[nCount]); } } for (size_t i = 0; i < nCount; ++i) maOrderIndices.push_back(i+nStart); } void SetKeepQuery( bool b ) { mbKeepQuery = b; } bool IsKeepQuery() const { return mbKeepQuery; } void SetUpdateRefs( bool b ) { mbUpdateRefs = b; } bool IsUpdateRefs() const { return mbUpdateRefs; } /** * Call this only during normal sorting, not from reordering. */ std::unique_ptr const & GetFirstArray() const { return mvppInfo[0]; } /** * Call this only during normal sorting, not from reordering. */ ScSortInfo & Get( sal_uInt16 nSort, SCCOLROW nInd ) { return mvppInfo[nSort][ nInd - nStart ]; } /** * Call this only during normal sorting, not from reordering. */ void Swap( SCCOLROW nInd1, SCCOLROW nInd2 ) { if (nInd1 == nInd2) // avoid self-move-assign return; SCSIZE n1 = static_cast(nInd1 - nStart); SCSIZE n2 = static_cast(nInd2 - nStart); for ( sal_uInt16 nSort = 0; nSort < static_cast(mvppInfo.size()); nSort++ ) { auto & ppInfo = mvppInfo[nSort]; std::swap(ppInfo[n1], ppInfo[n2]); } std::swap(maOrderIndices[n1], maOrderIndices[n2]); if (mpRows) { // Swap rows in data table. RowsType& rRows = *mpRows; std::swap(rRows[n1], rRows[n2]); } } void SetOrderIndices( std::vector&& rIndices ) { maOrderIndices = std::move(rIndices); } /** * @param rIndices indices are actual row positions on the sheet, not an * offset from the top row. */ void ReorderByRow( const std::vector& rIndices ) { if (!mpRows) return; RowsType& rRows = *mpRows; std::vector aOrderIndices2; aOrderIndices2.reserve(rIndices.size()); RowsType aRows2; aRows2.reserve(rRows.size()); for (const auto& rIndex : rIndices) { size_t nPos = rIndex - nStart; // switch to an offset to top row. aRows2.push_back(rRows[nPos]); aOrderIndices2.push_back(maOrderIndices[nPos]); } rRows.swap(aRows2); maOrderIndices.swap(aOrderIndices2); } sal_uInt16 GetUsedSorts() const { return mvppInfo.size(); } SCCOLROW GetStart() const { return nStart; } SCCOLROW GetLast() const { return mnLastIndex; } const std::vector& GetOrderIndices() const { return maOrderIndices; } RowsType& InitDataRows( size_t nRowSize, size_t nColSize ) { mpRows.reset(new RowsType); mpRows->resize(nRowSize, Row(nColSize)); return *mpRows; } RowsType* GetDataRows() { return mpRows.get(); } }; // Assume that we can handle 512MB, which with a ~100 bytes // ScSortInfoArray::Cell element for 500MB are about 5 million cells plus // overhead in one chunk. constexpr sal_Int32 kSortCellsChunk = 500 * 1024 * 1024 / sizeof(ScSortInfoArray::Cell); namespace { void initDataRows( ScSortInfoArray& rArray, ScTable& rTab, ScColContainer& rCols, SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2, bool bHiddenFiltered, bool bPattern, bool bCellNotes, bool bCellDrawObjects, bool bOnlyDataAreaExtras ) { // Fill row-wise data table. ScSortInfoArray::RowsType& rRows = rArray.InitDataRows(nRow2-nRow1+1, nCol2-nCol1+1); const std::vector& rOrderIndices = rArray.GetOrderIndices(); assert(!bOnlyDataAreaExtras || (rOrderIndices.size() == static_cast(nRow2 - nRow1 + 1) && nRow1 == rArray.GetStart())); ScDrawLayer* pDrawLayer = (bCellDrawObjects ? rTab.GetDoc().GetDrawLayer() : nullptr); for (SCCOL nCol = nCol1; nCol <= nCol2; ++nCol) { ScColumn& rCol = rCols[nCol]; // Skip reordering of cell formats if the whole span is on the same pattern entry. bool bUniformPattern = rCol.GetPatternCount(nRow1, nRow2) < 2u; sc::ColumnBlockConstPosition aBlockPos; rCol.InitBlockPosition(aBlockPos); std::map> aRowDrawObjects; if (pDrawLayer) aRowDrawObjects = pDrawLayer->GetObjectsAnchoredToRange(rTab.GetTab(), nCol, nRow1, nRow2); for (SCROW nR = nRow1; nR <= nRow2; ++nR) { const SCROW nRow = (bOnlyDataAreaExtras ? rOrderIndices[nR - rArray.GetStart()] : nR); ScSortInfoArray::Row& rRow = rRows[nR-nRow1]; ScSortInfoArray::Cell& rCell = rRow.maCells[nCol-nCol1]; if (!bOnlyDataAreaExtras) { rCell.maCell = rCol.GetCellValue(aBlockPos, nRow); rCell.mpAttr = rCol.GetCellTextAttr(aBlockPos, nRow); } if (bCellNotes) rCell.mpNote = rCol.GetCellNote(aBlockPos, nRow); if (pDrawLayer) rCell.maDrawObjects = aRowDrawObjects[nRow]; if (!bUniformPattern && bPattern) rCell.mpPattern = rCol.GetPattern(nRow); } } if (!bOnlyDataAreaExtras && bHiddenFiltered) { for (SCROW nRow = nRow1; nRow <= nRow2; ++nRow) { ScSortInfoArray::Row& rRow = rRows[nRow-nRow1]; rRow.mbHidden = rTab.RowHidden(nRow); rRow.mbFiltered = rTab.RowFiltered(nRow); } } } } std::unique_ptr ScTable::CreateSortInfoArray( const sc::ReorderParam& rParam ) { std::unique_ptr pArray; if (rParam.mbByRow) { // Create a sort info array with just the data table. SCROW nRow1 = rParam.maSortRange.aStart.Row(); SCROW nRow2 = rParam.maSortRange.aEnd.Row(); SCCOL nCol1 = rParam.maSortRange.aStart.Col(); SCCOL nCol2 = rParam.maSortRange.aEnd.Col(); pArray.reset(new ScSortInfoArray(0, nRow1, nRow2)); pArray->SetKeepQuery(rParam.mbHiddenFiltered); pArray->SetUpdateRefs(rParam.mbUpdateRefs); CreateColumnIfNotExists(nCol2); initDataRows( *pArray, *this, aCol, nCol1, nRow1, nCol2, nRow2, rParam.mbHiddenFiltered, rParam.maDataAreaExtras.mbCellFormats, true, true, false); } else { SCCOLROW nCol1 = rParam.maSortRange.aStart.Col(); SCCOLROW nCol2 = rParam.maSortRange.aEnd.Col(); pArray.reset(new ScSortInfoArray(0, nCol1, nCol2)); pArray->SetKeepQuery(rParam.mbHiddenFiltered); pArray->SetUpdateRefs(rParam.mbUpdateRefs); } return pArray; } std::unique_ptr ScTable::CreateSortInfoArray( const ScSortParam& rSortParam, SCCOLROW nInd1, SCCOLROW nInd2, bool bKeepQuery, bool bUpdateRefs ) { sal_uInt16 nUsedSorts = 1; while ( nUsedSorts < rSortParam.GetSortKeyCount() && rSortParam.maKeyState[nUsedSorts].bDoSort ) nUsedSorts++; std::unique_ptr pArray(new ScSortInfoArray( nUsedSorts, nInd1, nInd2 )); pArray->SetKeepQuery(bKeepQuery); pArray->SetUpdateRefs(bUpdateRefs); if ( rSortParam.bByRow ) { for ( sal_uInt16 nSort = 0; nSort < nUsedSorts; nSort++ ) { SCCOL nCol = static_cast(rSortParam.maKeyState[nSort].nField); ScColumn* pCol = &aCol[nCol]; sc::ColumnBlockConstPosition aBlockPos; pCol->InitBlockPosition(aBlockPos); for ( SCROW nRow = nInd1; nRow <= nInd2; nRow++ ) { ScSortInfo & rInfo = pArray->Get( nSort, nRow ); rInfo.maCell = pCol->GetCellValue(aBlockPos, nRow); rInfo.nOrg = nRow; } } CreateColumnIfNotExists(rSortParam.nCol2); initDataRows( *pArray, *this, aCol, rSortParam.nCol1, nInd1, rSortParam.nCol2, nInd2, bKeepQuery, rSortParam.aDataAreaExtras.mbCellFormats, true, true, false); } else { for ( sal_uInt16 nSort = 0; nSort < nUsedSorts; nSort++ ) { SCROW nRow = rSortParam.maKeyState[nSort].nField; for ( SCCOL nCol = static_cast(nInd1); nCol <= static_cast(nInd2); nCol++ ) { ScSortInfo & rInfo = pArray->Get( nSort, nCol ); rInfo.maCell = GetCellValue(nCol, nRow); rInfo.nOrg = nCol; } } } return pArray; } namespace { struct SortedColumn { typedef mdds::flat_segment_tree PatRangeType; sc::CellStoreType maCells; sc::CellTextAttrStoreType maCellTextAttrs; sc::BroadcasterStoreType maBroadcasters; sc::CellNoteStoreType maCellNotes; std::vector> maCellDrawObjects; PatRangeType maPatterns; PatRangeType::const_iterator miPatternPos; SortedColumn(const SortedColumn&) = delete; const SortedColumn operator=(const SortedColumn&) = delete; explicit SortedColumn( size_t nTopEmptyRows, const ScSheetLimits& rSheetLimits ) : maCells(nTopEmptyRows), maCellTextAttrs(nTopEmptyRows), maBroadcasters(nTopEmptyRows), maCellNotes(nTopEmptyRows), maPatterns(0, rSheetLimits.GetMaxRowCount(), nullptr), miPatternPos(maPatterns.begin()) {} void setPattern( SCROW nRow, const ScPatternAttr* pPat ) { miPatternPos = maPatterns.insert(miPatternPos, nRow, nRow+1, pPat).first; } }; struct SortedRowFlags { typedef mdds::flat_segment_tree FlagsType; FlagsType maRowsHidden; FlagsType maRowsFiltered; FlagsType::const_iterator miPosHidden; FlagsType::const_iterator miPosFiltered; SortedRowFlags(const ScSheetLimits& rSheetLimits) : maRowsHidden(0, rSheetLimits.GetMaxRowCount(), false), maRowsFiltered(0, rSheetLimits.GetMaxRowCount(), false), miPosHidden(maRowsHidden.begin()), miPosFiltered(maRowsFiltered.begin()) {} void setRowHidden( SCROW nRow, bool b ) { miPosHidden = maRowsHidden.insert(miPosHidden, nRow, nRow+1, b).first; } void setRowFiltered( SCROW nRow, bool b ) { miPosFiltered = maRowsFiltered.insert(miPosFiltered, nRow, nRow+1, b).first; } void swap( SortedRowFlags& r ) { maRowsHidden.swap(r.maRowsHidden); maRowsFiltered.swap(r.maRowsFiltered); // Just reset the position hints. miPosHidden = maRowsHidden.begin(); miPosFiltered = maRowsFiltered.begin(); } }; struct PatternSpan { SCROW mnRow1; SCROW mnRow2; const ScPatternAttr* mpPattern; PatternSpan( SCROW nRow1, SCROW nRow2, const ScPatternAttr* pPat ) : mnRow1(nRow1), mnRow2(nRow2), mpPattern(pPat) {} }; } bool ScTable::IsSortCollatorGlobal() const { return pSortCollator == &ScGlobal::GetCollator() || pSortCollator == &ScGlobal::GetCaseCollator(); } void ScTable::InitSortCollator( const ScSortParam& rPar ) { if ( !rPar.aCollatorLocale.Language.isEmpty() ) { if ( !pSortCollator || IsSortCollatorGlobal() ) pSortCollator = new CollatorWrapper( comphelper::getProcessComponentContext() ); pSortCollator->loadCollatorAlgorithm( rPar.aCollatorAlgorithm, rPar.aCollatorLocale, (rPar.bCaseSens ? 0 : SC_COLLATOR_IGNORES) ); } else { // SYSTEM DestroySortCollator(); pSortCollator = &ScGlobal::GetCollator(rPar.bCaseSens); } } void ScTable::DestroySortCollator() { if ( pSortCollator ) { if ( !IsSortCollatorGlobal() ) delete pSortCollator; pSortCollator = nullptr; } } namespace { template class ReorderNotifier { Hint maHint; public: ReorderNotifier( const ReorderMap& rMap, SCTAB nTab, Index nPos1, Index nPos2 ) : maHint(rMap, nTab, nPos1, nPos2) {} void operator() ( SvtListener* p ) { p->Notify(maHint); } }; class FormulaGroupPosCollector { sc::RefQueryFormulaGroup& mrQuery; public: explicit FormulaGroupPosCollector( sc::RefQueryFormulaGroup& rQuery ) : mrQuery(rQuery) {} void operator() ( const SvtListener* p ) { p->Query(mrQuery); } }; void fillSortedColumnArray( std::vector>& rSortedCols, SortedRowFlags& rRowFlags, std::vector& rCellListeners, ScSortInfoArray* pArray, SCTAB nTab, SCCOL nCol1, SCCOL nCol2, ScProgress* pProgress, const ScTable* pTable, bool bOnlyDataAreaExtras ) { assert(!bOnlyDataAreaExtras || !pArray->IsUpdateRefs()); SCROW nRow1 = pArray->GetStart(); ScSortInfoArray::RowsType* pRows = pArray->GetDataRows(); std::vector aOrderIndices = pArray->GetOrderIndices(); size_t nColCount = nCol2 - nCol1 + 1; std::vector> aSortedCols; // storage for copied cells. SortedRowFlags aRowFlags(pTable->GetDoc().GetSheetLimits()); aSortedCols.reserve(nColCount); for (size_t i = 0; i < nColCount; ++i) { // In the sorted column container, element positions and row // positions must match, else formula cells may mis-behave during // grouping. aSortedCols.push_back(std::make_unique(nRow1, pTable->GetDoc().GetSheetLimits())); } for (size_t i = 0; i < pRows->size(); ++i) { const SCROW nRow = nRow1 + i; ScSortInfoArray::Row& rRow = (*pRows)[i]; for (size_t j = 0; j < rRow.maCells.size(); ++j) { ScSortInfoArray::Cell& rCell = rRow.maCells[j]; // If bOnlyDataAreaExtras, // sc::CellStoreType aSortedCols.at(j)->maCells // and // sc::CellTextAttrStoreType aSortedCols.at(j)->maCellTextAttrs // are by definition all empty mdds::multi_type_vector, so nothing // needs to be done to push *all* empty. if (!bOnlyDataAreaExtras) { sc::CellStoreType& rCellStore = aSortedCols.at(j)->maCells; switch (rCell.maCell.meType) { case CELLTYPE_STRING: assert(rCell.mpAttr); rCellStore.push_back(*rCell.maCell.mpString); break; case CELLTYPE_VALUE: assert(rCell.mpAttr); rCellStore.push_back(rCell.maCell.mfValue); break; case CELLTYPE_EDIT: assert(rCell.mpAttr); rCellStore.push_back(rCell.maCell.mpEditText->Clone().release()); break; case CELLTYPE_FORMULA: { assert(rCell.mpAttr); ScAddress aOldPos = rCell.maCell.mpFormula->aPos; const ScAddress aCellPos(nCol1 + j, nRow, nTab); ScFormulaCell* pNew = rCell.maCell.mpFormula->Clone( aCellPos ); if (pArray->IsUpdateRefs()) { pNew->CopyAllBroadcasters(*rCell.maCell.mpFormula); pNew->GetCode()->AdjustReferenceOnMovedOrigin(aOldPos, aCellPos); } else { pNew->GetCode()->AdjustReferenceOnMovedOriginIfOtherSheet(aOldPos, aCellPos); } if (!rCellListeners.empty()) { // Original source cells will be deleted during // sc::CellStoreType::transfer(), SvtListener is a base // class, so we need to replace it. auto it( ::std::find( rCellListeners.begin(), rCellListeners.end(), rCell.maCell.mpFormula)); if (it != rCellListeners.end()) *it = pNew; } rCellStore.push_back(pNew); } break; default: //assert(!rCell.mpAttr); // This assert doesn't hold, for example // CopyCellsFromClipHandler may omit copying cells during // PasteSpecial for which CopyTextAttrsFromClipHandler // still copies a CellTextAttr. So if that really is not // expected then fix it there. rCellStore.push_back_empty(); } sc::CellTextAttrStoreType& rAttrStore = aSortedCols.at(j)->maCellTextAttrs; if (rCell.mpAttr) rAttrStore.push_back(*rCell.mpAttr); else rAttrStore.push_back_empty(); } if (pArray->IsUpdateRefs()) { // At this point each broadcaster instance is managed by 2 // containers. We will release those in the original storage // below before transferring them to the document. const SvtBroadcaster* pBroadcaster = pTable->GetBroadcaster( nCol1 + j, aOrderIndices[i]); sc::BroadcasterStoreType& rBCStore = aSortedCols.at(j)->maBroadcasters; if (pBroadcaster) // A const pointer would be implicitly converted to a bool type. rBCStore.push_back(const_cast(pBroadcaster)); else rBCStore.push_back_empty(); } // The same with cell note instances ... sc::CellNoteStoreType& rNoteStore = aSortedCols.at(j)->maCellNotes; if (rCell.mpNote) rNoteStore.push_back(const_cast(rCell.mpNote)); else rNoteStore.push_back_empty(); // Add cell anchored images aSortedCols.at(j)->maCellDrawObjects.push_back(rCell.maDrawObjects); if (rCell.mpPattern) aSortedCols.at(j)->setPattern(nRow, rCell.mpPattern); } if (!bOnlyDataAreaExtras && pArray->IsKeepQuery()) { // Hidden and filtered flags are first converted to segments. aRowFlags.setRowHidden(nRow, rRow.mbHidden); aRowFlags.setRowFiltered(nRow, rRow.mbFiltered); } if (pProgress) pProgress->SetStateOnPercent(i); } rSortedCols.swap(aSortedCols); rRowFlags.swap(aRowFlags); } void expandRowRange( ScRange& rRange, SCROW nTop, SCROW nBottom ) { if (nTop < rRange.aStart.Row()) rRange.aStart.SetRow(nTop); if (rRange.aEnd.Row() < nBottom) rRange.aEnd.SetRow(nBottom); } class FormulaCellCollectAction : public sc::ColumnSpanSet::ColumnAction { std::vector& mrCells; ScColumn* mpCol; public: explicit FormulaCellCollectAction( std::vector& rCells ) : mrCells(rCells), mpCol(nullptr) {} virtual void startColumn( ScColumn* pCol ) override { mpCol = pCol; } virtual void execute( SCROW nRow1, SCROW nRow2, bool bVal ) override { assert(mpCol); if (!bVal) return; mpCol->CollectFormulaCells(mrCells, nRow1, nRow2); } }; class ListenerStartAction : public sc::ColumnSpanSet::ColumnAction { ScColumn* mpCol; std::shared_ptr mpPosSet; sc::StartListeningContext maStartCxt; sc::EndListeningContext maEndCxt; public: explicit ListenerStartAction( ScDocument& rDoc ) : mpCol(nullptr), mpPosSet(std::make_shared(rDoc)), maStartCxt(rDoc, mpPosSet), maEndCxt(rDoc, mpPosSet) {} virtual void startColumn( ScColumn* pCol ) override { mpCol = pCol; } virtual void execute( SCROW nRow1, SCROW nRow2, bool bVal ) override { assert(mpCol); if (!bVal) return; mpCol->StartListeningFormulaCells(maStartCxt, maEndCxt, nRow1, nRow2); } }; } void ScTable::SortReorderAreaExtrasByRow( ScSortInfoArray* pArray, SCCOL nDataCol1, SCCOL nDataCol2, const ScDataAreaExtras& rDataAreaExtras, ScProgress* pProgress ) { const SCROW nRow1 = pArray->GetStart(); const SCROW nLastRow = pArray->GetLast(); const SCCOL nChunkCols = std::max( 1, kSortCellsChunk / (nLastRow - nRow1 + 1)); // Before data area. for (SCCOL nCol = rDataAreaExtras.mnStartCol; nCol < nDataCol1; nCol += nChunkCols) { const SCCOL nEndCol = std::min( nCol + nChunkCols - 1, nDataCol1 - 1); CreateColumnIfNotExists(nEndCol); initDataRows( *pArray, *this, aCol, nCol, nRow1, nEndCol, nLastRow, false, rDataAreaExtras.mbCellFormats, rDataAreaExtras.mbCellNotes, rDataAreaExtras.mbCellDrawObjects, true); SortReorderByRow( pArray, nCol, nEndCol, pProgress, true); } // Behind data area. for (SCCOL nCol = nDataCol2 + 1; nCol <= rDataAreaExtras.mnEndCol; nCol += nChunkCols) { const SCCOL nEndCol = std::min( nCol + nChunkCols - 1, rDataAreaExtras.mnEndCol); CreateColumnIfNotExists(nEndCol); initDataRows( *pArray, *this, aCol, nCol, nRow1, nEndCol, nLastRow, false, rDataAreaExtras.mbCellFormats, rDataAreaExtras.mbCellNotes, rDataAreaExtras.mbCellDrawObjects, true); SortReorderByRow( pArray, nCol, nEndCol, pProgress, true); } } void ScTable::SortReorderAreaExtrasByColumn( const ScSortInfoArray* pArray, SCROW nDataRow1, SCROW nDataRow2, const ScDataAreaExtras& rDataAreaExtras, ScProgress* pProgress ) { const SCCOL nCol1 = static_cast(pArray->GetStart()); const SCCOL nLastCol = static_cast(pArray->GetLast()); const SCROW nChunkRows = std::max( 1, kSortCellsChunk / (nLastCol - nCol1 + 1)); // Above data area. for (SCROW nRow = rDataAreaExtras.mnStartRow; nRow < nDataRow1; nRow += nChunkRows) { const SCROW nEndRow = std::min( nRow + nChunkRows - 1, nDataRow1 - 1); SortReorderByColumn( pArray, nRow, nEndRow, rDataAreaExtras.mbCellFormats, pProgress); } // Below data area. for (SCROW nRow = nDataRow2 + 1; nRow <= rDataAreaExtras.mnEndRow; nRow += nChunkRows) { const SCROW nEndRow = std::min( nRow + nChunkRows - 1, rDataAreaExtras.mnEndRow); SortReorderByColumn( pArray, nRow, nEndRow, rDataAreaExtras.mbCellFormats, pProgress); } } void ScTable::SortReorderByColumn( const ScSortInfoArray* pArray, SCROW nRow1, SCROW nRow2, bool bPattern, ScProgress* pProgress ) { SCCOLROW nStart = pArray->GetStart(); SCCOLROW nLast = pArray->GetLast(); std::vector aIndices = pArray->GetOrderIndices(); size_t nCount = aIndices.size(); // Cut formula grouping at row and reference boundaries before the reordering. ScRange aSortRange(nStart, nRow1, nTab, nLast, nRow2, nTab); for (SCCOL nCol = nStart; nCol <= static_cast(nLast); ++nCol) aCol[nCol].SplitFormulaGroupByRelativeRef(aSortRange); // Collect all listeners of cell broadcasters of sorted range. std::vector aCellListeners; if (!pArray->IsUpdateRefs()) { // Collect listeners of cell broadcasters. for (SCCOL nCol = nStart; nCol <= static_cast(nLast); ++nCol) aCol[nCol].CollectListeners(aCellListeners, nRow1, nRow2); // Remove any duplicate listener entries. We must ensure that we // notify each unique listener only once. std::sort(aCellListeners.begin(), aCellListeners.end()); aCellListeners.erase(std::unique(aCellListeners.begin(), aCellListeners.end()), aCellListeners.end()); // Notify the cells' listeners to stop listening. /* TODO: for performance this could be enhanced to stop and later * restart only listening to within the reordered range and keep * listening to everything outside untouched. */ sc::RefStopListeningHint aHint; for (auto const & l : aCellListeners) l->Notify(aHint); } // table to keep track of column index to position in the index table. std::vector aPosTable(nCount); for (size_t i = 0; i < nCount; ++i) aPosTable[aIndices[i]-nStart] = i; SCCOLROW nDest = nStart; for (size_t i = 0; i < nCount; ++i, ++nDest) { SCCOLROW nSrc = aIndices[i]; if (nDest != nSrc) { aCol[nDest].Swap(aCol[nSrc], nRow1, nRow2, bPattern); // Update the position of the index that was originally equal to nDest. size_t nPos = aPosTable[nDest-nStart]; aIndices[nPos] = nSrc; aPosTable[nSrc-nStart] = nPos; } if (pProgress) pProgress->SetStateOnPercent(i); } // Reset formula cell positions which became out-of-sync after column reordering. bool bUpdateRefs = pArray->IsUpdateRefs(); for (SCCOL nCol = nStart; nCol <= static_cast(nLast); ++nCol) aCol[nCol].ResetFormulaCellPositions(nRow1, nRow2, bUpdateRefs); if (pArray->IsUpdateRefs()) { // Set up column reorder map (for later broadcasting of reference updates). sc::ColRowReorderMapType aColMap; const std::vector& rOldIndices = pArray->GetOrderIndices(); for (size_t i = 0, n = rOldIndices.size(); i < n; ++i) { SCCOL nNew = i + nStart; SCCOL nOld = rOldIndices[i]; aColMap.emplace(nOld, nNew); } // Collect all listeners within sorted range ahead of time. std::vector aListeners; for (SCCOL nCol = nStart; nCol <= static_cast(nLast); ++nCol) aCol[nCol].CollectListeners(aListeners, nRow1, nRow2); // Get all area listeners that listen on one column within the range // and end their listening. ScRange aMoveRange( nStart, nRow1, nTab, nLast, nRow2, nTab); std::vector aAreaListeners = rDocument.GetBASM()->GetAllListeners( aMoveRange, sc::AreaOverlapType::OneColumnInside); { for (auto& rAreaListener : aAreaListeners) { rDocument.EndListeningArea(rAreaListener.maArea, rAreaListener.mbGroupListening, rAreaListener.mpListener); aListeners.push_back( rAreaListener.mpListener); } } // Remove any duplicate listener entries and notify all listeners // afterward. We must ensure that we notify each unique listener only // once. std::sort(aListeners.begin(), aListeners.end()); aListeners.erase(std::unique(aListeners.begin(), aListeners.end()), aListeners.end()); ReorderNotifier aFunc(aColMap, nTab, nRow1, nRow2); std::for_each(aListeners.begin(), aListeners.end(), aFunc); // Re-start area listeners on the reordered columns. { for (auto& rAreaListener : aAreaListeners) { ScRange aNewRange = rAreaListener.maArea; sc::ColRowReorderMapType::const_iterator itCol = aColMap.find( aNewRange.aStart.Col()); if (itCol != aColMap.end()) { aNewRange.aStart.SetCol( itCol->second); aNewRange.aEnd.SetCol( itCol->second); } rDocument.StartListeningArea(aNewRange, rAreaListener.mbGroupListening, rAreaListener.mpListener); } } } else // !(pArray->IsUpdateRefs()) { // Notify the cells' listeners to (re-)start listening. sc::RefStartListeningHint aHint; for (auto const & l : aCellListeners) l->Notify(aHint); } // Re-join formulas at row boundaries now that all the references have // been adjusted for column reordering. for (SCCOL nCol = nStart; nCol <= static_cast(nLast); ++nCol) { sc::CellStoreType& rCells = aCol[nCol].maCells; sc::CellStoreType::position_type aPos = rCells.position(nRow1); sc::SharedFormulaUtil::joinFormulaCellAbove(aPos); if (nRow2 < rDocument.MaxRow()) { aPos = rCells.position(aPos.first, nRow2+1); sc::SharedFormulaUtil::joinFormulaCellAbove(aPos); } } } void ScTable::SortReorderByRow( ScSortInfoArray* pArray, SCCOL nCol1, SCCOL nCol2, ScProgress* pProgress, bool bOnlyDataAreaExtras ) { assert(!pArray->IsUpdateRefs()); if (nCol2 < nCol1) return; // bOnlyDataAreaExtras: // Data area extras by definition do not have any cell content so no // formula cells either, so that handling doesn't need to be executed. // However, there may be listeners of formulas listening to broadcasters of // empty cells. SCROW nRow1 = pArray->GetStart(); SCROW nRow2 = pArray->GetLast(); // Collect all listeners of cell broadcasters of sorted range. std::vector aCellListeners; // When the update ref mode is disabled, we need to detach all formula // cells in the sorted range before reordering, and re-start them // afterward. if (!bOnlyDataAreaExtras) { sc::EndListeningContext aCxt(rDocument); DetachFormulaCells(aCxt, nCol1, nRow1, nCol2, nRow2); } // Collect listeners of cell broadcasters. for (SCCOL nCol = nCol1; nCol <= nCol2; ++nCol) aCol[nCol].CollectListeners(aCellListeners, nRow1, nRow2); // Remove any duplicate listener entries. We must ensure that we notify // each unique listener only once. std::sort(aCellListeners.begin(), aCellListeners.end()); aCellListeners.erase(std::unique(aCellListeners.begin(), aCellListeners.end()), aCellListeners.end()); // Notify the cells' listeners to stop listening. /* TODO: for performance this could be enhanced to stop and later * restart only listening to within the reordered range and keep * listening to everything outside untouched. */ { sc::RefStopListeningHint aHint; for (auto const & l : aCellListeners) l->Notify(aHint); } // Split formula groups at the sort range boundaries (if applicable). if (!bOnlyDataAreaExtras) { std::vector aRowBounds { nRow1, nRow2+1 }; for (SCCOL nCol = nCol1; nCol <= nCol2; ++nCol) SplitFormulaGroups(nCol, aRowBounds); } // Cells in the data rows only reference values in the document. Make // a copy before updating the document. std::vector> aSortedCols; // storage for copied cells. SortedRowFlags aRowFlags(GetDoc().GetSheetLimits()); fillSortedColumnArray(aSortedCols, aRowFlags, aCellListeners, pArray, nTab, nCol1, nCol2, pProgress, this, bOnlyDataAreaExtras); for (size_t i = 0, n = aSortedCols.size(); i < n; ++i) { SCCOL nThisCol = i + nCol1; if (!bOnlyDataAreaExtras) { { sc::CellStoreType& rDest = aCol[nThisCol].maCells; sc::CellStoreType& rSrc = aSortedCols[i]->maCells; rSrc.transfer(nRow1, nRow2, rDest, nRow1); } { sc::CellTextAttrStoreType& rDest = aCol[nThisCol].maCellTextAttrs; sc::CellTextAttrStoreType& rSrc = aSortedCols[i]->maCellTextAttrs; rSrc.transfer(nRow1, nRow2, rDest, nRow1); } } { sc::CellNoteStoreType& rSrc = aSortedCols[i]->maCellNotes; sc::CellNoteStoreType& rDest = aCol[nThisCol].maCellNotes; // Do the same as broadcaster storage transfer (to prevent double deletion). rDest.release_range(nRow1, nRow2); rSrc.transfer(nRow1, nRow2, rDest, nRow1); aCol[nThisCol].UpdateNoteCaptions(nRow1, nRow2); } // Update draw object positions aCol[nThisCol].UpdateDrawObjects(aSortedCols[i]->maCellDrawObjects, nRow1, nRow2); { // Get all row spans where the pattern is not NULL. std::vector aSpans = sc::toSpanArrayWithValue( aSortedCols[i]->maPatterns); for (const auto& rSpan : aSpans) { assert(rSpan.mpPattern); // should never be NULL. rDocument.GetPool()->Put(*rSpan.mpPattern); } for (const auto& rSpan : aSpans) { aCol[nThisCol].SetPatternArea(rSpan.mnRow1, rSpan.mnRow2, *rSpan.mpPattern); rDocument.GetPool()->Remove(*rSpan.mpPattern); } } aCol[nThisCol].CellStorageModified(); } if (!bOnlyDataAreaExtras && pArray->IsKeepQuery()) { aRowFlags.maRowsHidden.build_tree(); aRowFlags.maRowsFiltered.build_tree(); // Remove all flags in the range first. SetRowHidden(nRow1, nRow2, false); SetRowFiltered(nRow1, nRow2, false); std::vector aSpans = sc::toSpanArray(aRowFlags.maRowsHidden, nRow1); for (const auto& rSpan : aSpans) SetRowHidden(rSpan.mnRow1, rSpan.mnRow2, true); aSpans = sc::toSpanArray(aRowFlags.maRowsFiltered, nRow1); for (const auto& rSpan : aSpans) SetRowFiltered(rSpan.mnRow1, rSpan.mnRow2, true); } // Notify the cells' listeners to (re-)start listening. { sc::RefStartListeningHint aHint; for (auto const & l : aCellListeners) l->Notify(aHint); } if (!bOnlyDataAreaExtras) { // Re-group columns in the sorted range too. for (SCCOL i = nCol1; i <= nCol2; ++i) aCol[i].RegroupFormulaCells(); { sc::StartListeningContext aCxt(rDocument); AttachFormulaCells(aCxt, nCol1, nRow1, nCol2, nRow2); } } } void ScTable::SortReorderByRowRefUpdate( ScSortInfoArray* pArray, SCCOL nCol1, SCCOL nCol2, ScProgress* pProgress ) { assert(pArray->IsUpdateRefs()); if (nCol2 < nCol1) return; SCROW nRow1 = pArray->GetStart(); SCROW nRow2 = pArray->GetLast(); ScRange aMoveRange( nCol1, nRow1, nTab, nCol2, nRow2, nTab); sc::ColumnSpanSet aGrpListenerRanges; { // Get the range of formula group listeners within sorted range (if any). sc::QueryRange aQuery; ScBroadcastAreaSlotMachine* pBASM = rDocument.GetBASM(); std::vector aGrpListeners = pBASM->GetAllListeners( aMoveRange, sc::AreaOverlapType::InsideOrOverlap, sc::ListenerGroupType::Group); { for (const auto& rGrpListener : aGrpListeners) { assert(rGrpListener.mbGroupListening); SvtListener* pGrpLis = rGrpListener.mpListener; pGrpLis->Query(aQuery); rDocument.EndListeningArea(rGrpListener.maArea, rGrpListener.mbGroupListening, pGrpLis); } } ScRangeList aTmp; aQuery.swapRanges(aTmp); // If the range is within the sorted range, we need to expand its rows // to the top and bottom of the sorted range, since the formula cells // could be anywhere in the sorted range after reordering. for (size_t i = 0, n = aTmp.size(); i < n; ++i) { ScRange aRange = aTmp[i]; if (!aMoveRange.Intersects(aRange)) { // Doesn't overlap with the sorted range at all. aGrpListenerRanges.set(GetDoc(), aRange, true); continue; } if (aMoveRange.aStart.Col() <= aRange.aStart.Col() && aRange.aEnd.Col() <= aMoveRange.aEnd.Col()) { // Its column range is within the column range of the sorted range. expandRowRange(aRange, aMoveRange.aStart.Row(), aMoveRange.aEnd.Row()); aGrpListenerRanges.set(GetDoc(), aRange, true); continue; } // It intersects with the sorted range, but its column range is // not within the column range of the sorted range. Split it into // 2 ranges. ScRange aR1 = aRange; ScRange aR2 = aRange; if (aRange.aStart.Col() < aMoveRange.aStart.Col()) { // Left half is outside the sorted range while the right half is inside. aR1.aEnd.SetCol(aMoveRange.aStart.Col()-1); aR2.aStart.SetCol(aMoveRange.aStart.Col()); expandRowRange(aR2, aMoveRange.aStart.Row(), aMoveRange.aEnd.Row()); } else { // Left half is inside the sorted range while the right half is outside. aR1.aEnd.SetCol(aMoveRange.aEnd.Col()-1); aR2.aStart.SetCol(aMoveRange.aEnd.Col()); expandRowRange(aR1, aMoveRange.aStart.Row(), aMoveRange.aEnd.Row()); } aGrpListenerRanges.set(GetDoc(), aR1, true); aGrpListenerRanges.set(GetDoc(), aR2, true); } } // Split formula groups at the sort range boundaries (if applicable). std::vector aRowBounds { nRow1, nRow2+1 }; for (SCCOL nCol = nCol1; nCol <= nCol2; ++nCol) SplitFormulaGroups(nCol, aRowBounds); // Cells in the data rows only reference values in the document. Make // a copy before updating the document. std::vector> aSortedCols; // storage for copied cells. SortedRowFlags aRowFlags(GetDoc().GetSheetLimits()); std::vector aListenersDummy; fillSortedColumnArray(aSortedCols, aRowFlags, aListenersDummy, pArray, nTab, nCol1, nCol2, pProgress, this, false); for (size_t i = 0, n = aSortedCols.size(); i < n; ++i) { SCCOL nThisCol = i + nCol1; { sc::CellStoreType& rDest = aCol[nThisCol].maCells; sc::CellStoreType& rSrc = aSortedCols[i]->maCells; rSrc.transfer(nRow1, nRow2, rDest, nRow1); } { sc::CellTextAttrStoreType& rDest = aCol[nThisCol].maCellTextAttrs; sc::CellTextAttrStoreType& rSrc = aSortedCols[i]->maCellTextAttrs; rSrc.transfer(nRow1, nRow2, rDest, nRow1); } { sc::BroadcasterStoreType& rSrc = aSortedCols[i]->maBroadcasters; sc::BroadcasterStoreType& rDest = aCol[nThisCol].maBroadcasters; // Release current broadcasters first, to prevent them from getting deleted. rDest.release_range(nRow1, nRow2); // Transfer sorted broadcaster segment to the document. rSrc.transfer(nRow1, nRow2, rDest, nRow1); } { sc::CellNoteStoreType& rSrc = aSortedCols[i]->maCellNotes; sc::CellNoteStoreType& rDest = aCol[nThisCol].maCellNotes; // Do the same as broadcaster storage transfer (to prevent double deletion). rDest.release_range(nRow1, nRow2); rSrc.transfer(nRow1, nRow2, rDest, nRow1); aCol[nThisCol].UpdateNoteCaptions(nRow1, nRow2); } // Update draw object positions aCol[nThisCol].UpdateDrawObjects(aSortedCols[i]->maCellDrawObjects, nRow1, nRow2); { // Get all row spans where the pattern is not NULL. std::vector aSpans = sc::toSpanArrayWithValue( aSortedCols[i]->maPatterns); for (const auto& rSpan : aSpans) { assert(rSpan.mpPattern); // should never be NULL. rDocument.GetPool()->Put(*rSpan.mpPattern); } for (const auto& rSpan : aSpans) { aCol[nThisCol].SetPatternArea(rSpan.mnRow1, rSpan.mnRow2, *rSpan.mpPattern); rDocument.GetPool()->Remove(*rSpan.mpPattern); } } aCol[nThisCol].CellStorageModified(); } if (pArray->IsKeepQuery()) { aRowFlags.maRowsHidden.build_tree(); aRowFlags.maRowsFiltered.build_tree(); // Remove all flags in the range first. SetRowHidden(nRow1, nRow2, false); SetRowFiltered(nRow1, nRow2, false); std::vector aSpans = sc::toSpanArray(aRowFlags.maRowsHidden, nRow1); for (const auto& rSpan : aSpans) SetRowHidden(rSpan.mnRow1, rSpan.mnRow2, true); aSpans = sc::toSpanArray(aRowFlags.maRowsFiltered, nRow1); for (const auto& rSpan : aSpans) SetRowFiltered(rSpan.mnRow1, rSpan.mnRow2, true); } // Set up row reorder map (for later broadcasting of reference updates). sc::ColRowReorderMapType aRowMap; const std::vector& rOldIndices = pArray->GetOrderIndices(); for (size_t i = 0, n = rOldIndices.size(); i < n; ++i) { SCROW nNew = i + nRow1; SCROW nOld = rOldIndices[i]; aRowMap.emplace(nOld, nNew); } // Collect all listeners within sorted range ahead of time. std::vector aListeners; // Collect listeners of cell broadcasters. for (SCCOL nCol = nCol1; nCol <= nCol2; ++nCol) aCol[nCol].CollectListeners(aListeners, nRow1, nRow2); // Get all area listeners that listen on one row within the range and end // their listening. std::vector aAreaListeners = rDocument.GetBASM()->GetAllListeners( aMoveRange, sc::AreaOverlapType::OneRowInside); { for (auto& rAreaListener : aAreaListeners) { rDocument.EndListeningArea(rAreaListener.maArea, rAreaListener.mbGroupListening, rAreaListener.mpListener); aListeners.push_back( rAreaListener.mpListener); } } { // Get all formula cells from the former group area listener ranges. std::vector aFCells; FormulaCellCollectAction aAction(aFCells); aGrpListenerRanges.executeColumnAction(rDocument, aAction); aListeners.insert( aListeners.end(), aFCells.begin(), aFCells.end() ); } // Remove any duplicate listener entries. We must ensure that we notify // each unique listener only once. std::sort(aListeners.begin(), aListeners.end()); aListeners.erase(std::unique(aListeners.begin(), aListeners.end()), aListeners.end()); // Collect positions of all shared formula cells outside the sorted range, // and make them unshared before notifying them. sc::RefQueryFormulaGroup aFormulaGroupPos; aFormulaGroupPos.setSkipRange(ScRange(nCol1, nRow1, nTab, nCol2, nRow2, nTab)); std::for_each(aListeners.begin(), aListeners.end(), FormulaGroupPosCollector(aFormulaGroupPos)); const sc::RefQueryFormulaGroup::TabsType& rGroupTabs = aFormulaGroupPos.getAllPositions(); for (const auto& [rTab, rCols] : rGroupTabs) { for (const auto& [nCol, rCol] : rCols) { std::vector aBounds(rCol); rDocument.UnshareFormulaCells(rTab, nCol, aBounds); } } // Notify the listeners to update their references. ReorderNotifier aFunc(aRowMap, nTab, nCol1, nCol2); std::for_each(aListeners.begin(), aListeners.end(), aFunc); // Re-group formulas in affected columns. for (const auto& [rTab, rCols] : rGroupTabs) { for (const auto& rEntry : rCols) rDocument.RegroupFormulaCells(rTab, rEntry.first); } // Re-start area listeners on the reordered rows. for (const auto& rAreaListener : aAreaListeners) { ScRange aNewRange = rAreaListener.maArea; sc::ColRowReorderMapType::const_iterator itRow = aRowMap.find( aNewRange.aStart.Row()); if (itRow != aRowMap.end()) { aNewRange.aStart.SetRow( itRow->second); aNewRange.aEnd.SetRow( itRow->second); } rDocument.StartListeningArea(aNewRange, rAreaListener.mbGroupListening, rAreaListener.mpListener); } // Re-group columns in the sorted range too. for (SCCOL i = nCol1; i <= nCol2; ++i) aCol[i].RegroupFormulaCells(); { // Re-start area listeners on the old group listener ranges. ListenerStartAction aAction(rDocument); aGrpListenerRanges.executeColumnAction(rDocument, aAction); } } short ScTable::CompareCell( sal_uInt16 nSort, ScRefCellValue& rCell1, SCCOL nCell1Col, SCROW nCell1Row, ScRefCellValue& rCell2, SCCOL nCell2Col, SCROW nCell2Row ) const { short nRes = 0; CellType eType1 = rCell1.meType, eType2 = rCell2.meType; if (!rCell1.isEmpty()) { if (!rCell2.isEmpty()) { bool bErr1 = false; bool bStr1 = ( eType1 != CELLTYPE_VALUE ); if (eType1 == CELLTYPE_FORMULA) { if (rCell1.mpFormula->GetErrCode() != FormulaError::NONE) { bErr1 = true; bStr1 = false; } else if (rCell1.mpFormula->IsValue()) { bStr1 = false; } } bool bErr2 = false; bool bStr2 = ( eType2 != CELLTYPE_VALUE ); if (eType2 == CELLTYPE_FORMULA) { if (rCell2.mpFormula->GetErrCode() != FormulaError::NONE) { bErr2 = true; bStr2 = false; } else if (rCell2.mpFormula->IsValue()) { bStr2 = false; } } if ( bStr1 && bStr2 ) // only compare strings as strings! { OUString aStr1; OUString aStr2; if (eType1 == CELLTYPE_STRING) aStr1 = rCell1.mpString->getString(); else aStr1 = GetString(nCell1Col, nCell1Row); if (eType2 == CELLTYPE_STRING) aStr2 = rCell2.mpString->getString(); else aStr2 = GetString(nCell2Col, nCell2Row); bool bUserDef = aSortParam.bUserDef; // custom sort order bool bNaturalSort = aSortParam.bNaturalSort; // natural sort bool bCaseSens = aSortParam.bCaseSens; // case sensitivity ScUserList* pList = ScGlobal::GetUserList(); if (bUserDef && pList && pList->size() > aSortParam.nUserIndex ) { const ScUserListData& rData = (*pList)[aSortParam.nUserIndex]; if ( bNaturalSort ) nRes = naturalsort::Compare( aStr1, aStr2, bCaseSens, &rData, pSortCollator ); else { if ( bCaseSens ) nRes = sal::static_int_cast( rData.Compare(aStr1, aStr2) ); else nRes = sal::static_int_cast( rData.ICompare(aStr1, aStr2) ); } } if (!bUserDef) { if ( bNaturalSort ) nRes = naturalsort::Compare( aStr1, aStr2, bCaseSens, nullptr, pSortCollator ); else nRes = static_cast( pSortCollator->compareString( aStr1, aStr2 ) ); } } else if ( bStr1 ) // String <-> Number or Error { if (bErr2) nRes = -1; // String in front of Error else nRes = 1; // Number in front of String } else if ( bStr2 ) // Number or Error <-> String { if (bErr1) nRes = 1; // String in front of Error else nRes = -1; // Number in front of String } else if (bErr1 && bErr2) { // nothing, two Errors are equal } else if (bErr1) // Error <-> Number { nRes = 1; // Number in front of Error } else if (bErr2) // Number <-> Error { nRes = -1; // Number in front of Error } else // Mixed numbers { double nVal1 = rCell1.getValue(); double nVal2 = rCell2.getValue(); if (nVal1 < nVal2) nRes = -1; else if (nVal1 > nVal2) nRes = 1; } if ( !aSortParam.maKeyState[nSort].bAscending ) nRes = -nRes; } else nRes = -1; } else { if (!rCell2.isEmpty()) nRes = 1; else nRes = 0; // both empty } return nRes; } short ScTable::Compare( ScSortInfoArray* pArray, SCCOLROW nIndex1, SCCOLROW nIndex2 ) const { short nRes; sal_uInt16 nSort = 0; do { ScSortInfo& rInfo1 = pArray->Get( nSort, nIndex1 ); ScSortInfo& rInfo2 = pArray->Get( nSort, nIndex2 ); if ( aSortParam.bByRow ) nRes = CompareCell( nSort, rInfo1.maCell, static_cast(aSortParam.maKeyState[nSort].nField), rInfo1.nOrg, rInfo2.maCell, static_cast(aSortParam.maKeyState[nSort].nField), rInfo2.nOrg ); else nRes = CompareCell( nSort, rInfo1.maCell, static_cast(rInfo1.nOrg), aSortParam.maKeyState[nSort].nField, rInfo2.maCell, static_cast(rInfo2.nOrg), aSortParam.maKeyState[nSort].nField ); } while ( nRes == 0 && ++nSort < pArray->GetUsedSorts() ); if( nRes == 0 ) { ScSortInfo& rInfo1 = pArray->Get( 0, nIndex1 ); ScSortInfo& rInfo2 = pArray->Get( 0, nIndex2 ); if( rInfo1.nOrg < rInfo2.nOrg ) nRes = -1; else if( rInfo1.nOrg > rInfo2.nOrg ) nRes = 1; } return nRes; } void ScTable::QuickSort( ScSortInfoArray* pArray, SCCOLROW nLo, SCCOLROW nHi ) { if ((nHi - nLo) == 1) { if (Compare(pArray, nLo, nHi) > 0) pArray->Swap( nLo, nHi ); } else { SCCOLROW ni = nLo; SCCOLROW nj = nHi; do { while ((ni <= nHi) && (Compare(pArray, ni, nLo)) < 0) ni++; while ((nj >= nLo) && (Compare(pArray, nLo, nj)) < 0) nj--; if (ni <= nj) { if (ni != nj) pArray->Swap( ni, nj ); ni++; nj--; } } while (ni < nj); if ((nj - nLo) < (nHi - ni)) { if (nLo < nj) QuickSort(pArray, nLo, nj); if (ni < nHi) QuickSort(pArray, ni, nHi); } else { if (ni < nHi) QuickSort(pArray, ni, nHi); if (nLo < nj) QuickSort(pArray, nLo, nj); } } } short ScTable::Compare(SCCOLROW nIndex1, SCCOLROW nIndex2) const { short nRes; sal_uInt16 nSort = 0; const sal_uInt32 nMaxSorts = aSortParam.GetSortKeyCount(); if (aSortParam.bByRow) { do { SCCOL nCol = static_cast(aSortParam.maKeyState[nSort].nField); nRes = 0; if(nCol < GetAllocatedColumnsCount()) { ScRefCellValue aCell1 = aCol[nCol].GetCellValue(nIndex1); ScRefCellValue aCell2 = aCol[nCol].GetCellValue(nIndex2); nRes = CompareCell(nSort, aCell1, nCol, nIndex1, aCell2, nCol, nIndex2); } } while ( nRes == 0 && ++nSort < nMaxSorts && aSortParam.maKeyState[nSort].bDoSort ); } else { do { SCROW nRow = aSortParam.maKeyState[nSort].nField; ScRefCellValue aCell1; ScRefCellValue aCell2; if(nIndex1 < GetAllocatedColumnsCount()) aCell1 = aCol[nIndex1].GetCellValue(nRow); if(nIndex2 < GetAllocatedColumnsCount()) aCell2 = aCol[nIndex2].GetCellValue(nRow); nRes = CompareCell( nSort, aCell1, static_cast(nIndex1), nRow, aCell2, static_cast(nIndex2), nRow ); } while ( nRes == 0 && ++nSort < nMaxSorts && aSortParam.maKeyState[nSort].bDoSort ); } return nRes; } bool ScTable::IsSorted( SCCOLROW nStart, SCCOLROW nEnd ) const // over aSortParam { for (SCCOLROW i=nStart; i 0) return false; } return true; } void ScTable::DecoladeRow( ScSortInfoArray* pArray, SCROW nRow1, SCROW nRow2 ) { SCROW nRow; int nMax = nRow2 - nRow1; for (SCROW i = nRow1; (i + 4) <= nRow2; i += 4) { nRow = comphelper::rng::uniform_int_distribution(0, nMax-1); pArray->Swap(i, nRow1 + nRow); } } void ScTable::Sort( const ScSortParam& rSortParam, bool bKeepQuery, bool bUpdateRefs, ScProgress* pProgress, sc::ReorderParam* pUndo ) { sc::DelayDeletingBroadcasters delayDeletingBroadcasters(GetDoc()); InitSortCollator( rSortParam ); bGlobalKeepQuery = bKeepQuery; if (pUndo) { // Copy over the basic sort parameters. pUndo->maDataAreaExtras = rSortParam.aDataAreaExtras; pUndo->mbByRow = rSortParam.bByRow; pUndo->mbHiddenFiltered = bKeepQuery; pUndo->mbUpdateRefs = bUpdateRefs; pUndo->mbHasHeaders = rSortParam.bHasHeader; } // It is assumed that the data area has already been trimmed as necessary. aSortParam = rSortParam; // must be assigned before calling IsSorted() if (rSortParam.bByRow) { const SCROW nLastRow = rSortParam.nRow2; const SCROW nRow1 = (rSortParam.bHasHeader ? rSortParam.nRow1 + 1 : rSortParam.nRow1); if (nRow1 < nLastRow && !IsSorted(nRow1, nLastRow)) { if(pProgress) pProgress->SetState( 0, nLastRow-nRow1 ); std::unique_ptr pArray( CreateSortInfoArray( aSortParam, nRow1, nLastRow, bKeepQuery, bUpdateRefs)); if ( nLastRow - nRow1 > 255 ) DecoladeRow(pArray.get(), nRow1, nLastRow); QuickSort(pArray.get(), nRow1, nLastRow); if (pArray->IsUpdateRefs()) SortReorderByRowRefUpdate(pArray.get(), aSortParam.nCol1, aSortParam.nCol2, pProgress); else { SortReorderByRow(pArray.get(), aSortParam.nCol1, aSortParam.nCol2, pProgress, false); if (rSortParam.aDataAreaExtras.anyExtrasWanted()) SortReorderAreaExtrasByRow( pArray.get(), aSortParam.nCol1, aSortParam.nCol2, rSortParam.aDataAreaExtras, pProgress); } if (pUndo) { // Stored is the first data row without header row. pUndo->maSortRange = ScRange(rSortParam.nCol1, nRow1, nTab, rSortParam.nCol2, nLastRow, nTab); pUndo->maDataAreaExtras.mnStartRow = nRow1; pUndo->maOrderIndices = pArray->GetOrderIndices(); } } } else { const SCCOL nLastCol = rSortParam.nCol2; const SCCOL nCol1 = (rSortParam.bHasHeader ? rSortParam.nCol1 + 1 : rSortParam.nCol1); if (nCol1 < nLastCol && !IsSorted(nCol1, nLastCol)) { if(pProgress) pProgress->SetState( 0, nLastCol-nCol1 ); std::unique_ptr pArray( CreateSortInfoArray( aSortParam, nCol1, nLastCol, bKeepQuery, bUpdateRefs)); QuickSort(pArray.get(), nCol1, nLastCol); SortReorderByColumn(pArray.get(), rSortParam.nRow1, rSortParam.nRow2, rSortParam.aDataAreaExtras.mbCellFormats, pProgress); if (rSortParam.aDataAreaExtras.anyExtrasWanted() && !pArray->IsUpdateRefs()) SortReorderAreaExtrasByColumn( pArray.get(), rSortParam.nRow1, rSortParam.nRow2, rSortParam.aDataAreaExtras, pProgress); if (pUndo) { // Stored is the first data column without header column. pUndo->maSortRange = ScRange(nCol1, aSortParam.nRow1, nTab, nLastCol, aSortParam.nRow2, nTab); pUndo->maDataAreaExtras.mnStartCol = nCol1; pUndo->maOrderIndices = pArray->GetOrderIndices(); } } } DestroySortCollator(); } void ScTable::Reorder( const sc::ReorderParam& rParam ) { if (rParam.maOrderIndices.empty()) return; std::unique_ptr pArray(CreateSortInfoArray(rParam)); if (!pArray) return; if (rParam.mbByRow) { // Re-play sorting from the known sort indices. pArray->ReorderByRow(rParam.maOrderIndices); if (pArray->IsUpdateRefs()) SortReorderByRowRefUpdate( pArray.get(), rParam.maSortRange.aStart.Col(), rParam.maSortRange.aEnd.Col(), nullptr); else { SortReorderByRow( pArray.get(), rParam.maSortRange.aStart.Col(), rParam.maSortRange.aEnd.Col(), nullptr, false); if (rParam.maDataAreaExtras.anyExtrasWanted()) SortReorderAreaExtrasByRow( pArray.get(), rParam.maSortRange.aStart.Col(), rParam.maSortRange.aEnd.Col(), rParam.maDataAreaExtras, nullptr); } } else { // Ordering by column is much simpler. Just set the order indices and we are done. pArray->SetOrderIndices(std::vector(rParam.maOrderIndices)); SortReorderByColumn( pArray.get(), rParam.maSortRange.aStart.Row(), rParam.maSortRange.aEnd.Row(), rParam.maDataAreaExtras.mbCellFormats, nullptr); if (rParam.maDataAreaExtras.anyExtrasWanted() && !pArray->IsUpdateRefs()) SortReorderAreaExtrasByColumn( pArray.get(), rParam.maSortRange.aStart.Row(), rParam.maSortRange.aEnd.Row(), rParam.maDataAreaExtras, nullptr); } } namespace { class SubTotalRowFinder { const ScTable& mrTab; const ScSubTotalParam& mrParam; public: SubTotalRowFinder(const ScTable& rTab, const ScSubTotalParam& rParam) : mrTab(rTab), mrParam(rParam) {} bool operator() (size_t nRow, const ScFormulaCell* pCell) { if (!pCell->IsSubTotal()) return false; SCCOL nStartCol = mrParam.nCol1; SCCOL nEndCol = mrParam.nCol2; for (SCCOL nCol : mrTab.GetAllocatedColumnsRange(0, nStartCol - 1)) { if (mrTab.HasData(nCol, nRow)) return true; } for (SCCOL nCol : mrTab.GetAllocatedColumnsRange(nEndCol + 1, mrTab.GetDoc().MaxCol())) { if (mrTab.HasData(nCol, nRow)) return true; } return false; } }; } bool ScTable::TestRemoveSubTotals( const ScSubTotalParam& rParam ) { SCCOL nStartCol = rParam.nCol1; SCROW nStartRow = rParam.nRow1 + 1; // Header SCCOL nEndCol = ClampToAllocatedColumns(rParam.nCol2); SCROW nEndRow = rParam.nRow2; for (SCCOL nCol = nStartCol; nCol <= nEndCol; ++nCol) { const sc::CellStoreType& rCells = aCol[nCol].maCells; SubTotalRowFinder aFunc(*this, rParam); std::pair aPos = sc::FindFormula(rCells, nStartRow, nEndRow, aFunc); if (aPos.first != rCells.end()) return true; } return false; } namespace { struct RemoveSubTotalsHandler { std::set aRemoved; void operator() (size_t nRow, const ScFormulaCell* p) { if (p->IsSubTotal()) aRemoved.insert(nRow); } }; } void ScTable::RemoveSubTotals( ScSubTotalParam& rParam ) { SCCOL nStartCol = rParam.nCol1; SCROW nStartRow = rParam.nRow1 + 1; // Header SCCOL nEndCol = ClampToAllocatedColumns(rParam.nCol2); SCROW nEndRow = rParam.nRow2; // will change RemoveSubTotalsHandler aFunc; for (SCCOL nCol = nStartCol; nCol <= nEndCol; ++nCol) { const sc::CellStoreType& rCells = aCol[nCol].maCells; sc::ParseFormula(rCells.begin(), rCells, nStartRow, nEndRow, aFunc); } auto& aRows = aFunc.aRemoved; std::for_each(aRows.rbegin(), aRows.rend(), [this](const SCROW nRow) { RemoveRowBreak(nRow+1, false, true); rDocument.DeleteRow(0, nTab, rDocument.MaxCol(), nTab, nRow, 1); }); rParam.nRow2 -= aRows.size(); } // Delete hard number formats (for result formulas) static void lcl_RemoveNumberFormat( ScTable* pTab, SCCOL nCol, SCROW nRow ) { const ScPatternAttr* pPattern = pTab->GetPattern( nCol, nRow ); if ( pPattern->GetItemSet().GetItemState( ATTR_VALUE_FORMAT, false ) == SfxItemState::SET ) { auto pNewPattern = std::make_unique( *pPattern ); SfxItemSet& rSet = pNewPattern->GetItemSet(); rSet.ClearItem( ATTR_VALUE_FORMAT ); rSet.ClearItem( ATTR_LANGUAGE_FORMAT ); pTab->SetPattern( nCol, nRow, std::move(pNewPattern) ); } } namespace { struct RowEntry { sal_uInt16 nGroupNo; SCROW nSubStartRow; SCROW nDestRow; SCROW nFuncStart; SCROW nFuncEnd; }; } static TranslateId lcl_GetSubTotalStrId(int id) { switch ( id ) { case SUBTOTAL_FUNC_AVE: return STR_FUN_TEXT_AVG; case SUBTOTAL_FUNC_CNT: case SUBTOTAL_FUNC_CNT2: return STR_FUN_TEXT_COUNT; case SUBTOTAL_FUNC_MAX: return STR_FUN_TEXT_MAX; case SUBTOTAL_FUNC_MIN: return STR_FUN_TEXT_MIN; case SUBTOTAL_FUNC_PROD: return STR_FUN_TEXT_PRODUCT; case SUBTOTAL_FUNC_STD: case SUBTOTAL_FUNC_STDP: return STR_FUN_TEXT_STDDEV; case SUBTOTAL_FUNC_SUM: return STR_FUN_TEXT_SUM; case SUBTOTAL_FUNC_VAR: case SUBTOTAL_FUNC_VARP: return STR_FUN_TEXT_VAR; default: { return STR_EMPTYDATA; // added to avoid warnings } } } // new intermediate results // rParam.nRow2 is changed! bool ScTable::DoSubTotals( ScSubTotalParam& rParam ) { SCCOL nStartCol = rParam.nCol1; SCROW nStartRow = rParam.nRow1 + 1; // Header SCCOL nEndCol = rParam.nCol2; SCROW nEndRow = rParam.nRow2; // will change sal_uInt16 i; // Remove empty rows at the end // so that all exceeding (rDocument.MaxRow()) can be found by InsertRow (#35180#) // If sorted, all empty rows are at the end. SCSIZE nEmpty = GetEmptyLinesInBlock( nStartCol, nStartRow, nEndCol, nEndRow, DIR_BOTTOM ); nEndRow -= nEmpty; sal_uInt16 nLevelCount = 0; // Number of levels bool bDoThis = true; for (i=0; i 1 ); OUString aSubString; bool bIgnoreCase = !rParam.bCaseSens; OUString aCompString[MAXSUBTOTAL]; //TODO: sort? ScStyleSheet* pStyle = static_cast(rDocument.GetStyleSheetPool()->Find( ScResId(STR_STYLENAME_RESULT), SfxStyleFamily::Para )); bool bSpaceLeft = true; // Success when inserting? // For performance reasons collect formula entries so their // references don't have to be tested for updates each time a new row is // inserted RowEntry aRowEntry; ::std::vector< RowEntry > aRowVector; for (sal_uInt16 nLevel=0; nLevel 0) // otherwise only sort { for (i=0; i<=aRowEntry.nGroupNo; i++) { aSubString = GetString( nGroupCol[i], nStartRow ); if ( bIgnoreCase ) aCompString[i] = ScGlobal::getCharClass().uppercase( aSubString ); else aCompString[i] = aSubString; } // aSubString stays on the last bool bBlockVis = false; // group visible? aRowEntry.nSubStartRow = nStartRow; for (SCROW nRow=nStartRow; nRow<=nEndRow+1 && bSpaceLeft; nRow++) { bool bChanged; if (nRow>nEndRow) bChanged = true; else { bChanged = false; OUString aString; for (i=0; i<=aRowEntry.nGroupNo && !bChanged; i++) { aString = GetString( nGroupCol[i], nRow ); if (bIgnoreCase) aString = ScGlobal::getCharClass().uppercase(aString); // when sorting, blanks are separate group // otherwise blank cells are allowed below bChanged = ( ( !aString.isEmpty() || rParam.bDoSort ) && aString != aCompString[i] ); } if ( bChanged && bTestPrevSub ) { // No group change on rows that will contain subtotal formulas bChanged = std::none_of(aRowVector.begin(), aRowVector.end(), [&nRow](const RowEntry& rEntry) { return rEntry.nDestRow == nRow; }); } } if ( bChanged ) { aRowEntry.nDestRow = nRow; aRowEntry.nFuncStart = aRowEntry.nSubStartRow; aRowEntry.nFuncEnd = nRow-1; bSpaceLeft = rDocument.InsertRow( 0, nTab, rDocument.MaxCol(), nTab, aRowEntry.nDestRow, 1 ); DBShowRow( aRowEntry.nDestRow, bBlockVis ); if ( rParam.bPagebreak && nRow < rDocument.MaxRow() && aRowEntry.nSubStartRow != nStartRow && nLevel == 0) SetRowBreak(aRowEntry.nSubStartRow, false, true); if (bSpaceLeft) { for ( auto& rRowEntry : aRowVector) { if ( aRowEntry.nDestRow <= rRowEntry.nSubStartRow ) ++rRowEntry.nSubStartRow; if ( aRowEntry.nDestRow <= rRowEntry.nDestRow ) ++rRowEntry.nDestRow; if ( aRowEntry.nDestRow <= rRowEntry.nFuncStart ) ++rRowEntry.nFuncStart; if ( aRowEntry.nDestRow <= rRowEntry.nFuncEnd ) ++rRowEntry.nFuncEnd; } // collect formula positions aRowVector.push_back( aRowEntry ); OUString aOutString = aSubString; if (aOutString.isEmpty()) aOutString = ScResId( STR_EMPTYDATA ); aOutString += " "; TranslateId pStrId = STR_TABLE_ERGEBNIS; if ( nResCount == 1 ) pStrId = lcl_GetSubTotalStrId(pResFunc[0]); aOutString += ScResId(pStrId); SetString( nGroupCol[aRowEntry.nGroupNo], aRowEntry.nDestRow, nTab, aOutString ); ApplyStyle( nGroupCol[aRowEntry.nGroupNo], aRowEntry.nDestRow, pStyle ); ++nRow; ++nEndRow; aRowEntry.nSubStartRow = nRow; for (i=0; i<=aRowEntry.nGroupNo; i++) { aSubString = GetString( nGroupCol[i], nRow ); if ( bIgnoreCase ) aCompString[i] = ScGlobal::getCharClass().uppercase( aSubString ); else aCompString[i] = aSubString; } } } bBlockVis = !RowFiltered(nRow); } } } if (!aRowVector.empty()) { // generate global total SCROW nGlobalStartRow = aRowVector[0].nSubStartRow; SCROW nGlobalStartFunc = aRowVector[0].nFuncStart; SCROW nGlobalEndRow = 0; SCROW nGlobalEndFunc = 0; for (const auto& rRowEntry : aRowVector) { nGlobalEndRow = (nGlobalEndRow < rRowEntry.nDestRow) ? rRowEntry.nDestRow : nGlobalEndRow; nGlobalEndFunc = (nGlobalEndFunc < rRowEntry.nFuncEnd) ? rRowEntry.nFuncEnd : nGlobalEndRow; } for (sal_uInt16 nLevel = 0; nLevel no formula or // label and do not insert a row. continue; } // increment end row nGlobalEndRow++; // add row entry for formula aRowEntry.nGroupNo = nGroupNo; aRowEntry.nSubStartRow = nGlobalStartRow; aRowEntry.nFuncStart = nGlobalStartFunc; aRowEntry.nDestRow = nGlobalEndRow; aRowEntry.nFuncEnd = nGlobalEndFunc; // increment row nGlobalEndFunc++; bSpaceLeft = rDocument.InsertRow(0, nTab, rDocument.MaxCol(), nTab, aRowEntry.nDestRow, 1); if (bSpaceLeft) { aRowVector.push_back(aRowEntry); nEndRow++; DBShowRow(aRowEntry.nDestRow, true); // insert label OUString label = ScResId(STR_TABLE_GRAND) + " " + ScResId(lcl_GetSubTotalStrId(pResFunc[0])); SetString(nGroupCol[aRowEntry.nGroupNo], aRowEntry.nDestRow, nTab, label); ApplyStyle(nGroupCol[aRowEntry.nGroupNo], aRowEntry.nDestRow, pStyle); } } } // now insert the formulas ScComplexRefData aRef; aRef.InitFlags(); aRef.Ref1.SetAbsTab(nTab); aRef.Ref2.SetAbsTab(nTab); for (const auto& rRowEntry : aRowVector) { SCCOL nResCount = rParam.nSubTotals[rRowEntry.nGroupNo]; SCCOL* nResCols = rParam.pSubTotals[rRowEntry.nGroupNo].get(); ScSubTotalFunc* pResFunc = rParam.pFunctions[rRowEntry.nGroupNo].get(); for ( SCCOL nResult=0; nResult < nResCount; ++nResult ) { aRef.Ref1.SetAbsCol(nResCols[nResult]); aRef.Ref1.SetAbsRow(rRowEntry.nFuncStart); aRef.Ref2.SetAbsCol(nResCols[nResult]); aRef.Ref2.SetAbsRow(rRowEntry.nFuncEnd); ScTokenArray aArr(rDocument); aArr.AddOpCode( ocSubTotal ); aArr.AddOpCode( ocOpen ); aArr.AddDouble( static_cast(pResFunc[nResult]) ); aArr.AddOpCode( ocSep ); aArr.AddDoubleReference( aRef ); aArr.AddOpCode( ocClose ); aArr.AddOpCode( ocStop ); ScFormulaCell* pCell = new ScFormulaCell( rDocument, ScAddress(nResCols[nResult], rRowEntry.nDestRow, nTab), aArr); if ( rParam.bIncludePattern ) pCell->SetNeedNumberFormat(true); SetFormulaCell(nResCols[nResult], rRowEntry.nDestRow, pCell); if ( nResCols[nResult] != nGroupCol[rRowEntry.nGroupNo] ) { ApplyStyle( nResCols[nResult], rRowEntry.nDestRow, pStyle ); lcl_RemoveNumberFormat( this, nResCols[nResult], rRowEntry.nDestRow ); } } } //TODO: according to setting, shift intermediate-sum rows up? //TODO: create Outlines directly? if (bSpaceLeft) DoAutoOutline( nStartCol, nStartRow, nEndCol, nEndRow ); rParam.nRow2 = nEndRow; // new end return bSpaceLeft; } void ScTable::TopTenQuery( ScQueryParam& rParam ) { bool bSortCollatorInitialized = false; SCSIZE nEntryCount = rParam.GetEntryCount(); SCROW nRow1 = (rParam.bHasHeader ? rParam.nRow1 + 1 : rParam.nRow1); SCSIZE nCount = static_cast(rParam.nRow2 - nRow1 + 1); for ( SCSIZE i=0; (i(rEntry.nField)); aSortParam = aLocalSortParam; // used in CreateSortInfoArray, Compare if (!bSortCollatorInitialized) { bSortCollatorInitialized = true; InitSortCollator(aLocalSortParam); } std::unique_ptr pArray(CreateSortInfoArray(aSortParam, nRow1, rParam.nRow2, bGlobalKeepQuery, false)); DecoladeRow(pArray.get(), nRow1, rParam.nRow2); QuickSort(pArray.get(), nRow1, rParam.nRow2); std::unique_ptr const& ppInfo = pArray->GetFirstArray(); SCSIZE nValidCount = nCount; // Don't count note or blank cells, they are sorted to the end while (nValidCount > 0 && ppInfo[nValidCount - 1].maCell.isEmpty()) nValidCount--; // Don't count Strings, they are between Value and blank while (nValidCount > 0 && ppInfo[nValidCount - 1].maCell.hasString()) nValidCount--; if (nValidCount > 0) { if (rItem.meType == ScQueryEntry::ByString) { // by string ain't going to work rItem.meType = ScQueryEntry::ByValue; rItem.mfVal = 10; // 10 and 10% respectively } SCSIZE nVal = (rItem.mfVal >= 1 ? static_cast(rItem.mfVal) : 1); SCSIZE nOffset = 0; switch (rEntry.eOp) { case SC_TOPVAL: { rEntry.eOp = SC_GREATER_EQUAL; if (nVal > nValidCount) nVal = nValidCount; nOffset = nValidCount - nVal; // 1 <= nVal <= nValidCount } break; case SC_BOTVAL: { rEntry.eOp = SC_LESS_EQUAL; if (nVal > nValidCount) nVal = nValidCount; nOffset = nVal - 1; // 1 <= nVal <= nValidCount } break; case SC_TOPPERC: { rEntry.eOp = SC_GREATER_EQUAL; if (nVal > 100) nVal = 100; nOffset = nValidCount - (nValidCount * nVal / 100); if (nOffset >= nValidCount) nOffset = nValidCount - 1; } break; case SC_BOTPERC: { rEntry.eOp = SC_LESS_EQUAL; if (nVal > 100) nVal = 100; nOffset = (nValidCount * nVal / 100); if (nOffset >= nValidCount) nOffset = nValidCount - 1; } break; default: { // added to avoid warnings } } ScRefCellValue aCell = ppInfo[nOffset].maCell; if (aCell.hasNumeric()) rItem.mfVal = aCell.getValue(); else { OSL_FAIL("TopTenQuery: pCell no ValueData"); rEntry.eOp = SC_GREATER_EQUAL; rItem.mfVal = 0; } } else { rEntry.eOp = SC_GREATER_EQUAL; rItem.meType = ScQueryEntry::ByValue; rItem.mfVal = 0; } } break; default: { // added to avoid warnings } } } } if ( bSortCollatorInitialized ) DestroySortCollator(); } namespace { bool CanOptimizeQueryStringToNumber( const SvNumberFormatter* pFormatter, sal_uInt32 nFormatIndex, bool& bDateFormat ) { // tdf#105629: ScQueryEntry::ByValue queries are faster than ScQueryEntry::ByString. // The problem with this optimization is that the autofilter dialog apparently converts // the value to text and then converts that back to a number for filtering. // If that leads to any change of value (such as when time is rounded to seconds), // even matching values will be filtered out. Therefore query by value only for formats // where no such change should occur. if(const SvNumberformat* pEntry = pFormatter->GetEntry(nFormatIndex)) { switch(pEntry->GetType()) { case SvNumFormatType::NUMBER: case SvNumFormatType::FRACTION: case SvNumFormatType::SCIENTIFIC: return true; case SvNumFormatType::DATE: case SvNumFormatType::DATETIME: bDateFormat = true; break; default: break; } } return false; } class PrepareQueryItem { const ScDocument& mrDoc; const bool mbRoundForFilter; public: explicit PrepareQueryItem(const ScDocument& rDoc, bool bRoundForFilter) : mrDoc(rDoc), mbRoundForFilter(bRoundForFilter) {} void operator() (ScQueryEntry::Item& rItem) { rItem.mbRoundForFilter = mbRoundForFilter; if (rItem.meType != ScQueryEntry::ByString && rItem.meType != ScQueryEntry::ByDate) return; sal_uInt32 nIndex = 0; bool bNumber = mrDoc.GetFormatTable()-> IsNumberFormat(rItem.maString.getString(), nIndex, rItem.mfVal); // Advanced Filter creates only ByString queries that need to be // converted to ByValue if appropriate. rItem.mfVal now holds the value // if bNumber==true. if (rItem.meType == ScQueryEntry::ByString) { bool bDateFormat = false; if (bNumber && CanOptimizeQueryStringToNumber( mrDoc.GetFormatTable(), nIndex, bDateFormat )) rItem.meType = ScQueryEntry::ByValue; if (!bDateFormat) return; } // Double-check if the query by date is really appropriate. if (bNumber && ((nIndex % SV_COUNTRY_LANGUAGE_OFFSET) != 0)) { const SvNumberformat* pEntry = mrDoc.GetFormatTable()->GetEntry(nIndex); if (pEntry) { SvNumFormatType nNumFmtType = pEntry->GetType(); if (!(nNumFmtType & SvNumFormatType::DATE) || (nNumFmtType & SvNumFormatType::TIME)) rItem.meType = ScQueryEntry::ByValue; // not a date only else rItem.meType = ScQueryEntry::ByDate; // date only } else rItem.meType = ScQueryEntry::ByValue; // what the ... not a date } else rItem.meType = ScQueryEntry::ByValue; // not a date } }; void lcl_PrepareQuery( const ScDocument* pDoc, ScTable* pTab, ScQueryParam& rParam, bool bRoundForFilter ) { bool bTopTen = false; SCSIZE nEntryCount = rParam.GetEntryCount(); for ( SCSIZE i = 0; i < nEntryCount; ++i ) { ScQueryEntry& rEntry = rParam.GetEntry(i); if (!rEntry.bDoQuery) continue; ScQueryEntry::QueryItemsType& rItems = rEntry.GetQueryItems(); std::for_each(rItems.begin(), rItems.end(), PrepareQueryItem(*pDoc, bRoundForFilter)); if ( !bTopTen ) { switch ( rEntry.eOp ) { case SC_TOPVAL: case SC_BOTVAL: case SC_TOPPERC: case SC_BOTPERC: { bTopTen = true; } break; default: { } } } } if ( bTopTen ) { pTab->TopTenQuery( rParam ); } } } void ScTable::PrepareQuery( ScQueryParam& rQueryParam ) { lcl_PrepareQuery(&rDocument, this, rQueryParam, false); } SCSIZE ScTable::Query(const ScQueryParam& rParamOrg, bool bKeepSub) { ScQueryParam aParam( rParamOrg ); typedef std::unordered_set StrSetType; StrSetType aStrSet; bool bStarted = false; bool bOldResult = true; SCROW nOldStart = 0; SCROW nOldEnd = 0; SCSIZE nCount = 0; SCROW nOutRow = 0; SCROW nHeader = aParam.bHasHeader ? 1 : 0; lcl_PrepareQuery(&rDocument, this, aParam, true); if (!aParam.bInplace) { nOutRow = aParam.nDestRow + nHeader; if (nHeader > 0) CopyData( aParam.nCol1, aParam.nRow1, aParam.nCol2, aParam.nRow1, aParam.nDestCol, aParam.nDestRow, aParam.nDestTab ); } sc::TableColumnBlockPositionSet blockPos( GetDoc(), nTab ); // cache mdds access ScQueryEvaluator queryEvaluator(GetDoc(), *this, aParam); SCROW nRealRow2 = aParam.nRow2; for (SCROW j = aParam.nRow1 + nHeader; j <= nRealRow2; ++j) { bool bResult; // Filter result bool bValid = queryEvaluator.ValidQuery(j, nullptr, &blockPos); if (!bValid && bKeepSub) // Keep subtotals { for (SCCOL nCol=aParam.nCol1; nCol<=aParam.nCol2 && !bValid; nCol++) { ScRefCellValue aCell = GetCellValue(nCol, j); if (aCell.meType != CELLTYPE_FORMULA) continue; if (!aCell.mpFormula->IsSubTotal()) continue; if (RefVisible(aCell.mpFormula)) bValid = true; } } if (bValid) { if (aParam.bDuplicate) bResult = true; else { OUStringBuffer aStr; for (SCCOL k=aParam.nCol1; k <= aParam.nCol2; k++) { OUString aCellStr = GetString(k, j); aStr.append(aCellStr + u"\x0001"); } bResult = aStrSet.insert(aStr.makeStringAndClear()).second; // unique if inserted. } } else bResult = false; if (aParam.bInplace) { if (bResult == bOldResult && bStarted) nOldEnd = j; else { if (bStarted) DBShowRows(nOldStart,nOldEnd, bOldResult); nOldStart = nOldEnd = j; bOldResult = bResult; } bStarted = true; } else { if (bResult) { CopyData( aParam.nCol1,j, aParam.nCol2,j, aParam.nDestCol,nOutRow,aParam.nDestTab ); if( nTab == aParam.nDestTab ) // copy to self, changes may invalidate caching position hints blockPos.invalidate(); ++nOutRow; } } if (bResult) ++nCount; } if (aParam.bInplace && bStarted) DBShowRows(nOldStart,nOldEnd, bOldResult); if (aParam.bInplace) SetDrawPageSize(); return nCount; } bool ScTable::CreateExcelQuery(SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2, ScQueryParam& rQueryParam) { bool bValid = true; std::unique_ptr pFields(new SCCOL[nCol2-nCol1+1]); OUString aCellStr; SCCOL nCol = nCol1; OSL_ENSURE( rQueryParam.nTab != SCTAB_MAX, "rQueryParam.nTab no value, not bad but no good" ); SCTAB nDBTab = (rQueryParam.nTab == SCTAB_MAX ? nTab : rQueryParam.nTab); SCROW nDBRow1 = rQueryParam.nRow1; SCCOL nDBCol2 = rQueryParam.nCol2; // First row must be column headers while (bValid && (nCol <= nCol2)) { OUString aQueryStr = GetUpperCellString(nCol, nRow1); bool bFound = false; SCCOL i = rQueryParam.nCol1; while (!bFound && (i <= nDBCol2)) { if ( nTab == nDBTab ) aCellStr = GetUpperCellString(i, nDBRow1); else aCellStr = rDocument.GetUpperCellString(i, nDBRow1, nDBTab); bFound = (aCellStr == aQueryStr); if (!bFound) i++; } if (bFound) pFields[nCol - nCol1] = i; else bValid = false; nCol++; } if (bValid) { sal_uLong nVisible = 0; for ( nCol=nCol1; nCol<=ClampToAllocatedColumns(nCol2); nCol++ ) nVisible += aCol[nCol].VisibleCount( nRow1+1, nRow2 ); if ( nVisible > SCSIZE_MAX / sizeof(void*) ) { OSL_FAIL("too many filter criteria"); nVisible = 0; } SCSIZE nNewEntries = nVisible; rQueryParam.Resize( nNewEntries ); SCSIZE nIndex = 0; SCROW nRow = nRow1 + 1; svl::SharedStringPool& rPool = rDocument.GetSharedStringPool(); while (nRow <= nRow2) { nCol = nCol1; while (nCol <= nCol2) { aCellStr = GetInputString( nCol, nRow ); if (!aCellStr.isEmpty()) { if (nIndex < nNewEntries) { rQueryParam.GetEntry(nIndex).nField = pFields[nCol - nCol1]; rQueryParam.FillInExcelSyntax(rPool, aCellStr, nIndex, nullptr); nIndex++; if (nIndex < nNewEntries) rQueryParam.GetEntry(nIndex).eConnect = SC_AND; } else bValid = false; } nCol++; } nRow++; if (nIndex < nNewEntries) rQueryParam.GetEntry(nIndex).eConnect = SC_OR; } } return bValid; } bool ScTable::CreateStarQuery(SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2, ScQueryParam& rQueryParam) { // A valid StarQuery must be at least 4 columns wide. To be precise it // should be exactly 4 columns ... // Additionally, if this wasn't checked, a formula pointing to a valid 1-3 // column Excel style query range immediately left to itself would result // in a circular reference when the field name or operator or value (first // to third query range column) is obtained (#i58354#). Furthermore, if the // range wasn't sufficiently specified data changes wouldn't flag formula // cells for recalculation. if (nCol2 - nCol1 < 3) return false; bool bValid; OUString aCellStr; SCSIZE nIndex = 0; SCROW nRow = nRow1; OSL_ENSURE( rQueryParam.nTab != SCTAB_MAX, "rQueryParam.nTab no value, not bad but no good" ); SCTAB nDBTab = (rQueryParam.nTab == SCTAB_MAX ? nTab : rQueryParam.nTab); SCROW nDBRow1 = rQueryParam.nRow1; SCCOL nDBCol2 = rQueryParam.nCol2; SCSIZE nNewEntries = static_cast(nRow2-nRow1+1); rQueryParam.Resize( nNewEntries ); svl::SharedStringPool& rPool = rDocument.GetSharedStringPool(); do { ScQueryEntry& rEntry = rQueryParam.GetEntry(nIndex); bValid = false; // First column AND/OR if (nIndex > 0) { aCellStr = GetUpperCellString(nCol1, nRow); if ( aCellStr == ScResId(STR_TABLE_AND) ) { rEntry.eConnect = SC_AND; bValid = true; } else if ( aCellStr == ScResId(STR_TABLE_OR) ) { rEntry.eConnect = SC_OR; bValid = true; } } // Second column field name if ((nIndex < 1) || bValid) { bool bFound = false; aCellStr = GetUpperCellString(nCol1 + 1, nRow); for (SCCOL i=rQueryParam.nCol1; (i <= nDBCol2) && (!bFound); i++) { OUString aFieldStr; if ( nTab == nDBTab ) aFieldStr = GetUpperCellString(i, nDBRow1); else aFieldStr = rDocument.GetUpperCellString(i, nDBRow1, nDBTab); bFound = (aCellStr == aFieldStr); if (bFound) { rEntry.nField = i; bValid = true; } else bValid = false; } } // Third column operator =<>... if (bValid) { aCellStr = GetUpperCellString(nCol1 + 2, nRow); if (aCellStr.startsWith("<")) { if (aCellStr[1] == '>') rEntry.eOp = SC_NOT_EQUAL; else if (aCellStr[1] == '=') rEntry.eOp = SC_LESS_EQUAL; else rEntry.eOp = SC_LESS; } else if (aCellStr.startsWith(">")) { if (aCellStr[1] == '=') rEntry.eOp = SC_GREATER_EQUAL; else rEntry.eOp = SC_GREATER; } else if (aCellStr.startsWith("=")) rEntry.eOp = SC_EQUAL; } // Fourth column values if (bValid) { OUString aStr = GetString(nCol1 + 3, nRow); rEntry.GetQueryItem().maString = rPool.intern(aStr); rEntry.bDoQuery = true; } nIndex++; nRow++; } while (bValid && (nRow <= nRow2) /* && (nIndex < MAXQUERY) */ ); return bValid; } bool ScTable::CreateQueryParam(SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2, ScQueryParam& rQueryParam) { SCSIZE i, nCount; PutInOrder(nCol1, nCol2); PutInOrder(nRow1, nRow2); nCount = rQueryParam.GetEntryCount(); for (i=0; i < nCount; i++) rQueryParam.GetEntry(i).Clear(); // Standard query table bool bValid = CreateStarQuery(nCol1, nRow1, nCol2, nRow2, rQueryParam); // Excel Query table if (!bValid) bValid = CreateExcelQuery(nCol1, nRow1, nCol2, nRow2, rQueryParam); SvNumberFormatter* pFormatter = rDocument.GetFormatTable(); nCount = rQueryParam.GetEntryCount(); if (bValid) { // query type must be set for (i=0; i < nCount; i++) { ScQueryEntry::Item& rItem = rQueryParam.GetEntry(i).GetQueryItem(); sal_uInt32 nIndex = 0; bool bNumber = pFormatter->IsNumberFormat( rItem.maString.getString(), nIndex, rItem.mfVal); bool bDateFormat = false; rItem.meType = bNumber && CanOptimizeQueryStringToNumber( pFormatter, nIndex, bDateFormat ) ? ScQueryEntry::ByValue : (bDateFormat ? ScQueryEntry::ByDate : ScQueryEntry::ByString); } } else { for (i=0; i < nCount; i++) rQueryParam.GetEntry(i).Clear(); } return bValid; } bool ScTable::HasColHeader( SCCOL nStartCol, SCROW nStartRow, SCCOL nEndCol, SCROW nEndRow) const { if (nStartRow == nEndRow) // Assume only data. /* XXX NOTE: previous behavior still checked this one row and could * evaluate it has header row, but that doesn't make much sense. */ return false; if (nStartCol == nEndCol) { CellType eFirstCellType = GetCellType(nStartCol, nStartRow); CellType eSecondCellType = GetCellType(nStartCol, nStartRow+1); return ((eFirstCellType == CELLTYPE_STRING || eFirstCellType == CELLTYPE_EDIT) && (eSecondCellType != CELLTYPE_STRING && eSecondCellType != CELLTYPE_EDIT)); } for (SCCOL nCol=nStartCol; nCol<=nEndCol; nCol++) { CellType eType = GetCellType( nCol, nStartRow ); // Any non-text cell in first row => not headers. if (eType != CELLTYPE_STRING && eType != CELLTYPE_EDIT) return false; } // First row all text cells, any non-text cell in second row => headers. SCROW nTestRow = nStartRow + 1; for (SCCOL nCol=nStartCol; nCol<=nEndCol; nCol++) { CellType eType = GetCellType( nCol, nTestRow ); if (eType != CELLTYPE_STRING && eType != CELLTYPE_EDIT) return true; } // Also second row all text cells => first row not headers. return false; } bool ScTable::HasRowHeader( SCCOL nStartCol, SCROW nStartRow, SCCOL nEndCol, SCROW nEndRow ) const { if (nStartCol == nEndCol) // Assume only data. /* XXX NOTE: previous behavior still checked this one column and could * evaluate it has header column, but that doesn't make much sense. */ return false; if (nStartRow == nEndRow) { CellType eFirstCellType = GetCellType(nStartCol, nStartRow); CellType eSecondCellType = GetCellType(nStartCol+1, nStartRow); return ((eFirstCellType == CELLTYPE_STRING || eFirstCellType == CELLTYPE_EDIT) && (eSecondCellType != CELLTYPE_STRING && eSecondCellType != CELLTYPE_EDIT)); } for (SCROW nRow=nStartRow; nRow<=nEndRow; nRow++) { CellType eType = GetCellType( nStartCol, nRow ); // Any non-text cell in first column => not headers. if (eType != CELLTYPE_STRING && eType != CELLTYPE_EDIT) return false; } // First column all text cells, any non-text cell in second column => headers. SCCOL nTestCol = nStartCol + 1; for (SCROW nRow=nStartRow; nRow<=nEndRow; nRow++) { CellType eType = GetCellType( nRow, nTestCol ); if (eType != CELLTYPE_STRING && eType != CELLTYPE_EDIT) return true; } // Also second column all text cells => first column not headers. return false; } void ScTable::GetFilterEntries( SCCOL nCol, SCROW nRow1, SCROW nRow2, ScFilterEntries& rFilterEntries, bool bFiltering ) { if (nCol >= aCol.size()) return; sc::ColumnBlockConstPosition aBlockPos; aCol[nCol].InitBlockPosition(aBlockPos); aCol[nCol].GetFilterEntries(aBlockPos, nRow1, nRow2, rFilterEntries, bFiltering); } void ScTable::GetFilteredFilterEntries( SCCOL nCol, SCROW nRow1, SCROW nRow2, const ScQueryParam& rParam, ScFilterEntries& rFilterEntries, bool bFiltering ) { if (nCol >= aCol.size()) return; sc::ColumnBlockConstPosition aBlockPos; aCol[nCol].InitBlockPosition(aBlockPos); // remove the entry for this column from the query parameter ScQueryParam aParam( rParam ); aParam.RemoveEntryByField(nCol); lcl_PrepareQuery(&rDocument, this, aParam, true); ScQueryEvaluator queryEvaluator(GetDoc(), *this, aParam); for ( SCROW j = nRow1; j <= nRow2; ++j ) { if (queryEvaluator.ValidQuery(j)) { aCol[nCol].GetFilterEntries(aBlockPos, j, j, rFilterEntries, bFiltering); } } } bool ScTable::GetDataEntries(SCCOL nCol, SCROW nRow, std::set& rStrings) { if (!ValidCol(nCol) || nCol >= GetAllocatedColumnsCount()) return false; return aCol[nCol].GetDataEntries( nRow, rStrings); } sal_uInt64 ScTable::GetCellCount() const { sal_uInt64 nCellCount = 0; for ( SCCOL nCol=0; nCol < aCol.size(); nCol++ ) nCellCount += aCol[nCol].GetCellCount(); return nCellCount; } sal_uInt64 ScTable::GetWeightedCount() const { sal_uInt64 nCellCount = 0; for ( SCCOL nCol=0; nCol < aCol.size(); nCol++ ) nCellCount += aCol[nCol].GetWeightedCount(); return nCellCount; } sal_uInt64 ScTable::GetWeightedCount(SCROW nStartRow, SCROW nEndRow) const { sal_uInt64 nCellCount = 0; for ( SCCOL nCol=0; nCol < aCol.size(); nCol++ ) nCellCount += aCol[nCol].GetWeightedCount(nStartRow, nEndRow); return nCellCount; } sal_uInt64 ScTable::GetCodeCount() const { sal_uInt64 nCodeCount = 0; for ( SCCOL nCol=0; nCol < aCol.size(); nCol++ ) if ( aCol[nCol].GetCellCount() ) nCodeCount += aCol[nCol].GetCodeCount(); return nCodeCount; } sal_Int32 ScTable::GetMaxStringLen( SCCOL nCol, SCROW nRowStart, SCROW nRowEnd, rtl_TextEncoding eCharSet ) const { if ( IsColValid( nCol ) ) return aCol[nCol].GetMaxStringLen( nRowStart, nRowEnd, eCharSet ); else return 0; } sal_Int32 ScTable::GetMaxNumberStringLen( sal_uInt16& nPrecision, SCCOL nCol, SCROW nRowStart, SCROW nRowEnd ) const { if ( IsColValid( nCol ) ) return aCol[nCol].GetMaxNumberStringLen( nPrecision, nRowStart, nRowEnd ); else return 0; } void ScTable::UpdateSelectionFunction( ScFunctionData& rData, const ScMarkData& rMark ) { ScRangeList aRanges = rMark.GetMarkedRangesForTab( nTab ); ScRange aMarkArea( ScAddress::UNINITIALIZED ); if (rMark.IsMultiMarked()) aMarkArea = rMark.GetMultiMarkArea(); else if (rMark.IsMarked()) aMarkArea = rMark.GetMarkArea(); else { assert(!"ScTable::UpdateSelectionFunction - called without anything marked"); aMarkArea.aStart.SetCol(0); aMarkArea.aEnd.SetCol(rDocument.MaxCol()); } const SCCOL nStartCol = aMarkArea.aStart.Col(); const SCCOL nEndCol = ClampToAllocatedColumns(aMarkArea.aEnd.Col()); for (SCCOL nCol = nStartCol; nCol <= nEndCol && !rData.getError(); ++nCol) { if (mpColFlags && ColHidden(nCol)) continue; aCol[nCol].UpdateSelectionFunction(aRanges, rData, *mpHiddenRows); } } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */