tdf#88782 sc: autofill number sequences in merged cells

Improve FillAuto, FillAnalyse and FillSeries to continue
linear sequences of numbers in merged cells by skipping
the empty overlapped cells of the merged area, like other
spreadsheets do. For example this fix autofill, when merged
cells are used to highlight nth numbers. Instead of 1, 2 ->
1, 2, 2, 3, we get 1, 2 -> 1, 2, 3, 4 on the following fill:

+-+-+-+    +-+-+-+-+-+-+
|1| 2 | -> |1| 2 |3| 4 |
+-+-+-+    +-+-+-+-+-+-+

See the unit test document for more complex examples, and
use merge/unmerge to check the work of the algorithm. For
example, column C of the test document contains cells

EMPTY, EMPTY, EMPTY, 2, EMPTY, 4, EMPTY

calculated as 2, 4 during fill by skipping empty
overlapped cells, but keeping also the merged structure,
resulting the requested continuation of the linear sequence:

EMPTY, EMPTY, EMPTY, 6, EMPTY, 8, EMPTY

Note: special formats are not handled yet like date, string,
boolean and userlist.

Co-authored-by: Tibor Nagy (NISZ)

Change-Id: Ib431e8968f5d71e321b0e57cfb173534a0f5da31
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/103765
Tested-by: László Németh <nemeth@numbertext.org>
Reviewed-by: László Németh <nemeth@numbertext.org>
diff --git a/sc/inc/document.hxx b/sc/inc/document.hxx
index 62315b5..7fcb3af 100644
--- a/sc/inc/document.hxx
+++ b/sc/inc/document.hxx
@@ -1178,7 +1178,7 @@ public:
     * @return pointer to the double value stored in a numeric cell, or NULL
     *         if the cell at specified position is not a numeric cell.
     */
    double* GetValueCell( const ScAddress& rPos );
    SC_DLLPUBLIC double*                      GetValueCell( const ScAddress& rPos );

    SC_DLLPUBLIC svl::SharedStringPool&       GetSharedStringPool();
    const svl::SharedStringPool&              GetSharedStringPool() const;
diff --git a/sc/inc/table.hxx b/sc/inc/table.hxx
index 15a74d3..3c2ab08 100644
--- a/sc/inc/table.hxx
+++ b/sc/inc/table.hxx
@@ -1115,11 +1115,15 @@ private:
                                sal_uLong nFillCount, FillDir eFillDir, FillCmd eFillCmd,
                                FillDateCmd eFillDateCmd,
                                double nStepValue, double nMaxValue, sal_uInt16 nMinDigits,
                                bool bAttribs, ScProgress* pProgress );
                                bool bAttribs, ScProgress* pProgress,
                                bool bSkipOverlappedCells = false,
                                std::vector<sal_Int32>* pNonOverlappedCellIdx = nullptr);
    void        FillAnalyse( SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2,
                                FillCmd& rCmd, FillDateCmd& rDateCmd,
                                double& rInc, sal_uInt16& rMinDigits,
                                ScUserListData*& rListData, sal_uInt16& rListIndex);
                                ScUserListData*& rListData, sal_uInt16& rListIndex,
                                bool bHasFiltered, bool& rSkipOverlappedCells,
                                std::vector<sal_Int32>& rNonOverlappedCellIdx );
    void        FillAuto( SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2,
                        sal_uLong nFillCount, FillDir eFillDir, ScProgress* pProgress );

diff --git a/sc/qa/unit/copy_paste_test.cxx b/sc/qa/unit/copy_paste_test.cxx
index caeb052..325a883 100644
--- a/sc/qa/unit/copy_paste_test.cxx
+++ b/sc/qa/unit/copy_paste_test.cxx
@@ -12,6 +12,8 @@
#include <comphelper/processfactory.hxx>

#include <docsh.hxx>
#include <docfunc.hxx>
#include <cellmergeoption.hxx>
#include <tabvwsh.hxx>
#include <impex.hxx>
#include <viewfunc.hxx>
@@ -42,6 +44,7 @@ public:
    void testTdf53431_fillOnAutofilter();
    void testTdf40993_fillMergedCells();
    void testTdf43958_clickSelectOnMergedCells();
    void testTdf88782_autofillLinearNumbersInMergedCells();

    CPPUNIT_TEST_SUITE(ScCopyPasteTest);
    CPPUNIT_TEST(testCopyPasteXLS);
@@ -52,6 +55,7 @@ public:
    CPPUNIT_TEST(testTdf53431_fillOnAutofilter);
    CPPUNIT_TEST(testTdf40993_fillMergedCells);
    CPPUNIT_TEST(testTdf43958_clickSelectOnMergedCells);
    CPPUNIT_TEST(testTdf88782_autofillLinearNumbersInMergedCells);
    CPPUNIT_TEST_SUITE_END();

private:
@@ -620,6 +624,62 @@ void ScCopyPasteTest::testTdf43958_clickSelectOnMergedCells()
    lcl_clickAndCheckCurrentArea(2, 8, 2, 8);    // C9
}

void ScCopyPasteTest::testTdf88782_autofillLinearNumbersInMergedCells()
{
    ScDocShellRef xDocSh = loadDocAndSetupModelViewController("tdf88782_AutofillLinearNumbersInMergedCells.", FORMAT_ODS, true);
    ScDocument& rDoc = xDocSh->GetDocument();

    // Get the document controller
    ScTabViewShell* pView = xDocSh->GetBestViewShell(false);
    CPPUNIT_ASSERT(pView != nullptr);

    // merge the yellow cells
    ScCellMergeOption aMergeOptions(9, 11, 10, 13);     //J12:K14
    aMergeOptions.maTabs.insert(0);
    xDocSh->GetDocFunc().MergeCells(aMergeOptions, false, true, true, false);

    // fillauto numbers, these areas contains mostly merged cells
    pView->FillAuto(FILL_TO_BOTTOM, 1, 8, 3, 14, 7);    // B9:D15 ->  B9:D22
    pView->FillAuto(FILL_TO_BOTTOM, 5, 8, 7, 17, 10);   // F9:H18 ->  F9:H28
    pView->FillAuto(FILL_TO_BOTTOM, 9, 8, 10, 13, 6);   // J9:K14 ->  J9:K20
    pView->FillAuto(FILL_TO_RIGHT, 9, 30, 16, 35, 8);   //J31:Q36 -> J31:Y36
    pView->FillAuto(FILL_TO_LEFT, 9, 30, 16, 35, 8);    //J31:Q36 -> B31:Q36

    // compare the results of fill-down with the reference stored in the test file
    // this compare the whole area blindly, for concrete test cases, check the test file
    // the test file have instructions / explanations, so that is easy to understand
    for (int nCol = 1; nCol <= 10; nCol++) {
        for (int nRow = 8; nRow <= 27; nRow++) {
            CellType nType1 = rDoc.GetCellType(ScAddress(nCol, nRow, 0));
            CellType nType2 = rDoc.GetCellType(ScAddress(nCol + 22, nRow, 0));
            double* pValue1 = rDoc.GetValueCell(ScAddress(nCol, nRow, 0));
            double* pValue2 = rDoc.GetValueCell(ScAddress(nCol + 22, nRow, 0));

            CPPUNIT_ASSERT_EQUAL(nType1, nType2);
            if (pValue2 != nullptr)
                CPPUNIT_ASSERT_EQUAL(*pValue1, *pValue2);   //cells with number value
            else
                CPPUNIT_ASSERT_EQUAL(pValue1, pValue2);     //empty cells
        }
    }

    // compare the results of fill-right and left with the reference stored in the test file
    for (int nCol = 1; nCol <= 24; nCol++) {
        for (int nRow = 30; nRow <= 35; nRow++) {
            CellType nType1 = rDoc.GetCellType(ScAddress(nCol, nRow, 0));
            CellType nType2 = rDoc.GetCellType(ScAddress(nCol, nRow + 16, 0));
            double* pValue1 = rDoc.GetValueCell(ScAddress(nCol, nRow, 0));
            double* pValue2 = rDoc.GetValueCell(ScAddress(nCol, nRow + 16, 0));

            CPPUNIT_ASSERT_EQUAL(nType1, nType2);
            if (pValue2 != nullptr)
                CPPUNIT_ASSERT_EQUAL(*pValue1, *pValue2);
            else
                CPPUNIT_ASSERT_EQUAL(pValue1, pValue2);
        }
    }
}

ScCopyPasteTest::ScCopyPasteTest()
      : ScBootstrapFixture( "sc/qa/unit/data" )
{
diff --git a/sc/qa/unit/data/ods/tdf88782_AutofillLinearNumbersInMergedCells.ods b/sc/qa/unit/data/ods/tdf88782_AutofillLinearNumbersInMergedCells.ods
new file mode 100644
index 0000000..99fea70
--- /dev/null
+++ b/sc/qa/unit/data/ods/tdf88782_AutofillLinearNumbersInMergedCells.ods
Binary files differ
diff --git a/sc/source/core/data/table4.cxx b/sc/source/core/data/table4.cxx
index f6058d5..e2d8648 100644
--- a/sc/source/core/data/table4.cxx
+++ b/sc/source/core/data/table4.cxx
@@ -216,7 +216,9 @@ double approxDiff( double a, double b )
void ScTable::FillAnalyse( SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2,
                            FillCmd& rCmd, FillDateCmd& rDateCmd,
                            double& rInc, sal_uInt16& rMinDigits,
                            ScUserListData*& rListData, sal_uInt16& rListIndex)
                            ScUserListData*& rListData, sal_uInt16& rListIndex,
                            bool bHasFiltered, bool& rSkipOverlappedCells,
                            std::vector<sal_Int32>& rNonOverlappedCellIdx)
{
    OSL_ENSURE( nCol1==nCol2 || nRow1==nRow2, "FillAnalyse: invalid range" );

@@ -224,6 +226,7 @@ void ScTable::FillAnalyse( SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2,
    rMinDigits = 0;
    rListData = nullptr;
    rCmd = FILL_SIMPLE;
    rSkipOverlappedCells = false;
    if ( nScFillModeMouseModifier & KEY_MOD1 )
        return ;        // Ctrl-key: Copy

@@ -243,6 +246,100 @@ void ScTable::FillAnalyse( SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2,
        nCount = static_cast<SCSIZE>(nCol2 - nCol1 + 1);
    }

    // Try to analyse the merged cells only if there are no filtered rows in the destination area
    // Else fallback to the old way to avoid regression.
    // Filling merged cells into an area with filtered (hidden) rows, is a very complex task
    // that is not implemented, but not even decided how to do, even excel can't handle that well
    if (!bHasFiltered)
    {
        bool bHasOverlappedCells = false;
        bool bSkipOverlappedCells = true;
        SCCOL nColAkt = nCol1;
        SCROW nRowAkt = nRow1;

        // collect cells that are not empty or not overlapped
        rNonOverlappedCellIdx.resize(nCount);
        SCSIZE nValueCount = 0;
        for (SCSIZE i = 0; i < nCount; ++i)
        {
            const ScPatternAttr* pPattern = GetPattern(nColAkt, nRowAkt);
            bool bOverlapped
                = pPattern->GetItemSet().GetItemState(ATTR_MERGE_FLAG, false) == SfxItemState::SET
                  && pPattern->GetItem(ATTR_MERGE_FLAG).IsOverlapped();

            if (bOverlapped)
                bHasOverlappedCells = true;

            if (!bOverlapped || GetCellValue(nColAkt, nRowAkt).meType != CELLTYPE_NONE)
            {
                rNonOverlappedCellIdx[nValueCount++] = i;
                // if there is at least 1 non empty overlapped cell, then no cell should be skipped
                if (bOverlapped)
                    bSkipOverlappedCells = false;
            }

            nColAkt += nAddX;
            nRowAkt += nAddY;
        }
        rNonOverlappedCellIdx.resize(nValueCount);

        // if all the values are overlapped CELLTYPE_NONE, then there is no need to analyse it.
        if (nValueCount == 0)
            return;

        // if there is no overlapped cells, there is nothing to skip
        if (!bHasOverlappedCells)
            bSkipOverlappedCells = false;

        if (bSkipOverlappedCells)
        {
            nColAkt = nCol1 + rNonOverlappedCellIdx[0] * nAddX;
            nRowAkt = nRow1 + rNonOverlappedCellIdx[0] * nAddY;
            ScRefCellValue aPrevCell, aAktCell;
            aAktCell = GetCellValue(nColAkt, nRowAkt);
            CellType eCellType = aAktCell.meType;
            if (eCellType == CELLTYPE_VALUE)
            {
                // TODO: Check / handle special cases of number formats: like date, boolean
                bool bVal = true;
                if (nValueCount >= 2)
                {
                    for (SCSIZE i = 1; i < nValueCount && bVal; i++)
                    {
                        aPrevCell = aAktCell;
                        nColAkt = nCol1 + rNonOverlappedCellIdx[i] * nAddX;
                        nRowAkt = nRow1 + rNonOverlappedCellIdx[i] * nAddY;
                        aAktCell = GetCellValue(nColAkt, nRowAkt);
                        if (aAktCell.meType == CELLTYPE_VALUE)
                        {
                            double nDiff = approxDiff(aAktCell.mfValue, aPrevCell.mfValue);
                            if (i == 1)
                                rInc = nDiff;
                            if (!::rtl::math::approxEqual(nDiff, rInc, 13))
                                bVal = false;
                        }
                        else
                            bVal = false;
                    }
                    if (bVal)
                    {
                        rCmd = FILL_LINEAR;
                        rSkipOverlappedCells = true;
                        return;
                    }
                }
            }
            else if (eCellType == CELLTYPE_STRING || eCellType == CELLTYPE_EDIT)
            {
                // TODO: check / handle if it is a sequence of userlist string
                // or if the strings are composition of a number sequence and a constant string
            }
        }
    }

    //if it is not a FILL_LINEAR - CELLTYPE_VALUE - with merged cells [without hidden values]
    //then do it in the old way

    SCCOL nCol = nCol1;
    SCROW nRow = nRow1;

@@ -795,14 +892,18 @@ void ScTable::FillAuto( SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2,
        sal_uInt16 nMinDigits;
        ScUserListData* pListData = nullptr;
        sal_uInt16 nListIndex;
        bool nSkipOverlappedCells;
        std::vector<sal_Int32> aNonOverlappedCellIdx;
        if (bVertical)
            FillAnalyse(static_cast<SCCOL>(nCol),nRow1,
                    static_cast<SCCOL>(nCol),nRow2, eFillCmd,eDateCmd,
                    nInc,nMinDigits, pListData,nListIndex);
                    nInc, nMinDigits, pListData, nListIndex,
                    bHasFiltered, nSkipOverlappedCells, aNonOverlappedCellIdx);
        else
            FillAnalyse(nCol1,static_cast<SCROW>(nRow),
                    nCol2,static_cast<SCROW>(nRow), eFillCmd,eDateCmd,
                    nInc,nMinDigits, pListData,nListIndex);
                    nInc, nMinDigits, pListData, nListIndex,
                    bHasFiltered, nSkipOverlappedCells, aNonOverlappedCellIdx);

        if (pListData)
        {
@@ -860,12 +961,12 @@ void ScTable::FillAuto( SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2,
                FillSeries( static_cast<SCCOL>(nCol), nRow1,
                        static_cast<SCCOL>(nCol), nRow2, nFillCount, eFillDir,
                        eFillCmd, eDateCmd, nInc, nEndVal, nMinDigits, false,
                        pProgress );
                        pProgress, nSkipOverlappedCells, &aNonOverlappedCellIdx);
            else
                FillSeries( nCol1, static_cast<SCROW>(nRow), nCol2,
                        static_cast<SCROW>(nRow), nFillCount, eFillDir,
                        eFillCmd, eDateCmd, nInc, nEndVal, nMinDigits, false,
                        pProgress );
                        pProgress, nSkipOverlappedCells, &aNonOverlappedCellIdx);
            if (pProgress)
                nProgress = pProgress->GetState();
        }
@@ -919,8 +1020,15 @@ OUString ScTable::GetAutoFillPreview( const ScRange& rSource, SCCOL nEndX, SCROW
        sal_uInt16 nMinDigits;
        ScUserListData* pListData = nullptr;
        sal_uInt16 nListIndex;
        bool nSkipOverlappedCells;
        std::vector<sal_Int32> aNonOverlappedCellIdx;

        FillAnalyse(nCol1,nRow1, nCol2,nRow2, eFillCmd,eDateCmd, nInc,nMinDigits, pListData,nListIndex);
        // Todo: update this function to calculate with merged cell fills,
        //       after FillAnalyse / FillSeries fully handle them.
        // Now FillAnalyse called as if there are filtered rows, so it will work in the old way.
        FillAnalyse(nCol1, nRow1, nCol2, nRow2, eFillCmd, eDateCmd,
                    nInc, nMinDigits, pListData, nListIndex,
                    true, nSkipOverlappedCells, aNonOverlappedCellIdx);

        if ( pListData )                            // user defined list
        {
@@ -1693,7 +1801,8 @@ inline bool isOverflow( const double& rVal, const double& rMax, const double& rS
void ScTable::FillSeries( SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2,
                    sal_uLong nFillCount, FillDir eFillDir, FillCmd eFillCmd, FillDateCmd eFillDateCmd,
                    double nStepValue, double nMaxValue, sal_uInt16 nArgMinDigits,
                    bool bAttribs, ScProgress* pProgress )
                    bool bAttribs, ScProgress* pProgress,
                    bool bSkipOverlappedCells, std::vector<sal_Int32>* pNonOverlappedCellIdx )
{
    // The term 'inner' here refers to the loop in the filling direction i.e.
    // when filling vertically, the inner position is the row position whereas
@@ -1718,9 +1827,12 @@ void ScTable::FillSeries( SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2,
    SCCOLROW nIEnd;
    SCCOLROW nISource;
    ScRange aFillRange;
    sal_uLong nFillerCount;
    std::vector<bool> aIsNonEmptyCell;

    if (bVertical)
    {
        nFillerCount = (nRow2 - nRow1) + 1;
        nFillCount += (nRow2 - nRow1);
        if (nFillCount == 0)
            return;
@@ -1745,6 +1857,7 @@ void ScTable::FillSeries( SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2,
    }
    else
    {
        nFillerCount = (nCol2 - nCol1) + 1;
        nFillCount += (nCol2 - nCol1);
        if (nFillCount == 0)
            return;
@@ -1781,6 +1894,8 @@ void ScTable::FillSeries( SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2,
        // at least as long as the area to be filled and does not end earlier,
        // so we can treat it as entire area for performance reasons at least
        // in the vertical case.
        // This is not exact in case of merged cell fills with skipping overlapped parts, but
        // it is still a good upper estimation.
        ScCellValue aSrcCell;
        if (bVertical)
            aSrcCell = aCol[static_cast<SCCOL>(nOStart)].GetCellValue(static_cast<SCROW>(nISource));
@@ -1841,6 +1956,34 @@ void ScTable::FillSeries( SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2,
        // Source cell value. We need to clone the value since it may be inserted repeatedly.
        ScCellValue aSrcCell = aCol[nCol].GetCellValue(static_cast<SCROW>(nRow));

        // Maybe another source cell need to be searched, if the fill is going trough merged cells,
        // where overlapped parts does not contain any information, so they can be skipped.
        if (bSkipOverlappedCells)
        {
            // create a vector to make it easier to decide if a cell need to be filled, or skipped.
            aIsNonEmptyCell.resize(nFillerCount, false);

            SCCOLROW nfirstValueIdx;
            if (bPositive)
            {
                nfirstValueIdx = nISource + (*pNonOverlappedCellIdx)[0];
                for (auto i : (*pNonOverlappedCellIdx))
                    aIsNonEmptyCell[i] = true;
            }
            else
            {
                nfirstValueIdx = nISource - (nFillerCount - 1 - (*pNonOverlappedCellIdx).back());
                for (auto i : (*pNonOverlappedCellIdx))
                    aIsNonEmptyCell[nFillerCount - 1 - i] = true;
            }

            //Set the real source cell
            if (bVertical)
                aSrcCell = aCol[nOStart].GetCellValue(static_cast<SCROW>(nfirstValueIdx));
            else
                aSrcCell = aCol[nfirstValueIdx].GetCellValue(static_cast<SCROW>(nOStart));
        }

        const ScPatternAttr* pSrcPattern = aCol[nCol].GetPattern(static_cast<SCROW>(nRow));
        const ScCondFormatItem& rCondFormatItem = pSrcPattern->GetItem(ATTR_CONDITIONAL);
        const ScCondFormatIndexes& rCondFormatIndex = rCondFormatItem.GetCondFormatData();
@@ -1899,14 +2042,24 @@ void ScTable::FillSeries( SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2,

                bool bError = false;
                bool bOverflow = false;
                bool bNonEmpty = true;

                sal_uInt16 nDayOfMonth = 0;
                sal_Int32 nFillerIdx = 0;
                if (bSkipOverlappedCells && !aIsNonEmptyCell[0])
                    --nIndex;
                rInner = nIStart;
                while (true)
                {
                    if (bSkipOverlappedCells)
                    {
                        nFillerIdx = (nFillerIdx + 1) % nFillerCount;
                        bNonEmpty = aIsNonEmptyCell[nFillerIdx];
                    }

                    if(!ColHidden(nCol) && !RowHidden(nRow))
                    {
                        if (!bError)
                        if (!bError && bNonEmpty)
                        {
                            switch (eFillCmd)
                            {
@@ -1943,7 +2096,7 @@ void ScTable::FillSeries( SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2,

                        if (bError)
                            aCol[nCol].SetError(static_cast<SCROW>(nRow), FormulaError::NoValue);
                        else if (!bOverflow)
                        else if (!bOverflow && bNonEmpty)
                            aCol[nCol].SetValue(static_cast<SCROW>(nRow), nVal);

                        if (bAttribs && !bEntireArea && !bOverflow)
diff --git a/sc/source/ui/inc/cellmergeoption.hxx b/sc/source/ui/inc/cellmergeoption.hxx
index ff4a7cb..937b907 100644
--- a/sc/source/ui/inc/cellmergeoption.hxx
+++ b/sc/source/ui/inc/cellmergeoption.hxx
@@ -24,9 +24,9 @@ struct ScCellMergeOption
    bool mbCenter;

    explicit ScCellMergeOption(const ScRange& rRange);
    explicit ScCellMergeOption(SCCOL nStartCol, SCROW nStartRow,
                               SCCOL nEndCol, SCROW nEndRow,
                               bool bCenter = false);
    SC_DLLPUBLIC explicit ScCellMergeOption(SCCOL nStartCol, SCROW nStartRow,
                                            SCCOL nEndCol, SCROW nEndRow,
                                            bool bCenter = false);

    ScRange getSingleRange(SCTAB nTab) const;
    ScRange getFirstSingleRange() const;
diff --git a/sc/source/ui/inc/docfunc.hxx b/sc/source/ui/inc/docfunc.hxx
index e3a8117..1984c0d 100644
--- a/sc/source/ui/inc/docfunc.hxx
+++ b/sc/source/ui/inc/docfunc.hxx
@@ -191,7 +191,8 @@ public:

    void            ResizeMatrix( const ScRange& rOldRange, const ScAddress& rNewEnd );

    bool            MergeCells( const ScCellMergeOption& rOption, bool bContents,
    SC_DLLPUBLIC bool
                    MergeCells( const ScCellMergeOption& rOption, bool bContents,
                                        bool bRecord, bool bApi, bool bEmptyMergedCells = false );
    bool            UnmergeCells( const ScRange& rRange, bool bRecord, ScUndoRemoveMerge* pUndoRemoveMerge );
    bool            UnmergeCells( const ScCellMergeOption& rOption, bool bRecord, ScUndoRemoveMerge* pUndoRemoveMerge );