fpspreadsheet: Fix issue that Insert/DeleteRow/Col operation changes formulas in unaffected sheets (see https://forum.lazarus.freepascal.org/index.php/topic,59102.0.html). Add test cases for it.

git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@8259 8e941d3f-bd1b-0410-a28a-d453659cc2b4
This commit is contained in:
wp_xxyyzz
2022-04-21 16:45:44 +00:00
parent b0badc1cb4
commit b4ac1e075d
3 changed files with 265 additions and 15 deletions

View File

@ -267,9 +267,7 @@ begin
if TsCellExprNode(AExprNode).Error <> errOK then if TsCellExprNode(AExprNode).Error <> errOK then
exit; exit;
referencedSheet := TsCellExprNode(AExprNode).GetSheet; referencedSheet := TsCellExprNode(AExprNode).GetSheet;
if TsCellExprNode(AExprNode).Has3dLink and (referencedSheet <> changedSheet) then if (referencedSheet = nil) or (referencedSheet <> changedSheet) then
exit;
if referencedSheet = nil then
exit; exit;
if TsCellExprNode(AExprNode).Col > colIndex then begin if TsCellExprNode(AExprNode).Col > colIndex then begin
TsCellExprNode(AExprNode).Col := TsCellExprNode(AExprNode).Col - 1; TsCellExprNode(AExprNode).Col := TsCellExprNode(AExprNode).Col - 1;
@ -329,9 +327,7 @@ begin
if TsCellExprNode(AExprNode).Error <> errOK then if TsCellExprNode(AExprNode).Error <> errOK then
exit; exit;
referencedSheet := TsCellExprNode(AExprNode).GetSheet; referencedSheet := TsCellExprNode(AExprNode).GetSheet;
if TsCellExprNode(AExprNode).Has3dLink and (referencedSheet <> changedSheet) then if (referencedSheet = nil) or (referencedSheet <> changedSheet) then
exit;
if referencedSheet = nil then
exit; exit;
if TsCellExprNode(AExprNode).Row > rowIndex then begin if TsCellExprNode(AExprNode).Row > rowIndex then begin
TsCellExprNode(AExprNode).Row := TsCellExprNode(AExprNode).Row - 1; TsCellExprNode(AExprNode).Row := TsCellExprNode(AExprNode).Row - 1;
@ -393,11 +389,10 @@ begin
if TsCellExprNode(AExprNode).Error <> errOK then if TsCellExprNode(AExprNode).Error <> errOK then
exit; exit;
referencedSheet := TsCellExprNode(AExprNode).GetSheet; referencedSheet := TsCellExprNode(AExprNode).GetSheet;
if TsCellExprNode(AExprNode).Has3dLink and (referencedSheet <> changedSheet) then
exit;
if referencedSheet = nil then if referencedSheet = nil then
exit; exit;
if TsCellExprNode(AExprNode).Col >= colIndex then begin if (referencedSheet = changedSheet) and (TsCellExprNode(AExprNode).Col >= colIndex) then
begin
TsCellExprNode(AExprNode).Col := TsCellExprNode(AExprNode).Col + 1; TsCellExprNode(AExprNode).Col := TsCellExprNode(AExprNode).Col + 1;
MustRebuildFormulas := true; MustRebuildFormulas := true;
end; end;
@ -444,11 +439,10 @@ begin
if TsCellExprNode(AExprNode).Error <> errOK then if TsCellExprNode(AExprNode).Error <> errOK then
exit; exit;
referencedSheet := TsCellExprNode(AExprNode).GetSheet; referencedSheet := TsCellExprNode(AExprNode).GetSheet;
if TsCellExprNode(AExprNode).Has3dLink and (referencedSheet <> changedSheet) then
exit;
if referencedSheet = nil then if referencedSheet = nil then
exit; exit;
if TsCellExprNode(AExprNode).Row >= rowIndex then begin if (referencedSheet = changedSheet) and (TsCellExprNode(AExprNode).Row >= rowIndex) then
begin
TsCellExprNode(AExprNode).Row := TsCellExprNode(AExprNode).Row + 1; TsCellExprNode(AExprNode).Row := TsCellExprNode(AExprNode).Row + 1;
MustRebuildFormulas := true; MustRebuildFormulas := true;
end; end;

View File

@ -5585,7 +5585,12 @@ var
i: Integer; i: Integer;
rng: PsCellRange; rng: PsCellRange;
sheet: TsWorksheet; sheet: TsWorksheet;
wasAutoCalculating: Boolean;
begin begin
// Turn off auto-calculation of formulas
wasAutoCalculating := (boAutoCalc in Workbook.Options);
//Workbook.Options := Workbook.Options - [boAutoCalc];
// Update row indexes of cell comments // Update row indexes of cell comments
FComments.InsertRowOrCol(AIndex, IsRow); FComments.InsertRowOrCol(AIndex, IsRow);
@ -5600,7 +5605,7 @@ begin
sheet := FWorkbook.GetWorksheetByIndex(i); sheet := FWorkbook.GetWorksheetByIndex(i);
sheet.Formulas.FixReferences(AIndex, IsRow, false, self); sheet.Formulas.FixReferences(AIndex, IsRow, false, self);
end; end;
// Update cell indexes of cell records // Update cell indexes of cell records
FCells.InsertRowOrCol(AIndex, IsRow); FCells.InsertRowOrCol(AIndex, IsRow);
@ -5680,6 +5685,13 @@ begin
ChangedCell(0, AIndex); ChangedCell(0, AIndex);
end; end;
// Calculate formulas
if wasAutoCalculating then
begin
//Workbook.Options := Workbook.Options + [boAutoCalc];
//CalcFormulas;
end;
end; end;
{@@ ---------------------------------------------------------------------------- {@@ ----------------------------------------------------------------------------

View File

@ -28,6 +28,7 @@ type
AExpectedFormula: String = ''); AExpectedFormula: String = '');
procedure TestWorksheet(ATestKind: TWorksheetTestKind; ATestCase: Integer); procedure TestWorksheet(ATestKind: TWorksheetTestKind; ATestCase: Integer);
procedure TestFormulaErrors(ATest: Integer); procedure TestFormulaErrors(ATest: Integer);
procedure TestInsDelRowCol(ATestIndex: Integer);
published published
procedure AddConst_BIFF2; procedure AddConst_BIFF2;
@ -156,6 +157,20 @@ type
procedure Add_Number_NumString; procedure Add_Number_NumString;
procedure Equal_Number_NumString; procedure Equal_Number_NumString;
procedure UnaryMinusNumString; procedure UnaryMinusNumString;
procedure InsertRow_BeforeFormula;
procedure InsertRow_AfterFormula;
procedure InsertRow_AfterAll;
procedure InsertCol_BeforeFormula;
procedure InsertCol_AfterFormula;
procedure InsertCol_AfterAll;
procedure DeleteRow_BeforeFormula;
procedure DeleteRow_AfterFormula;
procedure DeleteRow_AfterAll;
procedure DeleteCol_BeforeFormula;
procedure DeleteCol_AfterFormula;
procedure DeleteCol_AfterAll;
end; end;
implementation implementation
@ -164,11 +179,10 @@ uses
{$IFDEF FORMULADEBUG} {$IFDEF FORMULADEBUG}
LazLogger, LazLogger,
{$ENDIF} {$ENDIF}
//Math,
typinfo, lazUTF8, fpsUtils; typinfo, lazUTF8, fpsUtils;
{ TSpreadExtendedFormulaTests } { TSpreadSingleFormulaTests }
procedure TSpreadSingleFormulaTests.SetUp; procedure TSpreadSingleFormulaTests.SetUp;
begin begin
@ -1169,6 +1183,236 @@ begin
end; end;
{-------------------------------------------------------------------------------
TestInsDelRowCol
Inserts/deletes a row or column and checks whether the references used in
formulas are correctly adapted.
Note: other row/col tests are contained in unit colrowtests.pas
-------------------------------------------------------------------------------}
procedure TSpreadSingleFormulaTests.TestInsDelRowCol(ATestIndex: Integer);
const
VALUE1 = 'abc-1';
VALUE2 = 'abc-2';
var
wb: TsWorkbook;
ws1: TsWorksheet;
ws2: TsWorksheet;
begin
wb := TsWorkbook.Create;
try
wb.Options := wb.Options + [boAutoCalc];
ws1 := wb.AddWorksheet('Sheet1');
ws1.WriteText(6, 4, VALUE1); // text 'abc-1' in cell E7 of sheet1
ws1.WriteFormula(1, 1, '=E7'); // formula =E7 in cell B2
ws2 := wb.AddWorksheet('Sheet2');
ws2.WriteText(6, 4, VALUE2); // text 'abc-2' in cell E7 of sheet2
ws2.WriteFormula(1, 1, '=E7'); // formula =E7 in cell B2
ws2.writeFormula(1, 2, '=Sheet1!E7'); // 3d formula in cell C2 referring to sheet1
CheckEquals(VALUE1, ws1.ReadAsText(1, 1), 'Read initial value mismatch, cell Sheet1!B2');
CheckEquals('E7', ws1.ReadFormula(1, 1), 'Read initial formula mismatch, cell Sheet1!B2');
CheckEquals(VALUE2, ws2.ReadAsText(1, 1), 'Read initial value mismatch, cell Sheet2!B2');
CheckEquals('E7', ws2.ReadFormula(1, 1), 'Read initial formula mismatch, cell Sheet2!B2');
CheckEquals(VALUE1, ws2.ReadAsText(1, 2), 'Read initial value mismatch, cell Sheet2!C2');
CheckEquals('Sheet1!E7', ws2.ReadFormula(1, 2), 'Read initial formula mismatch, cell Sheet2!C2');
case ATestIndex of
0: // Insert row in sheet1 before formula and referenced cell
begin
ws1.InsertRow(0);
CheckEquals(VALUE1, ws1.ReadAsText(2, 1), 'Read value mismatch, cell Sheet1!B2');
CheckEquals('E8', ws1.ReadFormula(2, 1), 'Read formula mismatch, cell Sheet1!B2');
CheckEquals(VALUE2, ws2.ReadAsText(1, 1), 'Read value mismatch, cell Sheet2!B2');
CheckEquals('E7', ws2.ReadFormula(1, 1), 'Read formula mismatch, cell Sheet2!B2');
CheckEquals(VALUE1, ws2.ReadAsText(1, 2), 'Read value mismatch, cell Sheet2!C2');
CheckEquals('Sheet1!E8', ws2.ReadFormula(1, 2), 'Read formula mismatch, cell Sheet2!C2');
end;
1: // Insert row in sheet1 after formula, but before referenced cell
begin
ws1.InsertRow(3);
CheckEquals(VALUE1, ws1.ReadAsText(1, 1), 'Read value mismatch, cell Sheet1!B2');
CheckEquals('E8', ws1.ReadFormula(1, 1), 'Read formula mismatch, cell Sheet1!B2');
CheckEquals(VALUE2, ws2.ReadAsText(1, 1), 'Read value mismatch, cell Sheet2!B2');
CheckEquals('E7', ws2.ReadFormula(1, 1), 'Read formula mismatch, cell Sheet2!B2');
CheckEquals(VALUE1, ws2.ReadAsText(1, 2), 'Read value mismatch, cell Sheet2!C2');
CheckEquals('Sheet1!E8', ws2.ReadFormula(1, 2), 'Read formula mismatch, cell Sheet2!C2');
end;
2: // Insert row in sheet1 after formula and referenced cell
begin
ws1.InsertRow(10);
CheckEquals(VALUE1, ws1.ReadAsText(1, 1), 'Read initial value mismatch, cell Sheet1!B2');
CheckEquals('E7', ws1.ReadFormula(1, 1), 'Read initial formula mismatch, cell Sheet1!B2');
CheckEquals(VALUE2, ws2.ReadAsText(1, 1), 'Read initial value mismatch, cell Sheet2!B2');
CheckEquals('E7', ws2.ReadFormula(1, 1), 'Read initial formula mismatch, cell Sheet2!B2');
CheckEquals(VALUE1, ws2.ReadAsText(1, 2), 'Read initial value mismatch, cell Sheet2!C2');
CheckEquals('Sheet1!E7', ws2.ReadFormula(1, 2), 'Read initial formula mismatch, cell Sheet2!C2');
end;
10: // Insert column in sheet1 before formula and referenced cell
begin
ws1.InsertCol(0);
CheckEquals(VALUE1, ws1.ReadAsText(1, 2), 'Read value mismatch, cell Sheet1!B2');
CheckEquals('F7', ws1.ReadFormula(1, 2), 'Read formula mismatch, cell Sheet1!B2');
CheckEquals(VALUE2, ws2.ReadAsText(1, 1), 'Read value mismatch, cell Sheet2!B2');
CheckEquals('E7', ws2.ReadFormula(1, 1), 'Read formula mismatch, cell Sheet2!B2');
CheckEquals(VALUE1, ws2.ReadAsText(1, 2), 'Read value mismatch, cell Sheet2!C2');
CheckEquals('Sheet1!F7', ws2.ReadFormula(1, 2), 'Read formula mismatch, cell Sheet2!C2');
end;
11: // Insert column in sheet1 after formula, but before referenced cell
begin
ws1.InsertCol(3);
CheckEquals(VALUE1, ws1.ReadAsText(1, 1), 'Read value mismatch, cell Sheet1!B2');
CheckEquals('F7', ws1.ReadFormula(1, 1), 'Read formula mismatch, cell Sheet1!B2');
CheckEquals(VALUE2, ws2.ReadAsText(1, 1), 'Read value mismatch, cell Sheet2!B2');
CheckEquals('E7', ws2.ReadFormula(1, 1), 'Read formula mismatch, cell Sheet2!B2');
CheckEquals(VALUE1, ws2.ReadAsText(1, 2), 'Read value mismatch, cell Sheet2!C2');
CheckEquals('Sheet1!F7', ws2.ReadFormula(1, 2), 'Read formula mismatch, cell Sheet2!C2');
end;
12: // Insert column in sheet1 after formula and referenced cell
begin
ws1.InsertCol(10);
CheckEquals(VALUE1, ws1.ReadAsText(1, 1), 'Read initial value mismatch, cell Sheet1!B2');
CheckEquals('E7', ws1.ReadFormula(1, 1), 'Read initial formula mismatch, cell Sheet1!B2');
CheckEquals(VALUE2, ws2.ReadAsText(1, 1), 'Read initial value mismatch, cell Sheet2!B2');
CheckEquals('E7', ws2.ReadFormula(1, 1), 'Read initial formula mismatch, cell Sheet2!B2');
CheckEquals(VALUE1, ws2.ReadAsText(1, 2), 'Read initial value mismatch, cell Sheet2!C2');
CheckEquals('Sheet1!E7', ws2.ReadFormula(1, 2), 'Read initial formula mismatch, cell Sheet2!C2');
end;
20: // Delete row from sheet1 before formula and referenced cell
begin
ws1.DeleteRow(0);
CheckEquals(VALUE1, ws1.ReadAsText(0, 1), 'Read value mismatch, cell Sheet1!B1');
CheckEquals('E6', ws1.ReadFormula(0, 1), 'Read formula mismatch, cell Sheet1!B1');
CheckEquals(VALUE2, ws2.ReadAsText(1, 1), 'Read value mismatch, cell Sheet2!B2');
CheckEquals('E7', ws2.ReadFormula(1, 1), 'Read formula mismatch, cell Sheet2!B2');
CheckEquals(VALUE1, ws2.ReadAsText(1, 2), 'Read value mismatch, cell Sheet2!C2');
CheckEquals('Sheet1!E6', ws2.ReadFormula(1, 2), 'Read formula mismatch, cell Sheet2!C2');
end;
21: // Delete row from sheet1 after formula, but before referenced cell
begin
ws1.DeleteRow(3);
CheckEquals(VALUE1, ws1.ReadAsText(1, 1), 'Read value mismatch, cell Sheet1!B2');
CheckEquals('E6', ws1.ReadFormula(1, 1), 'Read formula mismatch, cell Sheet1!B2');
CheckEquals(VALUE2, ws2.ReadAsText(1, 1), 'Read value mismatch, cell Sheet2!B2');
CheckEquals('E7', ws2.ReadFormula(1, 1), 'Read formula mismatch, cell Sheet2!B2');
CheckEquals(VALUE1, ws2.ReadAsText(1, 2), 'Read value mismatch, cell Sheet2!C2');
CheckEquals('Sheet1!E6', ws2.ReadFormula(1, 2), 'Read formula mismatch, cell Sheet2!C2');
end;
22: // Delete row from sheet1 after formula and referenced cell
begin
ws1.DeleteRow(10);
CheckEquals(VALUE1, ws1.ReadAsText(1, 1), 'Read value mismatch, cell Sheet1!B2');
CheckEquals('E7', ws1.ReadFormula(1, 1), 'Read formula mismatch, cell Sheet1!21');
CheckEquals(VALUE2, ws2.ReadAsText(1, 1), 'Read value mismatch, cell Sheet2!B2');
CheckEquals('E7', ws2.ReadFormula(1, 1), 'Read formula mismatch, cell Sheet2!B2');
CheckEquals(VALUE1, ws2.ReadAsText(1, 2), 'Read value mismatch, cell Sheet2!C2');
CheckEquals('Sheet1!E7', ws2.ReadFormula(1, 2), 'Read formula mismatch, cell Sheet2!C2');
end;
30: // Delete column from sheet1 before formula and referenced cell
begin
ws1.DeleteCol(0);
CheckEquals(VALUE1, ws1.ReadAsText(1, 0), 'Read value mismatch, cell Sheet1!A2');
CheckEquals('D7', ws1.ReadFormula(1, 0), 'Read formula mismatch, cell Sheet1!A2');
CheckEquals(VALUE2, ws2.ReadAsText(1, 1), 'Read value mismatch, cell Sheet2!B2');
CheckEquals('E7', ws2.ReadFormula(1, 1), 'Read formula mismatch, cell Sheet2!B2');
CheckEquals(VALUE1, ws2.ReadAsText(1, 2), 'Read value mismatch, cell Sheet2!C2');
CheckEquals('Sheet1!D7', ws2.ReadFormula(1, 2), 'Read formula mismatch, cell Sheet2!C2');
end;
31: // Delete column from sheet1 after formula, but before referenced cell
begin
ws1.DeleteCol(3);
CheckEquals(VALUE1, ws1.ReadAsText(1, 1), 'Read value mismatch, cell Sheet1!A2');
CheckEquals('D7', ws1.ReadFormula(1, 1), 'Read formula mismatch, cell Sheet1!A2');
CheckEquals(VALUE2, ws2.ReadAsText(1, 1), 'Read value mismatch, cell Sheet2!B2');
CheckEquals('E7', ws2.ReadFormula(1, 1), 'Read formula mismatch, cell Sheet2!B2');
CheckEquals(VALUE1, ws2.ReadAsText(1, 2), 'Read value mismatch, cell Sheet2!C2');
CheckEquals('Sheet1!D7', ws2.ReadFormula(1, 2), 'Read formula mismatch, cell Sheet2!C2');
end;
32: // Delete column from sheet1 after formula and referenced cell
begin
ws1.DeleteCol(10);
CheckEquals(VALUE1, ws1.ReadAsText(1, 1), 'Read value mismatch, cell Sheet1!B2');
CheckEquals('E7', ws1.ReadFormula(1, 1), 'Read formula mismatch, cell Sheet1!21');
CheckEquals(VALUE2, ws2.ReadAsText(1, 1), 'Read value mismatch, cell Sheet2!B2');
CheckEquals('E7', ws2.ReadFormula(1, 1), 'Read formula mismatch, cell Sheet2!B2');
CheckEquals(VALUE1, ws2.ReadAsText(1, 2), 'Read value mismatch, cell Sheet2!C2');
CheckEquals('Sheet1!E7', ws2.ReadFormula(1, 2), 'Read formula mismatch, cell Sheet2!C2');
end;
end;
// wb.WriteToFile('test.xlsx', sfOOXML, true); // Activate for looking at file, e.g. with Excel
finally
wb.Free;
end;
end;
procedure TSpreadSingleFormulaTests.InsertRow_BeforeFormula;
begin
TestInsDelRowCol(0);
end;
procedure TSpreadSingleFormulaTests.InsertRow_AfterFormula;
begin
TestInsDelRowCol(1);
end;
procedure TSpreadSingleFormulaTests.InsertRow_AfterAll;
begin
TestInsDelRowCol(2);
end;
procedure TSpreadSingleFormulaTests.InsertCol_BeforeFormula;
begin
TestInsDelRowCol(10);
end;
procedure TSpreadSingleFormulaTests.InsertCol_AfterFormula;
begin
TestInsDelRowCol(11);
end;
procedure TSpreadSingleFormulaTests.InsertCol_AfterAll;
begin
TestInsDelRowCol(12);
end;
procedure TSpreadSingleFormulaTests.DeleteRow_BeforeFormula;
begin
TestInsDelRowCol(20);
end;
procedure TSpreadSingleFormulaTests.DeleteRow_AfterFormula;
begin
TestInsDelRowCol(21);
end;
procedure TSpreadSingleFormulaTests.DeleteRow_AfterAll;
begin
TestInsDelRowCol(22);
end;
procedure TSpreadSingleFormulaTests.DeleteCol_BeforeFormula;
begin
TestInsDelRowCol(30);
end;
procedure TSpreadSingleFormulaTests.DeleteCol_AfterFormula;
begin
TestInsDelRowCol(31);
end;
procedure TSpreadSingleFormulaTests.DeleteCol_AfterAll;
begin
TestInsDelRowCol(32);
end;
initialization initialization
// Register to include these tests in a full run // Register to include these tests in a full run
RegisterTest(TSpreadSingleFormulaTests); RegisterTest(TSpreadSingleFormulaTests);