fpspreadsheet: Add test cases for insert/delete rows into worksheets containing formulas. Issues remaining when cells referred to by the formula are deleted. Add detection of error values to the formula scanner.

git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@3581 8e941d3f-bd1b-0410-a28a-d453659cc2b4
This commit is contained in:
wp_xxyyzz
2014-09-19 14:22:18 +00:00
parent 738daeb4f6
commit 4ed3b47628
5 changed files with 502 additions and 22 deletions

View File

@ -41,7 +41,8 @@ begin
//cell := Worksheet.WriteFormula(1, 0, 'Day(Date(2014, 1, 12))');
//cell := Worksheet.WriteFormula(1, 0, 'SUM(1,2,3)');
//cell := Worksheet.WriteFormula(1, 0, 'CELL("address",A1)');
cell := Worksheet.WriteFormula(1, 0, 'REPT("Hallo", 3)');
// cell := Worksheet.WriteFormula(1, 0, 'REPT("Hallo", 3)');
cell := Worksheet.WriteFormula(1, 0, '#REF!');
WriteLn('A1: ', worksheet.ReadAsUTF8Text(0, 0));
WriteLn('B1: ', worksheet.ReadAsUTF8Text(0, 1));
@ -76,6 +77,7 @@ begin
fekInteger : Write(' / integer value: ', IntToStr(formula[i].IntValue));
fekString : Write(' / string value: "', formula[i].StringValue, '"');
fekBool : Write(' / boolean value: ', BoolToStr(formula[i].DoubleValue <> 0, true));
fekErr : Write(' / error value: ', GetErrorValueStr(TsErrorValue(formula[i].IntValue)));
end;
WriteLn;
end;

View File

@ -57,20 +57,11 @@ uses
type
{ Tokens }
(* { Basic operands }
fekCell, fekCellRef, fekCellRange, fekCellOffset, fekNum, fekInteger,
fekString, fekBool, fekErr, fekMissingArg,
{ Basic operations }
fekAdd, fekSub, fekMul, fekDiv, fekPercent, fekPower, fekUMinus, fekUPlus,
fekConcat, // string concatenation
fekEqual, fekGreater, fekGreaterEqual, fekLess, fekLessEqual, fekNotEqual,
fekParen,
*)
TsTokenType = (
ttCell, ttCellRange, ttNumber, ttString, ttIdentifier,
ttPlus, ttMinus, ttMul, ttDiv, ttConcat, ttPercent, ttPower, ttLeft, ttRight,
ttLessThan, ttLargerThan, ttEqual, ttNotEqual, ttLessThanEqual, ttLargerThanEqual,
ttListSep, ttTrue, ttFalse, ttEOF
ttListSep, ttTrue, ttFalse, ttError, ttEOF
);
TsExprFloat = Double;
@ -409,7 +400,8 @@ type
constructor CreateDateTime(AParser: TsExpressionParser; AValue: TDateTime);
constructor CreateFloat(AParser: TsExpressionParser; AValue: TsExprFloat);
constructor CreateBoolean(AParser: TsExpressionParser; AValue: Boolean);
constructor CreateError(AParser: TsExpressionParser; AValue: TsErrorValue);
constructor CreateError(AParser: TsExpressionParser; AValue: TsErrorValue); overload;
constructor CreateError(AParser: TsExpressionParser; AValue: String); overload;
function AsString: string; override;
function AsRPNItem(ANext: PRPNItem): PRPNItem; override;
function NodeType : TsResultType; override;
@ -653,6 +645,7 @@ type
procedure ScanError(Msg: String);
protected
procedure SetSource(const AValue: String); virtual;
function DoError: TsTokenType;
function DoIdentifier: TsTokenType;
function DoNumber: TsTokenType;
function DoDelimiter: TsTokenType;
@ -842,6 +835,7 @@ uses
const
cNull = #0;
cDoubleQuote = '"';
cError = '#';
Digits = ['0'..'9']; // + decimalseparator
WhiteSpace = [' ', #13, #10, #9];
@ -998,6 +992,21 @@ begin
end;
end;
function TsExpressionScanner.DoError: TsTokenType;
var
C: Char;
s: String;
begin
C := CurrentChar;
while (not IsWordDelim(C)) and (C <> cNull) do
begin
FToken := FToken + C;
C := NextPos;
end;
S := UpperCase(Token);
Result := ttError;
end;
function TsExpressionScanner.DoIdentifier: TsTokenType;
var
C: Char;
@ -1137,6 +1146,8 @@ begin
Result := DoString
else if IsDigit(C) then
Result := DoNumber
else if (C = cError) then
Result := DoError
else if IsAlpha(C) or (C = '$') then
Result := DoIdentifier
else
@ -1662,6 +1673,8 @@ begin
Result := TsCellExprNode.Create(self, FWorksheet, CurrentToken)
else if (TokenType = ttCellRange) then
Result := TsCellRangeExprNode.Create(self, FWorksheet, CurrentToken)
else if (TokenType = ttError) then
Result := tsConstExprNode.CreateError(self, CurrentToken)
else if not (TokenType in [ttIdentifier]) then
ParserError(Format(SerrUnknownTokenAtPos, [Scanner.Pos, CurrentToken]))
else
@ -2648,6 +2661,28 @@ begin
FValue.ResError := AValue;
end;
constructor TsConstExprNode.CreateError(AParser: TsExpressionParser;
AValue: String);
var
err: TsErrorValue;
begin
if AValue = '#NULL!' then
err := errEmptyIntersection
else if AValue = '#DIV/0!' then
err := errDivideByZero
else if AValue = '#VALUE!' then
err := errWrongType
else if AVAlue = '#REF!' then
err := errIllegalRef
else if AVAlue = '#NAME?' then
err := errWrongName
else if AValue = '#FORMULA?' then
err := errFormulaNotSupported
else
AParser.ParserError('Unknown error type.');
CreateError(AParser, err);
end;
procedure TsConstExprNode.Check;
begin
// Nothing to check;
@ -2671,6 +2706,7 @@ begin
rtDateTime : Result := '''' + FormatDateTime('cccc', FValue.ResDateTime, Parser.FFormatSettings) + ''''; // Probably wrong !!!
rtBoolean : if FValue.ResBoolean then Result := 'TRUE' else Result := 'FALSE';
rtFloat : Result := FloatToStr(FValue.ResFloat, Parser.FFormatSettings);
rtError : Result := GetErrorValueStr(FValue.ResError);
end;
end;
@ -2682,6 +2718,7 @@ begin
rtDateTime : Result := RPNNumber(FValue.ResDateTime, ANext);
rtBoolean : Result := RPNBool(FValue.ResBoolean, ANext);
rtFloat : Result := RPNNumber(FValue.ResFloat, ANext);
rtError : Result := RPNErr(ord(FValue.ResError), ANext);
end;
end;

View File

@ -5068,9 +5068,13 @@ var
r, c, rr, cc: Cardinal;
r1, c1, r2, c2: Cardinal;
cell, nextcell, basecell: PCell;
lastCol, lastRow: Cardinal;
begin
lastCol := GetLastColIndex;
lastRow := GetLastOccupiedRowIndex;
// Loop along the column to be deleted and fix merged cells and shared formulas
for r := 0 to GetLastRowIndex do
for r := 0 to lastRow do
begin
cell := FindCell(r, ACol);
@ -5099,8 +5103,8 @@ begin
// Write adapted formula to the cell below.
WriteFormula(nextcell, basecell^.Formulavalue); //ReadFormulaAsString(nextcell));
// Have all cells sharing the formula use the new formula base
for rr := r to GetLastOccupiedRowIndex do
for cc := ACol+1 to GetLastOccupiedColIndex do
for rr := r to lastRow do
for cc := ACol+1 to lastCol do
begin
cell := FindCell(rr, cc);
if (cell <> nil) and (cell^.SharedFormulaBase = basecell) then
@ -5112,7 +5116,7 @@ begin
end;
// Delete cells
for r := GetLastRowIndex downto 0 do
for r := lastRow downto 0 do
RemoveCell(r, ACol);
// Update column index of cell records

View File

@ -23,6 +23,7 @@ type
DeleteCol: Integer;
DeleteRow: Integer;
Formula: String;
SollFormula: String;
SharedFormulaRowCount: Integer;
SharedFormulaColCount: Integer;
MergedColCount: Integer;
@ -31,7 +32,7 @@ type
end;
var
InsDelTestData: array[0..5] of TInsDelTestDataItem;
InsDelTestData: array[0..21] of TInsDelTestDataItem;
procedure InitTestData;
@ -54,6 +55,30 @@ type
procedure TestWriteRead_InsDelColRow_3; // first
procedure TestWriteRead_InsDelColRow_4; // middle
procedure TestWriteRead_InsDelColRow_5; // last
// Writes out simple cell layout and inserts rows
procedure TestWriteRead_InsDelColRow_6; // before first
procedure TestWriteRead_InsDelColRow_7; // middle
procedure TestWriteRead_InsDelColRow_8; // before last
// Writes out simple cell layout and deletes rows
procedure TestWriteRead_InsDelColRow_9; // first
procedure TestWriteRead_InsDelColRow_10; // middle
procedure TestWriteRead_InsDelColRow_11; // last
// Writes out cell layout with formula and inserts columns
procedure TestWriteRead_InsDelColRow_12; // before formula cell
procedure TestWriteRead_InsDelColRow_13; // after formula cell
// Writes out cell layout with formula and inserts rows
procedure TestWriteRead_InsDelColRow_14; // before formula cell
procedure TestWriteRead_InsDelColRow_15; // after formula cell
// Writes out cell layout with formula and deletes columns
procedure TestWriteRead_InsDelColRow_16; // before formula cell
procedure TestWriteRead_InsDelColRow_17; // after formula cell
procedure TestWriteRead_InsDelColRow_18; // cell in formula
// Writes out cell layout with formula and deletes rows
procedure TestWriteRead_InsDelColRow_19; // before formula cell
procedure TestWriteRead_InsDelColRow_20; // after formula cell
procedure TestWriteRead_InsDelColRow_21; // cell in formula
end;
implementation
@ -77,12 +102,17 @@ begin
DeleteCol := -1;
DeleteRow := -1;
Formula := '';
SollFormula := '';
SharedFormulaColCount := 0;
SharedFormulaRowCount := 0;
MergedColCount := 0;
MergedRowCount := 0;
end;
{ ---------------------------------------------------------------------------}
{ Simple layouts }
{ ---------------------------------------------------------------------------}
// Insert a column before col 0
with InsDelTestData[0] do begin
Layout := '12345678|'+
@ -160,6 +190,305 @@ begin
'3456789|'+
'4567890';
end;
// Insert a ROW before row 0
with InsDelTestData[6] do begin
Layout := '12345|'+
'23456|'+
'34567|'+
'45678|'+
'56789|'+
'67890|';
InsertRow := 0;
SollLayout := ' |'+
'12345|'+
'23456|'+
'34567|'+
'45678|'+
'56789|'+
'67890|';
end;
// Insert a ROW before row 2
with InsDelTestData[7] do begin
Layout := '12345|'+
'23456|'+
'34567|'+
'45678|'+
'56789|'+
'67890|';
InsertRow := 2;
SollLayout := '12345|'+
'23456|'+
' |'+
'34567|'+
'45678|'+
'56789|'+
'67890|';
end;
// Insert a ROW before last row
with InsDelTestData[8] do begin
Layout := '12345|'+
'23456|'+
'34567|'+
'45678|'+
'56789|'+
'67890|';
InsertRow := 5;
SollLayout := '12345|'+
'23456|'+
'34567|'+
'45678|'+
'56789|'+
' |'+
'67890|';
end;
// Delete the first row
with InsDelTestData[9] do begin
Layout := '12345|'+
'23456|'+
'34567|'+
'45678|'+
'56789|'+
'67890|';
DeleteRow := 0;
SollLayout := '23456|'+
'34567|'+
'45678|'+
'56789|'+
'67890|';
end;
// Delete row #2
with InsDelTestData[10] do begin
Layout := '12345|'+
'23456|'+
'34567|'+
'45678|'+
'56789|'+
'67890|';
DeleteRow := 2;
SollLayout := '12345|'+
'23456|'+
'45678|'+
'56789|'+
'67890|';
end;
// Delete last row
with InsDelTestData[11] do begin
Layout := '12345|'+
'23456|'+
'34567|'+
'45678|'+
'56789|'+
'67890|';
DeleteRow := 5;
SollLayout := '12345|'+
'23456|'+
'34567|'+
'45678|'+
'56789';
end;
{ ---------------------------------------------------------------------------}
{ Layouts with formula }
{ ---------------------------------------------------------------------------}
// Insert a column before #1, i.e. before formula cell
with InsDelTestData[12] do begin
Layout := '12345678|'+
'23456789|'+
'3456F890|'+ // "F" = Formula in row 2, col 4
'45678901|'+
'56789012|'+
'67890123';
InsertCol := 1;
Formula := 'C3';
SollFormula := 'D3'; // col index increases due to inserted col
SollLayout := '1 2345678|'+
'2 3456789|'+
'3 4565890|'+
'4 5678901|'+
'5 6789012|'+
'6 7890123';
end;
// Insert a column before #3, i.e. after formula cell
with InsDelTestData[13] do begin
Layout := '12345678|'+
'23456789|'+
'3456F890|'+ // "F" = Formula in row 2, col 4
'45678901|'+
'56789012|'+
'67890123';
InsertCol := 3;
Formula := 'C3';
SollFormula := 'C3'; // no change of cell because insertion is behind
SollLayout := '123 45678|'+
'234 56789|'+
'345 65890|'+
'456 78901|'+
'567 89012|'+
'678 90123';
end;
// Insert a row before #1, i.e. before formula cell
with InsDelTestData[14] do begin
Layout := '12345678|'+
'23456789|'+
'3456F890|'+ // "F" = Formula in row 2, col 4
'45678901|'+
'56789012|'+
'67890123';
InsertRow := 1;
Formula := 'E4';
SollFormula := 'E5'; // row index increaes due to inserted row
SollLayout := '12345678|'+
' |'+
'23456789|'+
'34568890|'+
'45678901|'+
'56789012|'+
'67890123';
end;
// Insert a row before #4, i.e. after formula cell
with InsDelTestData[15] do begin
Layout := '12345678|'+
'23456789|'+
'3456F890|'+ // "F" = Formula in row 2, col 4
'45678901|'+
'56789012|'+
'67890123';
InsertRow := 5;
Formula := 'E4';
SollFormula := 'E4'; // row index not changed dur to insert after cell
SollLayout := '12345678|'+
'23456789|'+
'34568890|'+
'45678901|'+
'56789012|'+
' |'+
'67890123';
end;
// Deletes column #1, i.e. before formula cell
with InsDelTestData[16] do begin
Layout := '12345678|'+
'23456789|'+
'3456F890|'+ // "F" = Formula in row 2, col 4
'45678901|'+
'56789012|'+
'67890123';
DeleteCol := 1;
Formula := 'C3';
SollFormula := 'B3'; // col index decreases due to delete before cell
SollLayout := '1345678|'+
'2456789|'+
'3565890|'+
'4678901|'+
'5789012|'+
'6890123';
end;
// Deletes column #5, i.e. after formula cell
with InsDelTestData[17] do begin
Layout := '12345678|'+
'23456789|'+
'3456F890|'+ // "F" = Formula in row 2, col 4
'45678901|'+
'56789012|'+
'67890123';
DeleteCol := 5;
Formula := 'C3';
SollFormula := 'C3'; // col index unchanged due to deleted after cell
SollLayout := '1234578|'+
'2345689|'+
'3456590|'+
'4567801|'+
'5678912|'+
'6789023';
end;
// Deletes column #2, i.e. cell appearing in formula is gone --> #REF! error
with InsDelTestData[18] do begin
Layout := '12345678|'+
'23456789|'+
'3456F890|'+ // "F" = Formula in row 2, col 4
'45678901|'+
'56789012|'+
'67890123';
DeleteCol := 2;
Formula := 'C3';
SollFormula := '#REF!'; // col index unchanged due to deletion after cell
SollLayout := '1245678|'+
'2356789|'+
'346E890|'+ // "E" = error
'4578901|'+
'5689012|'+
'6790123';
end;
// Deletes row #1, i.e. before formula cell
with InsDelTestData[19] do begin
Layout := '12345678|'+
'23456789|'+
'3456F890|'+ // "F" = Formula in row 2, col 4
'45678901|'+
'56789012|'+
'67890123';
DeleteRow := 1;
Formula := 'E4';
SollFormula := 'E3'; // row index decreases due to delete before cell
SollLayout := '12345678|'+
// '23456789|'+
'34568890|'+
'45678901|'+
'56789012|'+
'67890123';
end;
// Deletes row #4, i.e. after formula cell
with InsDelTestData[20] do begin
Layout := '12345678|'+
'23456789|'+
'3456F890|'+ // "F" = Formula in row 2, col 4
'45678901|'+
'56789012|'+
'67890123';
DeleteRow := 4;
Formula := 'E4';
SollFormula := 'E4'; // row index unchanged (delete is after cell)
SollLayout := '12345678|'+
'23456789|'+
'34568890|'+
'45678901|'+
// '56789012|'+
'67890123';
end;
// Deletes row #2, i.e. row containing cell used in formula --> #REF! error!
with InsDelTestData[21] do begin
Layout := '12345678|'+
'23456789|'+
'3456F890|'+ // "F" = Formula in row 2, col 4
'45678901|'+
'56789012|'+
'67890123';
DeleteRow := 3;
Formula := 'E4';
SollFormula := '#REF!';
SollLayout := '12345678|'+
'23456789|'+
'34568890|'+
// '45678901|'+
'56789012|'+
'67890123';
end;
end;
@ -211,6 +540,7 @@ begin
for col := 0 to Length(s)-1 do
case s[col+1] of
'0'..'9': MyWorksheet.WriteNumber(row, col, StrToInt(s[col+1]));
'F' : MyWorksheet.WriteFormula(row, col, InsDelTestData[ATestIndex].Formula);
' ' : ;
end;
end;
@ -237,7 +567,9 @@ begin
// Open the spreadsheet
MyWorkbook := TsWorkbook.Create;
try
MyWorkbook.Options := MyWorkbook.Options + [boReadFormulas, boAutoCalc];
MyWorkbook.ReadFromFile(TempFile, AFormat);
if AFormat = sfExcel2 then
MyWorksheet := MyWorkbook.GetFirstWorksheet
else
@ -253,10 +585,19 @@ begin
MyCell := MyWorksheet.FindCell(row, col);
if MyCell = nil then
actual := actual + ' '
else
else begin
case MyCell^.ContentType of
cctEmpty : actual := actual + ' ';
cctNumber: actual := actual + IntToStr(Round(Mycell^.NumberValue));
cctError : actual := actual + 'E';
end;
if HasFormula(MyCell) then begin
CheckEquals(
MyWorksheet.ReadFormulaAsString(MyCell),
InsDelTestData[ATestIndex].SollFormula,
'Formula mismatch, cell '+CellNotation(MyWorksheet, Row, Col)
);
end;
end;
end;
CheckEquals(actual, expected,
@ -309,6 +650,101 @@ begin
TestWriteRead_InsDelColRow(5);
end;
procedure TSpreadWriteRead_InsDelColRow_Tests.TestWriteRead_InsDelColRow_6;
// insert row before first one
begin
TestWriteRead_InsDelColRow(6);
end;
procedure TSpreadWriteRead_InsDelColRow_Tests.TestWriteRead_InsDelColRow_7;
// insert row before #2
begin
TestWriteRead_InsDelColRow(7);
end;
procedure TSpreadWriteRead_InsDelColRow_Tests.TestWriteRead_InsDelColRow_8;
// insert row before last one
begin
TestWriteRead_InsDelColRow(8);
end;
procedure TSpreadWriteRead_InsDelColRow_Tests.TestWriteRead_InsDelColRow_9;
// delete first row
begin
TestWriteRead_InsDelColRow(9);
end;
procedure TSpreadWriteRead_InsDelColRow_Tests.TestWriteRead_InsDelColRow_10;
// delete row #2
begin
TestWriteRead_InsDelColRow(10);
end;
procedure TSpreadWriteRead_InsDelColRow_Tests.TestWriteRead_InsDelColRow_11;
// delete last row
begin
TestWriteRead_InsDelColRow(11);
end;
procedure TSpreadWriteRead_InsDelColRow_Tests.TestWriteRead_InsDelColRow_12;
// insert column before formula cell
begin
TestWriteRead_InsDelColRow(12);
end;
procedure TSpreadWriteRead_InsDelColRow_Tests.TestWriteRead_InsDelColRow_13;
// insert column after formula cell
begin
TestWriteRead_InsDelColRow(13);
end;
procedure TSpreadWriteRead_InsDelColRow_Tests.TestWriteRead_InsDelColRow_14;
// insert row before formula cell
begin
TestWriteRead_InsDelColRow(14);
end;
procedure TSpreadWriteRead_InsDelColRow_Tests.TestWriteRead_InsDelColRow_15;
// insert row after formula cell
begin
TestWriteRead_InsDelColRow(15);
end;
procedure TSpreadWriteRead_InsDelColRow_Tests.TestWriteRead_InsDelColRow_16;
// delete column before formula cell
begin
TestWriteRead_InsDelColRow(16);
end;
procedure TSpreadWriteRead_InsDelColRow_Tests.TestWriteRead_InsDelColRow_17;
// delete column after formula cell
begin
TestWriteRead_InsDelColRow(17);
end;
procedure TSpreadWriteRead_InsDelColRow_Tests.TestWriteRead_InsDelColRow_18;
// delete column containing a cell used in formula
begin
TestWriteRead_InsDelColRow(18);
end;
procedure TSpreadWriteRead_InsDelColRow_Tests.TestWriteRead_InsDelColRow_19;
// delete row before formula cell
begin
TestWriteRead_InsDelColRow(19);
end;
procedure TSpreadWriteRead_InsDelColRow_Tests.TestWriteRead_InsDelColRow_20;
// delete row after formula cell
begin
TestWriteRead_InsDelColRow(20);
end;
procedure TSpreadWriteRead_InsDelColRow_Tests.TestWriteRead_InsDelColRow_21;
// delete row containing a cell used in formula
begin
TestWriteRead_InsDelColRow(21);
end;
initialization
RegisterTest(TSpreadWriteRead_InsDelColRow_Tests);

View File

@ -48,6 +48,7 @@
<Unit1>
<Filename Value="datetests.pas"/>
<IsPartOfProject Value="True"/>
<UnitName Value="datetests"/>
</Unit1>
<Unit2>
<Filename Value="stringtests.pas"/>
@ -56,7 +57,6 @@
<Unit3>
<Filename Value="numberstests.pas"/>
<IsPartOfProject Value="True"/>
<UnitName Value="numberstests"/>
</Unit3>
<Unit4>
<Filename Value="manualtests.pas"/>
@ -66,20 +66,20 @@
<Unit5>
<Filename Value="testsutility.pas"/>
<IsPartOfProject Value="True"/>
<UnitName Value="testsutility"/>
</Unit5>
<Unit6>
<Filename Value="internaltests.pas"/>
<IsPartOfProject Value="True"/>
<UnitName Value="internaltests"/>
</Unit6>
<Unit7>
<Filename Value="formattests.pas"/>
<IsPartOfProject Value="True"/>
<UnitName Value="formattests"/>
</Unit7>
<Unit8>
<Filename Value="colortests.pas"/>
<IsPartOfProject Value="True"/>
<UnitName Value="colortests"/>
</Unit8>
<Unit9>
<Filename Value="fonttests.pas"/>
@ -106,15 +106,16 @@
<Unit14>
<Filename Value="emptycelltests.pas"/>
<IsPartOfProject Value="True"/>
<UnitName Value="emptycelltests"/>
</Unit14>
<Unit15>
<Filename Value="errortests.pas"/>
<IsPartOfProject Value="True"/>
<UnitName Value="errortests"/>
</Unit15>
<Unit16>
<Filename Value="virtualmodetests.pas"/>
<IsPartOfProject Value="True"/>
<UnitName Value="virtualmodetests"/>
</Unit16>
<Unit17>
<Filename Value="insertdeletetests.pas"/>