tdf#137205 sc: autofill date sequences in merged cells

Improve FillAnalyse to discover and continue linear sequences of dates
with the only differences of months or years in merged cells by skipping
the empty overlapped cells of the merged area.

Follow-up of commit Ib431e8968f5d71e321b0e57cfb173534a0f5da31
(tdf#88782 sc: autofill number sequences in merged cells)

Co-authored-by: Tibor Nagy (NISZ)
Change-Id: I1e37efd34858f53691bf867ebefc2fe26e39e676
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/103967
Tested-by: Jenkins
Tested-by: László Németh <nemeth@numbertext.org>
Reviewed-by: László Németh <nemeth@numbertext.org>
diff --git a/sc/qa/unit/copy_paste_test.cxx b/sc/qa/unit/copy_paste_test.cxx
index 325a883..12e0b64 100644
--- a/sc/qa/unit/copy_paste_test.cxx
+++ b/sc/qa/unit/copy_paste_test.cxx
@@ -45,6 +45,7 @@ public:
    void testTdf40993_fillMergedCells();
    void testTdf43958_clickSelectOnMergedCells();
    void testTdf88782_autofillLinearNumbersInMergedCells();
    void tdf137205_autofillDatesInMergedCells();

    CPPUNIT_TEST_SUITE(ScCopyPasteTest);
    CPPUNIT_TEST(testCopyPasteXLS);
@@ -56,6 +57,7 @@ public:
    CPPUNIT_TEST(testTdf40993_fillMergedCells);
    CPPUNIT_TEST(testTdf43958_clickSelectOnMergedCells);
    CPPUNIT_TEST(testTdf88782_autofillLinearNumbersInMergedCells);
    CPPUNIT_TEST(tdf137205_autofillDatesInMergedCells);
    CPPUNIT_TEST_SUITE_END();

private:
@@ -638,7 +640,7 @@ void ScCopyPasteTest::testTdf88782_autofillLinearNumbersInMergedCells()
    aMergeOptions.maTabs.insert(0);
    xDocSh->GetDocFunc().MergeCells(aMergeOptions, false, true, true, false);

    // fillauto numbers, these areas contains mostly merged cells
    // fillauto numbers, these areas contain 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
@@ -679,6 +681,35 @@ void ScCopyPasteTest::testTdf88782_autofillLinearNumbersInMergedCells()
        }
    }
}
void ScCopyPasteTest::tdf137205_autofillDatesInMergedCells()
{
    ScDocShellRef xDocSh = loadDocAndSetupModelViewController("tdf137205_AutofillDatesInMergedCells.", FORMAT_ODS, true);
    ScDocument& rDoc = xDocSh->GetDocument();

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

    // fillauto dates, this areas contain only merged cells
    pView->FillAuto(FILL_TO_RIGHT, 1, 5, 4, 7, 8);   //B6:E8

    // compare the results of fill-right with the reference stored in the test file
    // this compare the whole area blindly, for concrete test cases, check the test file
    for (int nCol = 5; nCol <= 12; nCol++) {
        for (int nRow = 5; nRow <= 7; nRow++) {
            CellType nType1 = rDoc.GetCellType(ScAddress(nCol, nRow, 0));
            CellType nType2 = rDoc.GetCellType(ScAddress(nCol, nRow + 5, 0));
            double* pValue1 = rDoc.GetValueCell(ScAddress(nCol, nRow, 0));
            double* pValue2 = rDoc.GetValueCell(ScAddress(nCol, nRow + 5, 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
        }
    }
}

ScCopyPasteTest::ScCopyPasteTest()
      : ScBootstrapFixture( "sc/qa/unit/data" )
diff --git a/sc/qa/unit/data/ods/tdf137205_AutofillDatesInMergedCells.ods b/sc/qa/unit/data/ods/tdf137205_AutofillDatesInMergedCells.ods
new file mode 100644
index 0000000..9a62d57
--- /dev/null
+++ b/sc/qa/unit/data/ods/tdf137205_AutofillDatesInMergedCells.ods
Binary files differ
diff --git a/sc/source/core/data/table4.cxx b/sc/source/core/data/table4.cxx
index e2d8648..11570e3 100644
--- a/sc/source/core/data/table4.cxx
+++ b/sc/source/core/data/table4.cxx
@@ -254,15 +254,15 @@ void ScTable::FillAnalyse( SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2,
    {
        bool bHasOverlappedCells = false;
        bool bSkipOverlappedCells = true;
        SCCOL nColAkt = nCol1;
        SCROW nRowAkt = nRow1;
        SCCOL nColCurr = nCol1;
        SCROW nRowCurr = 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);
            const ScPatternAttr* pPattern = GetPattern(nColCurr, nRowCurr);
            bool bOverlapped
                = pPattern->GetItemSet().GetItemState(ATTR_MERGE_FLAG, false) == SfxItemState::SET
                  && pPattern->GetItem(ATTR_MERGE_FLAG).IsOverlapped();
@@ -270,7 +270,7 @@ void ScTable::FillAnalyse( SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2,
            if (bOverlapped)
                bHasOverlappedCells = true;

            if (!bOverlapped || GetCellValue(nColAkt, nRowAkt).meType != CELLTYPE_NONE)
            if (!bOverlapped || GetCellValue(nColCurr, nRowCurr).meType != CELLTYPE_NONE)
            {
                rNonOverlappedCellIdx[nValueCount++] = i;
                // if there is at least 1 non empty overlapped cell, then no cell should be skipped
@@ -278,8 +278,8 @@ void ScTable::FillAnalyse( SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2,
                    bSkipOverlappedCells = false;
            }

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

@@ -293,26 +293,106 @@ void ScTable::FillAnalyse( SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2,

        if (bSkipOverlappedCells)
        {
            nColAkt = nCol1 + rNonOverlappedCellIdx[0] * nAddX;
            nRowAkt = nRow1 + rNonOverlappedCellIdx[0] * nAddY;
            ScRefCellValue aPrevCell, aAktCell;
            aAktCell = GetCellValue(nColAkt, nRowAkt);
            CellType eCellType = aAktCell.meType;
            nColCurr = nCol1 + rNonOverlappedCellIdx[0] * nAddX;
            nRowCurr = nRow1 + rNonOverlappedCellIdx[0] * nAddY;
            ScRefCellValue aPrevCell, aCurrCell;
            aCurrCell = GetCellValue(nColCurr, nRowCurr);
            CellType eCellType = aCurrCell.meType;
            if (eCellType == CELLTYPE_VALUE)
            {
                // TODO: Check / handle special cases of number formats: like date, boolean
                // TODO: Check / handle special cases of number formats like boolean
                bool bVal = true;
                if (nValueCount >= 2)
                SvNumFormatType nCurrCellFormatType
                    = rDocument.GetFormatTable()->GetType(GetNumberFormat(nColCurr, nRowCurr));
                if (nCurrCellFormatType == SvNumFormatType::DATE)
                {
                    if (nValueCount >= 2)
                    {
                        long nCmpInc = 0;
                        FillDateCmd eType = FILL_YEAR;  // just some temporary default values
                        long nDDiff = 0, nMDiff = 0, nYDiff = 0; // to avoid warnings
                        Date aNullDate = rDocument.GetFormatTable()->GetNullDate();
                        Date aCurrDate = aNullDate, aPrevDate = aNullDate;
                        aCurrDate.AddDays(aCurrCell.mfValue);
                        for (SCSIZE i = 1; i < nValueCount && bVal; i++)
                        {
                            aPrevCell = aCurrCell;
                            aPrevDate = aCurrDate;
                            nColCurr = nCol1 + rNonOverlappedCellIdx[i] * nAddX;
                            nRowCurr = nRow1 + rNonOverlappedCellIdx[i] * nAddY;
                            aCurrCell = GetCellValue(nColCurr, nRowCurr);
                            if (aCurrCell.meType == CELLTYPE_VALUE)
                            {
                                aCurrDate = aNullDate + static_cast<sal_Int32>(aCurrCell.mfValue);
                                if (eType != FILL_DAY) {
                                    nDDiff = aCurrDate.GetDay()
                                             - static_cast<long>(aPrevDate.GetDay());
                                    nMDiff = aCurrDate.GetMonth()
                                             - static_cast<long>(aPrevDate.GetMonth());
                                    nYDiff = aCurrDate.GetYear()
                                             - static_cast<long>(aPrevDate.GetYear());
                                }
                                if (i == 1)
                                {
                                    if (nDDiff != 0)
                                    {
                                        eType = FILL_DAY;
                                        nCmpInc = aCurrDate - aPrevDate;
                                    }
                                    else
                                    {
                                        eType = FILL_MONTH;
                                        nCmpInc = nMDiff + 12 * nYDiff;
                                    }
                                }
                                else if (eType == FILL_DAY)
                                {
                                    if (aCurrDate - aPrevDate != nCmpInc)
                                        bVal = false;
                                }
                                else
                                {
                                    if (nDDiff || (nMDiff + 12 * nYDiff != nCmpInc))
                                        bVal = false;
                                }
                            }
                            else
                                bVal = false;   // No date is also not ok
                        }
                        if (bVal)
                        {
                            if (eType == FILL_MONTH && (nCmpInc % 12 == 0))
                            {
                                eType = FILL_YEAR;
                                nCmpInc /= 12;
                            }
                            rCmd = FILL_DATE;
                            rDateCmd = eType;
                            rInc = nCmpInc;
                            rSkipOverlappedCells = true;
                            return;
                        }
                    }
                    else
                    {
                        rCmd = FILL_DATE;
                        rDateCmd = FILL_DAY;
                        rInc = 1.0;
                        rSkipOverlappedCells = true;
                        return;
                    }
                }
                else 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)
                        aPrevCell = aCurrCell;
                        nColCurr = nCol1 + rNonOverlappedCellIdx[i] * nAddX;
                        nRowCurr = nRow1 + rNonOverlappedCellIdx[i] * nAddY;
                        aCurrCell = GetCellValue(nColCurr, nRowCurr);
                        if (aCurrCell.meType == CELLTYPE_VALUE)
                        {
                            double nDiff = approxDiff(aAktCell.mfValue, aPrevCell.mfValue);
                            double nDiff = approxDiff(aCurrCell.mfValue, aPrevCell.mfValue);
                            if (i == 1)
                                rInc = nDiff;
                            if (!::rtl::math::approxEqual(nDiff, rInc, 13))
@@ -892,18 +972,18 @@ void ScTable::FillAuto( SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2,
        sal_uInt16 nMinDigits;
        ScUserListData* pListData = nullptr;
        sal_uInt16 nListIndex;
        bool nSkipOverlappedCells;
        bool bSkipOverlappedCells;
        std::vector<sal_Int32> aNonOverlappedCellIdx;
        if (bVertical)
            FillAnalyse(static_cast<SCCOL>(nCol),nRow1,
                    static_cast<SCCOL>(nCol),nRow2, eFillCmd,eDateCmd,
                    nInc, nMinDigits, pListData, nListIndex,
                    bHasFiltered, nSkipOverlappedCells, aNonOverlappedCellIdx);
                    bHasFiltered, bSkipOverlappedCells, aNonOverlappedCellIdx);
        else
            FillAnalyse(nCol1,static_cast<SCROW>(nRow),
                    nCol2,static_cast<SCROW>(nRow), eFillCmd,eDateCmd,
                    nInc, nMinDigits, pListData, nListIndex,
                    bHasFiltered, nSkipOverlappedCells, aNonOverlappedCellIdx);
                    bHasFiltered, bSkipOverlappedCells, aNonOverlappedCellIdx);

        if (pListData)
        {
@@ -961,12 +1041,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, nSkipOverlappedCells, &aNonOverlappedCellIdx);
                        pProgress, bSkipOverlappedCells, &aNonOverlappedCellIdx);
            else
                FillSeries( nCol1, static_cast<SCROW>(nRow), nCol2,
                        static_cast<SCROW>(nRow), nFillCount, eFillDir,
                        eFillCmd, eDateCmd, nInc, nEndVal, nMinDigits, false,
                        pProgress, nSkipOverlappedCells, &aNonOverlappedCellIdx);
                        pProgress, bSkipOverlappedCells, &aNonOverlappedCellIdx);
            if (pProgress)
                nProgress = pProgress->GetState();
        }
@@ -1020,7 +1100,7 @@ OUString ScTable::GetAutoFillPreview( const ScRange& rSource, SCCOL nEndX, SCROW
        sal_uInt16 nMinDigits;
        ScUserListData* pListData = nullptr;
        sal_uInt16 nListIndex;
        bool nSkipOverlappedCells;
        bool bSkipOverlappedCells;
        std::vector<sal_Int32> aNonOverlappedCellIdx;

        // Todo: update this function to calculate with merged cell fills,
@@ -1028,7 +1108,7 @@ OUString ScTable::GetAutoFillPreview( const ScRange& rSource, SCCOL nEndX, SCROW
        // 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);
                    true, bSkipOverlappedCells, aNonOverlappedCellIdx);

        if ( pListData )                            // user defined list
        {
@@ -1963,25 +2043,25 @@ void ScTable::FillSeries( SCCOL nCol1, SCROW nRow1, SCCOL nCol2, SCROW nRow2,
            // create a vector to make it easier to decide if a cell need to be filled, or skipped.
            aIsNonEmptyCell.resize(nFillerCount, false);

            SCCOLROW nfirstValueIdx;
            SCCOLROW nFirstValueIdx;
            if (bPositive)
            {
                nfirstValueIdx = nISource + (*pNonOverlappedCellIdx)[0];
                nFirstValueIdx = nISource + (*pNonOverlappedCellIdx)[0];
                for (auto i : (*pNonOverlappedCellIdx))
                    aIsNonEmptyCell[i] = true;
            }
            else
            {
                nfirstValueIdx = nISource - (nFillerCount - 1 - (*pNonOverlappedCellIdx).back());
                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));
                aSrcCell = aCol[nOStart].GetCellValue(static_cast<SCROW>(nFirstValueIdx));
            else
                aSrcCell = aCol[nfirstValueIdx].GetCellValue(static_cast<SCROW>(nOStart));
                aSrcCell = aCol[nFirstValueIdx].GetCellValue(static_cast<SCROW>(nOStart));
        }

        const ScPatternAttr* pSrcPattern = aCol[nCol].GetPattern(static_cast<SCROW>(nRow));