diff --git a/components/fpspreadsheet/examples/other/demo_expression_parser.pas b/components/fpspreadsheet/examples/other/demo_expression_parser.pas index 8f3db5e44..7c9c9bc81 100644 --- a/components/fpspreadsheet/examples/other/demo_expression_parser.pas +++ b/components/fpspreadsheet/examples/other/demo_expression_parser.pas @@ -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; diff --git a/components/fpspreadsheet/fpsexprparser.pas b/components/fpspreadsheet/fpsexprparser.pas index 268579c29..6aa12dc43 100644 --- a/components/fpspreadsheet/fpsexprparser.pas +++ b/components/fpspreadsheet/fpsexprparser.pas @@ -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; diff --git a/components/fpspreadsheet/fpspreadsheet.pas b/components/fpspreadsheet/fpspreadsheet.pas index 76b25124b..8147f57fb 100755 --- a/components/fpspreadsheet/fpspreadsheet.pas +++ b/components/fpspreadsheet/fpspreadsheet.pas @@ -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 diff --git a/components/fpspreadsheet/tests/insertdeletetests.pas b/components/fpspreadsheet/tests/insertdeletetests.pas index ecc9a59d6..c133616f1 100644 --- a/components/fpspreadsheet/tests/insertdeletetests.pas +++ b/components/fpspreadsheet/tests/insertdeletetests.pas @@ -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,11 +585,20 @@ 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, 'Test empty cell layout mismatch, cell '+CellNotation(MyWorksheet, Row, Col)); @@ -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); diff --git a/components/fpspreadsheet/tests/spreadtestgui.lpi b/components/fpspreadsheet/tests/spreadtestgui.lpi index 6c7085907..83b01a2a7 100644 --- a/components/fpspreadsheet/tests/spreadtestgui.lpi +++ b/components/fpspreadsheet/tests/spreadtestgui.lpi @@ -48,6 +48,7 @@ + @@ -56,7 +57,6 @@ - @@ -66,20 +66,20 @@ - + - + @@ -106,15 +106,16 @@ - + +