From b94ba1fac6eaa915379d7be7a44024ebab80eecc Mon Sep 17 00:00:00 2001 From: wp_xxyyzz Date: Wed, 16 May 2018 17:44:02 +0000 Subject: [PATCH] fpspreadsheet: Simplify handling of 3d references. Fix bug related to 3d reference type 'Sheet1:Sheet2!A1'. git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@6422 8e941d3f-bd1b-0410-a28a-d453659cc2b4 --- .../source/common/fpsexprparser.pas | 514 ++++++++---------- .../fpspreadsheet/source/common/fpsutils.pas | 9 +- .../fpspreadsheet/source/common/xlsbiff5.pas | 17 +- .../fpspreadsheet/source/common/xlsbiff8.pas | 7 +- .../fpspreadsheet/source/common/xlscommon.pas | 3 - .../fpspreadsheet/tests/formulatests.pas | 178 +++--- .../tests/testcases_calc3dformula.inc | 8 + 7 files changed, 334 insertions(+), 402 deletions(-) diff --git a/components/fpspreadsheet/source/common/fpsexprparser.pas b/components/fpspreadsheet/source/common/fpsexprparser.pas index e88179648..bafbeff94 100644 --- a/components/fpspreadsheet/source/common/fpsexprparser.pas +++ b/components/fpspreadsheet/source/common/fpsexprparser.pas @@ -57,8 +57,8 @@ type { Tokens } TsTokenType = ( - ttCell, ttCellRange, ttSheetName, ttCellRangeODS, - ttNumber, ttString, ttIdentifier, +// ttCell, ttCellRange, ttSheetName, ttCellRangeODS, + ttNumber, ttString, ttIdentifier, ttSpreadsheetAddress, ttPlus, ttMinus, ttMul, ttDiv, ttConcat, ttPercent, ttPower, ttLeft, ttRight, ttLessThan, ttLargerThan, ttEqual, ttNotEqual, ttLessThanEqual, ttLargerThanEqual, ttListSep, ttTrue, ttFalse, ttMissingArg, ttError, ttEOF @@ -590,18 +590,19 @@ type FRow, FCol: Cardinal; FFlags: TsRelFlags; FCell: PCell; + FSheetName: String; FIsRef: Boolean; - FOtherSheet: Boolean; protected function GetCol: Cardinal; function GetRow: Cardinal; + function GetSheet: TsWorksheet; function GetSheetIndex: Integer; - procedure GetNodeValue(out Result: TsExpressionResult); override; + function GetSheetName: String; + function GetWorkbook: TsWorkbook; + procedure GetNodeValue(out AResult: TsExpressionResult); override; public constructor Create(AParser: TsExpressionParser; AWorksheet: TsWorksheet; - ARow, ACol: Cardinal; AFlags: TsRelFlags; OtherSheet: Boolean); overload; - constructor Create(AParser: TsExpressionParser; AWorksheet: TsWorksheet; - ACellString: String; Othersheet: Boolean); overload; + ASheetName: String; ARow, ACol: Cardinal; AFlags: TsRelFlags); function AsRPNItem(ANext: PRPNItem): PRPNItem; override; function AsString: string; override; procedure Check; override; @@ -619,7 +620,7 @@ type // FWorksheet2: TsWorksheet; FRow: array[TsCellRangeIndex] of Cardinal; FCol: array[TsCellRangeIndex] of Cardinal; - FSheet: array[TsCellRangeIndex] of Integer; + FSheetIndex: array[TsCellRangeIndex] of Integer; FFlags: TsRelFlags; F3dRange: Boolean; protected @@ -629,10 +630,7 @@ type function GetWorkbook: TsWorkbook; public constructor Create(AParser: TsExpressionParser; AWorksheet: TsWorksheet; - ARangeString: String); overload; - constructor Create(AParser: TsExpressionParser; AWorksheet: TsWorksheet; - ASheet1, ASheet2: String; ARow1,ACol1, ARow2, ACol2: Cardinal; - AFlags: TsRelFlags; Is3DRange: Boolean); overload; + ASheet1, ASheet2: String; ARange: TsCellRange; AFlags: TsRelFlags); function AsRPNItem(ANext: PRPNItem): PRPNItem; override; function AsString: String; override; procedure Check; override; @@ -652,12 +650,16 @@ type FTokenType: TsTokenType; FSheetNameTerminator: Char; FSavedSheetNameTerminator: Char; + FCellRange: TsCellRange; + FFlags: TsRelFlags; + FSheet1, FSheet2: String; private FParser: TsExpressionParser; function GetCurrentChar: Char; procedure ScanError(Msg: String); protected procedure SetSource(const AValue: String); virtual; + function DoCellRangeODS: TsTokenType; function DoError: TsTokenType; function DoIdentifier: TsTokenType; function DoNumber: TsTokenType; @@ -676,6 +678,10 @@ type out ARow1, ACol1, ARow2, ACol2: Cardinal; out AFlags: TsRelFlags); function GetToken: TsTokenType; property Token: String read FToken; + property TokenCellRange: TsCellRange read FCellRange; + property TokenFlags: TsRelFlags read FFlags; + property TokenSheet1: String read FSheet1; + property TokenSheet2: String read FSheet2; property TokenType: TsTokenType read FTokenType; property Source: String read FSource write SetSource; property Pos: Integer read FPos; @@ -699,17 +705,13 @@ type FDialect: TsFormulaDialect; FSourceCell: PCell; FDestCell: PCell; -// FActiveCell: PCell; procedure CheckEOF; -// procedure CheckNodes(var ALeft, ARight: TsExprNode); -// function ConvertNode(Todo: TsExprNode; ToType: TsResultType): TsExprNode; function GetAsBoolean: Boolean; function GetAsDateTime: TDateTime; function GetAsFloat: TsExprFloat; function GetAsInteger: Int64; function GetAsString: String; function GetRPNFormula: TsRPNFormula; -// function MatchNodes(Todo, Match: TsExprNode): TsExprNode; procedure SetBuiltIns(const AValue: TsBuiltInExprCategories); procedure SetDialect(const AValue: TsFormulaDialect); procedure SetIdentifiers(const AValue: TsExprIdentifierDefs); @@ -945,6 +947,99 @@ begin FSavedSheetNameTerminator := '!'; end; +{ Scans until closing square bracket is reached. In OpenDocument, this is + a cell or cell range identifier. + It has the structure [sheet1.C1R1:sheet2.C2R2] } +function TsExpressionScanner.DoCellRangeODS: TsTokenType; +type + TScannerStateODS = (ssSheet1, ssCol1, ssRow1, ssSheet2, ssCol2, ssRow2); +var + C: Char; + prevC: Char; + state: TScannerStateODS; + val: Integer; +begin + FSheet1 := ''; + FSheet2 := ''; + FCellRange.Row1 := Cardinal(-1); + FCellRange.Col1 := Cardinal(-1); + FCellRange.Row2 := Cardinal(-1); + FCellRange.Col2 := Cardinal(-1); + FFlags := rfAllRel; + + state := ssSheet1; + FToken := ''; + C := NextPos; + prevC := #0; + while (C <> ']') and (C <> cNULL) do begin + case C of + cNULL: ScanError(rsUnexpectedEndOfExpression); + '.': begin + if (state = ssSheet1) then + begin + FSheet1 := FToken; + state := ssCol1; + end else + if (state = ssSheet2) then + begin + FSheet2 := FToken; + state := ssCol2; + end else + ScanError(rsIllegalODSCellRange); + FToken := ''; + val := 0; + end; + ':': if (state = ssRow1) then + begin + FCellRange.Row1 := val-1; + state := ssSheet2; + FToken := ''; + end else + ScanError(rsIllegalODSCellRange); + '$': case state of + ssCol1: if prevC = '.' then Exclude(FFlags, rfRelCol) else Exclude(FFlags, rfRelRow); + ssCol2: if prevC = '.' then Exclude(FFlags, rfRelCol2) else Exclude(FFlags, rfRelRow2); + end; + else + if (state in [ssSheet1, ssSheet2]) then + FToken := FToken + C + else + case C of + 'A'..'Z': + val := val*10 + ord(C) - ord('A'); + 'a'..'z': + val := val*10 + ord(C) - ord('a'); + '0'..'9': + if state = ssCol1 then begin + FCellRange.Col1 := val; + val := ord(C) - ord('0'); + state := ssRow1; + end else + if state = ssCol2 then begin + FCellRange.Col2 := val; + val := ord(C) - ord('0'); + state := ssRow2; + end; + end; + end; + prevC := C; + C := NextPos; + end; + if C <> ']' then + ScanError(Format(rsRightSquareBracketExpected, [FPos, C])); + case state of + ssRow1: + if val > 0 then FCellRange.Row1 := val - 1 else ScanError(rsIllegalODSCellRange); + ssRow2: + if val > 0 then FCellRange.Row2 := val - 1 else ScanError(rsIllegalODSCellRange); + end; + if FCellRange.Col2 = Cardinal(-1) then Exclude(FFlags, rfRelCol2); + if FCellRange.Row2 = Cardinal(-1) then Exclude(FFlags, rfRelRow2); + C := NextPos; + Result := ttSpreadsheetAddress; + FTokenType := Result; +end; + function TsExpressionScanner.DoDelimiter: TsTokenType; var B : Boolean; @@ -1014,33 +1109,24 @@ var flags: TsRelFlags; begin C := CurrentChar; - if C = FSheetNameTerminator then C := NextPos; - while (not IsWordDelim(C)) and (C <> cNull) and (C <> FSheetNameTerminator) do + while (not IsWordDelim(C)) and (C <> cNULL) do begin - { - if ((FParser.Dialect = fdOpenDocument) and (C = ']')) then begin - C := NextPos; - FSheetNameTerminator := FSavedSheetNameTerminator; - break; - end; - } FToken := FToken + C; C := NextPos; end; - if C = FSheetNameTerminator then + if ParseCellRangeString(FToken, FSheet1, FSheet2, + FCellRange.Row1, FCellRange.Col1, FCellRange.Row2, FCellRange.Col2, FFlags + ) and (C <> '(') + then begin - C := NextPos; - result := ttSheetName; + Result := ttSpreadsheetAddress; exit; end; S := LowerCase(FToken); - if ParseCellString(S, row, col, flags) and (C <> '(') then - Result := ttCell - else if ParseCellRangeString(S, row, col, row2, col2, flags) and (C <> '(') then - Result := ttCellRange - else if (S = 'true') and (C <> '(') then + + if (S = 'true') and (C <> '(') then Result := ttTrue else if (S = 'false') and (C <> '(') then Result := ttFalse @@ -1166,53 +1252,6 @@ begin C := NextPos; end; -(* -function TsExpressionScanner.DoSquareBracket: TsTokenType; -var - C: Char; - r1,c1,r2,c2: Cardinal; - flags: TsRelFlags; - isRange: Boolean; - sheetName: String; -begin - isRange := false; - FToken := ''; - sheetName := ''; - C := NextPos; - while (C <> ']') do - begin - case C of - cNull: ScanError(rsUnexpectedEndOfExpression); - '.' : begin - sheetName := FToken; - FToken := ''; - end; - ':' : begin isRange := true; FToken := FToken + C; end; - else FToken := FToken + C; - end; - C := NextPos; - end; - C := NextPos; - - if sheetName <> '' then begin - - if isRange then - begin - if ParseCellRangeString(FToken, r1, c1, r2, c2, flags) then - Result := ttCellRange - else - Result := ttError; -// ScanError(Format(SErrInvalidCellRange, [FToken])); - end else - begin - if ParseCellString(FToken, r1, c1, flags) then - Result := ttCell - else - Result := ttError; -// ScanError(Format(SErrInvalidCell, [FToken])); - end; -end;*) - function TsExpressionScanner.DoString: TsTokenType; @@ -1257,8 +1296,8 @@ begin FToken := ''; SkipWhiteSpace; C := FChar^; - if {(FParser.Dialect = fdOpenDocument) and }(C = '[') then - Result := ttCellRangeODS + if (C = '[') then + Result := DoCellRangeODS else if C = cNull then Result := ttEOF else if IsDelim(C) then @@ -1738,10 +1777,9 @@ var optional: Boolean; token: String; prevTokenType: TsTokenType; - sheetname, sheetname2: String; - r1, c1, r2, c2: Cardinal; + sheetname1, sheetname2: String; + rng: TsCellRange; flags: TsRelFlags; - sheet: TsWorksheet; begin {$ifdef debugexpr} Writeln('Primitive : ',TokenName(TokenType),': ',CurrentToken);{$endif debugexpr} SetLength(Args, 0); @@ -1761,40 +1799,18 @@ begin Result := TsConstExprNode.CreateBoolean(self, false) else if (TokenType = ttString) then Result := TsConstExprNode.CreateString(self, CurrentToken) - else if (TokenType = ttCell) then - Result := TsCellExprNode.Create(self, FWorksheet, CurrentToken, false) - else if (TokenType = ttCellRange) then - Result := TsCellRangeExprNode.Create(self, FWorksheet, CurrentToken) - else if (TokenType = ttCellRangeODS) then + else if (TokenType = ttSpreadsheetAddress) then begin - FScanner.GetCellRangeParamsODS(sheetname, sheetname2, r1, c1, r2, c2, flags); - if (sheetname2 = '') and (r2 = cardinal(-1)) and (c2 = cardinal(-1)) then - begin - if sheetname = '' then - Result := TsCellExprNode.Create(self, FWorksheet, r1, c1, flags, false) - else begin - sheet := FWorksheet.Workbook.GetWorksheetByName(sheetName); - Result := TsCellExprNode.Create(self, sheet, r1, c1, flags, true); - end; - end + sheetname1 := FScanner.TokenSheet1; + sheetname2 := FScanner.TokenSheet2; + rng := FScanner.TokenCellRange; + flags := FScanner.TokenFlags; + if (sheetname2 = '') and + (rng.Row2 = Cardinal(-1)) and (rng.Col2 = Cardinal(-1)) + then + Result := TsCellExprNode.Create(self, FWorksheet, sheetname1, rng.Row1, rng.Col1, flags) else - Result := TsCellRangeExprNode.Create(self, FWorksheet, sheetname, sheetname2, - r1, c1, r2, c2, flags, (sheetname <> '')); - end - else if (TokenType = ttSheetName) then - begin - sheetName := CurrentToken; - GetToken; - if TokenType = ttCell then begin - sheet := FWorksheet.Workbook.GetWorksheetByName(sheetName); - Result := TsCellExprNode.Create(self, sheet, CurrentToken, true) - end else - if TokenType = ttCellRange then begin - if FDialect = fdOpenDocument then - Result := TsCellRangeExprNode.Create(self, FWorksheet, CurrentToken) - else - Result := TsCellrangeExprNode.Create(self, FWorksheet, sheetName+'!'+CurrentToken); - end; + Result := TsCellRangeExprNode.Create(self, FWorksheet, sheetname1, sheetname2, rng, flags) end else if (TokenType = ttError) then Result := TsConstExprNode.CreateError(self, CurrentToken) @@ -1989,8 +2005,8 @@ procedure TsExpressionParser.SetRPNFormula(const AFormula: TsRPNFormula); ID: TsExprIdentifierDef; i, n: Integer; args: TsExprArgumentArray; - sheet: TsWorksheet; sn, sn2: string; + rng: TsCellRange; begin if AIndex < 0 then exit; @@ -2007,7 +2023,7 @@ procedure TsExpressionParser.SetRPNFormula(const AFormula: TsRPNFormula); else begin flags := AFormula[AIndex].RelFlags; - ANode := TsCellExprNode.Create(self, FWorksheet, r, c, flags, false); + ANode := TsCellExprNode.Create(self, FWorksheet, '', r, c, flags); end; dec(AIndex); end; @@ -2021,28 +2037,27 @@ procedure TsExpressionParser.SetRPNFormula(const AFormula: TsRPNFormula); else begin flags := AFormula[AIndex].RelFlags; - sheet := FWorksheet.Workbook.GetWorksheetByIndex(idx); - ANode := TsCellExprNode.Create(Self, sheet, r, c, flags, true); + sn := FWorksheet.Workbook.GetWorksheetByIndex(idx).Name; + ANode := TsCellExprNode.Create(Self, FWorksheet, sn, r, c, flags); end; dec(AIndex); end; fekCellRange, fekCellRange3D: begin - r := AFormula[AIndex].Row; - c := AFormula[AIndex].Col; - r2 := AFormula[AIndex].Row2; - c2 := AFormula[AIndex].Col2; + rng.Row1 := AFormula[AIndex].Row; + rng.Col1 := AFormula[AIndex].Col; + rng.Row2 := AFormula[AIndex].Row2; + rng.Col2 := AFormula[AIndex].Col2; flags := AFormula[AIndex].RelFlags; if fek = fekCellRange then - ANode := TsCellRangeExprNode.Create(self, FWorksheet, - FWorksheet.Name, FWorksheet.Name, r, c, r2, c2, flags, false) + ANode := TsCellRangeExprNode.Create(self, FWorksheet, '', '', rng, flags) else begin sn := FWorksheet.Workbook.GetWorksheetByIndex(AFormula[AIndex].Sheet).Name; - if AFormula[AIndex].Sheet2 = -1 then - sn2 := sn + if AFormula[AIndex].Sheet2 <> -1 then + sn2 := FWorksheet.Workbook.GetWorksheetByIndex(AFormula[AIndex].Sheet2).Name else - sn2 := FWorksheet.Workbook.GetWorksheetByIndex(AFormula[AIndex].Sheet2).Name; - ANode := TsCellRangeExprNode.Create(self, FWorksheet, sn,sn2, r,c, r2,c2, flags, true); + sn2 := ''; + ANode := TsCellRangeExprNode.Create(self, FWorksheet, sn,sn2, rng, flags); end; dec(AIndex); end; @@ -3743,62 +3758,33 @@ begin FCallBack(Result, FArgumentParams); end; - (* -{ TsSheetNameExprNode } -constructor TsSheetNameExprNode.Create(AParser: TsExpressionParser; - ASheetName: string); -begin - FParser := AParser; - FSheetName := ASheetName; -end; - -function TsSheetNameExprNode.AsRPNItem(ANext: PRPNItem): PRPNItem; -begin - Result := ANext; -end; - -function TsSheetnameExprNode.AsString: string; -begin - Result := ''; -end; - *) { TsCellExprNode } constructor TsCellExprNode.Create(AParser: TsExpressionParser; - AWorksheet: TsWorksheet; ARow,ACol: Cardinal; AFlags: TsRelFlags; - OtherSheet: Boolean); + AWorksheet: TsWorksheet; ASheetName: String; ARow, ACol: Cardinal; + AFlags: TsRelFlags); begin FParser := AParser; FWorksheet := AWorksheet; + FSheetName := ASheetName; FRow := ARow; FCol := ACol; FFlags := AFlags; - FCell := AWorksheet.FindCell(FRow, FCol); - FOtherSheet := OtherSheet; -end; - -constructor TsCellExprNode.Create(AParser: TsExpressionParser; - AWorksheet: TsWorksheet; ACellString: String; OtherSheet: Boolean); -var - r, c: Cardinal; - flags: TsRelFlags; -begin - ParseCellString(ACellString, r, c, flags); - Create(AParser, AWorksheet, r, c, flags, OtherSheet); + FCell := GetSheet.FindCell(FRow, FCol); end; function TsCellExprNode.AsRPNItem(ANext: PRPNItem): PRPNItem; begin if FIsRef then begin - if FOtherSheet then + if Has3dLink then Result := RPNCellRef3D(GetSheetIndex, GetRow, GetCol, FFlags, ANext) else Result := RPNCellRef(GetRow, GetCol, FFlags, ANext) end else begin - if FOtherSheet then + if Has3dLink then Result := RPNCellValue3D(GetSheetIndex, GetRow, GetCol, FFlags, ANext) else Result := RPNCellValue(GetRow, GetCol, FFlags, ANext); @@ -3811,15 +3797,15 @@ var begin r := Getrow; c := GetCol; - if FOtherSheet then + if Has3dLink then case FParser.Dialect of fdExcelA1: - Result := Format('%s!%s', [FWorksheet.Name, GetCellString(r, c, FFlags)]); + Result := Format('%s!%s', [GetSheetName, GetCellString(r, c, FFlags)]); fdExcelR1C1: - Result := Format('%s!%s', [FWorksheet.Name, + Result := Format('%s!%s', [GetSheetName, GetCellString_R1C1(r, c, FFlags, FParser.FSourceCell^.Row, FParser.FSourceCell^.Col)]); fdOpenDocument: - Result := Format('[%s.%s]', [FWorksheet.Name, GetCellString(r, c, FFlags)]); + Result := Format('[%s.%s]', [GetSheetName, GetCellString(r, c, FFlags)]); end else case FParser.Dialect of @@ -3854,7 +3840,7 @@ begin Result := FCol - FParser.FSourceCell^.Col + FParser.FDestCell^.Col; end; -procedure TsCellExprNode.GetNodeValue(out Result: TsExpressionResult); +procedure TsCellExprNode.GetNodeValue(out AResult: TsExpressionResult); var cell: PCell; begin @@ -3871,10 +3857,10 @@ begin raise ECalcEngine.CreateFmt(rsCircularReference, [GetCellString(cell^.Row, cell^.Col)]); end; - Result.ResultType := rtCell; - Result.ResRow := GetRow; - Result.ResCol := GetCol; - Result.Worksheet := FWorksheet; + AResult.ResultType := rtCell; + AResult.ResRow := GetRow; + AResult.ResCol := GetCol; + AResult.Worksheet := GetSheet; end; { See: GetCol } @@ -3885,17 +3871,38 @@ begin Result := FRow - FParser.FSourceCell^.Row + FParser.FDestCell^.Row; end; -function TsCellExprNode.GetSheetIndex: Integer; -var - book: TsWorkbook; +function TsCellExprNode.GetSheet: TsWorksheet; begin - book := FWorksheet.Workbook; - Result := book.GetWorksheetIndex(FWorksheet); + if FSheetName = '' then + Result := FWorksheet + else + Result := GetWorkbook.GetWorksheetByName(FSheetName); +end; + +function TsCellExprNode.GetSheetIndex: Integer; +begin + if FSheetName = '' then + Result := GetWorkbook.GetWorksheetIndex(FWorksheet) + else + Result := GetWorkbook.GetWorksheetIndex(FSheetName); +end; + +function TsCellExprNode.GetSheetName: String; +begin + if FSheetName = '' then + Result := FWorksheet.Name + else + Result := FSheetName; +end; + +function TsCellExprNode.GetWorkbook: TsWorkbook; +begin + Result := FWorksheet.Workbook; end; function TsCellExprNode.Has3DLink: Boolean; begin - Result := FOtherSheet; + Result := FSheetName <> ''; end; function TsCellExprNode.NodeType: TsResultType; @@ -3903,152 +3910,71 @@ begin Result := rtCell; end; - (* -{ TsSheetCellExprNode } - -constructor TsSheetCellExprNode.Create(AParser: TsExpressionParser; - AWorksheet: TsWorksheet; ACellString: String); -var - r, c: Cardinal; - flags: TsRelFlags; - p: Integer; - sheetname: String; -begin - (* - case AParser.Dialect of - fdExcelA1, fdExcelR1C1: p := pos('!', ACellString); - fdOpendocument: p := pos('.', ACellString); - else raise Exception.Create('TsSheetCellExprNode: Parser dialect not supported.'); - end; - sheetname := copy(ACellString, 1, p-1); - ACellString := copy(ACellString, p+1, MaxInt); - *) - ParseCellString(ACellString, r, c, flags); - Create(AParser, AWorksheet, r, c, flags); -end; - -function TsSheetCellExprNode.AsRPNItem(ANext: PRPNItem): PRPNItem; -begin - if FIsRef then - Result := RPNCellRef3D(GetSheetIndex, GetRow, GetCol, FFlags, ANext) - else - Result := RPNCellValue3D(GetSheetIndex, GetRow, GetCol, FFlags, ANext); -end; - -function TsSheetCellExprNode.AsString: String; -begin - case FParser.Dialect of - fdExcelA1: - Result := Format('%s!%s', [ - FWorksheet.Name, - GetCellString(GetRow, GetCol, FFlags) - ]); - fdExcelR1C1: - Result := Format('%s!%s', [ - FWorksheet.Name, - GetCellString_R1C1(GetRow, GetCol, FFlags, FParser.FSourceCell^.Row, FParser.FSourceCell^.Col) - ]); - fdOpenDocument: - Result := Format('[%s.%s]', [ - FWorksheet.Name, - GetCellString(GetRow, GetCol, FFlags) - ]); - else - raise Exception.Create('TsSheetCellExprNode: Parser dialect not supported.'); - end; -end; - -function TsSheetCellExprNode.GetSheetIndex: Integer; -var - book: TsWorkbook; -begin - book := FWorksheet.Workbook; - Result := book.GetWorksheetIndex(FWorksheet); -end; - *) - -{ TsCellExprNode } { TsCellRangeExprNode } constructor TsCellRangeExprNode.Create(AParser: TsExpressionParser; - AWorksheet: TsWorksheet; ARangeString: String); -var - r1, c1, r2, c2: Cardinal; - sheet1, sheet2: String; - flags: TsRelFlags; + AWorksheet: TsWorksheet; ASheet1, ASheet2: String; ARange: TsCellRange; + AFlags: TsRelFlags); begin - ParseCellRangeString(ARangeString, sheet1, sheet2, r1, c1, r2, c2, flags); - if (sheet1 = '') then begin - sheet1 := AWorksheet.Name; - sheet2 := sheet1; - end; - Create(AParser, AWorksheet, sheet1, sheet2, r1, c1, r2, c2, - flags, (AWorksheet.Name <> sheet1) ); -end; + if (ASheet1 = '') and (ASheet2 <> '') then + raise Exception.Create('Invalid parameters in cell range'); -constructor TsCellRangeExprNode.Create(AParser: TsExpressionParser; - AWorksheet: TsWorksheet; ASheet1, ASheet2: String; - ARow1, ACol1, ARow2, ACol2: Cardinal; AFlags: tsRelFlags; Is3DRange: Boolean); -var - tmp: Integer; -begin FParser := AParser; FWorksheet := AWorksheet; FFlags := []; - if (ASheet1 = '') and (ASheet2 <> '') then - raise Exception.Create('Invalid parameters in cell range'); + F3dRange := ((ASheet1 <> '') and (ASheet2 <> '') { and (ASheet1 <> ASheet2)}) or + ((ASheet1 <> '') and (ASheet2 = '')); - FSheet[1] := GetWorkbook.GetWorksheetIndex(ASheet1); + FSheetIndex[1] := GetWorkbook.GetWorksheetIndex(ASheet1); if ASheet2 <> '' then - FSheet[2] := GetWorkbook.GetWorksheetIndex(ASheet2) + FSheetIndex[2] := GetWorkbook.GetWorksheetIndex(ASheet2) else - FSheet[2] := FSheet[1]; - EnsureOrder(FSheet[1], FSheet[2]); + FSheetIndex[2] := FSheetIndex[1]; + EnsureOrder(FSheetIndex[1], FSheetIndex[2]); - if ARow2 = Cardinal(-1) then - ARow2 := ARow1; - if ARow1 <= ARow2 then + if ARange.Row2 = Cardinal(-1) then + ARange.Row2 := ARange.Row1; + if ARange.Row1 <= Arange.Row2 then begin - FRow[1] := ARow1; - FRow[2] := ARow2; - FCol[1] := ACol1; + FRow[1] := ARange.Row1; + FRow[2] := ARange.Row2; + FCol[1] := ARange.Col1; if rfRelRow in AFlags then Include(FFlags, rfRelRow); if rfRelRow2 in AFlags then Include(FFlags, rfRelRow2); end else begin - FRow[1] := ARow2; - FRow[2] := ARow1; + FRow[1] := ARange.Row2; + FRow[2] := ARange.Row1; if rfRelRow in AFlags then Include(FFlags, rfRelRow2); if rfRelRow2 in AFlags then Include(FFlags, rfRelRow); end; - if ACol2 = Cardinal(-1) then - ACol2 := ACol1; - if ACol1 <= ACol2 then + if ARange.Col2 = Cardinal(-1) then + ARange.Col2 := ARange.Col1; + if ARange.Col1 <= ARange.Col2 then begin - FCol[1] := ACol1; - FCol[2] := ACol2; + FCol[1] := ARange.Col1; + FCol[2] := ARange.Col2; if (rfRelCol in AFlags) then Include(FFlags, rfRelCol); if (rfRelCol2 in AFlags) then Include(FFlags, rfRelCol2); end else begin - FCol[1] := ACol2; - FCol[2] := ACol1; + FCol[1] := ARange.Col2; + FCol[2] := ARange.Col1; if (rfRelCol in AFlags) then Include(FFlags, rfRelCol2); if (rfRelCol2 in AFlags) then Include(FFlags, rfRelCol); end; - F3dRange := Is3dRange; end; function TsCellRangeExprNode.AsRPNItem(ANext: PRPNItem): PRPNItem; begin if F3dRange then Result := RPNCellRange3D( - FSheet[1], GetRow(1), Integer(GetCol(1)), - FSheet[2], GetRow(2), Integer(GetCol(2)), + FSheetIndex[1], GetRow(1), Integer(GetCol(1)), + FSheetIndex[2], GetRow(2), Integer(GetCol(2)), FFlags, ANext ) else @@ -4064,14 +3990,14 @@ var r1, c1, r2, c2: Cardinal; s1, s2: String; begin - if FSheet[1] = -1 then + if FSheetIndex[1] = -1 then s1 := FWorksheet.Name else - s1 := Workbook.GetWorksheetByIndex(FSheet[1]).Name; - if FSheet[2] = -1 then + s1 := Workbook.GetWorksheetByIndex(FSheetIndex[1]).Name; + if FSheetIndex[2] = -1 then s2 := FWorksheet.Name else - s2 := Workbook.GetWorksheetByIndex(FSheet[2]).Name; + s2 := Workbook.GetWorksheetByIndex(FSheetIndex[2]).Name; r1 := GetRow(1); c1 := GetCol(1); r2 := GetRow(2); @@ -4134,7 +4060,7 @@ begin begin r[i] := GetRow(i); c[i] := GetCol(i); - s[i] := FSheet[i]; + s[i] := FSheetIndex[i]; end; if not F3dRange then begin diff --git a/components/fpspreadsheet/source/common/fpsutils.pas b/components/fpspreadsheet/source/common/fpsutils.pas index 7f5a221b3..4672f5629 100644 --- a/components/fpspreadsheet/source/common/fpsutils.pas +++ b/components/fpspreadsheet/source/common/fpsutils.pas @@ -944,7 +944,14 @@ begin ASheet2 := copy(s1, p+1, MaxInt); end; end; - Result := ParseCellRangeString(s2, ARow1, ACol1, ARow2, ACol2, AFlags); + + p := pos(':', s2); + if p = 0 then begin + ARow2 := Cardinal(-1); + ACol2 := Cardinal(-1); + Result := ParseCellString(s2, ARow1, ACol1, AFlags); + end else + Result := ParseCellRangeString(s2, ARow1, ACol1, ARow2, ACol2, AFlags); end; {@@ ---------------------------------------------------------------------------- diff --git a/components/fpspreadsheet/source/common/xlsbiff5.pas b/components/fpspreadsheet/source/common/xlsbiff5.pas index 9cc8d5571..caf53610e 100644 --- a/components/fpspreadsheet/source/common/xlsbiff5.pas +++ b/components/fpspreadsheet/source/common/xlsbiff5.pas @@ -58,7 +58,7 @@ interface uses Classes, SysUtils, fpcanvas, lconvencoding, - fpsTypes, fpspreadsheet, fpsrpn, + fpsTypes, fpspreadsheet, xlscommon, {$ifdef USE_NEW_OLE} fpolebasic, @@ -1502,7 +1502,6 @@ procedure TsSpreadBIFF5Writer.WriteDefinedName(AStream: TStream; var memstream: TMemoryStream; rng: TsCellRange; - idx: Integer; j: Integer; begin // Since this is a variable length record we begin by writing the formula @@ -1761,9 +1760,7 @@ end; -------------------------------------------------------------------------------} procedure TsSpreadBIFF5Writer.WriteGlobalLinkTable(AStream: TStream); var - sheet: TsWorksheet; globalLinks: TsBIFFExternSheetList; - sheetList: TsBIFFExternSheetList; i: Integer; begin { collect the global links data } @@ -1912,11 +1909,7 @@ end; procedure TsSpreadBIFF5Writer.WriteLocalLinkTable(AStream: TStream; AWorksheet: TsWorksheet); var - L: TStringList; - cell: PCell; - found: Boolean; - i, j, n: Integer; - sheet: TsWorksheet; + i, n: Integer; externSheetList: TsBIFFExternSheetList; externsheet: TsBIFFExternSheet; begin @@ -1990,7 +1983,7 @@ function TsSpreadBIFF5Writer.WriteRPNSheetIndex(AStream: TStream; var p: Int64; externSheetList: TsBIFFExternSheetList; - externSheetIdx1, externSheetIdx2: Integer; + externSheetIdx: Integer; s: String; begin if ADocumentURL <> '' then // Supporting only internal links @@ -2001,7 +1994,7 @@ begin externSheetList := FLinkLists.GetLocalLinks(FWorksheet); s := FWorkbook.GetWorksheetByIndex(ASheet1).Name; - externSheetIdx1 := externSheetList.IndexOfSheet(s); + externSheetIdx := externSheetList.IndexOfSheet(s); if ASheet2 = -1 then ASheet2 := ASheet1; @@ -2009,7 +2002,7 @@ begin // One-based index of the EXTERNBOOK record to which this reference belongs. // For internal references ("3D references") this must be written as a // negative value. - AStream.WriteWord(WordToLE(word(-(externSheetIdx1 + 1)))); + AStream.WriteWord(WordToLE(word(-(externSheetIdx + 1)))); // 8 unused bytes AStream.WriteQWord(0); diff --git a/components/fpspreadsheet/source/common/xlsbiff8.pas b/components/fpspreadsheet/source/common/xlsbiff8.pas index a61909140..d7f492568 100644 --- a/components/fpspreadsheet/source/common/xlsbiff8.pas +++ b/components/fpspreadsheet/source/common/xlsbiff8.pas @@ -55,7 +55,7 @@ interface uses Classes, SysUtils, fpcanvas, DateUtils, contnrs, lazutf8, - fpstypes, fpspreadsheet, fpsrpn, xlscommon, + fpstypes, fpspreadsheet, xlscommon, {$ifdef USE_NEW_OLE} fpolebasic, {$else} @@ -729,7 +729,6 @@ end; function TsBIFF8ExternSheetList.IndexOfSheets(ABookName: String; ASheetIndex1, ASheetIndex2: Integer): Integer; var - book: TsBIFF8ExternBook; P: PsBIFF8ExternSheet; tmp: Integer; idx: Integer; @@ -3134,13 +3133,9 @@ end; procedure TsSpreadBIFF8Writer.WriteDefinedNames(AStream: TStream); var - externbook: TsBIFF8ExternBook; bookIdx: Integer; sheet: TsWorksheet; i: Integer; - idx: Word; - extSheetIdx: Integer; - sheetList: TsBIFFExternSheetList; begin if (FBiff8ExternBooks = nil) or (FBiff8ExternSheets = nil) then exit; diff --git a/components/fpspreadsheet/source/common/xlscommon.pas b/components/fpspreadsheet/source/common/xlscommon.pas index 88c207290..9976eeaf1 100644 --- a/components/fpspreadsheet/source/common/xlscommon.pas +++ b/components/fpspreadsheet/source/common/xlscommon.pas @@ -1790,8 +1790,6 @@ var ansistr: AnsiString; s: String; sheetlist: TsBIFFExternSheetList; - sheet: TsBIFFExternSheet; - idx: Integer; begin sheetList := FLinkLists.GetSheetList(AWorksheet); @@ -3964,7 +3962,6 @@ procedure TsSpreadBIFFWriter.WriteDefinedNames(AStream: TStream); var sheet: TsWorksheet; i: Integer; - idx: Word; extSheetIdx: Integer; sheetList: TsBIFFExternSheetList; begin diff --git a/components/fpspreadsheet/tests/formulatests.pas b/components/fpspreadsheet/tests/formulatests.pas index e032fbf4e..8503a00ee 100644 --- a/components/fpspreadsheet/tests/formulatests.pas +++ b/components/fpspreadsheet/tests/formulatests.pas @@ -709,106 +709,112 @@ var sollValues: array of TsExpressionResult; formula, actualformula: String; begin - // Create test workbook - workbook := TsWorkbook.Create; + TempFile := GetTempFileName; try - workbook.Options := workbook.Options + [boCalcBeforeSaving]; + // Create test workbook + workbook := TsWorkbook.Create; + try + workbook.Options := workbook.Options + [boCalcBeforeSaving]; - sheet1 := workBook.AddWorksheet('Sheet1'); - sheet2 := workbook.AddWorksheet('Sheet2'); - sheet3 := workbook.AddWorksheet('Sheet3'); + sheet1 := workBook.AddWorksheet('Sheet1'); + sheet2 := workbook.AddWorksheet('Sheet2'); + sheet3 := workbook.AddWorksheet('Sheet3'); - { Write out test formulas. - This include file creates various formulas in column A and stores - the expected results in the array SollValues. } - Row := 0; - TempFile := GetTempFileName; - {$I testcases_calc3dformula.inc} - workbook.WriteToFile(TempFile, AFormat, true); - finally - workbook.Free; - end; + { Write out test formulas. + This include file creates various formulas in column A and stores + the expected results in the array SollValues. } + Row := 0; + {$I testcases_calc3dformula.inc} + workbook.WriteToFile(TempFile, AFormat, true); + finally + workbook.Free; + end; - // Open the workbook - workbook := TsWorkbook.Create; - try - workbook.Options := workbook.Options + [boReadFormulas]; - workbook.ReadFromFile(TempFile, AFormat); - if AFormat = sfExcel2 then - Fail('This test should not be executed') - else - sheet1 := workbook.GetWorksheetByName('Sheet1'); - if sheet1=nil then - Fail('Error in test code. Failed to get named worksheet'); + // Open the workbook + workbook := TsWorkbook.Create; + try + workbook.Options := workbook.Options + [boReadFormulas]; + workbook.ReadFromFile(TempFile, AFormat); + workbook.CalcFormulas; - for row := 0 to sheet1.GetLastRowIndex do - begin - cell := sheet1.FindCell(Row, 0); - if (Cell = nil) then - Fail('Error in test code: failed to get cell ' + CellNotation(sheet1, Row, 0)); - formula := sheet1.ReadAsText(cell); + if AFormat = sfExcel2 then + Fail('This test should not be executed') + else + sheet1 := workbook.GetWorksheetByName('Sheet1'); + if sheet1=nil then + Fail('Error in test code. Failed to get named worksheet'); - cell := sheet1.FindCell(Row, 1); - if (cell = nil) then - fail('Error in test code: Failed to get cell ' + CellNotation(sheet1, Row, 1)); - case cell^.ContentType of - cctBool : actual := BooleanResult(cell^.BoolValue); - cctNumber : actual := FloatResult(cell^.NumberValue); - cctDateTime : actual := DateTimeResult(cell^.DateTimeValue); - cctUTF8String : actual := StringResult(cell^.UTF8StringValue); - cctError : actual := ErrorResult(cell^.ErrorValue); - cctEmpty : actual := EmptyResult; - else fail('ContentType not supported'); - end; - actualformula := cell^.FormulaValue; + for row := 0 to sheet1.GetLastRowIndex do + begin + cell := sheet1.FindCell(Row, 0); + if (Cell = nil) then + Fail('Error in test code: failed to get cell ' + CellNotation(sheet1, Row, 0)); + formula := sheet1.ReadAsText(cell); - expected := SollValues[row]; - // Cell does not store integers! - if expected.ResultType = rtInteger then expected := FloatResult(expected.ResInteger); + cell := sheet1.FindCell(Row, 1); + if (cell = nil) then + fail('Error in test code: Failed to get cell ' + CellNotation(sheet1, Row, 1)); + case cell^.ContentType of + cctBool : actual := BooleanResult(cell^.BoolValue); + cctNumber : actual := FloatResult(cell^.NumberValue); + cctDateTime : actual := DateTimeResult(cell^.DateTimeValue); + cctUTF8String : actual := StringResult(cell^.UTF8StringValue); + cctError : actual := ErrorResult(cell^.ErrorValue); + cctEmpty : actual := EmptyResult; + else fail('ContentType not supported'); + end; + actualformula := cell^.FormulaValue; - (* - // The now function result is volatile, i.e. changes continuously. The - // time for the soll value was created such that we can expect to have - // the file value in the same second. Therefore we neglect the milliseconds. - if formula = '=NOW()' then begin - // Round soll value to seconds - DecodeTime(expected.ResDateTime, hr,min,sec,msec); - expected.ResDateTime := EncodeTime(hr, min, sec, 0); - // Round formula value to seconds - DecodeTime(actual.ResDateTime, hr,min,sec,msec); - actual.ResDateTime := EncodeTime(hr,min,sec,0); - end; *) + expected := SollValues[row]; + // Cell does not store integers! + if expected.ResultType = rtInteger then expected := FloatResult(expected.ResInteger); - case actual.ResultType of - rtBoolean: - CheckEquals(BoolToStr(expected.ResBoolean), BoolToStr(actual.ResBoolean), - 'Test read calculated formula result mismatch, cell '+CellNotation(sheet1, Row, 1)); - rtFloat: - {$if (defined(mswindows)) or (FPC_FULLVERSION>=20701)} - // FPC 2.6.x and trunk on Windows need this, also FPC trunk on Linux x64 - CheckEquals(expected.ResFloat, actual.ResFloat, - 'Test read calculated formula result mismatch, cell '+CellNotation(sheet1, Row, 1)); - {$else} - // Non-Windows: test without error margin - CheckEquals(expected.ResFloat, actual.ResFloat, - 'Test read calculated formula result mismatch, cell '+CellNotation(sheet1, Row, 1)); - {$endif} - rtString: - CheckEquals(expected.ResString, actual.ResString, - 'Test read calculated formula result mismatch, cell '+CellNotation(sheet1, Row, 1)); - rtError: - CheckEquals( - GetEnumName(TypeInfo(TsErrorValue), ord(expected.ResError)), - GetEnumname(TypeInfo(TsErrorValue), ord(actual.ResError)), - 'Test read calculated formula error value mismatch, cell '+CellNotation(sheet1, Row, 1)); + { + // The now function result is volatile, i.e. changes continuously. The + // time for the soll value was created such that we can expect to have + // the file value in the same second. Therefore we neglect the milliseconds. + if formula = '=NOW()' then begin + // Round soll value to seconds + DecodeTime(expected.ResDateTime, hr,min,sec,msec); + expected.ResDateTime := EncodeTime(hr, min, sec, 0); + // Round formula value to seconds + DecodeTime(actual.ResDateTime, hr,min,sec,msec); + actual.ResDateTime := EncodeTime(hr,min,sec,0); + end; } + + case actual.ResultType of + rtBoolean: + CheckEquals(BoolToStr(expected.ResBoolean), BoolToStr(actual.ResBoolean), + 'Test read calculated formula result mismatch, cell '+CellNotation(sheet1, Row, 1)); + rtFloat: + {$if (defined(mswindows)) or (FPC_FULLVERSION>=20701)} + // FPC 2.6.x and trunk on Windows need this, also FPC trunk on Linux x64 + CheckEquals(expected.ResFloat, actual.ResFloat, + 'Test read calculated formula result mismatch, cell '+CellNotation(sheet1, Row, 1)); + {$else} + // Non-Windows: test without error margin + CheckEquals(expected.ResFloat, actual.ResFloat, + 'Test read calculated formula result mismatch, cell '+CellNotation(sheet1, Row, 1)); + {$endif} + rtString: + CheckEquals(expected.ResString, actual.ResString, + 'Test read calculated formula result mismatch, cell '+CellNotation(sheet1, Row, 1)); + rtError: + CheckEquals( + GetEnumName(TypeInfo(TsErrorValue), ord(expected.ResError)), + GetEnumname(TypeInfo(TsErrorValue), ord(actual.ResError)), + 'Test read calculated formula error value mismatch, cell '+CellNotation(sheet1, Row, 1)); + end; + + CheckEquals(formula, actualformula, + 'Read formula string mismatch, cell ' +CellNotation(sheet1, Row, 1)); end; - CheckEquals( - formula, actualformula, 'Read formula string mismatch, cell ' +CellNotation(sheet1, Row, 1)); + finally + workbook.Free; end; finally - workbook.Free; DeleteFile(TempFile); end; end; diff --git a/components/fpspreadsheet/tests/testcases_calc3dformula.inc b/components/fpspreadsheet/tests/testcases_calc3dformula.inc index 9513717ea..5c42eab93 100644 --- a/components/fpspreadsheet/tests/testcases_calc3dformula.inc +++ b/components/fpspreadsheet/tests/testcases_calc3dformula.inc @@ -12,6 +12,7 @@ sheet3.WriteText(1, 2, 'B'); // C2 = 'B' sheet3.WriteNumber(1, 1, 2.0); // B2 = 2.0 + sheet3.WriteNumber(3, 4, 100); // E4 = 100.0 //------------------------------------------------------------------------------ @@ -78,6 +79,13 @@ SetLength(SollValues, Row+1); SollValues[Row] := FloatResult(10.0); + inc(Row); { A10 } + formula := 'SUM(Sheet2:Sheet3!E4)'; + sheet1.WriteText(Row, 0, formula); + sheet1.WriteFormula(Row, 1, formula); + SetLength(SollValues, Row+1); + SollValues[Row] := FloatResult(110.0); + { inc(Row); formula := 'D1&Sheet2!B3%"BC"';