diff options
Diffstat (limited to 'sc/source/core/data/conditio.cxx')
-rw-r--r-- | sc/source/core/data/conditio.cxx | 2335 |
1 files changed, 2335 insertions, 0 deletions
diff --git a/sc/source/core/data/conditio.cxx b/sc/source/core/data/conditio.cxx new file mode 100644 index 000000000..dae08455b --- /dev/null +++ b/sc/source/core/data/conditio.cxx @@ -0,0 +1,2335 @@ +/* -*- 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 <scitems.hxx> +#include <svl/numformat.hxx> +#include <rtl/math.hxx> +#include <sal/log.hxx> +#include <unotools/collatorwrapper.hxx> + +#include <com/sun/star/sheet/ConditionOperator2.hpp> + +#include <attrib.hxx> +#include <conditio.hxx> +#include <formulacell.hxx> +#include <document.hxx> +#include <compiler.hxx> +#include <rangelst.hxx> +#include <rangenam.hxx> +#include <rangeutl.hxx> +#include <colorscale.hxx> +#include <cellvalue.hxx> +#include <editutil.hxx> +#include <tokenarray.hxx> +#include <fillinfo.hxx> +#include <refupdatecontext.hxx> +#include <formula/errorcodes.hxx> +#include <svl/sharedstring.hxx> +#include <svl/sharedstringpool.hxx> +#include <memory> +#include <numeric> + +using namespace formula; + +ScFormatEntry::ScFormatEntry(ScDocument* pDoc): + mpDoc(pDoc) +{ +} + +bool ScFormatEntry::operator==( const ScFormatEntry& r ) const +{ + return IsEqual(r, false); +} + +// virtual +bool ScFormatEntry::IsEqual( const ScFormatEntry& /*r*/, bool /*bIgnoreSrcPos*/ ) const +{ + // By default, return false; this makes sense for all cases except ScConditionEntry + // As soon as databar and color scale are tested we need to think about the range + return false; +} + +void ScFormatEntry::startRendering() +{ +} + +void ScFormatEntry::endRendering() +{ +} + +static bool lcl_HasRelRef( ScDocument* pDoc, const ScTokenArray* pFormula, sal_uInt16 nRecursion = 0 ) +{ + if (pFormula) + { + FormulaTokenArrayPlainIterator aIter( *pFormula ); + FormulaToken* t; + for( t = aIter.Next(); t; t = aIter.Next() ) + { + switch( t->GetType() ) + { + case svDoubleRef: + { + ScSingleRefData& rRef2 = t->GetDoubleRef()->Ref2; + if ( rRef2.IsColRel() || rRef2.IsRowRel() || rRef2.IsTabRel() ) + return true; + [[fallthrough]]; + } + + case svSingleRef: + { + ScSingleRefData& rRef1 = *t->GetSingleRef(); + if ( rRef1.IsColRel() || rRef1.IsRowRel() || rRef1.IsTabRel() ) + return true; + } + break; + + case svIndex: + { + if( t->GetOpCode() == ocName ) // DB areas always absolute + if( ScRangeData* pRangeData = pDoc->FindRangeNameBySheetAndIndex( t->GetSheet(), t->GetIndex()) ) + if( (nRecursion < 42) && lcl_HasRelRef( pDoc, pRangeData->GetCode(), nRecursion + 1 ) ) + return true; + } + break; + + // #i34474# function result dependent on cell position + case svByte: + { + switch( t->GetOpCode() ) + { + case ocRow: // ROW() returns own row index + case ocColumn: // COLUMN() returns own column index + case ocSheet: // SHEET() returns own sheet index + case ocCell: // CELL() may return own cell address + return true; + default: + { + // added to avoid warnings + } + } + } + break; + + default: + { + // added to avoid warnings + } + } + } + } + return false; +} + +namespace { + +void start_listen_to(ScFormulaListener& rListener, const ScTokenArray* pTokens, const ScRangeList& rRangeList) +{ + size_t n = rRangeList.size(); + for (size_t i = 0; i < n; ++i) + { + const ScRange & rRange = rRangeList[i]; + rListener.addTokenArray(pTokens, rRange); + } +} + +} + +void ScConditionEntry::StartListening() +{ + if (!pCondFormat) + return; + + const ScRangeList& rRanges = pCondFormat->GetRange(); + mpListener->stopListening(); + start_listen_to(*mpListener, pFormula1.get(), rRanges); + start_listen_to(*mpListener, pFormula2.get(), rRanges); + + mpListener->setCallback([&]() { pCondFormat->DoRepaint();}); +} + +void ScConditionEntry::SetParent(ScConditionalFormat* pParent) +{ + pCondFormat = pParent; + StartListening(); +} + +ScConditionEntry::ScConditionEntry( const ScConditionEntry& r ) : + ScFormatEntry(r.mpDoc), + eOp(r.eOp), + nOptions(r.nOptions), + nVal1(r.nVal1), + nVal2(r.nVal2), + aStrVal1(r.aStrVal1), + aStrVal2(r.aStrVal2), + aStrNmsp1(r.aStrNmsp1), + aStrNmsp2(r.aStrNmsp2), + eTempGrammar1(r.eTempGrammar1), + eTempGrammar2(r.eTempGrammar2), + bIsStr1(r.bIsStr1), + bIsStr2(r.bIsStr2), + aSrcPos(r.aSrcPos), + aSrcString(r.aSrcString), + bRelRef1(r.bRelRef1), + bRelRef2(r.bRelRef2), + bFirstRun(true), + mpListener(new ScFormulaListener(*r.mpDoc)), + eConditionType( r.eConditionType ), + pCondFormat(r.pCondFormat) +{ + // ScTokenArray copy ctor creates a flat copy + if (r.pFormula1) + pFormula1.reset( new ScTokenArray( *r.pFormula1 ) ); + if (r.pFormula2) + pFormula2.reset( new ScTokenArray( *r.pFormula2 ) ); + + StartListening(); + // Formula cells are created at IsValid +} + +ScConditionEntry::ScConditionEntry( ScDocument& rDocument, const ScConditionEntry& r ) : + ScFormatEntry(&rDocument), + eOp(r.eOp), + nOptions(r.nOptions), + nVal1(r.nVal1), + nVal2(r.nVal2), + aStrVal1(r.aStrVal1), + aStrVal2(r.aStrVal2), + aStrNmsp1(r.aStrNmsp1), + aStrNmsp2(r.aStrNmsp2), + eTempGrammar1(r.eTempGrammar1), + eTempGrammar2(r.eTempGrammar2), + bIsStr1(r.bIsStr1), + bIsStr2(r.bIsStr2), + aSrcPos(r.aSrcPos), + aSrcString(r.aSrcString), + bRelRef1(r.bRelRef1), + bRelRef2(r.bRelRef2), + bFirstRun(true), + mpListener(new ScFormulaListener(rDocument)), + eConditionType( r.eConditionType), + pCondFormat(r.pCondFormat) +{ + // Real copy of the formulas (for Ref Undo) + if (r.pFormula1) + pFormula1 = r.pFormula1->Clone(); + if (r.pFormula2) + pFormula2 = r.pFormula2->Clone(); + + // Formula cells are created at IsValid + // TODO: But not in the Clipboard! So interpret beforehand! +} + +ScConditionEntry::ScConditionEntry( ScConditionMode eOper, + const OUString& rExpr1, const OUString& rExpr2, ScDocument& rDocument, const ScAddress& rPos, + const OUString& rExprNmsp1, const OUString& rExprNmsp2, + FormulaGrammar::Grammar eGrammar1, FormulaGrammar::Grammar eGrammar2, + Type eType ) : + ScFormatEntry(&rDocument), + eOp(eOper), + nOptions(0), + nVal1(0.0), + nVal2(0.0), + aStrNmsp1(rExprNmsp1), + aStrNmsp2(rExprNmsp2), + eTempGrammar1(eGrammar1), + eTempGrammar2(eGrammar2), + bIsStr1(false), + bIsStr2(false), + aSrcPos(rPos), + bRelRef1(false), + bRelRef2(false), + bFirstRun(true), + mpListener(new ScFormulaListener(rDocument)), + eConditionType(eType), + pCondFormat(nullptr) +{ + Compile( rExpr1, rExpr2, rExprNmsp1, rExprNmsp2, eGrammar1, eGrammar2, false ); + + // Formula cells are created at IsValid +} + +ScConditionEntry::ScConditionEntry( ScConditionMode eOper, + const ScTokenArray* pArr1, const ScTokenArray* pArr2, + ScDocument& rDocument, const ScAddress& rPos ) : + ScFormatEntry(&rDocument), + eOp(eOper), + nOptions(0), + nVal1(0.0), + nVal2(0.0), + eTempGrammar1(FormulaGrammar::GRAM_DEFAULT), + eTempGrammar2(FormulaGrammar::GRAM_DEFAULT), + bIsStr1(false), + bIsStr2(false), + aSrcPos(rPos), + bRelRef1(false), + bRelRef2(false), + bFirstRun(true), + mpListener(new ScFormulaListener(rDocument)), + eConditionType(ScFormatEntry::Type::Condition), + pCondFormat(nullptr) +{ + if ( pArr1 ) + { + pFormula1.reset( new ScTokenArray( *pArr1 ) ); + SimplifyCompiledFormula( pFormula1, nVal1, bIsStr1, aStrVal1 ); + bRelRef1 = lcl_HasRelRef( mpDoc, pFormula1.get() ); + } + if ( pArr2 ) + { + pFormula2.reset( new ScTokenArray( *pArr2 ) ); + SimplifyCompiledFormula( pFormula2, nVal2, bIsStr2, aStrVal2 ); + bRelRef2 = lcl_HasRelRef( mpDoc, pFormula2.get() ); + } + + StartListening(); + + // Formula cells are created at IsValid +} + +ScConditionEntry::~ScConditionEntry() +{ +} + +void ScConditionEntry::SimplifyCompiledFormula( std::unique_ptr<ScTokenArray>& rFormula, + double& rVal, + bool& rIsStr, + OUString& rStrVal ) +{ + if ( rFormula->GetLen() != 1 ) + return; + + // Single (constant number)? + FormulaToken* pToken = rFormula->FirstToken(); + if ( pToken->GetOpCode() != ocPush ) + return; + + if ( pToken->GetType() == svDouble ) + { + rVal = pToken->GetDouble(); + rFormula.reset(); // Do not remember as formula + } + else if ( pToken->GetType() == svString ) + { + rIsStr = true; + rStrVal = pToken->GetString().getString(); + rFormula.reset(); // Do not remember as formula + } +} + +void ScConditionEntry::SetOperation(ScConditionMode eMode) +{ + eOp = eMode; +} + +void ScConditionEntry::Compile( const OUString& rExpr1, const OUString& rExpr2, + const OUString& rExprNmsp1, const OUString& rExprNmsp2, + FormulaGrammar::Grammar eGrammar1, FormulaGrammar::Grammar eGrammar2, bool bTextToReal ) +{ + if ( !rExpr1.isEmpty() || !rExpr2.isEmpty() ) + { + ScCompiler aComp( *mpDoc, aSrcPos ); + + if ( !rExpr1.isEmpty() ) + { + pFormula1.reset(); + aComp.SetGrammar( eGrammar1 ); + if ( mpDoc->IsImportingXML() && !bTextToReal ) + { + // temporary formula string as string tokens + pFormula1.reset( new ScTokenArray(*mpDoc) ); + pFormula1->AssignXMLString( rExpr1, rExprNmsp1 ); + // bRelRef1 is set when the formula is compiled again (CompileXML) + } + else + { + pFormula1 = aComp.CompileString( rExpr1, rExprNmsp1 ); + SimplifyCompiledFormula( pFormula1, nVal1, bIsStr1, aStrVal1 ); + bRelRef1 = lcl_HasRelRef( mpDoc, pFormula1.get() ); + } + } + + if ( !rExpr2.isEmpty() ) + { + pFormula2.reset(); + aComp.SetGrammar( eGrammar2 ); + if ( mpDoc->IsImportingXML() && !bTextToReal ) + { + // temporary formula string as string tokens + pFormula2.reset( new ScTokenArray(*mpDoc) ); + pFormula2->AssignXMLString( rExpr2, rExprNmsp2 ); + // bRelRef2 is set when the formula is compiled again (CompileXML) + } + else + { + pFormula2 = aComp.CompileString( rExpr2, rExprNmsp2 ); + SimplifyCompiledFormula( pFormula2, nVal2, bIsStr2, aStrVal2 ); + bRelRef2 = lcl_HasRelRef( mpDoc, pFormula2.get() ); + } + } + } + + StartListening(); +} + +/** + * Create formula cells + */ +void ScConditionEntry::MakeCells( const ScAddress& rPos ) +{ + if ( mpDoc->IsClipOrUndo() ) // Never calculate in the Clipboard! + return; + + if ( pFormula1 && !pFCell1 && !bRelRef1 ) + { + // pFCell1 will hold a flat-copied ScTokenArray sharing ref-counted + // code tokens with pFormula1 + pFCell1.reset( new ScFormulaCell(*mpDoc, rPos, *pFormula1) ); + pFCell1->SetFreeFlying(true); + pFCell1->StartListeningTo( *mpDoc ); + } + + if ( pFormula2 && !pFCell2 && !bRelRef2 ) + { + // pFCell2 will hold a flat-copied ScTokenArray sharing ref-counted + // code tokens with pFormula2 + pFCell2.reset( new ScFormulaCell(*mpDoc, rPos, *pFormula2) ); + pFCell2->SetFreeFlying(true); + pFCell2->StartListeningTo( *mpDoc ); + } +} + +void ScConditionEntry::SetIgnoreBlank(bool bSet) +{ + // The bit SC_COND_NOBLANKS is set if blanks are not ignored + // (only of valid) + if (bSet) + nOptions &= ~SC_COND_NOBLANKS; + else + nOptions |= SC_COND_NOBLANKS; +} + +/** + * Delete formula cells, so we re-compile at the next IsValid + */ +void ScConditionEntry::CompileAll() +{ + pFCell1.reset(); + pFCell2.reset(); +} + +void ScConditionEntry::CompileXML() +{ + // First parse the formula source position if it was stored as text + if ( !aSrcString.isEmpty() ) + { + ScAddress aNew; + /* XML is always in OOo:A1 format, although R1C1 would be more amenable + * to compression */ + if ( aNew.Parse( aSrcString, *mpDoc ) & ScRefFlags::VALID ) + aSrcPos = aNew; + // if the position is invalid, there isn't much we can do at this time + aSrcString.clear(); + } + + // Convert the text tokens that were created during XML import into real tokens. + Compile( GetExpression(aSrcPos, 0, 0, eTempGrammar1), + GetExpression(aSrcPos, 1, 0, eTempGrammar2), + aStrNmsp1, aStrNmsp2, eTempGrammar1, eTempGrammar2, true ); + + // Importing ocDde/ocWebservice? + if (pFormula1) + mpDoc->CheckLinkFormulaNeedingCheck(*pFormula1); + if (pFormula2) + mpDoc->CheckLinkFormulaNeedingCheck(*pFormula2); +} + +void ScConditionEntry::SetSrcString( const OUString& rNew ) +{ + // aSrcString is only evaluated in CompileXML + SAL_WARN_IF( !mpDoc->IsImportingXML(), "sc", "SetSrcString is only valid for XML import" ); + + aSrcString = rNew; +} + +void ScConditionEntry::SetFormula1( const ScTokenArray& rArray ) +{ + pFormula1.reset(); + if( rArray.GetLen() > 0 ) + { + pFormula1.reset( new ScTokenArray( rArray ) ); + bRelRef1 = lcl_HasRelRef( mpDoc, pFormula1.get() ); + } + + StartListening(); +} + +void ScConditionEntry::SetFormula2( const ScTokenArray& rArray ) +{ + pFormula2.reset(); + if( rArray.GetLen() > 0 ) + { + pFormula2.reset( new ScTokenArray( rArray ) ); + bRelRef2 = lcl_HasRelRef( mpDoc, pFormula2.get() ); + } + + StartListening(); +} + +void ScConditionEntry::UpdateReference( sc::RefUpdateContext& rCxt ) +{ + if(pCondFormat) + aSrcPos = pCondFormat->GetRange().Combine().aStart; + ScAddress aOldSrcPos = aSrcPos; + bool bChangedPos = false; + if (rCxt.meMode == URM_INSDEL && rCxt.maRange.Contains(aSrcPos)) + { + ScAddress aErrorPos( ScAddress::UNINITIALIZED ); + if (!aSrcPos.Move(rCxt.mnColDelta, rCxt.mnRowDelta, rCxt.mnTabDelta, aErrorPos, *mpDoc)) + { + assert(!"can't move ScConditionEntry"); + } + bChangedPos = aSrcPos != aOldSrcPos; + } + + if (pFormula1) + { + sc::RefUpdateResult aRes; + switch (rCxt.meMode) + { + case URM_INSDEL: + aRes = pFormula1->AdjustReferenceOnShift(rCxt, aOldSrcPos); + break; + case URM_MOVE: + aRes = pFormula1->AdjustReferenceOnMove(rCxt, aOldSrcPos, aSrcPos); + break; + default: + ; + } + + if (aRes.mbReferenceModified || bChangedPos) + pFCell1.reset(); // is created again in IsValid + } + + if (pFormula2) + { + sc::RefUpdateResult aRes; + switch (rCxt.meMode) + { + case URM_INSDEL: + aRes = pFormula2->AdjustReferenceOnShift(rCxt, aOldSrcPos); + break; + case URM_MOVE: + aRes = pFormula2->AdjustReferenceOnMove(rCxt, aOldSrcPos, aSrcPos); + break; + default: + ; + } + + if (aRes.mbReferenceModified || bChangedPos) + pFCell2.reset(); // is created again in IsValid + } + + StartListening(); +} + +void ScConditionEntry::UpdateInsertTab( sc::RefUpdateInsertTabContext& rCxt ) +{ + if (pFormula1) + { + pFormula1->AdjustReferenceOnInsertedTab(rCxt, aSrcPos); + pFCell1.reset(); + } + + if (pFormula2) + { + pFormula2->AdjustReferenceOnInsertedTab(rCxt, aSrcPos); + pFCell2.reset(); + } + + ScRangeUpdater::UpdateInsertTab(aSrcPos, rCxt); +} + +void ScConditionEntry::UpdateDeleteTab( sc::RefUpdateDeleteTabContext& rCxt ) +{ + if (pFormula1) + { + pFormula1->AdjustReferenceOnDeletedTab(rCxt, aSrcPos); + pFCell1.reset(); + } + + if (pFormula2) + { + pFormula2->AdjustReferenceOnDeletedTab(rCxt, aSrcPos); + pFCell2.reset(); + } + + ScRangeUpdater::UpdateDeleteTab(aSrcPos, rCxt); + StartListening(); +} + +void ScConditionEntry::UpdateMoveTab( sc::RefUpdateMoveTabContext& rCxt ) +{ + if (pFormula1) + { + pFormula1->AdjustReferenceOnMovedTab(rCxt, aSrcPos); + pFCell1.reset(); + } + + if (pFormula2) + { + pFormula2->AdjustReferenceOnMovedTab(rCxt, aSrcPos); + pFCell2.reset(); + } + + StartListening(); +} + +static bool lcl_IsEqual( const std::unique_ptr<ScTokenArray>& pArr1, const std::unique_ptr<ScTokenArray>& pArr2 ) +{ + // We only compare the non-RPN array + if ( pArr1 && pArr2 ) + return pArr1->EqualTokens( pArr2.get() ); + else + return !pArr1 && !pArr2; // Both 0? -> the same +} + +// virtual +bool ScConditionEntry::IsEqual( const ScFormatEntry& rOther, bool bIgnoreSrcPos ) const +{ + if (GetType() != rOther.GetType()) + return false; + + const ScConditionEntry& r = static_cast<const ScConditionEntry&>(rOther); + + bool bEq = (eOp == r.eOp && nOptions == r.nOptions && + lcl_IsEqual( pFormula1, r.pFormula1 ) && + lcl_IsEqual( pFormula2, r.pFormula2 )); + + if (!bIgnoreSrcPos) + { + // for formulas, the reference positions must be compared, too + // (including aSrcString, for inserting the entries during XML import) + if ( bEq && ( pFormula1 || pFormula2 ) && ( aSrcPos != r.aSrcPos || aSrcString != r.aSrcString ) ) + bEq = false; + } + + // If not formulas, compare values + if ( bEq && !pFormula1 && ( nVal1 != r.nVal1 || aStrVal1 != r.aStrVal1 || bIsStr1 != r.bIsStr1 ) ) + bEq = false; + if ( bEq && !pFormula2 && ( nVal2 != r.nVal2 || aStrVal2 != r.aStrVal2 || bIsStr2 != r.bIsStr2 ) ) + bEq = false; + + return bEq; +} + +void ScConditionEntry::Interpret( const ScAddress& rPos ) +{ + // Create formula cells + // Note: New Broadcaster (Note cells) may be inserted into the document! + if ( ( pFormula1 && !pFCell1 ) || ( pFormula2 && !pFCell2 ) ) + MakeCells( rPos ); + + // Evaluate formulas + bool bDirty = false; // 1 and 2 separate? + + std::unique_ptr<ScFormulaCell> pTemp1; + ScFormulaCell* pEff1 = pFCell1.get(); + if ( bRelRef1 ) + { + pTemp1.reset(pFormula1 ? new ScFormulaCell(*mpDoc, rPos, *pFormula1) : new ScFormulaCell(*mpDoc, rPos)); + pEff1 = pTemp1.get(); + pEff1->SetFreeFlying(true); + } + if ( pEff1 ) + { + if (!pEff1->IsRunning()) // Don't create 522 + { + //TODO: Query Changed instead of Dirty! + if (pEff1->GetDirty() && !bRelRef1 && mpDoc->GetAutoCalc()) + bDirty = true; + if (pEff1->IsValue()) + { + bIsStr1 = false; + nVal1 = pEff1->GetValue(); + aStrVal1.clear(); + } + else + { + bIsStr1 = true; + aStrVal1 = pEff1->GetString().getString(); + nVal1 = 0.0; + } + } + } + pTemp1.reset(); + + std::unique_ptr<ScFormulaCell> pTemp2; + ScFormulaCell* pEff2 = pFCell2.get(); //@ 1!=2 + if ( bRelRef2 ) + { + pTemp2.reset(pFormula2 ? new ScFormulaCell(*mpDoc, rPos, *pFormula2) : new ScFormulaCell(*mpDoc, rPos)); + pEff2 = pTemp2.get(); + pEff2->SetFreeFlying(true); + } + if ( pEff2 ) + { + if (!pEff2->IsRunning()) // Don't create 522 + { + if (pEff2->GetDirty() && !bRelRef2 && mpDoc->GetAutoCalc()) + bDirty = true; + if (pEff2->IsValue()) + { + bIsStr2 = false; + nVal2 = pEff2->GetValue(); + aStrVal2.clear(); + } + else + { + bIsStr2 = true; + aStrVal2 = pEff2->GetString().getString(); + nVal2 = 0.0; + } + } + } + pTemp2.reset(); + + // If IsRunning, the last values remain + if (bDirty && !bFirstRun) + { + // Repaint everything for dependent formats + DataChanged(); + } + + bFirstRun = false; +} + +static bool lcl_GetCellContent( ScRefCellValue& rCell, bool bIsStr1, double& rArg, OUString& rArgStr, + const ScDocument* pDoc ) +{ + + if (rCell.isEmpty()) + return !bIsStr1; + + bool bVal = true; + + switch (rCell.meType) + { + case CELLTYPE_VALUE: + rArg = rCell.mfValue; + break; + case CELLTYPE_FORMULA: + { + bVal = rCell.mpFormula->IsValue(); + if (bVal) + rArg = rCell.mpFormula->GetValue(); + else + rArgStr = rCell.mpFormula->GetString().getString(); + } + break; + case CELLTYPE_STRING: + case CELLTYPE_EDIT: + bVal = false; + if (rCell.meType == CELLTYPE_STRING) + rArgStr = rCell.mpString->getString(); + else if (rCell.mpEditText) + rArgStr = ScEditUtil::GetString(*rCell.mpEditText, pDoc); + break; + default: + ; + } + + return bVal; +} + +void ScConditionEntry::FillCache() const +{ + if(mpCache) + return; + + const ScRangeList& rRanges = pCondFormat->GetRange(); + mpCache.reset(new ScConditionEntryCache); + size_t nListCount = rRanges.size(); + for( size_t i = 0; i < nListCount; i++ ) + { + const ScRange & rRange = rRanges[i]; + SCROW nRow = rRange.aEnd.Row(); + SCCOL nCol = rRange.aEnd.Col(); + SCCOL nColStart = rRange.aStart.Col(); + SCROW nRowStart = rRange.aStart.Row(); + SCTAB nTab = rRange.aStart.Tab(); + + // temporary fix to workaround slow duplicate entry + // conditions, prevent to use a whole row + if(nRow == mpDoc->MaxRow()) + { + bool bShrunk = false; + mpDoc->ShrinkToUsedDataArea(bShrunk, nTab, nColStart, nRowStart, + nCol, nRow, false); + } + + for( SCROW r = nRowStart; r <= nRow; r++ ) + for( SCCOL c = nColStart; c <= nCol; c++ ) + { + ScRefCellValue aCell(*mpDoc, ScAddress(c, r, nTab)); + if (aCell.isEmpty()) + continue; + + double nVal = 0.0; + OUString aStr; + if (!lcl_GetCellContent(aCell, false, nVal, aStr, mpDoc)) + { + std::pair<ScConditionEntryCache::StringCacheType::iterator, bool> aResult = + mpCache->maStrings.emplace(aStr, 1); + + if(!aResult.second) + aResult.first->second++; + } + else + { + std::pair<ScConditionEntryCache::ValueCacheType::iterator, bool> aResult = + mpCache->maValues.emplace(nVal, 1); + + if(!aResult.second) + aResult.first->second++; + + ++(mpCache->nValueItems); + } + } + } +} + +bool ScConditionEntry::IsDuplicate( double nArg, const OUString& rStr ) const +{ + FillCache(); + + if(rStr.isEmpty()) + { + ScConditionEntryCache::ValueCacheType::iterator itr = mpCache->maValues.find(nArg); + if(itr == mpCache->maValues.end()) + return false; + else + { + return itr->second > 1; + } + } + else + { + ScConditionEntryCache::StringCacheType::iterator itr = mpCache->maStrings.find(rStr); + if(itr == mpCache->maStrings.end()) + return false; + else + { + return itr->second > 1; + } + } +} + +bool ScConditionEntry::IsTopNElement( double nArg ) const +{ + FillCache(); + + if(mpCache->nValueItems <= nVal1) + return true; + + size_t nCells = 0; + for(ScConditionEntryCache::ValueCacheType::const_reverse_iterator itr = mpCache->maValues.rbegin(), + itrEnd = mpCache->maValues.rend(); itr != itrEnd; ++itr) + { + if(nCells >= nVal1) + return false; + if(itr->first <= nArg) + return true; + nCells += itr->second; + } + + return true; +} + +bool ScConditionEntry::IsBottomNElement( double nArg ) const +{ + FillCache(); + + if(mpCache->nValueItems <= nVal1) + return true; + + size_t nCells = 0; + for(const auto& [rVal, rCount] : mpCache->maValues) + { + if(nCells >= nVal1) + return false; + if(rVal >= nArg) + return true; + nCells += rCount; + } + + return true; +} + +bool ScConditionEntry::IsTopNPercent( double nArg ) const +{ + FillCache(); + + size_t nCells = 0; + size_t nLimitCells = static_cast<size_t>(mpCache->nValueItems*nVal1/100); + for(ScConditionEntryCache::ValueCacheType::const_reverse_iterator itr = mpCache->maValues.rbegin(), + itrEnd = mpCache->maValues.rend(); itr != itrEnd; ++itr) + { + if(nCells >= nLimitCells) + return false; + if(itr->first <= nArg) + return true; + nCells += itr->second; + } + + return true; +} + +bool ScConditionEntry::IsBottomNPercent( double nArg ) const +{ + FillCache(); + + size_t nCells = 0; + size_t nLimitCells = static_cast<size_t>(mpCache->nValueItems*nVal1/100); + for(const auto& [rVal, rCount] : mpCache->maValues) + { + if(nCells >= nLimitCells) + return false; + if(rVal >= nArg) + return true; + nCells += rCount; + } + + return true; +} + +bool ScConditionEntry::IsBelowAverage( double nArg, bool bEqual ) const +{ + FillCache(); + + double nSum = std::accumulate(mpCache->maValues.begin(), mpCache->maValues.end(), double(0), + [](const double& rSum, const ScConditionEntryCache::ValueCacheType::value_type& rEntry) { + return rSum + rEntry.first * rEntry.second; }); + + if(bEqual) + return (nArg <= nSum/mpCache->nValueItems); + else + return (nArg < nSum/mpCache->nValueItems); +} + +bool ScConditionEntry::IsAboveAverage( double nArg, bool bEqual ) const +{ + FillCache(); + + double nSum = std::accumulate(mpCache->maValues.begin(), mpCache->maValues.end(), double(0), + [](const double& rSum, const ScConditionEntryCache::ValueCacheType::value_type& rEntry) { + return rSum + rEntry.first * rEntry.second; }); + + if(bEqual) + return (nArg >= nSum/mpCache->nValueItems); + else + return (nArg > nSum/mpCache->nValueItems); +} + +bool ScConditionEntry::IsError( const ScAddress& rPos ) const +{ + ScRefCellValue rCell(*mpDoc, rPos); + + if (rCell.meType == CELLTYPE_FORMULA) + { + if (rCell.mpFormula->GetErrCode() != FormulaError::NONE) + return true; + } + + return false; +} + +bool ScConditionEntry::IsValid( double nArg, const ScAddress& rPos ) const +{ + // Interpret must already have been called + if ( bIsStr1 ) + { + switch( eOp ) + { + case ScConditionMode::BeginsWith: + case ScConditionMode::EndsWith: + case ScConditionMode::ContainsText: + case ScConditionMode::NotContainsText: + break; + case ScConditionMode::NotEqual: + return true; + default: + return false; + } + } + + if ( eOp == ScConditionMode::Between || eOp == ScConditionMode::NotBetween ) + if ( bIsStr2 ) + return false; + + double nComp1 = nVal1; // Copy, so that it can be changed + double nComp2 = nVal2; + + if ( eOp == ScConditionMode::Between || eOp == ScConditionMode::NotBetween ) + if ( nComp1 > nComp2 ) + { + // Right order for value range + double nTemp = nComp1; nComp1 = nComp2; nComp2 = nTemp; + } + + // All corner cases need to be tested with ::rtl::math::approxEqual! + bool bValid = false; + switch (eOp) + { + case ScConditionMode::NONE: + break; // Always sal_False + case ScConditionMode::Equal: + bValid = ::rtl::math::approxEqual( nArg, nComp1 ); + break; + case ScConditionMode::NotEqual: + bValid = !::rtl::math::approxEqual( nArg, nComp1 ); + break; + case ScConditionMode::Greater: + bValid = ( nArg > nComp1 ) && !::rtl::math::approxEqual( nArg, nComp1 ); + break; + case ScConditionMode::EqGreater: + bValid = ( nArg >= nComp1 ) || ::rtl::math::approxEqual( nArg, nComp1 ); + break; + case ScConditionMode::Less: + bValid = ( nArg < nComp1 ) && !::rtl::math::approxEqual( nArg, nComp1 ); + break; + case ScConditionMode::EqLess: + bValid = ( nArg <= nComp1 ) || ::rtl::math::approxEqual( nArg, nComp1 ); + break; + case ScConditionMode::Between: + bValid = ( nArg >= nComp1 && nArg <= nComp2 ) || + ::rtl::math::approxEqual( nArg, nComp1 ) || ::rtl::math::approxEqual( nArg, nComp2 ); + break; + case ScConditionMode::NotBetween: + bValid = ( nArg < nComp1 || nArg > nComp2 ) && + !::rtl::math::approxEqual( nArg, nComp1 ) && !::rtl::math::approxEqual( nArg, nComp2 ); + break; + case ScConditionMode::Duplicate: + case ScConditionMode::NotDuplicate: + if( pCondFormat ) + { + bValid = IsDuplicate( nArg, OUString() ); + if( eOp == ScConditionMode::NotDuplicate ) + bValid = !bValid; + } + break; + case ScConditionMode::Direct: + bValid = nComp1 != 0.0; + break; + case ScConditionMode::Top10: + bValid = IsTopNElement( nArg ); + break; + case ScConditionMode::Bottom10: + bValid = IsBottomNElement( nArg ); + break; + case ScConditionMode::TopPercent: + bValid = IsTopNPercent( nArg ); + break; + case ScConditionMode::BottomPercent: + bValid = IsBottomNPercent( nArg ); + break; + case ScConditionMode::AboveAverage: + case ScConditionMode::AboveEqualAverage: + bValid = IsAboveAverage( nArg, eOp == ScConditionMode::AboveEqualAverage ); + break; + case ScConditionMode::BelowAverage: + case ScConditionMode::BelowEqualAverage: + bValid = IsBelowAverage( nArg, eOp == ScConditionMode::BelowEqualAverage ); + break; + case ScConditionMode::Error: + case ScConditionMode::NoError: + bValid = IsError( rPos ); + if( eOp == ScConditionMode::NoError ) + bValid = !bValid; + break; + case ScConditionMode::BeginsWith: + if(aStrVal1.isEmpty()) + { + OUString aStr = OUString::number(nVal1); + OUString aStr2 = OUString::number(nArg); + bValid = aStr2.startsWith(aStr); + } + else + { + OUString aStr2 = OUString::number(nArg); + bValid = aStr2.startsWith(aStrVal1); + } + break; + case ScConditionMode::EndsWith: + if(aStrVal1.isEmpty()) + { + OUString aStr = OUString::number(nVal1); + OUString aStr2 = OUString::number(nArg); + bValid = aStr2.endsWith(aStr); + } + else + { + OUString aStr2 = OUString::number(nArg); + bValid = aStr2.endsWith(aStrVal1); + } + break; + case ScConditionMode::ContainsText: + case ScConditionMode::NotContainsText: + if(aStrVal1.isEmpty()) + { + OUString aStr = OUString::number(nVal1); + OUString aStr2 = OUString::number(nArg); + bValid = aStr2.indexOf(aStr) != -1; + } + else + { + OUString aStr2 = OUString::number(nArg); + bValid = aStr2.indexOf(aStrVal1) != -1; + } + + if( eOp == ScConditionMode::NotContainsText ) + bValid = !bValid; + break; + default: + SAL_WARN("sc", "unknown operation at ScConditionEntry"); + break; + } + return bValid; +} + +bool ScConditionEntry::IsValidStr( const OUString& rArg, const ScAddress& rPos ) const +{ + bool bValid = false; + // Interpret must already have been called + if ( eOp == ScConditionMode::Direct ) // Formula is independent from the content + return nVal1 != 0.0; + + if ( eOp == ScConditionMode::Duplicate || eOp == ScConditionMode::NotDuplicate ) + { + if( pCondFormat && !rArg.isEmpty() ) + { + bValid = IsDuplicate( 0.0, rArg ); + if( eOp == ScConditionMode::NotDuplicate ) + bValid = !bValid; + return bValid; + } + } + + // If number contains condition, always false, except for "not equal". + if ( !bIsStr1 && (eOp != ScConditionMode::Error && eOp != ScConditionMode::NoError) ) + return ( eOp == ScConditionMode::NotEqual ); + if ( eOp == ScConditionMode::Between || eOp == ScConditionMode::NotBetween ) + if ( !bIsStr2 ) + return false; + + OUString aUpVal1( aStrVal1 ); //TODO: As a member? (Also set in Interpret) + OUString aUpVal2( aStrVal2 ); + + if ( eOp == ScConditionMode::Between || eOp == ScConditionMode::NotBetween ) + if (ScGlobal::GetCollator().compareString( aUpVal1, aUpVal2 ) > 0) + { + // Right order for value range + OUString aTemp( aUpVal1 ); aUpVal1 = aUpVal2; aUpVal2 = aTemp; + } + + switch ( eOp ) + { + case ScConditionMode::Equal: + bValid = (ScGlobal::GetCollator().compareString( + rArg, aUpVal1 ) == 0); + break; + case ScConditionMode::NotEqual: + bValid = (ScGlobal::GetCollator().compareString( + rArg, aUpVal1 ) != 0); + break; + case ScConditionMode::TopPercent: + case ScConditionMode::BottomPercent: + case ScConditionMode::Top10: + case ScConditionMode::Bottom10: + case ScConditionMode::AboveAverage: + case ScConditionMode::BelowAverage: + return false; + case ScConditionMode::Error: + case ScConditionMode::NoError: + bValid = IsError( rPos ); + if(eOp == ScConditionMode::NoError) + bValid = !bValid; + break; + case ScConditionMode::BeginsWith: + bValid = ScGlobal::GetTransliteration().isMatch(aUpVal1, rArg); + break; + case ScConditionMode::EndsWith: + { + sal_Int32 nStart = rArg.getLength(); + const sal_Int32 nLen = aUpVal1.getLength(); + if (nLen > nStart) + bValid = false; + else + { + nStart = nStart - nLen; + sal_Int32 nMatch1(0), nMatch2(0); + bValid = ScGlobal::GetTransliteration().equals(rArg, nStart, nLen, nMatch1, + aUpVal1, 0, nLen, nMatch2); + } + } + break; + case ScConditionMode::ContainsText: + case ScConditionMode::NotContainsText: + { + const OUString aArgStr(ScGlobal::getCharClass().lowercase(rArg)); + const OUString aValStr(ScGlobal::getCharClass().lowercase(aUpVal1)); + bValid = aArgStr.indexOf(aValStr) != -1; + + if(eOp == ScConditionMode::NotContainsText) + bValid = !bValid; + } + break; + default: + { + sal_Int32 nCompare = ScGlobal::GetCollator().compareString( + rArg, aUpVal1 ); + switch ( eOp ) + { + case ScConditionMode::Greater: + bValid = ( nCompare > 0 ); + break; + case ScConditionMode::EqGreater: + bValid = ( nCompare >= 0 ); + break; + case ScConditionMode::Less: + bValid = ( nCompare < 0 ); + break; + case ScConditionMode::EqLess: + bValid = ( nCompare <= 0 ); + break; + case ScConditionMode::Between: + case ScConditionMode::NotBetween: + // Test for NOTBETWEEN: + bValid = ( nCompare < 0 || + ScGlobal::GetCollator().compareString( rArg, + aUpVal2 ) > 0 ); + if ( eOp == ScConditionMode::Between ) + bValid = !bValid; + break; + // ScConditionMode::Direct already handled above + default: + SAL_WARN("sc", "unknown operation in ScConditionEntry"); + bValid = false; + break; + } + } + } + return bValid; +} + +bool ScConditionEntry::IsCellValid( ScRefCellValue& rCell, const ScAddress& rPos ) const +{ + const_cast<ScConditionEntry*>(this)->Interpret(rPos); // Evaluate formula + + if ( eOp == ScConditionMode::Direct ) + return nVal1 != 0.0; + + double nArg = 0.0; + OUString aArgStr; + bool bVal = lcl_GetCellContent( rCell, bIsStr1, nArg, aArgStr, mpDoc ); + if (bVal) + return IsValid( nArg, rPos ); + else + return IsValidStr( aArgStr, rPos ); +} + +OUString ScConditionEntry::GetExpression( const ScAddress& rCursor, sal_uInt16 nIndex, + sal_uInt32 nNumFmt, + const FormulaGrammar::Grammar eGrammar ) const +{ + assert( nIndex <= 1); + OUString aRet; + + if ( FormulaGrammar::isEnglish( eGrammar) && nNumFmt == 0 ) + nNumFmt = mpDoc->GetFormatTable()->GetStandardIndex( LANGUAGE_ENGLISH_US ); + + if ( nIndex==0 ) + { + if ( pFormula1 ) + { + ScCompiler aComp(*mpDoc, rCursor, *pFormula1, eGrammar); + OUStringBuffer aBuffer; + aComp.CreateStringFromTokenArray( aBuffer ); + aRet = aBuffer.makeStringAndClear(); + } + else if (bIsStr1) + { + aRet = "\"" + aStrVal1 + "\""; + } + else + mpDoc->GetFormatTable()->GetInputLineString(nVal1, nNumFmt, aRet); + } + else if ( nIndex==1 ) + { + if ( pFormula2 ) + { + ScCompiler aComp(*mpDoc, rCursor, *pFormula2, eGrammar); + OUStringBuffer aBuffer; + aComp.CreateStringFromTokenArray( aBuffer ); + aRet = aBuffer.makeStringAndClear(); + } + else if (bIsStr2) + { + aRet = "\"" + aStrVal2 + "\""; + } + else + mpDoc->GetFormatTable()->GetInputLineString(nVal2, nNumFmt, aRet); + } + + return aRet; +} + +std::unique_ptr<ScTokenArray> ScConditionEntry::CreateFlatCopiedTokenArray( sal_uInt16 nIndex ) const +{ + assert(nIndex <= 1); + std::unique_ptr<ScTokenArray> pRet; + + if ( nIndex==0 ) + { + if ( pFormula1 ) + pRet.reset(new ScTokenArray( *pFormula1 )); + else + { + pRet.reset(new ScTokenArray(*mpDoc)); + if (bIsStr1) + { + svl::SharedStringPool& rSPool = mpDoc->GetSharedStringPool(); + pRet->AddString(rSPool.intern(aStrVal1)); + } + else + pRet->AddDouble( nVal1 ); + } + } + else if ( nIndex==1 ) + { + if ( pFormula2 ) + pRet.reset(new ScTokenArray( *pFormula2 )); + else + { + pRet.reset(new ScTokenArray(*mpDoc)); + if (bIsStr2) + { + svl::SharedStringPool& rSPool = mpDoc->GetSharedStringPool(); + pRet->AddString(rSPool.intern(aStrVal2)); + } + else + pRet->AddDouble( nVal2 ); + } + } + + return pRet; +} + +/** + * Return a position that's adjusted to allow textual representation + * of expressions if possible + */ +ScAddress ScConditionEntry::GetValidSrcPos() const +{ + SCTAB nMinTab = aSrcPos.Tab(); + SCTAB nMaxTab = nMinTab; + + for (sal_uInt16 nPass = 0; nPass < 2; nPass++) + { + ScTokenArray* pFormula = nPass ? pFormula2.get() : pFormula1.get(); + if (pFormula) + { + for ( auto t: pFormula->References() ) + { + ScSingleRefData& rRef1 = *t->GetSingleRef(); + ScAddress aAbs = rRef1.toAbs(*mpDoc, aSrcPos); + if (!rRef1.IsTabDeleted()) + { + if (aAbs.Tab() < nMinTab) + nMinTab = aAbs.Tab(); + if (aAbs.Tab() > nMaxTab) + nMaxTab = aAbs.Tab(); + } + if ( t->GetType() == svDoubleRef ) + { + ScSingleRefData& rRef2 = t->GetDoubleRef()->Ref2; + aAbs = rRef2.toAbs(*mpDoc, aSrcPos); + if (!rRef2.IsTabDeleted()) + { + if (aAbs.Tab() < nMinTab) + nMinTab = aAbs.Tab(); + if (aAbs.Tab() > nMaxTab) + nMaxTab = aAbs.Tab(); + } + } + } + } + } + + ScAddress aValidPos = aSrcPos; + SCTAB nTabCount = mpDoc->GetTableCount(); + if ( nMaxTab >= nTabCount && nMinTab > 0 ) + aValidPos.SetTab( aSrcPos.Tab() - nMinTab ); // so the lowest tab ref will be on 0 + + if ( aValidPos.Tab() >= nTabCount ) + aValidPos.SetTab( nTabCount - 1 ); // ensure a valid position even if some references will be invalid + + return aValidPos; +} + +void ScConditionEntry::DataChanged() const +{ + //FIXME: Nothing so far +} + +bool ScConditionEntry::MarkUsedExternalReferences() const +{ + bool bAllMarked = false; + for (sal_uInt16 nPass = 0; !bAllMarked && nPass < 2; nPass++) + { + ScTokenArray* pFormula = nPass ? pFormula2.get() : pFormula1.get(); + if (pFormula) + bAllMarked = mpDoc->MarkUsedExternalReferences(*pFormula, aSrcPos); + } + return bAllMarked; +} + +ScFormatEntry* ScConditionEntry::Clone(ScDocument* pDoc) const +{ + return new ScConditionEntry(*pDoc, *this); +} + +ScConditionMode ScConditionEntry::GetModeFromApi(css::sheet::ConditionOperator nOperation) +{ + ScConditionMode eMode = ScConditionMode::NONE; + switch (static_cast<sal_Int32>(nOperation)) + { + case css::sheet::ConditionOperator2::EQUAL: + eMode = ScConditionMode::Equal; + break; + case css::sheet::ConditionOperator2::LESS: + eMode = ScConditionMode::Less; + break; + case css::sheet::ConditionOperator2::GREATER: + eMode = ScConditionMode::Greater; + break; + case css::sheet::ConditionOperator2::LESS_EQUAL: + eMode = ScConditionMode::EqLess; + break; + case css::sheet::ConditionOperator2::GREATER_EQUAL: + eMode = ScConditionMode::EqGreater; + break; + case css::sheet::ConditionOperator2::NOT_EQUAL: + eMode = ScConditionMode::NotEqual; + break; + case css::sheet::ConditionOperator2::BETWEEN: + eMode = ScConditionMode::Between; + break; + case css::sheet::ConditionOperator2::NOT_BETWEEN: + eMode = ScConditionMode::NotBetween; + break; + case css::sheet::ConditionOperator2::FORMULA: + eMode = ScConditionMode::Direct; + break; + case css::sheet::ConditionOperator2::DUPLICATE: + eMode = ScConditionMode::Duplicate; + break; + case css::sheet::ConditionOperator2::NOT_DUPLICATE: + eMode = ScConditionMode::NotDuplicate; + break; + default: + break; + } + return eMode; +} + +void ScConditionEntry::startRendering() +{ + mpCache.reset(); +} + +void ScConditionEntry::endRendering() +{ + mpCache.reset(); +} + +bool ScConditionEntry::NeedsRepaint() const +{ + return mpListener->NeedsRepaint(); +} + +ScCondFormatEntry::ScCondFormatEntry( ScConditionMode eOper, + const OUString& rExpr1, const OUString& rExpr2, + ScDocument& rDocument, const ScAddress& rPos, + const OUString& rStyle, + const OUString& rExprNmsp1, const OUString& rExprNmsp2, + FormulaGrammar::Grammar eGrammar1, + FormulaGrammar::Grammar eGrammar2, + ScFormatEntry::Type eType ) : + ScConditionEntry( eOper, rExpr1, rExpr2, rDocument, rPos, rExprNmsp1, rExprNmsp2, eGrammar1, eGrammar2, eType ), + aStyleName( rStyle ), + eCondFormatType( eType ) +{ +} + +ScCondFormatEntry::ScCondFormatEntry( ScConditionMode eOper, + const ScTokenArray* pArr1, const ScTokenArray* pArr2, + ScDocument& rDocument, const ScAddress& rPos, + const OUString& rStyle ) : + ScConditionEntry( eOper, pArr1, pArr2, rDocument, rPos ), + aStyleName( rStyle ) +{ +} + +ScCondFormatEntry::ScCondFormatEntry( const ScCondFormatEntry& r ) : + ScConditionEntry( r ), + aStyleName( r.aStyleName ), + eCondFormatType( r.eCondFormatType) +{ +} + +ScCondFormatEntry::ScCondFormatEntry( ScDocument& rDocument, const ScCondFormatEntry& r ) : + ScConditionEntry( rDocument, r ), + aStyleName( r.aStyleName ), + eCondFormatType( r.eCondFormatType) +{ +} + +// virtual +bool ScCondFormatEntry::IsEqual( const ScFormatEntry& r, bool bIgnoreSrcPos ) const +{ + return ScConditionEntry::IsEqual(r, bIgnoreSrcPos) && + (aStyleName == static_cast<const ScCondFormatEntry&>(r).aStyleName); +} + +ScCondFormatEntry::~ScCondFormatEntry() +{ +} + +void ScCondFormatEntry::DataChanged() const +{ + if ( pCondFormat ) + pCondFormat->DoRepaint(); +} + +ScFormatEntry* ScCondFormatEntry::Clone( ScDocument* pDoc ) const +{ + return new ScCondFormatEntry( *pDoc, *this ); +} + +void ScConditionEntry::CalcAll() +{ + if (pFCell1 || pFCell2) + { + if (pFCell1) + pFCell1->SetDirty(); + if (pFCell2) + pFCell2->SetDirty(); + pCondFormat->DoRepaint(); + } +} + +ScCondDateFormatEntry::ScCondDateFormatEntry( ScDocument* pDoc ) + : ScFormatEntry( pDoc ) + , meType(condformat::TODAY) +{ +} + +ScCondDateFormatEntry::ScCondDateFormatEntry( ScDocument* pDoc, const ScCondDateFormatEntry& rFormat ): + ScFormatEntry( pDoc ), + meType( rFormat.meType ), + maStyleName( rFormat.maStyleName ) +{ +} + +bool ScCondDateFormatEntry::IsValid( const ScAddress& rPos ) const +{ + ScRefCellValue rCell(*mpDoc, rPos); + + if (!rCell.hasNumeric()) + // non-numerical cell. + return false; + + if( !mpCache ) + mpCache.reset( new Date( Date::SYSTEM ) ); + + const Date& rActDate = *mpCache; + SvNumberFormatter* pFormatter = mpDoc->GetFormatTable(); + sal_Int32 nCurrentDate = rActDate - pFormatter->GetNullDate(); + + double nVal = rCell.getValue(); + sal_Int32 nCellDate = static_cast<sal_Int32>(::rtl::math::approxFloor(nVal)); + Date aCellDate = pFormatter->GetNullDate(); + aCellDate.AddDays(nCellDate); + + switch(meType) + { + case condformat::TODAY: + if( nCurrentDate == nCellDate ) + return true; + break; + case condformat::TOMORROW: + if( nCurrentDate == nCellDate -1 ) + return true; + break; + case condformat::YESTERDAY: + if( nCurrentDate == nCellDate + 1) + return true; + break; + case condformat::LAST7DAYS: + if( nCurrentDate >= nCellDate && nCurrentDate - 7 < nCellDate ) + return true; + break; + case condformat::LASTWEEK: + { + const DayOfWeek eDay = rActDate.GetDayOfWeek(); + if( eDay != SUNDAY ) + { + Date aBegin(rActDate - (8 + static_cast<sal_Int32>(eDay))); + Date aEnd(rActDate - (2 + static_cast<sal_Int32>(eDay))); + return aCellDate.IsBetween( aBegin, aEnd ); + } + else + { + Date aBegin(rActDate - 8); + Date aEnd(rActDate - 1); + return aCellDate.IsBetween( aBegin, aEnd ); + } + } + break; + case condformat::THISWEEK: + { + const DayOfWeek eDay = rActDate.GetDayOfWeek(); + if( eDay != SUNDAY ) + { + Date aBegin(rActDate - (1 + static_cast<sal_Int32>(eDay))); + Date aEnd(rActDate + (5 - static_cast<sal_Int32>(eDay))); + return aCellDate.IsBetween( aBegin, aEnd ); + } + else + { + Date aEnd( rActDate + 6); + return aCellDate.IsBetween( rActDate, aEnd ); + } + } + break; + case condformat::NEXTWEEK: + { + const DayOfWeek eDay = rActDate.GetDayOfWeek(); + if( eDay != SUNDAY ) + { + return aCellDate.IsBetween( rActDate + (6 - static_cast<sal_Int32>(eDay)), + rActDate + (12 - static_cast<sal_Int32>(eDay)) ); + } + else + { + return aCellDate.IsBetween( rActDate + 7, rActDate + 13 ); + } + } + break; + case condformat::LASTMONTH: + if( rActDate.GetMonth() == 1 ) + { + if( aCellDate.GetMonth() == 12 && rActDate.GetYear() == aCellDate.GetNextYear() ) + return true; + } + else if( rActDate.GetYear() == aCellDate.GetYear() ) + { + if( rActDate.GetMonth() == aCellDate.GetMonth() + 1) + return true; + } + break; + case condformat::THISMONTH: + if( rActDate.GetYear() == aCellDate.GetYear() ) + { + if( rActDate.GetMonth() == aCellDate.GetMonth() ) + return true; + } + break; + case condformat::NEXTMONTH: + if( rActDate.GetMonth() == 12 ) + { + if( aCellDate.GetMonth() == 1 && rActDate.GetYear() == aCellDate.GetYear() - 1 ) + return true; + } + else if( rActDate.GetYear() == aCellDate.GetYear() ) + { + if( rActDate.GetMonth() == aCellDate.GetMonth() - 1) + return true; + } + break; + case condformat::LASTYEAR: + if( rActDate.GetYear() == aCellDate.GetNextYear() ) + return true; + break; + case condformat::THISYEAR: + if( rActDate.GetYear() == aCellDate.GetYear() ) + return true; + break; + case condformat::NEXTYEAR: + if( rActDate.GetYear() == aCellDate.GetYear() - 1 ) + return true; + break; + } + + return false; +} + +void ScCondDateFormatEntry::SetDateType( condformat::ScCondFormatDateType eType ) +{ + meType = eType; +} + +void ScCondDateFormatEntry::SetStyleName( const OUString& rStyleName ) +{ + maStyleName = rStyleName; +} + +ScFormatEntry* ScCondDateFormatEntry::Clone( ScDocument* pDoc ) const +{ + return new ScCondDateFormatEntry( pDoc, *this ); +} + +void ScCondDateFormatEntry::startRendering() +{ + mpCache.reset(); +} + +void ScCondDateFormatEntry::endRendering() +{ + mpCache.reset(); +} + +ScConditionalFormat::ScConditionalFormat(sal_uInt32 nNewKey, ScDocument* pDocument) : + pDoc( pDocument ), + nKey( nNewKey ) +{ +} + +std::unique_ptr<ScConditionalFormat> ScConditionalFormat::Clone(ScDocument* pNewDoc) const +{ + // Real copy of the formula (for Ref Undo/between documents) + if (!pNewDoc) + pNewDoc = pDoc; + + std::unique_ptr<ScConditionalFormat> pNew(new ScConditionalFormat(nKey, pNewDoc)); + pNew->SetRange( maRanges ); // prerequisite for listeners + + for (const auto& rxEntry : maEntries) + { + ScFormatEntry* pNewEntry = rxEntry->Clone(pNewDoc); + pNew->maEntries.push_back( std::unique_ptr<ScFormatEntry>(pNewEntry) ); + pNewEntry->SetParent(pNew.get()); + } + + return pNew; +} + +bool ScConditionalFormat::EqualEntries( const ScConditionalFormat& r, bool bIgnoreSrcPos ) const +{ + if( size() != r.size()) + return false; + + //TODO: Test for same entries in reverse order? + if (! std::equal(maEntries.begin(), maEntries.end(), r.maEntries.begin(), + [&bIgnoreSrcPos](const std::unique_ptr<ScFormatEntry>& p1, const std::unique_ptr<ScFormatEntry>& p2) -> bool + { + return p1->IsEqual(*p2, bIgnoreSrcPos); + })) + return false; + + // right now don't check for same range + // we only use this method to merge same conditional formats from + // old ODF data structure + return true; +} + +void ScConditionalFormat::SetRange( const ScRangeList& rRanges ) +{ + maRanges = rRanges; + SAL_WARN_IF(maRanges.empty(), "sc", "the conditional format range is empty! will result in a crash later!"); +} + +void ScConditionalFormat::AddEntry( ScFormatEntry* pNew ) +{ + maEntries.push_back( std::unique_ptr<ScFormatEntry>(pNew)); + pNew->SetParent(this); +} + +void ScConditionalFormat::RemoveEntry(size_t n) +{ + if (n < maEntries.size()) + { + maEntries.erase(maEntries.begin() + n); + DoRepaint(); + } +} + +bool ScConditionalFormat::IsEmpty() const +{ + return maEntries.empty(); +} + +size_t ScConditionalFormat::size() const +{ + return maEntries.size(); +} + +ScDocument* ScConditionalFormat::GetDocument() +{ + return pDoc; +} + +ScConditionalFormat::~ScConditionalFormat() +{ +} + +const ScFormatEntry* ScConditionalFormat::GetEntry( sal_uInt16 nPos ) const +{ + if ( nPos < size() ) + return maEntries[nPos].get(); + else + return nullptr; +} + +OUString ScConditionalFormat::GetCellStyle( ScRefCellValue& rCell, const ScAddress& rPos ) const +{ + for (const auto& rxEntry : maEntries) + { + if(rxEntry->GetType() == ScFormatEntry::Type::Condition || + rxEntry->GetType() == ScFormatEntry::Type::ExtCondition) + { + const ScCondFormatEntry& rEntry = static_cast<const ScCondFormatEntry&>(*rxEntry); + if (rEntry.IsCellValid(rCell, rPos)) + return rEntry.GetStyle(); + } + else if(rxEntry->GetType() == ScFormatEntry::Type::Date) + { + const ScCondDateFormatEntry& rEntry = static_cast<const ScCondDateFormatEntry&>(*rxEntry); + if (rEntry.IsValid( rPos )) + return rEntry.GetStyleName(); + } + } + + return OUString(); +} + +ScCondFormatData ScConditionalFormat::GetData( ScRefCellValue& rCell, const ScAddress& rPos ) const +{ + ScCondFormatData aData; + for(const auto& rxEntry : maEntries) + { + if( (rxEntry->GetType() == ScFormatEntry::Type::Condition || + rxEntry->GetType() == ScFormatEntry::Type::ExtCondition) && + aData.aStyleName.isEmpty()) + { + const ScCondFormatEntry& rEntry = static_cast<const ScCondFormatEntry&>(*rxEntry); + if (rEntry.IsCellValid(rCell, rPos)) + aData.aStyleName = rEntry.GetStyle(); + } + else if(rxEntry->GetType() == ScFormatEntry::Type::Colorscale && !aData.mxColorScale) + { + const ScColorScaleFormat& rEntry = static_cast<const ScColorScaleFormat&>(*rxEntry); + aData.mxColorScale = rEntry.GetColor(rPos); + } + else if(rxEntry->GetType() == ScFormatEntry::Type::Databar && !aData.pDataBar) + { + const ScDataBarFormat& rEntry = static_cast<const ScDataBarFormat&>(*rxEntry); + aData.pDataBar = rEntry.GetDataBarInfo(rPos); + } + else if(rxEntry->GetType() == ScFormatEntry::Type::Iconset && !aData.pIconSet) + { + const ScIconSetFormat& rEntry = static_cast<const ScIconSetFormat&>(*rxEntry); + aData.pIconSet = rEntry.GetIconSetInfo(rPos); + } + else if(rxEntry->GetType() == ScFormatEntry::Type::Date && aData.aStyleName.isEmpty()) + { + const ScCondDateFormatEntry& rEntry = static_cast<const ScCondDateFormatEntry&>(*rxEntry); + if ( rEntry.IsValid( rPos ) ) + aData.aStyleName = rEntry.GetStyleName(); + } + } + return aData; +} + +void ScConditionalFormat::DoRepaint() +{ + // all conditional format cells + pDoc->RepaintRange( maRanges ); +} + +void ScConditionalFormat::CompileAll() +{ + for(auto& rxEntry : maEntries) + if(rxEntry->GetType() == ScFormatEntry::Type::Condition || + rxEntry->GetType() == ScFormatEntry::Type::ExtCondition) + static_cast<ScCondFormatEntry&>(*rxEntry).CompileAll(); +} + +void ScConditionalFormat::CompileXML() +{ + for(auto& rxEntry : maEntries) + if(rxEntry->GetType() == ScFormatEntry::Type::Condition || + rxEntry->GetType() == ScFormatEntry::Type::ExtCondition) + static_cast<ScCondFormatEntry&>(*rxEntry).CompileXML(); +} + +void ScConditionalFormat::UpdateReference( sc::RefUpdateContext& rCxt, bool bCopyAsMove ) +{ + if (rCxt.meMode == URM_COPY && bCopyAsMove) + { + // ScConditionEntry::UpdateReference() obtains its aSrcPos from + // maRanges and does not update it on URM_COPY, but it's needed later + // for the moved position, so update maRanges beforehand. + maRanges.UpdateReference(URM_MOVE, pDoc, rCxt.maRange, rCxt.mnColDelta, rCxt.mnRowDelta, rCxt.mnTabDelta); + for (auto& rxEntry : maEntries) + rxEntry->UpdateReference(rCxt); + } + else + { + for (auto& rxEntry : maEntries) + rxEntry->UpdateReference(rCxt); + maRanges.UpdateReference(rCxt.meMode, pDoc, rCxt.maRange, rCxt.mnColDelta, rCxt.mnRowDelta, rCxt.mnTabDelta); + } +} + +void ScConditionalFormat::InsertRow(SCTAB nTab, SCCOL nColStart, SCCOL nColEnd, SCROW nRowPos, SCSIZE nSize) +{ + maRanges.InsertRow(nTab, nColStart, nColEnd, nRowPos, nSize); +} + +void ScConditionalFormat::InsertCol(SCTAB nTab, SCROW nRowStart, SCROW nRowEnd, SCCOL nColPos, SCSIZE nSize) +{ + maRanges.InsertCol(nTab, nRowStart, nRowEnd, nColPos, nSize); +} + +void ScConditionalFormat::UpdateInsertTab( sc::RefUpdateInsertTabContext& rCxt ) +{ + for (size_t i = 0, n = maRanges.size(); i < n; ++i) + { + // We assume that the start and end sheet indices are equal. + ScRange & rRange = maRanges[i]; + SCTAB nTab = rRange.aStart.Tab(); + + if (nTab < rCxt.mnInsertPos) + // Unaffected. + continue; + + rRange.aStart.IncTab(rCxt.mnSheets); + rRange.aEnd.IncTab(rCxt.mnSheets); + } + + for (auto& rxEntry : maEntries) + rxEntry->UpdateInsertTab(rCxt); +} + +void ScConditionalFormat::UpdateDeleteTab( sc::RefUpdateDeleteTabContext& rCxt ) +{ + for (size_t i = 0, n = maRanges.size(); i < n; ++i) + { + // We assume that the start and end sheet indices are equal. + ScRange & rRange = maRanges[i]; + SCTAB nTab = rRange.aStart.Tab(); + + if (nTab < rCxt.mnDeletePos) + // Left of the deleted sheet(s). Unaffected. + continue; + + if (nTab <= rCxt.mnDeletePos+rCxt.mnSheets-1) + { + // On the deleted sheet(s). + rRange.aStart.SetTab(-1); + rRange.aEnd.SetTab(-1); + continue; + } + + // Right of the deleted sheet(s). Adjust the sheet indices. + rRange.aStart.IncTab(-1*rCxt.mnSheets); + rRange.aEnd.IncTab(-1*rCxt.mnSheets); + } + + for (auto& rxEntry : maEntries) + rxEntry->UpdateDeleteTab(rCxt); +} + +void ScConditionalFormat::UpdateMoveTab( sc::RefUpdateMoveTabContext& rCxt ) +{ + size_t n = maRanges.size(); + SCTAB nMinTab = std::min<SCTAB>(rCxt.mnOldPos, rCxt.mnNewPos); + SCTAB nMaxTab = std::max<SCTAB>(rCxt.mnOldPos, rCxt.mnNewPos); + for(size_t i = 0; i < n; ++i) + { + ScRange & rRange = maRanges[i]; + SCTAB nTab = rRange.aStart.Tab(); + if(nTab < nMinTab || nTab > nMaxTab) + { + continue; + } + + if (nTab == rCxt.mnOldPos) + { + rRange.aStart.SetTab(rCxt.mnNewPos); + rRange.aEnd.SetTab(rCxt.mnNewPos); + continue; + } + + if (rCxt.mnNewPos < rCxt.mnOldPos) + { + rRange.aStart.IncTab(); + rRange.aEnd.IncTab(); + } + else + { + rRange.aStart.IncTab(-1); + rRange.aEnd.IncTab(-1); + } + } + + for (auto& rxEntry : maEntries) + rxEntry->UpdateMoveTab(rCxt); +} + +void ScConditionalFormat::DeleteArea( SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2 ) +{ + if (maRanges.empty()) + return; + + SCTAB nTab = maRanges[0].aStart.Tab(); + maRanges.DeleteArea( nCol1, nRow1, nTab, nCol2, nRow2, nTab ); +} + +void ScConditionalFormat::RenameCellStyle(std::u16string_view rOld, const OUString& rNew) +{ + for(const auto& rxEntry : maEntries) + if(rxEntry->GetType() == ScFormatEntry::Type::Condition || + rxEntry->GetType() == ScFormatEntry::Type::ExtCondition) + { + ScCondFormatEntry& rFormat = static_cast<ScCondFormatEntry&>(*rxEntry); + if(rFormat.GetStyle() == rOld) + rFormat.UpdateStyleName( rNew ); + } +} + +bool ScConditionalFormat::MarkUsedExternalReferences() const +{ + bool bAllMarked = false; + for(const auto& rxEntry : maEntries) + if(rxEntry->GetType() == ScFormatEntry::Type::Condition || + rxEntry->GetType() == ScFormatEntry::Type::ExtCondition) + { + const ScCondFormatEntry& rFormat = static_cast<const ScCondFormatEntry&>(*rxEntry); + bAllMarked = rFormat.MarkUsedExternalReferences(); + if (bAllMarked) + break; + } + + return bAllMarked; +} + +void ScConditionalFormat::startRendering() +{ + for(auto& rxEntry : maEntries) + { + rxEntry->startRendering(); + } +} + +void ScConditionalFormat::endRendering() +{ + for(auto& rxEntry : maEntries) + { + rxEntry->endRendering(); + } +} + +void ScConditionalFormat::CalcAll() +{ + for(const auto& rxEntry : maEntries) + { + if (rxEntry->GetType() == ScFormatEntry::Type::Condition || + rxEntry->GetType() == ScFormatEntry::Type::ExtCondition) + { + ScCondFormatEntry& rFormat = static_cast<ScCondFormatEntry&>(*rxEntry); + rFormat.CalcAll(); + } + } +} + +ScConditionalFormatList::ScConditionalFormatList(const ScConditionalFormatList& rList) +{ + for(const auto& rxFormat : rList) + InsertNew( rxFormat->Clone() ); +} + +ScConditionalFormatList::ScConditionalFormatList(ScDocument& rDoc, const ScConditionalFormatList& rList) +{ + for(const auto& rxFormat : rList) + InsertNew( rxFormat->Clone(&rDoc) ); +} + +void ScConditionalFormatList::InsertNew( std::unique_ptr<ScConditionalFormat> pNew ) +{ + m_ConditionalFormats.insert(std::move(pNew)); +} + +ScConditionalFormat* ScConditionalFormatList::GetFormat( sal_uInt32 nKey ) +{ + auto itr = m_ConditionalFormats.find(nKey); + if (itr != m_ConditionalFormats.end()) + return itr->get(); + + SAL_WARN("sc", "ScConditionalFormatList: Entry not found"); + return nullptr; +} + +const ScConditionalFormat* ScConditionalFormatList::GetFormat( sal_uInt32 nKey ) const +{ + auto itr = m_ConditionalFormats.find(nKey); + if (itr != m_ConditionalFormats.end()) + return itr->get(); + + SAL_WARN("sc", "ScConditionalFormatList: Entry not found"); + return nullptr; +} + +void ScConditionalFormatList::CompileAll() +{ + for (auto const& it : m_ConditionalFormats) + { + it->CompileAll(); + } +} + +void ScConditionalFormatList::CompileXML() +{ + for (auto const& it : m_ConditionalFormats) + { + it->CompileXML(); + } +} + +void ScConditionalFormatList::UpdateReference( sc::RefUpdateContext& rCxt ) +{ + for (auto const& it : m_ConditionalFormats) + { + it->UpdateReference(rCxt); + } + + if (rCxt.meMode == URM_INSDEL) + { + // need to check which must be deleted + CheckAllEntries(); + } +} + +void ScConditionalFormatList::InsertRow(SCTAB nTab, SCCOL nColStart, SCCOL nColEnd, SCROW nRowPos, SCSIZE nSize) +{ + for (auto const& it : m_ConditionalFormats) + { + it->InsertRow(nTab, nColStart, nColEnd, nRowPos, nSize); + } +} + +void ScConditionalFormatList::InsertCol(SCTAB nTab, SCROW nRowStart, SCROW nRowEnd, SCCOL nColPos, SCSIZE nSize) +{ + for (auto const& it : m_ConditionalFormats) + { + it->InsertCol(nTab, nRowStart, nRowEnd, nColPos, nSize); + } +} + +void ScConditionalFormatList::UpdateInsertTab( sc::RefUpdateInsertTabContext& rCxt ) +{ + for (auto const& it : m_ConditionalFormats) + { + it->UpdateInsertTab(rCxt); + } +} + +void ScConditionalFormatList::UpdateDeleteTab( sc::RefUpdateDeleteTabContext& rCxt ) +{ + for (auto const& it : m_ConditionalFormats) + { + it->UpdateDeleteTab(rCxt); + } +} + +void ScConditionalFormatList::UpdateMoveTab( sc::RefUpdateMoveTabContext& rCxt ) +{ + for (auto const& it : m_ConditionalFormats) + { + it->UpdateMoveTab(rCxt); + } +} + +void ScConditionalFormatList::RenameCellStyle( std::u16string_view rOld, const OUString& rNew ) +{ + for (auto const& it : m_ConditionalFormats) + { + it->RenameCellStyle(rOld, rNew); + } +} + +bool ScConditionalFormatList::CheckAllEntries(const Link<ScConditionalFormat*,void>& rLink) +{ + bool bValid = true; + + // need to check which must be deleted + iterator itr = m_ConditionalFormats.begin(); + while(itr != m_ConditionalFormats.end()) + { + if ((*itr)->GetRange().empty()) + { + bValid = false; + if (rLink.IsSet()) + rLink.Call(itr->get()); + itr = m_ConditionalFormats.erase(itr); + } + else + ++itr; + } + + return bValid; +} + +void ScConditionalFormatList::DeleteArea( SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2 ) +{ + for (auto& rxFormat : m_ConditionalFormats) + rxFormat->DeleteArea( nCol1, nRow1, nCol2, nRow2 ); + + CheckAllEntries(); +} + +ScConditionalFormatList::iterator ScConditionalFormatList::begin() +{ + return m_ConditionalFormats.begin(); +} + +ScConditionalFormatList::const_iterator ScConditionalFormatList::begin() const +{ + return m_ConditionalFormats.begin(); +} + +ScConditionalFormatList::iterator ScConditionalFormatList::end() +{ + return m_ConditionalFormats.end(); +} + +ScConditionalFormatList::const_iterator ScConditionalFormatList::end() const +{ + return m_ConditionalFormats.end(); +} + +ScRangeList ScConditionalFormatList::GetCombinedRange() const +{ + ScRangeList aRange; + for (auto& itr: m_ConditionalFormats) + { + const ScRangeList& rRange = itr->GetRange(); + for (size_t i = 0, n = rRange.size(); i < n; ++i) + { + aRange.Join(rRange[i]); + } + } + return aRange; +} + +void ScConditionalFormatList::RemoveFromDocument(ScDocument& rDoc) const +{ + ScRangeList aRange = GetCombinedRange(); + ScMarkData aMark(rDoc.GetSheetLimits()); + aMark.MarkFromRangeList(aRange, true); + sal_uInt16 const pItems[2] = { sal_uInt16(ATTR_CONDITIONAL),0}; + rDoc.ClearSelectionItems(pItems, aMark); +} + +void ScConditionalFormatList::AddToDocument(ScDocument& rDoc) const +{ + for (auto& itr: m_ConditionalFormats) + { + const ScRangeList& rRange = itr->GetRange(); + if (rRange.empty()) + continue; + + SCTAB nTab = rRange.front().aStart.Tab(); + rDoc.AddCondFormatData(rRange, nTab, itr->GetKey()); + } +} + +size_t ScConditionalFormatList::size() const +{ + return m_ConditionalFormats.size(); +} + +bool ScConditionalFormatList::empty() const +{ + return m_ConditionalFormats.empty(); +} + +void ScConditionalFormatList::erase( sal_uLong nIndex ) +{ + auto itr = m_ConditionalFormats.find(nIndex); + if (itr != end()) + m_ConditionalFormats.erase(itr); +} + +void ScConditionalFormatList::startRendering() +{ + for (auto const& it : m_ConditionalFormats) + { + it->startRendering(); + } +} + +void ScConditionalFormatList::endRendering() +{ + for (auto const& it : m_ConditionalFormats) + { + it->endRendering(); + } +} + +void ScConditionalFormatList::clear() +{ + m_ConditionalFormats.clear(); +} + +sal_uInt32 ScConditionalFormatList::getMaxKey() const +{ + if (m_ConditionalFormats.empty()) + return 0; + return (*m_ConditionalFormats.rbegin())->GetKey(); +} + +void ScConditionalFormatList::CalcAll() +{ + for (const auto& aEntry : m_ConditionalFormats) + { + aEntry->CalcAll(); + } + +} + +ScCondFormatData::ScCondFormatData() {} + +ScCondFormatData::ScCondFormatData(ScCondFormatData&&) = default; + +ScCondFormatData::~ScCondFormatData() {} + + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |