diff --git a/components/fpspreadsheet/examples/fpsspeedtest/fpsspeedtest.lpi b/components/fpspreadsheet/examples/fpsspeedtest/fpsspeedtest.lpi index f15d13114..cda4257b4 100644 --- a/components/fpspreadsheet/examples/fpsspeedtest/fpsspeedtest.lpi +++ b/components/fpspreadsheet/examples/fpsspeedtest/fpsspeedtest.lpi @@ -1,7 +1,7 @@ - + @@ -21,9 +21,10 @@ - - - + + + + diff --git a/components/fpspreadsheet/examples/other/demo_expression_parser.pas b/components/fpspreadsheet/examples/other/demo_expression_parser.pas index 7d23ae617..a72fb2e69 100644 --- a/components/fpspreadsheet/examples/other/demo_expression_parser.pas +++ b/components/fpspreadsheet/examples/other/demo_expression_parser.pas @@ -51,7 +51,7 @@ begin parser := TsSpreadsheetParser.Create(worksheet); try try - parser.Expression := cell^.FormulaValue; + parser.Expression := worksheet.ReadFormula(cell); res := parser.Evaluate; WriteLn('A2: ', parser.Expression); diff --git a/components/fpspreadsheet/examples/other/demo_search.pas b/components/fpspreadsheet/examples/other/demo_search.pas index b82447952..b27133631 100644 --- a/components/fpspreadsheet/examples/other/demo_search.pas +++ b/components/fpspreadsheet/examples/other/demo_search.pas @@ -7,7 +7,7 @@ uses cthreads, {$ENDIF}{$ENDIF} SysUtils, Classes, TypInfo, - fpsTypes, fpSpreadsheet, fpsSearch, fpsUtils, laz_fpspreadsheet; + fpsTypes, fpSpreadsheet, fpsSearch, fpsUtils; var workbook: TsWorkbook; diff --git a/components/fpspreadsheet/examples/other/demo_write_headerfooter_images.lpi b/components/fpspreadsheet/examples/other/demo_write_headerfooter_images.lpi index ac29775f3..b552dafc9 100644 --- a/components/fpspreadsheet/examples/other/demo_write_headerfooter_images.lpi +++ b/components/fpspreadsheet/examples/other/demo_write_headerfooter_images.lpi @@ -55,7 +55,7 @@ - + diff --git a/components/fpspreadsheet/examples/read_write/excel2demo/excel2read.lpr b/components/fpspreadsheet/examples/read_write/excel2demo/excel2read.lpr index 644dbd2f6..893565a0e 100644 --- a/components/fpspreadsheet/examples/read_write/excel2demo/excel2read.lpr +++ b/components/fpspreadsheet/examples/read_write/excel2demo/excel2read.lpr @@ -50,7 +50,7 @@ begin UTF8ToConsole(MyWorkSheet.ReadAsText(CurCell^.Row, CurCell^.Col)) ); if HasFormula(CurCell) then - Write(' (Formula ', CurCell^.FormulaValue, ')'); + Write(' (Formula ', MyWorksheet.ReadFormula(CurCell), ')'); WriteLn; end; diff --git a/components/fpspreadsheet/examples/read_write/excel5demo/excel5read.lpr b/components/fpspreadsheet/examples/read_write/excel5demo/excel5read.lpr index d9fc15711..173c5402c 100644 --- a/components/fpspreadsheet/examples/read_write/excel5demo/excel5read.lpr +++ b/components/fpspreadsheet/examples/read_write/excel5demo/excel5read.lpr @@ -48,7 +48,7 @@ begin ' Col: ', CurCell^.Col, ' Value: ', UTF8ToConsole(MyWorkSheet.ReadAsText(CurCell^.Row, CurCell^.Col))); if HasFormula(CurCell) then - Write(' - Formula: ', CurCell^.FormulaValue); + Write(' - Formula: ', MyWorksheet.ReadFormula(CurCell)); WriteLn; end; diff --git a/components/fpspreadsheet/examples/read_write/htmldemo/htmlread_http.lpi b/components/fpspreadsheet/examples/read_write/htmldemo/htmlread_http.lpi index 03abf3d2a..47963ec28 100644 --- a/components/fpspreadsheet/examples/read_write/htmldemo/htmlread_http.lpi +++ b/components/fpspreadsheet/examples/read_write/htmldemo/htmlread_http.lpi @@ -1,7 +1,7 @@ - + @@ -23,9 +23,16 @@ - + + + + + + + + diff --git a/components/fpspreadsheet/examples/read_write/htmldemo/htmlread_http.lpr b/components/fpspreadsheet/examples/read_write/htmldemo/htmlread_http.lpr index bc9bfa82d..0f9c9b9d0 100644 --- a/components/fpspreadsheet/examples/read_write/htmldemo/htmlread_http.lpr +++ b/components/fpspreadsheet/examples/read_write/htmldemo/htmlread_http.lpr @@ -20,7 +20,7 @@ var const // url = 'http://unicode.e-workers.de/entities.php'; - url = 'http://www.freepascal.org/docs.var'; + url = 'https://www.freepascal.org/docs.var'; begin stream := TMemoryStream.Create; diff --git a/components/fpspreadsheet/examples/read_write/wikitabledemo/wikitableread.lpr b/components/fpspreadsheet/examples/read_write/wikitabledemo/wikitableread.lpr index 4b15b1264..1935c76e7 100644 --- a/components/fpspreadsheet/examples/read_write/wikitabledemo/wikitableread.lpr +++ b/components/fpspreadsheet/examples/read_write/wikitabledemo/wikitableread.lpr @@ -53,7 +53,7 @@ begin UTF8ToConsole(MyWorkSheet.ReadAsText(CurCell^.Row, CurCell^.Col)) ); if HasFormula(CurCell) then - WriteLn(' Formula: ', CurCell^.FormulaValue) + WriteLn(' Formula: ', MyWorksheet.ReadFormula(Curcell)) else WriteLn; end; diff --git a/components/fpspreadsheet/examples/visual/fpschart/workbookchartsource-deprecated/fpschartsource.lpi b/components/fpspreadsheet/examples/visual/fpschart/workbookchartsource-deprecated/fpschartsource.lpi index 062015548..e1053bc56 100644 --- a/components/fpspreadsheet/examples/visual/fpschart/workbookchartsource-deprecated/fpschartsource.lpi +++ b/components/fpspreadsheet/examples/visual/fpschart/workbookchartsource-deprecated/fpschartsource.lpi @@ -1,7 +1,7 @@ - + @@ -44,9 +44,10 @@ - - - + + + + diff --git a/components/fpspreadsheet/examples/visual/fpschart/workbookchartsource-deprecated/fpschartsource.lpr b/components/fpspreadsheet/examples/visual/fpschart/workbookchartsource-deprecated/fpschartsource.lpr index b09f7b04f..008b98874 100644 --- a/components/fpspreadsheet/examples/visual/fpschart/workbookchartsource-deprecated/fpschartsource.lpr +++ b/components/fpspreadsheet/examples/visual/fpschart/workbookchartsource-deprecated/fpschartsource.lpr @@ -7,8 +7,7 @@ uses cthreads, {$ENDIF}{$ENDIF} Interfaces, // this includes the LCL widgetset - Forms, mainform, tachartlazaruspkg - { you can add units after this }; + Forms, mainform; {$R *.res} diff --git a/components/fpspreadsheet/examples/visual/fpschart/workbookchartsource-deprecated/mainform.lfm b/components/fpspreadsheet/examples/visual/fpschart/workbookchartsource-deprecated/mainform.lfm index 01319f44a..282609382 100644 --- a/components/fpspreadsheet/examples/visual/fpschart/workbookchartsource-deprecated/mainform.lfm +++ b/components/fpspreadsheet/examples/visual/fpschart/workbookchartsource-deprecated/mainform.lfm @@ -7,7 +7,7 @@ object Form1: TForm1 ClientHeight = 634 ClientWidth = 877 OnCreate = FormCreate - LCLVersion = '1.7' + LCLVersion = '1.9.0.0' object Panel1: TPanel Left = 0 Height = 40 @@ -77,7 +77,6 @@ object Form1: TForm1 WorkbookSource = sWorkbookSource1 Align = alClient AutoAdvance = aaDown - ColCount = 27 DefaultColWidth = 64 DefaultRowHeight = 22 Font.Color = clBlack @@ -85,7 +84,6 @@ object Form1: TForm1 Font.Name = 'Calibri' Options = [goFixedVertLine, goFixedHorzLine, goVertLine, goHorzLine, goRangeSelect, goEditing, goThumbTracking] ParentFont = False - RowCount = 101 TabOrder = 1 TitleFont.Color = clBlack TitleFont.Height = -15 @@ -110,12 +108,16 @@ object Form1: TForm1 Width = 470 AxisList = < item + Marks.LabelBrush.Style = bsClear Minors = <> Title.LabelFont.Orientation = 900 + Title.LabelBrush.Style = bsClear end item Alignment = calBottom + Marks.LabelBrush.Style = bsClear Minors = <> + Title.LabelBrush.Style = bsClear end> BackColor = clWhite Foot.Brush.Color = clBtnFace @@ -141,12 +143,16 @@ object Form1: TForm1 Width = 470 AxisList = < item + Marks.LabelBrush.Style = bsClear Minors = <> Title.LabelFont.Orientation = 900 + Title.LabelBrush.Style = bsClear end item Alignment = calBottom + Marks.LabelBrush.Style = bsClear Minors = <> + Title.LabelBrush.Style = bsClear end> BackColor = clWhite Depth = 10 @@ -179,13 +185,17 @@ object Form1: TForm1 AxisList = < item Visible = False + Marks.LabelBrush.Style = bsClear Minors = <> Title.LabelFont.Orientation = 900 + Title.LabelBrush.Style = bsClear end item Visible = False Alignment = calBottom + Marks.LabelBrush.Style = bsClear Minors = <> + Title.LabelBrush.Style = bsClear end> Foot.Brush.Color = clBtnFace Foot.Font.Color = clBlue @@ -210,7 +220,7 @@ object Form1: TForm1 end object sWorkbookSource1: TsWorkbookSource AutoDetectFormat = False - FileFormat = sfOOXML + FileFormat = sfUser Options = [] left = 184 top = 320 diff --git a/components/fpspreadsheet/examples/visual/fpschart/workbookchartsource-deprecated/mainform.pas b/components/fpspreadsheet/examples/visual/fpschart/workbookchartsource-deprecated/mainform.pas index d9904121e..2322a2056 100644 --- a/components/fpspreadsheet/examples/visual/fpschart/workbookchartsource-deprecated/mainform.pas +++ b/components/fpspreadsheet/examples/visual/fpschart/workbookchartsource-deprecated/mainform.pas @@ -7,7 +7,7 @@ interface uses Classes, SysUtils, FileUtil, TAGraph, TASeries, Forms, Controls, Graphics, Dialogs, ExtCtrls, StdCtrls, - fpstypes, fpspreadsheetctrls, fpspreadsheetgrid, fpspreadsheetchart; + fpstypes, fpspreadsheetctrls, fpspreadsheetgrid, fpspreadsheetchart, fpsallformats; type diff --git a/components/fpspreadsheet/examples/visual/fpschart/worksheetchartsource/fpschart.lpi b/components/fpspreadsheet/examples/visual/fpschart/worksheetchartsource/fpschart.lpi index d5e17cd01..d92634005 100644 --- a/components/fpspreadsheet/examples/visual/fpschart/worksheetchartsource/fpschart.lpi +++ b/components/fpspreadsheet/examples/visual/fpschart/worksheetchartsource/fpschart.lpi @@ -1,7 +1,7 @@ - + @@ -24,9 +24,16 @@ - + + + + + + + + diff --git a/components/fpspreadsheet/examples/visual/fpsctrls_no_install/demo_ctrls.lpi b/components/fpspreadsheet/examples/visual/fpsctrls_no_install/demo_ctrls.lpi index 0bf369805..63f664605 100644 --- a/components/fpspreadsheet/examples/visual/fpsctrls_no_install/demo_ctrls.lpi +++ b/components/fpspreadsheet/examples/visual/fpsctrls_no_install/demo_ctrls.lpi @@ -1,7 +1,7 @@ - + @@ -21,9 +21,10 @@ - - - + + + + @@ -51,7 +52,7 @@ - + diff --git a/components/fpspreadsheet/examples/visual/wikitablemaker/wikitablemaker.lpi b/components/fpspreadsheet/examples/visual/wikitablemaker/wikitablemaker.lpi index 95f1b7b7f..1afa0edbd 100644 --- a/components/fpspreadsheet/examples/visual/wikitablemaker/wikitablemaker.lpi +++ b/components/fpspreadsheet/examples/visual/wikitablemaker/wikitablemaker.lpi @@ -1,7 +1,7 @@ - + @@ -83,9 +83,10 @@ - - - + + + + @@ -124,7 +125,7 @@ - + diff --git a/components/fpspreadsheet/examples/visual/zoom/zdmain.lfm b/components/fpspreadsheet/examples/visual/zoom/zdmain.lfm index 993817daf..8e8ed99f6 100644 --- a/components/fpspreadsheet/examples/visual/zoom/zdmain.lfm +++ b/components/fpspreadsheet/examples/visual/zoom/zdmain.lfm @@ -7,7 +7,7 @@ object MainForm: TMainForm ClientHeight = 476 ClientWidth = 678 OnCreate = FormCreate - LCLVersion = '1.7' + LCLVersion = '1.9.0.0' object Grid: TsWorksheetGrid Left = 5 Height = 386 @@ -20,12 +20,10 @@ object MainForm: TMainForm WorkbookSource = Grid.internal Anchors = [akTop, akLeft, akRight, akBottom] AutoAdvance = aaDown - ColCount = 27 DefaultColWidth = 64 DefaultRowHeight = 22 MouseWheelOption = mwGrid Options = [goFixedVertLine, goFixedHorzLine, goVertLine, goHorzLine, goRangeSelect, goRowSizing, goColSizing, goEditing, goThumbTracking, goSmoothScroll] - RowCount = 101 TabOrder = 0 OnMouseWheel = GridMouseWheel end diff --git a/components/fpspreadsheet/examples/visual/zoom/zdmain.pas b/components/fpspreadsheet/examples/visual/zoom/zdmain.pas index 4f7254257..9e6507156 100644 --- a/components/fpspreadsheet/examples/visual/zoom/zdmain.pas +++ b/components/fpspreadsheet/examples/visual/zoom/zdmain.pas @@ -7,7 +7,7 @@ interface uses Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls, Spin, Buttons, Types, - fpstypes, fpspreadsheet, fpspreadsheetgrid; + fpstypes, fpspreadsheet, fpspreadsheetgrid, {%H-}fpsAllFormats; type diff --git a/components/fpspreadsheet/examples/visual/zoom/zoomdemo.lpi b/components/fpspreadsheet/examples/visual/zoom/zoomdemo.lpi index fdb9184de..0a0e04a61 100644 --- a/components/fpspreadsheet/examples/visual/zoom/zoomdemo.lpi +++ b/components/fpspreadsheet/examples/visual/zoom/zoomdemo.lpi @@ -1,7 +1,7 @@ - + @@ -18,9 +18,10 @@ - - - + + + + diff --git a/components/fpspreadsheet/source/common/fpsclasses.pas b/components/fpspreadsheet/source/common/fpsclasses.pas index c0987d60e..92c1cddbd 100644 --- a/components/fpspreadsheet/source/common/fpsclasses.pas +++ b/components/fpspreadsheet/source/common/fpsclasses.pas @@ -6,7 +6,7 @@ interface uses Classes, SysUtils, avglvltree, - fpstypes; + fpstypes, fpsExprParser; type { forward declarations } @@ -61,7 +61,8 @@ type function GetFirst: PsRowCol; function GetLast: PsRowCol; procedure InsertRowOrCol(AIndex: Cardinal; IsRow: Boolean); - procedure MoveAlongRow(ARow, AFromCol, AToCol: Cardinal); + procedure MoveAlongCol(ARow, ACol, AToRow: Cardinal); + procedure MoveAlongRow(ARow, ACol, AToCol: Cardinal); procedure Remove(ARow, ACol: Cardinal); overload; end; @@ -174,6 +175,32 @@ type function GetEnumerator: TsCellRangeEnumerator; end; + { TsFormulas } + TsFormulaEnumerator = class(TsRowColEnumerator) + protected + function GetCurrent: PsFormula; + public + function GetEnumerator: TsFormulaEnumerator; inline; + property Current: PsFormula read GetCurrent; + end; + + TsFormulas = class(TsRowColAVLTree) + protected + procedure DisposeData(var AData: Pointer); override; + function NewData: Pointer; override; + public + function AddFormula(ARow, ACol: Cardinal; AFormula: String = ''; + AParsedFormula: TsExpressionParser = nil): PsFormula; + procedure DeleteFormula(ACell: PCell); overload; + procedure DeleteFormula(ARow, ACol: Cardinal); overload; + procedure DeleteRowOrCol(AIndex: Cardinal; IsRow: Boolean); + function FindFormula(ACell: PCell): PsFormula; overload; + function FindFormula(ARow, ACol: Cardinal): PsFormula; overload; + procedure InsertRowOrCol(AIndex: Cardinal; IsRow: Boolean); + // enumerators + function GetEnumerator: TsFormulaEnumerator; + end; + { TsCellFormatList } TsCellFormatList = class(TFPList) private @@ -223,6 +250,146 @@ begin end; +{ Call-back function for formulas when rows/cols are inserted/deleted } + +function FixDeletedCol(AExprNode: TsExprNode; AData: Pointer): Boolean; +var + colIndex: Cardinal; + rng: TsCellRange; +begin + Result := false; + colIndex := PtrInt(AData); + if AExprNode is TsCellExprNode then + begin + if not TsCellExprNode(AExprNode).Has3dLink then + if TsCellExprNode(AExprNode).Col > colIndex then begin + TsCellExprNode(AExprNode).Col := TsCellexprNode(AExprNode).Col - 1; + Result := true; + end else + if TsCellExprNode(AExprNode).Col = colIndex then begin + TsCellExprNode(AExprNode).Error := errIllegalRef; + Result := true; + end; + end else + if AExprNode is TsCellRangeExprNode then + begin + if not TsCellRangeExprNode(AExprNode).Has3dLink then begin + rng := TsCellRangeExprNode(AExprNode).Range; + if (rng.Col1 = colIndex) and (rng.Col2 = colIndex) then begin + TsCellRangeExprNode(AExprNode).Error := errIllegalRef; + Result := true; + end else begin + if rng.Col1 > colIndex then begin + dec(rng.Col1); + Result := true; + end; + if rng.Col2 >= colIndex then begin + dec(rng.Col2); + Result := true; + end; + TsCellRangeExprNode(AExprNode).Range := rng; + end; + end; + end; +end; + +function FixDeletedRow(AExprNode: TsExprNode; AData: Pointer): Boolean; +var + rowIndex: Cardinal; + rng: TsCellRange; +begin + Result := false; + rowIndex := PtrInt(AData); + if AExprNode is TsCellExprNode then + begin + if not TsCellExprNode(AExprNode).Has3dLink then + if TsCellExprNode(AExprNode).Row > rowIndex then begin + TsCellExprNode(AExprNode).Row := TsCellExprNode(AExprNode).Row - 1; + Result := true; + end else + if TsCellExprNode(AExprNode).Row = rowIndex then begin + TsCelLExprNode(AExprNode).Error := errIllegalRef; + Result := true; + end; + end else + if AExprNode is TsCellRangeExprNode then + begin + if not TsCellRangeExprNode(AExprNode).Has3dLink then begin + rng := TsCellRangeExprNode(AExprNode).Range; + if (rng.Row1 = rowIndex) and (rng.Row2 = rowIndex) then begin + TsCellRangeExprNode(AExprNode).Error := errIllegalRef; + Result := true; + end else + begin + if rng.Row1 > rowIndex then begin + dec(rng.Row1); + Result := true; + end; + if rng.Row2 >= rowIndex then begin + dec(rng.Row2); + Result := true; + end; + TsCellRangeExprNode(AExprNode).Range := rng; + end; + end; + end; +end; + +function FixInsertedCol(AExprNode: TsExprNode; AData: Pointer): Boolean; +var + colIndex: Cardinal; + rng: TsCellRange; +begin + Result := false; + colIndex := PtrInt(AData); + if AExprNode is TsCellExprNode then + begin + if not TsCellExprNode(AExprNode).Has3dLink then + if TsCellExprNode(AExprNode).Col >= colIndex then begin + TsCellExprNode(AExprNode).Col := TsCellexprNode(AExprNode).Col + 1; + Result := true; + end; + end else + if AExprNode is TsCellRangeExprNode then + begin + if not TsCellRangeExprNode(AExprNode).Has3dLink then begin + rng := TsCellRangeExprNode(AExprNode).Range; + if rng.Col1 >= colIndex then inc(rng.Col1); + if rng.Col2 >= colIndex then inc(rng.Col2); + TsCellRangeExprNode(AExprNode).Range := rng; + Result := true; + end; + end; +end; + +function FixInsertedRow(AExprNode: TsExprNode; AData: Pointer): Boolean; +var + rowIndex: Cardinal; + rng: TsCellRange; +begin + Result := false; + rowIndex := PtrInt(AData); + if AExprNode is TsCellExprNode then + begin + if not TsCellexprNode(AExprNode).Has3dLink then + if TsCellExprNode(AExprNode).Row >= rowIndex then begin + TsCellExprNode(AExprNode).Row := TsCellExprNode(AExprNode).Row + 1; + Result := true; + end; + end else + if AExprNode is TsCellRangeExprNode then + begin + if not TsCellRangeExprNode(AExprNode).Has3dLink then begin + rng := TsCellRangeExprNode(AExprNode).Range; + if rng.Row1 >= rowIndex then inc(rng.Row1); + if rng.Row2 >= rowIndex then inc(rng.Row2); + TsCellRangeExprNode(AExprNode).Range := rng; + Result := true; + end; + end; +end; + + {==============================================================================} { TsRowColEnumerator } { A specialized enumerator for TsRowColAVLTree using the pointers to the data } @@ -608,20 +775,64 @@ begin end; end; +{@@ ---------------------------------------------------------------------------- + This method moves the cell in the specified column (ACol) and at row AFromRow + along the row before the row with index AToRow. +-------------------------------------------------------------------------------} +procedure TsRowColAVLTree.MoveAlongCol(ARow, ACol, AToRow: Cardinal); +var + r: Cardinal; + node: TAvgLvlTreeNode; + item: PsRowCol; +begin + if ARow = AToRow then + exit; + + if ARow < AToRow then + begin + node := FindLowest; + while Assigned(node) do + begin + item := PsRowCol(node.Data); + if item^.Col = ACol then break; + node := FindSuccessor(node); + end; + r := ARow; + while r < AToRow do begin + Exchange(r, ACol, r+1, ACol); + inc(r); + end; + end else + begin + node:= FindHighest; + while Assigned(node) do + begin + item := PsRowCol(node.Data); + if item^.Col = ACol then break; + node := FindPrecessor(node); + end; + r := ARow; + while r > AToRow do begin + Exchange(r, ACol, r-1, ACol); + dec(r); + end; + end; +end; + {@@ ---------------------------------------------------------------------------- This method moves the cell in the specified row (ARow) and at column AFromCol along the row before the column with index AToCol. -------------------------------------------------------------------------------} -procedure TsRowColAVLTree.MoveAlongRow(ARow, AFromCol, AToCol: Cardinal); +procedure TsRowColAVLTree.MoveAlongRow(ARow, ACol, AToCol: Cardinal); var c: Cardinal; node: TAvgLvlTreeNode; item: PsRowCol; begin - if AFromCol = AToCol then + if ACol = AToCol then exit; - if AFromCol < AToCol then + if ACol < AToCol then begin node := FindLowest; while Assigned(node) do @@ -631,7 +842,7 @@ begin if item^.Row = ARow then break; node := FindSuccessor(node); end; - c := AFromCol; + c := ACol; while c < AToCol do begin Exchange(ARow, c, ARow, c+1); inc(c); @@ -646,7 +857,7 @@ begin if item^.Row = ARow then break; node := FindPrecessor(node); end; - c := AFromCol; + c := ACol; while c > AToCol do begin Exchange(ARow, c, ARow, c-1); dec(c); @@ -926,16 +1137,16 @@ end; { TsHyperlinkEnumerator: enumerator for the TsHyperlinks AVLTree } {==============================================================================} -function TsHyperlinkEnumerator.GetEnumerator: TsHyperlinkEnumerator; -begin - Result := self; -end; - function TsHyperlinkEnumerator.GetCurrent: PsHyperlink; begin Result := PsHyperlink(inherited GetCurrent); end; +function TsHyperlinkEnumerator.GetEnumerator: TsHyperlinkEnumerator; +begin + Result := self; +end; + {==============================================================================} { TsHyperlinks: an AVLTree to store hyperlink records for cells } @@ -1186,6 +1397,151 @@ begin end; +{==============================================================================} +{ TsFormulaEnumerator } +{==============================================================================} +function TsFormulaEnumerator.GetCurrent: PsFormula; +begin + Result := PsFormula(inherited GetCurrent); +end; + +function TsFormulaEnumerator.GetEnumerator: TsFormulaEnumerator; +begin + Result := self; +end; + + +{==============================================================================} +{ TsFormulas } +{==============================================================================} +function TsFormulas.AddFormula(ARow, ACol: Cardinal; AFormula: String = ''; + AParsedFormula: TsExpressionParser = nil): PsFormula; +begin + Result := PsFormula(FindByRowCol(ARow, ACol)); + if Result = nil then + Result := PsFormula(Add(ARow, ACol)); + Result^.Text := AFormula; // unparsed formula + Result^.Parser := AParsedFormula; // if nil, will be parsed on next calculation + Result^.CalcState := csNotCalculated; +end; + +procedure TsFormulas.DeleteFormula(ACell: PCell); +begin + if ACell <> nil then + Delete(ACell^.Row, ACell^.Col); +end; + +procedure TsFormulas.DeleteFormula(ARow, ACol: Cardinal); +begin + Delete(ARow, ACol); // will release memory automatically +end; + +procedure TsFormulas.DeleteRowOrCol(AIndex: Cardinal; IsRow: Boolean); +var + node, nextnode: TAvgLvlTreeNode; + formula: PsFormula; + changed: Boolean; +begin + node := FindLowest; + while Assigned(node) do + begin + changed := false; + nextnode := FindSuccessor(node); + formula := PsFormula(node.Data); + if IsRow then + begin + // Remove and destroy the formula record if it is in the deleted row + if formula^.Row = AIndex then + Delete(node) + else + if formula^.Row > AIndex then + dec(formula^.Row); + // Update all RowCol records at row indexes above the deleted row + changed := formula^.Parser.IterateNodes(@FixDeletedRow, Pointer(PtrInt(AIndex))); + end else + begin + // Remove and destroy the formula record if it is in the deleted column + if formula^.Col = AIndex then + Delete(node) + else begin + if formula^.Col > AIndex then + dec(formula^.Col); + // Update all RowCol records at column indexes above the deleted column + changed := formula^.Parser.IterateNodes(@FixDeletedCol, Pointer(PtrInt(AIndex))); + end; + end; + // Recreate the formula if required. + if changed then + formula^.Text := formula^.Parser.Expression; + node := nextnode; + end; +end; + +procedure TsFormulas.DisposeData(var AData: Pointer); +begin + if AData <> nil then begin + PsFormula(AData)^.Text := ''; + PsFormula(AData)^.Parser.Free; + Dispose(PsFormula(AData)); + end; + AData := nil; +end; + +function TsFormulas.FindFormula(ACell: PCell): PsFormula; +begin + if ACell <> nil then + Result := PsFormula(FindbyRowCol(ACell^.Row, ACell^.Col)) + else + Result := nil; +end; + +function TsFormulas.FindFormula(ARow, ACol: Cardinal): PsFormula; +begin + Result := PsFormula(FindByRowCol(ARow, ACol)); +end; + +// Formula enumerators (use in "for ... in" syntax) +function TsFormulas.GetEnumerator: TsFormulaEnumerator; +begin + Result := TsFormulaEnumerator.Create(self, 0, 0, $7FFFFFFF, $7FFFFFFF, false); +end; + +procedure TsFormulas.InsertRowOrCol(AIndex: Cardinal; IsRow: Boolean); +var + node: TAvgLvlTreeNode; + formula: PsFormula; + changed: Boolean; +begin + node := FindLowest; + while Assigned(node) do begin + formula := PsFormula(node.Data); + if IsRow then + begin + if formula^.Row >= AIndex then inc(formula^.Row); + changed := formula^.Parser.IterateNodes(@FixInsertedRow, Pointer(PtrInt(AIndex))); + end else + begin + if formula^.Col >= AIndex then inc(formula^.Col); + changed := formula^.Parser.IterateNodes(@FixInsertedCol, Pointer(PtrInt(AIndex))); + end; + if changed then + formula^.Text := formula^.Parser.Expression; + node := FindSuccessor(node); + end; +end; + +function TsFormulas.NewData: Pointer; +var + f: PsFormula; +begin + New(f); + f^.Text := ''; + f^.Parser := nil; + f^.CalcState := csNotCalculated; + Result := f; +end; + + {==============================================================================} { TsCellFormatList } {==============================================================================} diff --git a/components/fpspreadsheet/source/common/fpsexprparser.pas b/components/fpspreadsheet/source/common/fpsexprparser.pas index 498afbc91..13f702000 100644 --- a/components/fpspreadsheet/source/common/fpsexprparser.pas +++ b/components/fpspreadsheet/source/common/fpsexprparser.pas @@ -45,7 +45,7 @@ // Keep spaces in formula {$mode objfpc} -{$h+} +{$H+} unit fpsExprParser; interface @@ -78,8 +78,10 @@ const ]; type + // Forward declarations TsExpressionParser = class; TsBuiltInExpressionManager = class; + TsExprNode = class; TsResultType = (rtEmpty, rtBoolean, rtInteger, rtFloat, rtDateTime, rtString, rtCell, rtCellRange, rtHyperlink, rtError, rtMissingArg, rtAny); @@ -104,6 +106,10 @@ type PsExpressionResult = ^TsExpressionResult; TsExprParameterArray = array of TsExpressionResult; + { Function executed when iterating through all nodes (Parser.IterateNodes). + The function returns true if the text formula has to be rebuilt. } + TsExprNodeFunc = function(ANode: TsExprNode; AData: Pointer): Boolean; + { TsExprNode } TsExprNode = class(TObject) private @@ -116,6 +122,7 @@ type function AsString: string; virtual; abstract; procedure Check; virtual; //abstract; function Has3DLink: Boolean; virtual; + function IterateNodes(AFunc: TsExprNodeFunc; AData: Pointer): Boolean; virtual; function NodeType: TsResultType; virtual; abstract; function NodeValue: TsExpressionResult; property Parser: TsExpressionParser read FParser; @@ -134,6 +141,7 @@ type constructor Create(AParser: TsExpressionParser; ALeft, ARight: TsExprNode); destructor Destroy; override; function Has3DLink: Boolean; override; + function IterateNodes(AFunc: TsExprNodeFunc; AData: Pointer): boolean; override; property Left: TsExprNode read FLeft; property Right: TsExprNode read FRight; end; @@ -555,6 +563,7 @@ type function AsString: String; override; procedure Check; override; function Has3DLink: Boolean; override; + function IterateNodes(AFunc: TsExprNodeFunc; AData: Pointer): Boolean; override; property ArgumentNodes: TsExprArgumentArray read FArgumentNodes; property ArgumentParams: TsExprParameterArray read FArgumentParams; end; @@ -592,12 +601,11 @@ type FCell: PCell; FSheetName: String; FIsRef: Boolean; + FError: TsErrorValue; protected function GetCol: Cardinal; function GetRow: Cardinal; function GetSheet: TsBasicWorksheet; - function GetSheetIndex: Integer; - function GetSheetName: String; function GetWorkbook: TsBasicWorkbook; procedure GetNodeValue(out AResult: TsExpressionResult); override; public @@ -606,8 +614,14 @@ type function AsRPNItem(ANext: PRPNItem): PRPNItem; override; function AsString: string; override; procedure Check; override; + function GetSheetIndex: Integer; + function GetSheetName: String; function Has3DLink: Boolean; override; + function IterateNodes(AFunc: TsExprNodeFunc; AData: Pointer): Boolean; override; function NodeType: TsResultType; override; + property Col: Cardinal read FCol write FCol; // Be careful when modifying Col and Row + property Row: Cardinal read FRow write FRow; + property Error: TsErrorValue read FError write FError; property Worksheet: TsBasicWorksheet read FWorksheet; end; @@ -622,10 +636,13 @@ type FSheetIndex: array[TsCellRangeIndex] of Integer; FFlags: TsRelFlags; F3dRange: Boolean; + FError: TsErrorValue; + function GetRange: TsCellRange; + procedure SetRange(const ARange: TsCellRange); protected function GetCol(AIndex: TsCellRangeIndex): Cardinal; function GetRow(AIndex: TsCellRangeIndex): Cardinal; - procedure GetNodeValue(out Result: TsExpressionResult); override; + procedure GetNodeValue(out AResult: TsExpressionResult); override; function GetWorkbook: TsBasicWorkbook; public constructor Create(AParser: TsExpressionParser; AWorksheet: TsBasicWorksheet; @@ -633,8 +650,12 @@ type function AsRPNItem(ANext: PRPNItem): PRPNItem; override; function AsString: String; override; procedure Check; override; + function GetSheetIndex(AIndex: TsCellRangeIndex): Integer; function Has3DLink: Boolean; override; + function IterateNodes(AFunc: TsExprNodeFunc; AData: Pointer): Boolean; override; function NodeType: TsResultType; override; + property Error: TsErrorValue read FError write FError; + property Range: TsCellRange read GetRange write SetRange; // Be careful! property Workbook: TsBasicWorkbook read GetWorkbook; property Worksheet: TsBasicWorksheet read FWorksheet; end; @@ -744,8 +765,8 @@ type function TokenType: TsTokenType; procedure CreateHashList; property Scanner: TsExpressionScanner read FScanner; - property ExprNode: TsExprNode read FExprNode; property Dirty: Boolean read FDirty; + property ExprNode: TsExprNode read FExprNode; public constructor Create(AWorksheet: TsBasicWorksheet); virtual; @@ -756,6 +777,7 @@ type function Evaluate: TsExpressionResult; procedure EvaluateExpression(out AResult: TsExpressionResult); function Has3DLinks: Boolean; + function IterateNodes(AFunc: TsExprNodeFunc; AData: Pointer): boolean; procedure PrepareCopyMode(ASourceCell, ADestCell: PCell); function ResultType: TsResultType; @@ -820,6 +842,16 @@ type property Identifiers[AIndex: Integer]: TsBuiltInExprIdentifierDef read GetI; end; + { TsFormula } + TsFormula = record + Row, Col: Cardinal; + Text: String; + Parser: TsExpressionParser; + CalcState: TsCalcState; + end; + PsFormula = ^TsFormula; + + { Exception classes } EExprParser = class(Exception); ECalcEngine = class(Exception); @@ -1494,8 +1526,10 @@ end; procedure TsExpressionParser.EvaluateExpression(out AResult: TsExpressionResult); begin + { // Not needed. May be missing after copying formulas if (FExpression = '') then ParserError(rsExpressionEmpty); + } if not Assigned(FExprNode) then ParserError(rsErrorInExpression); FExprNode.GetNodeValue(AResult); @@ -1941,6 +1975,12 @@ begin Result := FExprNode.Has3DLink; end; +function TsExpressionParser.IterateNodes(AFunc: TsExprNodeFunc; + AData: Pointer): Boolean; +begin + Result := FExprNode.IterateNodes(AFunc, AData); +end; + procedure TsExpressionParser.SetDialect(const AValue: TsFormulaDialect); begin if FDialect = AValue then exit; @@ -2723,6 +2763,12 @@ begin Result := false; end; +function TsExprNode.IterateNodes(AFunc: TsExprNodeFunc; AData: Pointer): Boolean; +begin + Unused(AFunc, AData); + // to be overridden by descendant classes +end; + function TsExprNode.NodeValue: TsExpressionResult; begin GetNodeValue(Result); @@ -2773,6 +2819,12 @@ begin Result := FLeft.Has3DLink or FRight.Has3DLink; end; +function TsBinaryOperationExprNode.IterateNodes(AFunc: TsExprNodeFunc; + AData: Pointer): Boolean; +begin + Result := FLeft.IterateNodes(AFunc, AData) or FRight.IterateNodes(AFunc, AData); +end; + function TsBinaryOperationExprNode.HasError(out AResult: TsExpressionResult): Boolean; begin Result := Left.HasError(AResult) or Right.HasError(AResult); @@ -3724,6 +3776,16 @@ begin Result := false; end; +function TsFunctionExprNode.IterateNodes(AFunc: TsExprNodeFunc; + AData: Pointer): Boolean; +var + i: Integer; +begin + Result := false; + for i:=0 to High(FArgumentParams) do + Result := Result or FArgumentNodes[i].IterateNodes(AFunc, AData); +end; + { TsFunctionCallBackExprNode } @@ -3773,12 +3835,16 @@ begin FRow := ARow; FCol := ACol; FFlags := AFlags; + FError := errOK; FCell := (GetSheet as TsWorksheet).FindCell(FRow, FCol); if Has3DLink then FParser.FContains3DRef := true; end; function TsCellExprNode.AsRPNItem(ANext: PRPNItem): PRPNItem; begin + if FError <> errOK then + Result := RPNErr(FError, ANext) + else if FIsRef then begin if Has3dLink then @@ -3798,6 +3864,11 @@ function TsCellExprNode.AsString: string; var r, c: Cardinal; begin + if FError <> errOK then begin + Result := GetErrorValueStr(FError); + exit; + end; + r := Getrow; c := GetCol; if Has3dLink then @@ -3843,6 +3914,46 @@ begin Result := FCol - FParser.FSourceCell^.Col + FParser.FDestCell^.Col; end; +procedure TsCellExprNode.GetNodeValue(out AResult: TsExpressionResult); +var + cell: PCell; + formula: PsFormula; + sheet: TsWorksheet; +begin + if FError <> errOK then begin + AResult.ResultType := rtError; + AResult.ResError := FError; + { + AResult.ResRow := GetRow; + AResult.ResCol := GetCol; + AResult.Worksheet := GetSheet; + } + exit; + end; + + if Parser.CopyMode then + cell := (FWorksheet as TsWorksheet).FindCell(GetRow, GetCol) + else + cell := FCell; + + if (cell <> nil) and HasFormula(cell) then begin + sheet := TsWorksheet(cell^.Worksheet); + formula := sheet.Formulas.FindFormula(cell^.Row, cell^.Col); + case formula^.CalcState of + csNotCalculated: + sheet.CalcFormula(formula); + csCalculating: + raise ECalcEngine.CreateFmt(rsCircularReference, [GetCellString(cell^.Row, cell^.Col)]); + end; + end; + + AResult.ResultType := rtCell; + AResult.ResRow := GetRow; + AResult.ResCol := GetCol; + AResult.Worksheet := GetSheet; +end; + +(* procedure TsCellExprNode.GetNodeValue(out AResult: TsExpressionResult); var cell: PCell; @@ -3865,6 +3976,7 @@ begin AResult.ResCol := GetCol; AResult.Worksheet := GetSheet; end; +*) { See: GetCol } function TsCellExprNode.GetRow: Cardinal; @@ -3878,8 +3990,10 @@ function TsCellExprNode.GetSheet: TsBasicWorksheet; begin if FSheetName = '' then Result := FWorksheet - else + else begin Result := (GetWorkbook as TsWorkbook).GetWorksheetByName(FSheetName); + if Result = nil then FError := errIllegalREF; + end; end; function TsCellExprNode.GetSheetIndex: Integer; @@ -3916,6 +4030,12 @@ begin Result := rtCell; end; +function TsCellExprNode.IterateNodes(AFunc: TsExprNodeFunc; + AData: Pointer): Boolean; +begin + Result := AFunc(self, AData); +end; + { TsCellRangeExprNode } @@ -3932,15 +4052,25 @@ begin FParser := AParser; FWorksheet := AWorksheet; FFlags := []; + FError := errOK; book := TsWorkbook(GetWorkbook); F3dRange := ((ASheet1 <> '') and (ASheet2 <> '') { and (ASheet1 <> ASheet2)}) or ((ASheet1 <> '') and (ASheet2 = '')); FSheetIndex[1] := book.GetWorksheetIndex(ASheet1); - if ASheet2 <> '' then - FSheetIndex[2] := book.GetWorksheetIndex(ASheet2) + { + if FSheetIndex[1] = -1 then + FError := errIllegalREF else + } + if ASheet2 <> '' then begin + FSheetIndex[2] := book.GetWorksheetIndex(ASheet2); + { + if FSheetIndex[2] = -1 then + FError := errIllegalREF; + } + end else FSheetIndex[2] := FSheetIndex[1]; EnsureOrder(FSheetIndex[1], FSheetIndex[2]); @@ -3982,6 +4112,9 @@ end; function TsCellRangeExprNode.AsRPNItem(ANext: PRPNItem): PRPNItem; begin + if FError <> errOK then + Result := RPNErr(FError, ANext) + else if F3dRange then Result := RPNCellRange3D( FSheetIndex[1], GetRow(1), Integer(GetCol(1)), @@ -4001,6 +4134,11 @@ var r1, c1, r2, c2: Cardinal; s1, s2: String; begin + if FError <> errOK then begin + Result := GetErrorValueStr(FError); + exit; + end; + if FSheetIndex[1] = -1 then s1 := FWorksheet.Name else @@ -4058,7 +4196,7 @@ begin Result := FCol[AIndex] - FParser.FSourceCell^.Col + FParser.FDestCell^.Col; end; -procedure TsCellRangeExprNode.GetNodeValue(out Result: TsExpressionResult); +procedure TsCellRangeExprNode.GetNodeValue(out AResult: TsExpressionResult); var r, c, s: Array[TsCellRangeIndex] of Integer; rr, cc, ss: Integer; @@ -4066,7 +4204,14 @@ var cell: PCell; book: TsWorkbook; sheet: TsWorksheet; + formula: PsFormula; begin + if FError <> errOK then begin + AResult.ResultType := rtError; + AResult.ResError := FError; + exit; + end; + for i in TsCellRangeIndex do begin r[i] := GetRow(i); @@ -4081,29 +4226,35 @@ begin for ss := s[1] to s[2] do begin sheet := (Workbook as TsWorkbook).GetWorksheetByIndex(ss); - for rr := r[1] to r[2] do - for cc := c[1] to c[2] do - begin - cell := sheet.FindCell(rr, cc); - if HasFormula(cell) then - case sheet.GetCalcState(cell) of - csNotCalculated: - sheet.CalcFormula(cell); - csCalculating: - raise ECalcEngine.Create(rsCircularReference); - end; - end; + for formula in sheet.Formulas do + if (formula^.Row >= r[1]) and (formula^.Row <= r[2]) and + (formula^.Col >= c[1]) and (formula^.Col <= c[2]) + then + case formula^.CalcState of + csNotCalculated: + sheet.CalcFormula(formula); + csCalculating: + raise ECalcEngine.Create(rsCircularReference); + end; end; - Result.ResultType := rtCellRange; - Result.ResCellRange.Row1 := r[1]; - Result.ResCellRange.Col1 := c[1]; - Result.ResCellRange.Row2 := r[2]; - Result.ResCellRange.Col2 := c[2]; - Result.ResCellRange.Sheet1 := s[1]; - Result.ResCellRange.Sheet2 := s[2]; - Result.Worksheet := FWorksheet; -// Result.Worksheet2 := FWorksheet2; + AResult.ResultType := rtCellRange; + AResult.ResCellRange.Row1 := r[1]; + AResult.ResCellRange.Col1 := c[1]; + AResult.ResCellRange.Row2 := r[2]; + AResult.ResCellRange.Col2 := c[2]; + AResult.ResCellRange.Sheet1 := s[1]; + AResult.ResCellRange.Sheet2 := s[2]; + AResult.Worksheet := FWorksheet; +end; + +// Be careful when modifying GetRange - it may break everything +function TsCellRangeExprNode.GetRange: TsCellRange; +begin + Result.Row1 := FRow[1]; + Result.Col1 := FCol[1]; + Result.Row2 := FRow[2]; + Result.Col2 := FCol[2]; end; function TsCellRangeExprNode.GetRow(AIndex: TsCellRangeIndex): Cardinal; @@ -4115,7 +4266,15 @@ end; function TsCellRangeExprNode.GetWorkbook: TsBasicWorkbook; begin - Result := (FWorksheet as TsWorksheet).Workbook; + if FWorksheet = nil then + Result := nil + else + Result := (FWorksheet as TsWorksheet).Workbook; +end; + +function TsCellRangeExprNode.GetSheetIndex(AIndex: TsCellRangeIndex): Integer; +begin + Result := FSheetIndex[AIndex]; end; function TsCellRangeExprNode.Has3DLink: Boolean; @@ -4123,11 +4282,25 @@ begin Result := F3dRange; end; +function TsCellRangeExprNode.IterateNodes(AFunc: TsExprNodeFunc; + AData: Pointer): Boolean; +begin + Result := AFunc(self, AData); +end; + function TsCellRangeExprNode.NodeType: TsResultType; begin Result := rtCellRange; end; +procedure TsCellRangeExprNode.SetRange(const ARange: TsCellRange); +begin + FRow[1] := ARange.Row1; + FCol[1] := ARange.Col1; + FRow[2] := ARange.Row2; + FCol[2] := ARange.Col2; +end; + {------------------------------------------------------------------------------} { Conversion of arguments to simple data types } diff --git a/components/fpspreadsheet/source/common/fpsopendocument.pas b/components/fpspreadsheet/source/common/fpsopendocument.pas index 3452a41de..5d175eab8 100644 --- a/components/fpspreadsheet/source/common/fpsopendocument.pas +++ b/components/fpspreadsheet/source/common/fpsopendocument.pas @@ -112,7 +112,7 @@ type function FindNumFormatByName(ANumFmtName: String): Integer; function FindRowStyleByName(AStyleName: String): Integer; function FindTableStyleByName(AStyleName: String): Integer; - procedure FixFormulas; +// procedure FixFormulas; procedure ReadCell(ANode: TDOMNode; ARow, ACol: Integer; AFormatIndex: Integer; out AColsRepeated: Integer); procedure ReadColumns(ATableNode: TDOMNode); @@ -134,6 +134,7 @@ type procedure ReadRowStyle(AStyleNode: TDOMNode); procedure ReadShapes(ATableNode: TDOMNode); procedure ReadSheetProtection(ANode: TDOMNode; ASheet: TsBasicWorksheet); + procedure ReadSheets(ANode: TDOMNode); procedure ReadTableStyle(AStyleNode: TDOMNode); protected @@ -1435,7 +1436,7 @@ begin exit; Result := -1; end; - + (* procedure TsSpreadOpenDocReader.FixFormulas; procedure FixCell(ACell: PCell); @@ -1480,7 +1481,7 @@ begin for cell in sheet.Cells do if HasFormula(cell) then FixCell(cell); end; -end; +end; *) procedure TsSpreadOpenDocReader.ReadAutomaticStyles(AStylesNode: TDOMNode); var @@ -2387,7 +2388,8 @@ procedure TsSpreadOpenDocReader.ReadFormula(ARow, ACol: Cardinal; AStyleIndex: Integer; ACellNode: TDOMNode); var cell: PCell; - formula: String; + formula: PsFormula; + formulaStr: String; // stylename: String; floatValue: Double; boolValue: Boolean; @@ -2418,31 +2420,38 @@ begin } fmt := TsWorkbook(Workbook).GetPointerToCellFormat(cell^.FormatIndex); - formula := ''; + formulaStr := ''; if (boReadFormulas in FWorkbook.Options) then begin // Read formula, trim it, ... - formula := GetAttrValue(ACellNode, 'table:formula'); - if formula <> '' then + formulaStr := GetAttrValue(ACellNode, 'table:formula'); + if formulaStr <> '' then begin // Formulas written by Spread begin with 'of:=', by Excel with 'msof:='. // Remove that. And both use different list separators. - p := pos('=', formula); - ns := Copy(formula, 1, p-2); + p := pos('=', formulaStr); + ns := Copy(formulaStr, 1, p-2); case ns of 'of' : FPointSeparatorSettings.ListSeparator := ';'; 'msoxl': FPointSeparatorSettings.ListSeparator := ','; end; - Delete(formula, 1, p); + Delete(formulaStr, 1, p); end; // ... and store in cell's FormulaValue field. + formula := TsWorksheet(FWorksheet).Formulas.AddFormula(ARow, ACol); + formula^.Parser := TsSpreadsheetParser.Create(FWorksheet); + formula^.Parser.Dialect := fdOpenDocument; // Parse in ODS dialect + formula^.Parser.Expression := formulaStr; + formula^.Parser.Dialect := fdExcelA1; // Convert formula to Excel A1 dialect + formula^.Text := formula^.Parser.Expression; + { cell^.FormulaValue := formula; // Note: This formula is still in OpenDocument dialect. Conversion to // ExcelA1 dialect (used by fps) is postponed until all sheets have beeon // read (--> FixFormulas) because of possible references to other sheets // which might not have been loaded yet at this moment. - + } {$IFDEF FPSpreadDebug} DebugLn(' Formula found: ' + formula); {$ENDIF} @@ -2587,6 +2596,7 @@ begin if not Assigned(SpreadSheetNode) then raise EFPSpreadsheet.Create('[TsSpreadOpenDocReader.ReadFromStream] Node "office:spreadsheet" not found.'); + ReadSheets(SpreadsheetNode); ReadDocumentProtection(SpreadsheetNode); ReadDateMode(SpreadSheetNode); @@ -2613,7 +2623,8 @@ begin end; sheetName := GetAttrValue(TableNode, 'table:name'); - FWorkSheet := TsWorkbook(FWorkbook).AddWorksheet(sheetName, true); + FWorksheet := TsWorkbook(FWorkbook).GetWorksheetByName(sheetName); +// FWorkSheet := TsWorkbook(FWorkbook).AddWorksheet(sheetName, true); tablestyleName := GetAttrValue(TableNode, 'table:style-name'); // Read protection ReadSheetProtection(TableNode, FWorksheet); @@ -2660,7 +2671,7 @@ begin end; // Convert formulas from OpenDocument to ExcelA1 dialect - FixFormulas; +// FixFormulas; // Active sheet if FActiveSheet <> '' then @@ -4085,6 +4096,24 @@ begin (ASheet as TsWorksheet).Protect(false); end; +procedure TsSpreadOpenDocReader.ReadSheets(ANode: TDOMNode); +var + nodename: String; + sheetName: String; +begin + ANode := ANode.FirstChild; + while ANode <> nil do begin + nodeName := ANode.NodeName; + if nodeName = 'table:table' then begin + sheetName := GetAttrValue(ANode, 'table:name'); + if sheetName <> '' then + // Create worksheet immediately because it may be needed for 3d formulas + (FWorkbook as TsWorkbook).AddWorksheet(sheetname, true); + end; + ANode := ANode.NextSibling; + end; +end; + procedure TsSpreadOpenDocReader.ReadStyles(AStylesNode: TDOMNode); var styleNode: TDOMNode; @@ -7564,8 +7593,9 @@ procedure TsSpreadOpenDocWriter.WriteFormula(AStream: TStream; const ARow, ACol: Cardinal; ACell: PCell); var lStyle: String = ''; + formula: PsFormula; + formulaStr: String; parser: TsExpressionParser; - formula: String; valuetype: String; value: string; valueStr: String; @@ -7577,6 +7607,7 @@ var fmt: TsCellFormat; ignoreFormulas: Boolean; sheet: TsWorksheet; + oldDialect: TsFormulaDialect; begin Unused(ARow, ACol); ignoreFormulas := (boIgnoreFormulas in FWorkbook.Options); @@ -7608,16 +7639,32 @@ begin FWorkbook.AddErrorMsg(rsODSHyperlinksOfTextCellsOnly, [GetCellString(ARow, ACol)]); // Formula string + formula := sheet.Formulas.FindFormula(ACell); + if ignoreFormulas then begin - formula := ACell^.FormulaValue; - if (formula <> '') then begin - if not ((pos('of:=', formula) = 1) or (pos('=', formula) = 1)) then - formula := 'of:=' + formula; + formulaStr := formula^.Text; + if (formulaStr <> '') then begin + if not ((pos('of:=', formulaStr) = 1) or (pos('=', formulaStr) = 1)) then + formulaStr := 'of:=' + formulaStr; end; end else begin valueStr := ''; - // Convert string formula to the format needed by ods: semicolon list separators! + if formula^.Parser = nil then begin + formula^.Parser := TsSpreadsheetParser.Create(FWorksheet); + formula^.Parser.Expression := formula^.Text; + end; + // Convert string formula to the format needed by ods + oldDialect := formula^.Parser.Dialect; + try + formula^.Parser.Dialect := fdOpenDocument; + formulaStr := formula^.Parser.Expression; // Formula converted to ODS dialect + if (formulaStr <> '') and (formulastr[1] <> '=') then + formulaStr := '=' + formulaStr; + finally + formula^.Parser.Dialect := oldDialect; + end; + { parser := TsSpreadsheetParser.Create(FWorksheet); try parser.Expression := ACell^.FormulaValue; // Formula still in Excel dialect @@ -7628,7 +7675,7 @@ begin finally parser.Free; end; - + } case ACell^.ContentType of cctNumber: begin @@ -7675,23 +7722,24 @@ begin end; { Fix special xml characters } - formula := UTF8TextToXMLText(formula); + formulaStr := UTF8TextToXMLText(formulaStr); { We are writing a very rudimentary formula here without result and result data type. Seems to work... } - if not ignoreFormulas or (sheet.GetCalcState(ACell) = csCalculated) then +// if not ignoreFormulas or (sheet.GetCalcState(ACell) = csCalculated) then + if not ignoreFormulas or (formula^.CalcState = csCalculated) then // LOOKS STRANGE - IS THIS CORRECT? AppendToStream(AStream, Format( '' + comment + valueStr + '', [ - formula, valuetype, value, lStyle, spannedStr + formulaStr, valuetype, value, lStyle, spannedStr ])) else begin AppendToStream(AStream, Format( ' '' then AppendToStream(AStream, '>' + comment + '') else diff --git a/components/fpspreadsheet/source/common/fpspreadsheet.pas b/components/fpspreadsheet/source/common/fpspreadsheet.pas index 2073be866..42afbb075 100644 --- a/components/fpspreadsheet/source/common/fpspreadsheet.pas +++ b/components/fpspreadsheet/source/common/fpspreadsheet.pas @@ -23,7 +23,7 @@ uses clocale, {$endif}{$endif}{$endif} Classes, SysUtils, fpimage, avglvltree, lconvencoding, - fpsTypes, fpsClasses, fpsNumFormat, fpsPageLayout, fpsImages; + fpsTypes, fpsExprParser, fpsClasses, fpsNumFormat, fpsPageLayout, fpsImages; type { Forward declarations } @@ -67,6 +67,7 @@ type FComments: TsComments; FMergedCells: TsMergedCells; FHyperlinks: TsHyperlinks; + FFormulas: TsFormulas; FImages: TFPList; FRows, FCols: TIndexedAVLTree; // This lists contain only rows or cols with styles different from default FActiveCellRow: Cardinal; @@ -109,16 +110,12 @@ type procedure SetVirtualRowCount(AValue: Cardinal); procedure SetZoomFactor(AValue: Double); - { Callback procedures called when iterating through all cells } - procedure DeleteColCallback(data, arg: Pointer); - procedure DeleteRowCallback(data, arg: Pointer); - procedure InsertColCallback(data, arg: Pointer); - procedure InsertRowCallback(data, arg: Pointer); - protected function CellUsedInFormula(ARow, ACol: Cardinal): Boolean; // Remove and delete cells + procedure DeleteRowOrCol(AIndex: Integer; IsRow: Boolean); + procedure InsertRowOrCol(AIndex: Integer; IsRow: Boolean); function RemoveCell(ARow, ACol: Cardinal): PCell; procedure RemoveAndFreeCell(ARow, ACol: Cardinal); @@ -190,8 +187,8 @@ type function IsEmpty: Boolean; { Writing of values } - function WriteBlank(ARow, ACol: Cardinal): PCell; overload; - procedure WriteBlank(ACell: PCell); overload; + function WriteBlank(ARow, ACol: Cardinal; KeepFormula: Boolean = false): PCell; overload; + procedure WriteBlank(ACell: PCell; KeepFormula: Boolean = false); overload; function WriteBoolValue(ARow, ACol: Cardinal; AValue: Boolean): PCell; overload; procedure WriteBoolValue(ACell: PCell; AValue: Boolean); overload; @@ -251,7 +248,7 @@ type function WriteRPNFormula(ARow, ACol: Cardinal; AFormula: TsRPNFormula): PCell; overload; procedure WriteRPNFormula(ACell: PCell; - AFormula: TsRPNFormula); overload; + ARPNFormula: TsRPNFormula); overload; function WriteText(ARow, ACol: Cardinal; AText: String; ARichTextParams: TsRichTextParams = nil): PCell; overload; @@ -378,13 +375,11 @@ type { Formulas } function BuildRPNFormula(ACell: PCell; ADestCell: PCell = nil): TsRPNFormula; - procedure CalcFormula(ACell: PCell); + procedure CalcFormula(AFormula: PsFormula); procedure CalcFormulas; procedure CalcSheet; function ConvertFormulaDialect(ACell: PCell; ADialect: TsFormulaDialect): String; function ConvertRPNFormulaToStringFormula(const AFormula: TsRPNFormula): String; - function GetCalcState(ACell: PCell): TsCalcState; - procedure SetCalcState(ACell: PCell; AValue: TsCalcState); { Data manipulation methods - For Cells } procedure CopyCell(AFromCell, AToCell: PCell); overload; @@ -454,6 +449,7 @@ type procedure InsertCol(ACol: Cardinal); procedure InsertRow(ARow: Cardinal); procedure MoveCol(AFromCol, AToCol: Cardinal); + procedure MoveRow(AFromRow, AToRow: Cardinal); function ReadDefaultColWidth(AUnits: TsSizeUnits): Single; function ReadDefaultRowHeight(AUnits: TsSizeUnits): Single; function ReadColFont(ACol: PCol): TsFont; @@ -524,6 +520,12 @@ type procedure UnmergeCells(ARow, ACol: Cardinal); overload; procedure UnmergeCells(ARange: String); overload; + { Formulas } + procedure DeleteFormula(ACell: PCell); + function ReadFormula(ARow, ACol: Cardinal): String; overload; + function ReadFormula(ACell: PCell): String; overload; + procedure UseFormulaInCell(ACell: PCell; AFormula: PsFormula); + { Embedded images } procedure CalcImageCell(AIndex: Integer; x, y, AWidth, AHeight: Double; out ARow, ACol: Cardinal; out ARowOffs, AColOffs, AScaleX, AScaleY: Double); @@ -570,6 +572,8 @@ type property MergedCells: TsMergedCells read FMergedCells; {@@ List of hyperlink information records } property Hyperlinks: TsHyperlinks read FHyperlinks; + {@@ List of all formulas used in the sheet } + property Formulas: TsFormulas read FFormulas; {@@ FormatSettings for localization of some formatting strings } property FormatSettings: TFormatSettings read GetFormatSettings; {@@ Parameters to be used for printing by the Office applications } @@ -849,7 +853,7 @@ implementation uses Math, StrUtils, DateUtils, TypInfo, lazutf8, lazFileUtils, URIParser, uvirtuallayer_ole, {%H-}fpsPatches, fpsStrings, fpsUtils, fpsHTMLUtils, - fpsReaderWriter, fpsCurrency, fpsExprParser; + fpsReaderWriter, fpsCurrency; (* const @@ -1089,9 +1093,9 @@ begin end; -{------------------------------------------------------------------------------} +{==============================================================================} { TsWorksheet } -{------------------------------------------------------------------------------} +{==============================================================================} {@@ ---------------------------------------------------------------------------- Constructor of the TsWorksheet class. @@ -1106,6 +1110,7 @@ begin FComments := TsComments.Create; FMergedCells := TsMergedCells.Create; FHyperlinks := TsHyperlinks.Create; + FFormulas := TsFormulas.Create; FImages := TFPList.Create; FPageLayout := TsPageLayout.Create(self); @@ -1146,6 +1151,7 @@ begin FComments.Free; FMergedCells.Free; FHyperlinks.Free; + FFormulas.Free; FImages.Free; inherited Destroy; @@ -1164,105 +1170,120 @@ end; function TsWorksheet.BuildRPNFormula(ACell: PCell; ADestCell: PCell = nil): TsRPNFormula; var - parser: TsSpreadsheetParser; + formula: PsFormula; begin - if not HasFormula(ACell) then begin + if (ACell = nil) or (not HasFormula(ACell)) then begin SetLength(Result, 0); exit; end; - parser := TsSpreadsheetParser.Create(self); - try - if ADestCell <> nil then - parser.PrepareCopyMode(ACell, ADestCell); - parser.Expression := ACell^.FormulaValue; - Result := parser.RPNFormula; - finally - parser.Free; + formula := FFormulas.FindFormula(ACell^.Row, ACell^.Col); + if formula = nil then begin + SetLength(Result, 0); + exit; end; + + if ADestCell <> nil then begin + formula^.Parser.PrepareCopyMode(ACell, ADestCell); + Result := formula^.Parser.RPNFormula; + formula^.Parser.PrepareCopyMode(nil, nil); + end else + Result := formula^.Parser.RPNFormula; end; {@@ ---------------------------------------------------------------------------- - Calculates the formula in a cell - Should not be called by itself because the result may depend on other cells + Calculates the provided formula + + Should not be called by itself because the result may depend on other formulas which may have not yet been calculated. It is better to call CalcFormulas instead. - @param ACell Cell containing the formula. + @param AFormula Formula to be calculated. The formula belongs to the + cell specified by the formula's Row and Col parameters. -------------------------------------------------------------------------------} -procedure TsWorksheet.CalcFormula(ACell: PCell); +procedure TsWorksheet.CalcFormula(AFormula: PsFormula); var - parser: TsSpreadsheetParser; + lCell, lCellRef: PCell; + parser: TsExpressionParser = nil; + has3DLink: Boolean; res: TsExpressionResult; p: Integer; link, txt: String; - cell: PCell; - formula: String; - has3DLink: Boolean; begin - if (boIgnoreFormulas in Workbook.Options) then + if (boIgnoreFormulas in Workbook.Options) or (AFormula = nil) then exit; - formula := ACell^.FormulaValue; - ACell^.Flags := ACell^.Flags + [cfCalculating] - [cfCalculated]; + if (AFormula^.Text = '') and (AFormula^.Parser = nil) then + raise ECalcEngine.Create('CalcFormula: no formula specified.'); - parser := TsSpreadsheetParser.Create(self); - try + AFormula^.CalcState := csCalculating; + if AFormula^.Parser = nil then begin + parser := TsSpreadsheetParser.Create(self); try - parser.Expression := ACell^.FormulaValue; + parser.Expression := AFormula^.Text; has3DLink := parser.Contains3DRef; - res := parser.Evaluate; + AFormula^.Parser := parser; except - on E:ECalcEngine do + on E:ECalcEngine do begin + Workbook.AddErrorMsg(E.Message); + res := ErrorResult(errIllegalRef); + end; + end; + end; + + if AFormula^.Parser <> nil then + try + res := AFormula^.Parser.Evaluate; + if AFormula^.Text = '' then + AFormula^.Text := AFormula^.Parser.Expression; + except + on E: ECalcEngine do begin Workbook.AddErrorMsg(E.Message); - Res := ErrorResult(errIllegalRef); + res := ErrorResult(errIllegalRef); end; end; - case res.ResultType of - rtEmpty : WriteBlank(ACell); - rtError : WriteErrorValue(ACell, res.ResError); - rtInteger : WriteNumber(ACell, res.ResInteger); - rtFloat : WriteNumber(ACell, res.ResFloat); - rtDateTime : WriteDateTime(ACell, res.ResDateTime); - rtString : WriteText(ACell, res.ResString); - rtHyperlink : begin - link := ArgToString(res); - p := pos(HYPERLINK_SEPARATOR, link); - if p > 0 then - begin - txt := Copy(link, p+Length(HYPERLINK_SEPARATOR), Length(link)); - link := Copy(link, 1, p-1); - end else - txt := link; - WriteHyperlink(ACell, link); - WriteText(ACell, txt); - end; - rtBoolean : WriteBoolValue(ACell, res.ResBoolean); - rtCell : begin - cell := (res.Worksheet as TsWorksheet).FindCell(res.ResRow, res.ResCol); - if cell <> nil then - case cell^.ContentType of - cctNumber : WriteNumber(ACell, cell^.NumberValue); - cctDateTime : WriteDateTime(ACell, cell^.DateTimeValue); - cctUTF8String: WriteText(ACell, cell^.UTF8StringValue); - cctBool : WriteBoolValue(ACell, cell^.Boolvalue); - cctError : WriteErrorValue(ACell, cell^.ErrorValue); - cctEmpty : WriteBlank(ACell); - end - else - WriteBlank(ACell); - end; - end; - if has3DLink then Include(ACell^.Flags, cf3DFormula) - else Exclude(ACell^.Flags, cf3DFormula); - finally - parser.Free; + // Find or create the formula cell + lCell := GetCell(AFormula^.Row, AFormula^.Col); + // Assign formula result + case res.ResultType of + rtEmpty : WriteBlank(lCell, true); + rtError : WriteErrorValue(lCell, res.ResError); + rtInteger : WriteNumber(lCell, res.ResInteger); + rtFloat : WriteNumber(lCell, res.ResFloat); + rtDateTime : WriteDateTime(lCell, res.ResDateTime); + rtString : WriteText(lCell, res.ResString); + rtHyperlink : begin + link := ArgToString(res); + p := pos(HYPERLINK_SEPARATOR, link); + if p > 0 then + begin + txt := Copy(link, p+Length(HYPERLINK_SEPARATOR), Length(link)); + link := Copy(link, 1, p-1); + end else + txt := link; + WriteHyperlink(lCell, link); + WriteText(lCell, txt); + end; + rtBoolean : WriteBoolValue(lCell, res.ResBoolean); + rtCell : begin + lCellRef := (res.Worksheet as TsWorksheet).FindCell(res.ResRow, res.ResCol); + if lCellRef <> nil then + case lCellRef^.ContentType of + cctNumber : WriteNumber(lCell, lCellRef^.NumberValue); + cctDateTime : WriteDateTime(lCell, lCellRef^.DateTimeValue); + cctUTF8String: WriteText(lCell, lCellRef^.UTF8StringValue); + cctBool : WriteBoolValue(lCell, lCellRef^.Boolvalue); + cctError : WriteErrorValue(lCell, lCellRef^.ErrorValue); + cctEmpty : WriteBlank(lCell, true); + end + else + WriteBlank(lCell, true); + end; end; // Restore the formula. Could have been erased by WriteBlank or WriteText('') - ACell^.FormulaValue := formula; - ACell^.Flags := ACell^.Flags + [cfCalculated] - [cfCalculating]; + AFormula^.CalcState := csCalculated; end; {@@ ---------------------------------------------------------------------------- @@ -1280,42 +1301,39 @@ begin end; {@@ ---------------------------------------------------------------------------- - Calculates all formulas of the worksheet. + Calculates all formulas of the worksheet Since formulas may reference not-yet-calculated cells, this occurs in two steps: - 1. All formula cells are marked as "not calculated". - 2. Cells are calculated. If referenced cells are found as being + 1. All formulas are marked as "not calculated". + 2. Formulas are calculated. If formulas in referenced are found as being "not calculated" they are calculated and then tagged as "calculated". - This results in an iterative calculation procedure. In the end, all cells + This results in an iterative calculation procedure. In the end, all formulas are calculated. NOTE: IF THE WORKSHEET CONTAINS CELLS WHICH LINK TO OTHER WORKSHEETS THEN - THIS CALCULATION MAY NOT BE CORRECT. USE THE SAME METHOD OF THE WORKBOOK - INSTEAD !!! + THIS CALCULATION MAY NOT BE CORRECT. USE THE METHOD CalcFormulas OF THE + WORKBOOK INSTEAD !!! -------------------------------------------------------------------------------} procedure TsWorksheet.CalcSheet; var - cell: PCell; - i: Integer; + formula: PsFormula; begin if (boIgnoreFormulas in Workbook.Options) then exit; - // prevent infinite loop due to triggering of formula calculation whenever - // a cell changes during execution of CalcFormulas. + { prevent infinite loop due to triggerung of formula recalculation whenever + a cell changes during execution of CalcFormulas } inc(FWorkbook.FCalculationLock); try - // Step 1 - mark all formula cells as "not calculated" - for cell in FCells do - if HasFormula(cell) then - SetCalcState(cell, csNotCalculated); + // State 1 - mark all formulas as "not calculated" + for formula in FFormulas do + formula^.CalcState := csNotCalculated; - // Step 2 - calculate cells. If a not-yet-calculated cell is found it is - // calculated and then marked as such. - for cell in FCells do - if HasFormula(cell) and (cell^.ContentType <> cctError) then - CalcFormula(cell); + // State 2 - calculate formulas. If a formula required during calculation + // is found as not-yet-calculated, then it is calculated immediately. + for formula in FFormulas do + CalcFormula(formula); finally dec(FWorkbook.FCalculationLock); end; @@ -1920,19 +1938,29 @@ end; -------------------------------------------------------------------------------} procedure TsWorksheet.CopyFormula(AFromCell, AToCell: PCell); var - rpnFormula: TsRPNFormula; + srcFormula, destFormula: PsFormula; + rpn: TsRPNFormula; begin if (AFromCell = nil) or (AToCell = nil) then exit; - if AFromCell^.FormulaValue = '' then - AToCell^.FormulaValue := '' - else - begin - // Here we convert the formula to an rpn formula as seen from source... - rpnFormula := BuildRPNFormula(AFromCell, AToCell); - // ... and here we reconstruct the string formula as seen from destination cell. - AToCell^.FormulaValue := ConvertRPNFormulaToStringFormula(rpnFormula); + DeleteFormula(AToCell); + + if not HasFormula(AFromCell) then + exit; + + srcFormula := FFormulas.FindFormula(AFromCell^.Row, AFromCell^.Col); + destFormula := FFormulas.AddFormula(AToCell^.Row, AToCell^.Col); + destFormula.Parser := TsSpreadsheetParser.Create(self); + + srcFormula^.Parser.PrepareCopyMode(AFromCell, AToCell); + try + rpn := srcFormula^.Parser.RPNFormula; + destFormula^.Parser.RPNFormula := rpn; + destFormula^.Text := destFormula^.Parser.Expression; + UseFormulaInCell(AToCell, destFormula); + finally + srcFormula^.Parser.PrepareCopyMode(nil, nil); end; ChangedCell(AToCell^.Row, AToCell^.Col); @@ -1961,10 +1989,22 @@ end; -------------------------------------------------------------------------------} procedure TsWorksheet.CopyValue(AFromCell, AToCell: PCell); begin - if (AFromCell = nil) or (AToCell = nil) then + if (AToCell = nil) then // AFromCell is allowed to be empty exit; - CopyCellValue(AFromCell, AToCell); + if AFromCell <> nil then begin + AToCell^.ContentType := AFromCell^.ContentType; + AToCell^.NumberValue := AFromCell^.NumberValue; + AToCell^.DateTimeValue := AFromCell^.DateTimeValue; + AToCell^.BoolValue := AFromCell^.BoolValue; + AToCell^.ErrorValue := AFromCell^.ErrorValue; + AToCell^.UTF8StringValue := AFromCell^.UTF8StringValue; + end else + AToCell^.ContentType := cctEmpty; + + // Note: As confirmed with Excel, the formula is not to be copied here. + // But that of the destination cell must be erased. + DeleteFormula(AToCell); ChangedCell(AToCell^.Row, AToCell^.Col); end; @@ -2088,6 +2128,17 @@ begin if HasComment(ACell) then WriteComment(ACell, ''); + // Does cell have a hyperlink? --> remove it + if HasHyperlink(ACell) then + WriteHyperlink(ACell, ''); + + // Does cell have a formula? --> remove it + if HasFormula(ACell) then + WriteFormula(ACell, ''); + + // To do: Check if the cell is referencec by a formula. In this case we have + // a #REF! error. + // Cell is part of a merged block? --> Erase content, formatting etc. if IsMerged(ACell) then begin @@ -2104,116 +2155,6 @@ begin ChangedCell(r, c); end; -{@@ ---------------------------------------------------------------------------- - Internal call-back procedure for looping through all cells when deleting - a specified column. Deletion happens in DeleteCol BEFORE this callback! --------------------------------------------------------------------------------} -procedure TsWorksheet.DeleteColCallback(data, arg: Pointer); -var - cell: PCell; - col: Cardinal; - formula: TsRPNFormula; - i: Integer; -begin - col := LongInt({%H-}PtrInt(arg)); - cell := PCell(data); - if cell = nil then // This should not happen. Just to make sure... - exit; - - // Update column index of moved cell - if (cell^.Col > col) then - dec(cell^.Col); - - // Update formulas - if HasFormula(cell) then - begin - // (1) create an rpn formula - formula := BuildRPNFormula(cell); - // (2) update cell addresses affected by the deletion of the column - for i:=0 to High(formula) do - begin - if (formula[i].ElementKind in [fekCell, fekCellRef, fekCellRange]) then - begin - if formula[i].Col = col then - begin - formula[i].ElementKind := fekErr; - formula[i].IntValue := ord(errIllegalRef); - end else - if formula[i].Col > col then - dec(formula[i].Col); - if (formula[i].ElementKind = fekCellRange) then - begin - if (formula[i].Col2 = col) then - begin - formula[i].ElementKind := fekErr; - formula[i].IntValue := ord(errIllegalRef); - end - else - if (formula[i].Col2 > col) then - dec(formula[i].Col2); - end; - end; - end; - // (3) convert rpn formula back to string formula - cell^.FormulaValue := ConvertRPNFormulaToStringFormula(formula); - end; -end; - -{@@ ---------------------------------------------------------------------------- - Internal call-back procedure for looping through all cells when deleting - a specified row. Deletion happens in DeleteRow BEFORE this callback! --------------------------------------------------------------------------------} -procedure TsWorksheet.DeleteRowCallback(data, arg: Pointer); -var - cell: PCell; - row: Cardinal; - formula: TsRPNFormula; - i: Integer; -begin - row := LongInt({%H-}PtrInt(arg)); - cell := PCell(data); - if cell = nil then // This should not happen. Just to make sure... - exit; - - // Update row index of moved cell - if (cell^.Row > row) then - dec(cell^.Row); - - // Update formulas - if HasFormula(cell) then - begin - // (1) create an rpn formula - formula := BuildRPNFormula(cell); - // (2) update cell addresses affected by the deletion of the column - for i:=0 to High(formula) do - begin - if (formula[i].ElementKind in [fekCell, fekCellRef, fekCellRange]) then - begin - if formula[i].Row = row then - begin - formula[i].ElementKind := fekErr; - formula[i].IntValue := ord(errIllegalRef); - end else - if formula[i].Row > row then - dec(formula[i].Row); - if (formula[i].ElementKind = fekCellRange) then - begin - if (formula[i].Row2 = row) then - begin - formula[i].ElementKind := fekErr; - formula[i].IntValue := ord(errIllegalRef); - end - else - if (formula[i].Row2 > row) then - dec(formula[i].Row2); - end; - end; - end; - // (3) convert rpn formula back to string formula - cell^.FormulaValue := ConvertRPNFormulaToStringFormula(formula); - end; -end; - {@@ ---------------------------------------------------------------------------- Erases content and formatting of a cell. The cell still occupies memory. @@ -2937,26 +2878,17 @@ end; function TsWorksheet.ReadFormulaAsString(ACell: PCell; ALocalized: Boolean = false): String; var - parser: TsSpreadsheetParser; + formula: PsFormula; begin Result := ''; if ACell = nil then exit; if HasFormula(ACell) then begin + formula := FFormulas.FindFormula(ACell^.Row, ACell^.Col); if ALocalized then - begin - // case (1): Formula is localized and has to be converted to default syntax // !!!! Is this comment correct? - parser := TsSpreadsheetParser.Create(self); - try - parser.Expression := ACell^.FormulaValue; - Result := parser.LocalizedExpression[Workbook.FormatSettings]; - finally - parser.Free; - end; - end + Result := formula^.Parser.LocalizedExpression[Workbook.FormatSettings] else - // case (2): Formula is already in default syntax - Result := ACell^.FormulaValue; + Result := formula^.Parser.Expression; end; end; @@ -2996,21 +2928,21 @@ end; function TsWorksheet.ConvertFormulaDialect(ACell: PCell; ADialect: TsFormulaDialect): String; var - parser: TsSpreadsheetParser; + oldDialect: TsFormulaDialect; + formula: PsFormula; begin - if ACell^.Formulavalue <> '' then - begin - parser := TsSpreadsheetParser.Create(self); - try - parser.Expression := ACell^.FormulaValue; - parser.Dialect := ADialect; - parser.PrepareCopyMode(ACell, ACell); - Result := parser.Expression; - finally - parser.Free; - end; - end else - Result := ''; + Result := ''; + if (ACell = nil) or (not HasFormula(ACell)) then + exit; + + formula := FFormulas.findFormula(ACell^.Row, ACell^.Col); + oldDialect := formula^.Parser.Dialect; + try + formula^.Parser.Dialect := ADialect; + Result := formula^.Parser.Expression; + finally + formula^.Parser.Dialect := oldDialect; + end; end; {@@ ---------------------------------------------------------------------------- @@ -3035,60 +2967,6 @@ begin end; end; -{@@ ---------------------------------------------------------------------------- - Returns the CalcState flag of the specified cell. This flag tells whether a - formula in the cell has not yet been calculated (csNotCalculated), is - currently being calculated (csCalculating), or has already been calculated - (csCalculated). - - @param ACell Pointer to cell considered - @return Enumerated value of the cell's calculation state - (csNotCalculated, csCalculating, csCalculated) --------------------------------------------------------------------------------} -function TsWorksheet.GetCalcState(ACell: PCell): TsCalcState; -var - calcState: TsCellFlags; -begin - Result := csNotCalculated; - if (ACell = nil) then - exit; - calcState := ACell^.Flags * [cfCalculating, cfCalculated]; - if calcState = [] then - Result := csNotCalculated - else - if calcState = [cfCalculating] then - Result := csCalculating - else - if calcState = [cfCalculated] then - Result := csCalculated - else - raise EFPSpreadsheet.Create('[TsWorksheet.GetCalcState] Illegal cell flags.'); -end; - -{@@ ---------------------------------------------------------------------------- - Set the CalcState flag of the specified cell. This flag tells whether a - formula in the cell has not yet been calculated (csNotCalculated), is - currently being calculated (csCalculating), or has already been calculated - (csCalculated). - - For internal use only! - - @param ACell Pointer to cell considered - @param AValue New value for the calculation state of the cell - (csNotCalculated, csCalculating, csCalculated) --------------------------------------------------------------------------------} -procedure TsWorksheet.SetCalcState(ACell: PCell; AValue: TsCalcState); -begin - case AValue of - csNotCalculated: - ACell^.Flags := ACell^.Flags - [cfCalculated, cfCalculating]; - csCalculating: - ACell^.Flags := ACell^.Flags + [cfCalculating] - [cfCalculated]; - csCalculated: - ACell^.Flags := ACell^.Flags + [cfCalculated] - [cfCalculating]; - end; -end; - {@@ ---------------------------------------------------------------------------- Returns the index of the effective cell format to be used at the specified cell. @@ -3133,15 +3011,6 @@ begin if fmtIndex = 0 then fmtIndex := GetColFormatIndex(ACol); end; - { - if (cell <> nil) and (cell^.FormatIndex > 0) then - fmtIndex := cell^.FormatIndex - else begin - fmtIndex := GetRowFormatIndex(ARow); - if fmtIndex = 0 then - fmtIndex := GetColFormatIndex(ACol); - end; - } Result := FWorkbook.GetPointerToCellFormat(fmtIndex); end; @@ -3153,18 +3022,6 @@ function TsWorksheet.GetPointerToEffectiveCellFormat(ACell: PCell): PsCellFormat var fmtIndex: Integer; begin - { - fmtIndex := 0; - if (ACell <> nil) then begin - if (ACell^.FormatIndex > 0) then - fmtIndex := ACell^.FormatIndex - else begin - fmtIndex := GetRowFormatIndex(ACell^.Row); - if fmtIndex = 0 then - fmtIndex := GetColFormatIndex(ACell^.Col); - end; - end; - } if (ACell <> nil) then fmtIndex := ACell^.FormatIndex else @@ -3720,6 +3577,70 @@ begin Result := (ACell <> nil) and (cfMerged in ACell^.Flags); end; +{@@ ---------------------------------------------------------------------------- + Deletes the formula assigned to the specified cell +-------------------------------------------------------------------------------} +procedure TsWorksheet.DeleteFormula(ACell: PCell); +begin + if HasFormula(ACell) then begin + FFormulas.DeleteFormula(ACell); + ACell^.Flags := ACell^.Flags - [cfHasFormula, cf3dFormula]; + end; +end; + +{@@ ---------------------------------------------------------------------------- + Reads the formula assigned to a cell in the specified row and column +-------------------------------------------------------------------------------} +function TsWorksheet.ReadFormula(ARow, ACol: Cardinal): String; +var + cell: PCell; +begin + cell := FindCell(ARow, ACol); + Result := ReadFormula(cell) +end; + +{@@ ---------------------------------------------------------------------------- + Reads the formula assigned to a specified cell +-------------------------------------------------------------------------------} +function TsWorksheet.ReadFormula(ACell: PCell): String; +var + formula: PsFormula; +begin + Result := ''; + if ACell = nil then + exit; + + formula := Formulas.FindFormula(ACell); + if formula = nil then + exit; + + Result := formula^.Text; + + if (Result = '') and (formula^.Parser <> nil) then + Result := formula^.Parser.Expression; +end; + +{@@ ---------------------------------------------------------------------------- + Uses a formula in the specified a cell +-------------------------------------------------------------------------------} +procedure TsWorksheet.UseFormulaInCell(ACell: PCell; AFormula: PsFormula); +begin + Assert(ACell <> nil); + + if AFormula <> nil then + begin + AFormula^.Col := ACell^.Col; + AFormula^.Row := ACell^.Row; + + ACell^.ContentType := cctFormula; + + ACell^.Flags := ACell^.Flags + [cfHasFormula]; + if (AFormula^.Parser <> nil) and AFormula^.Parser.Has3DLinks then + ACell^.Flags := ACell^.Flags + [cf3dFormula]; + end else + DeleteFormula(ACell); +end; + {@@ ---------------------------------------------------------------------------- Returns the parameters of the image stored in the internal image list at the specified index. @@ -4615,6 +4536,7 @@ begin end; end; + ACell^.UTF8StringValue := AText; if (AText = '') then begin { Initially, the cell was destroyed here if AText = '' and the cell is not @@ -4622,18 +4544,16 @@ begin This is not good... The calling procedure cannot be notified that ACell is destroyed here. See issue #0030049 } - WriteBlank(ACell); - exit; + ACell^.ContentType := cctEmpty; + end else + begin + ACell^.ContentType := cctUTF8String; + SetLength(ACell^.RichTextParams, Length(ARichTextParams)); + if Length(ARichTextParams) > 0 then + for i:=0 to High(ARichTextParams) do + ACell^.RichTextParams[i] := ARichTextParams[i]; end; - ACell^.ContentType := cctUTF8String; - ACell^.UTF8StringValue := AText; - - SetLength(ACell^.RichTextParams, Length(ARichTextParams)); - if Length(ARichTextParams) > 0 then - for i:=0 to High(ARichTextParams) do - ACell^.RichTextParams[i] := ARichTextParams[i]; - ChangedCell(ACell^.Row, ACell^.Col); end; @@ -4885,29 +4805,37 @@ end; {@@ ---------------------------------------------------------------------------- Writes an empty cell - @param ARow The row of the cell - @param ACol The column of the cell + @param ARow The row of the cell + @param ACol The column of the cell + @param KeepFormula Does not erase the formula. Off by default because it + would be very confusing if the formula had a + non-blank result. @return Pointer to the cell Note: Empty cells are useful when, for example, a border line extends along a range of cells including empty cells. -------------------------------------------------------------------------------} -function TsWorksheet.WriteBlank(ARow, ACol: Cardinal): PCell; +function TsWorksheet.WriteBlank(ARow, ACol: Cardinal; + KeepFormula: Boolean = false): PCell; begin Result := GetCell(ARow, ACol); - WriteBlank(Result); + WriteBlank(Result, KeepFormula); end; {@@ ---------------------------------------------------------------------------- Writes an empty cell @param ACel Pointer to the cell + @param KeepFormula Does not erase the formula. Off by default because it + would be very confusing if the formula had a + non-blank result. Note: Empty cells are useful when, for example, a border line extends along a range of cells including empty cells. -------------------------------------------------------------------------------} -procedure TsWorksheet.WriteBlank(ACell: PCell); +procedure TsWorksheet.WriteBlank(ACell: PCell; KeepFormula: Boolean = false); begin if ACell <> nil then begin - ACell^.FormulaValue := ''; + if not KeepFormula then + DeleteFormula(ACell); // NOTE: Erase the formula because if it would return a non-blank result // this would be very confusing! if HasHyperlink(ACell) then @@ -5044,7 +4972,8 @@ begin if ACell = nil then exit; - ACell^.FormulaValue := ''; + if HasFormula(ACell) then + DeleteFormula(ACell); if AValue = '' then begin @@ -5629,6 +5558,8 @@ end; @param ACell Pointer to the cell @param AFormula Formula string to be written. A leading '=' will be removed. + If AFormula is '' then an formula already assigned to this + cell is deleted. @param ALocalized If true, the formula is expected to have decimal and list separators of the workbook's FormatSettings. Otherwise uses dot and comma, respectively. @@ -5637,43 +5568,46 @@ procedure TsWorksheet.WriteFormula(ACell: PCell; AFormula: string; ALocalized: Boolean = false); var parser: TsExpressionParser; + formula: PsFormula; begin if ACell = nil then exit; + if AFormula = '' then begin + DeleteFormula(ACell); + ChangedCell(ACell^.Row, ACell^.Col); + exit; + end; + + formula := FFormulas.AddFormula(ACell^.Row, ACell^.Col, AFormula); + if not (boIgnoreFormulas in Workbook.Options) then begin // Remove '='; is not stored internally - if (AFormula <> '') and (AFormula[1] = '=') then + if (AFormula[1] = '=') then AFormula := Copy(AFormula, 2, Length(AFormula)); - if ALocalized then begin - // Convert "localized" formula to standard format - parser := TsSpreadsheetParser.Create(self); - try - parser.LocalizedExpression[Workbook.FormatSettings] := AFormula; - parser.Expression := AFormula; - { - if ALocalized then - // Convert "localized" formula to standard format - parser.LocalizedExpression[Workbook.FormatSettings] := AFormula - else - parser.Expression := AFormula; - AFormula := parser.Expression; - if parser.Has3DLinks - then ACell.Flags := ACell.Flags + [cf3dFormula] - else ACell.Flags := ACell.Flags - [cf3dFormula]; - } - finally - parser.Free; - end; - end; + parser := TsSpreadsheetParser.Create(self); + if ALocalized then + parser.LocalizedExpression[Workbook.FormatSettings] := AFormula + else + parser.Expression := AFormula; + AFormula := parser.Expression; + + if parser.Has3DLinks then + ACell.Flags := ACell.Flags + [cf3dFormula] + else + ACell.Flags := ACell.Flags - [cf3dFormula]; + + formula^.Text := AFormula; + formula^.Parser := parser; // parser will be destroyed by formula end; - // Store formula in cell - if AFormula <> '' then - ACell^.ContentType := cctFormula; - ACell^.FormulaValue := AFormula; + // Set formula flags in cell + ACell^.ContentType := cctFormula; + ACell^.Flags := ACell^.Flags + [cfHasFormula]; + + // Notify controls of changed cell ChangedCell(ACell^.Row, ACell^.Col); end; @@ -5909,14 +5843,28 @@ end; @see TsFormulaElements @see CreateRPNFormula -------------------------------------------------------------------------------} -procedure TsWorksheet.WriteRPNFormula(ACell: PCell; AFormula: TsRPNFormula); +procedure TsWorksheet.WriteRPNFormula(ACell: PCell; ARPNFormula: TsRPNFormula); +var + formula: PsFormula; + parser: TsSpreadsheetParser; begin if ACell = nil then exit; + formula := FFormulas.FindFormula(ACell); + if formula = nil then begin + formula := FFormulas.AddFormula(ACell^.Row, ACell^.Col); + formula^.Parser := TsSpreadsheetParser.Create(self); + end; + formula^.Parser.RPNFormula := ARPNFormula; + formula^.Text := formula^.Parser.Expression; + UseFormulaInCell(ACell, formula); + ACell^.ContentType := cctFormula; + (* + formuila.Parsed := TsSpreadsheetParser. ACell^.ContentType := cctFormula; ACell^.FormulaValue := ConvertRPNFormulaToStringFormula(AFormula); - + *) ChangedCell(ACell^.Row, ACell^.Col); end; @@ -7401,198 +7349,104 @@ end; @param ACol Index of the column to be deleted -------------------------------------------------------------------------------} procedure TsWorksheet.DeleteCol(ACol: Cardinal); -var - col: PCol; - i: Integer; - r: Cardinal; - cell: PCell; - firstRow, lastRow: Cardinal; begin - lastRow := GetLastOccupiedRowIndex; - firstRow := GetFirstRowIndex; - - // Fix merged cells - FMergedCells.DeleteRowOrCol(ACol, false); - - // Fix comments - FComments.DeleteRowOrCol(ACol, false); - - // Fix hyperlinks - FHyperlinks.DeleteRowOrCol(ACol, false); - - // Delete cells - for r := lastRow downto firstRow do - RemoveAndFreeCell(r, ACol); - - // Update column index of cell records - for cell in FCells do - DeleteColCallback(cell, {%H-}pointer(PtrInt(ACol))); - - // Update column index of col records - for i:=FCols.Count-1 downto 0 do begin - col := PCol(FCols.Items[i]); - if col^.Col > ACol then - dec(col^.Col) - else - break; - end; - - // Update first and last column index - UpDateCaches; - - ChangedCell(0, ACol); + DeleteRowOrCol(ACol, false); end; {@@ ---------------------------------------------------------------------------- - Deletes the row at the index specified. Cells with greader row indexes are + Deletes the row at the index specified. Cells with greater row indexes are moved one row up. Merged cell blocks and cell references in formulas are considered as well. @param ARow Index of the row to be deleted -------------------------------------------------------------------------------} procedure TsWorksheet.DeleteRow(ARow: Cardinal); -var - row: PRow; - i: Integer; - c: Cardinal; - firstCol, lastCol: Cardinal; - cell: PCell; begin - firstCol := GetFirstColIndex; - lastCol := GetLastOccupiedColIndex; - - // Fix merged cells - FMergedCells.DeleteRowOrCol(ARow, true); - - // Fix comments - FComments.DeleteRowOrCol(ARow, true); - - // Fix hyperlinks - FHyperlinks.DeleteRowOrCol(ARow, true); - - // Delete cells - for c := lastCol downto firstCol do - RemoveAndFreeCell(ARow, c); - - // Update row index of cell records - for cell in FCells do - DeleteRowCallback(cell, {%H-}pointer(PtrInt(ARow))); - - // Update row index of row records - for i:=FRows.Count-1 downto 0 do - begin - row := PRow(FRows.Items[i]); - if row^.Row > ARow then - dec(row^.Row) - else - break; - end; - - // Update first and last row index - UpdateCaches; - - ChangedCell(ARow, 0); + DeleteRowOrCol(ARow, true); end; {@@ ---------------------------------------------------------------------------- - Inserts a column BEFORE the index specified. Cells with greater column indexes - are moved one column to the right. Merged cell blocks and cell references in - formulas are considered as well. + Deletes the row or column at the index specified. AIsRow determines whether + the index is a row or column index. + + Cells with greader row/column indexes are moved one row up/left. + Merged cell blocks and cell references in formulas are considered as well. + + @param AIndex Index of the row to be deleted + @param IsRow If TRUE then AIndex is a row index, otherwise a column index +-------------------------------------------------------------------------------} +procedure TsWorksheet.DeleteRowOrCol(AIndex: Integer; IsRow: Boolean); +var + cell: PCell; + row: PRow; + col: PCol; + i: Integer; + r: Cardinal; + formula: PsFormula; +begin + // Fix merged cells + FMergedCells.DeleteRowOrCol(AIndex, IsRow); + + // Fix comments + FComments.DeleteRowOrCol(AIndex, IsRow); + + // Fix hyperlinks + FHyperlinks.DeleteRowOrCol(AIndex, IsRow); + + // Fix formulas + FFormulas.DeleteRowOrCol(AIndex, IsRow); + + // Delete cells + FCells.DeleteRowOrCol(AIndex, IsRow); + + // Fix formula flags + for cell in FCells do + if HasFormula(cell) and (FFormulas.FindFormula(cell) = nil) then + cell^.Flags := cell^.flags - [cfHasFormula, cf3dFormula]; + + // Fix formula left-overs (formulas having no cell) + for formula in FFormulas do + if FindCell(formula^.Row, formula^.Col) = nil then + FFormulas.DeleteFormula(formula^.Row, formula^.Col); + + if IsRow then + begin + for i:= FRows.Count-1 downto 0 do begin + row := PRow(FRows.Items[i]); + if row^.Row > AIndex then + dec(row^.Row) + else + break; + end; + // Update first and last row index + UpdateCaches; + ChangedCell(AIndex, 0); + end else + begin + // Update column index of col records + for i:=FCols.Count-1 downto 0 do begin + col := PCol(FCols.Items[i]); + if col^.Col > AIndex then + dec(col^.Col) + else + break; + end; + // Update first and last column index + UpDateCaches; + ChangedCell(0, AIndex); + end; +end; + +{@@ ---------------------------------------------------------------------------- + Inserts a column BEFORE the column index specified. + Cells with greater column indexes are moved one row to the right. + Merged cell blocks and cell references in formulas are considered as well. @param ACol Index of the column before which a new column is inserted. -------------------------------------------------------------------------------} procedure TsWorksheet.InsertCol(ACol: Cardinal); -var - col: PCol; - i: Integer; - cell: PCell; - rng: PsCellRange; begin - // Update column index of comments - FComments.InsertRowOrCol(ACol, false); - - // Update column index of hyperlinks - FHyperlinks.InsertRowOrCol(ACol, false); - - // Update column index of cell records - for cell in FCells do - InsertColCallback(cell, {%H-}pointer(PtrInt(ACol))); - - // Update column index of column records - for i:=0 to FCols.Count-1 do begin - col := PCol(FCols.Items[i]); - if col^.Col >= ACol then inc(col^.Col); - end; - - // Update first and last column index - UpdateCaches; - - // Fix merged cells - for rng in FMergedCells do - begin - // The new column is at the LEFT of the merged block - // --> Shift entire range to the right by 1 column - if (ACol < rng^.Col1) then - begin - // The former first column is no longer merged --> un-tag its cells - for cell in Cells.GetColEnumerator(rng^.Col1, rng^.Row1, rng^.Row2) do - Exclude(cell^.Flags, cfMerged); - - // Shift merged block to the right - // Don't call "MergeCells" here - this would add a new merged block - // because of the new merge base! --> infinite loop! - inc(rng^.Col1); - inc(rng^.Col2); - // The right column needs to be tagged - for cell in Cells.GetColEnumerator(rng^.Col2, rng^.Row1, rng^.Row2) do - Include(cell^.Flags, cfMerged); - end else - // The new column goes through this cell block --> Shift only the right - // column of the range to the right by 1 - if (ACol >= rng^.Col1) and (ACol <= rng^.Col2) then - MergeCells(rng^.Row1, rng^.Col1, rng^.Row2, rng^.Col2+1); - end; - - ChangedCell(0, ACol); -end; - -procedure TsWorksheet.InsertColCallback(data, arg: Pointer); -var - cell: PCell; - col: Cardinal; - formula: TsRPNFormula; - i: Integer; -begin - col := LongInt({%H-}PtrInt(arg)); - cell := PCell(data); - if cell = nil then // This should not happen. Just to make sure... - exit; - - // Update row index of moved cells - if cell^.Col >= col then - inc(cell^.Col); - - // Update formulas - if HasFormula(cell) and (cell^.FormulaValue <> '' ) then - begin - // (1) create an rpn formula - formula := BuildRPNFormula(cell); - // (2) update cell addresses affected by the insertion of a column - for i:=0 to Length(formula)-1 do - begin - case formula[i].ElementKind of - fekCell, fekCellRef: - if formula[i].Col >= col then inc(formula[i].Col); - fekCellRange: - begin - if formula[i].Col >= col then inc(formula[i].Col); - if formula[i].Col2 >= col then inc(formula[i].Col2); - end; - end; - end; - // (3) convert rpn formula back to string formula - cell^.FormulaValue := ConvertRPNFormulaToStringFormula(formula); - end; + InsertRowOrCol(ACol, false); end; {@@ ---------------------------------------------------------------------------- @@ -7603,102 +7457,125 @@ end; @param ARow Index of the row before which a new row is inserted. -------------------------------------------------------------------------------} procedure TsWorksheet.InsertRow(ARow: Cardinal); -var - row: PRow; - i: Integer; - cell: PCell; - rng: PsCellRange; begin - // Update row index of cell comments - FComments.InsertRowOrCol(ARow, true); - - // Update row index of cell hyperlinks - FHyperlinks.InsertRowOrCol(ARow, true); - - // Update row index of cell records - for cell in FCells do - InsertRowCallback(cell, {%H-}pointer(PtrInt(ARow))); - - // Update row index of row records - for i:=0 to FRows.Count-1 do begin - row := PRow(FRows.Items[i]); - if row^.Row >= ARow then inc(row^.Row); - end; - - // Update first and last row index - UpdateCaches; - - // Fix merged cells - for rng in FMergedCells do - begin - // The new row is ABOVE the merged block --> Shift entire range down by 1 row - if (ARow < rng^.Row1) then - begin - // The formerly first row is no longer merged --> un-tag its cells - for cell in Cells.GetRowEnumerator(rng^.Row1, rng^.Col1, rng^.Col2) do - Exclude(cell^.Flags, cfMerged); - - // Shift merged block down - // (Don't call "MergeCells" here - this would add a new merged block - // because of the new merge base! --> infinite loop!) - inc(rng^.Row1); - inc(rng^.Row2); - // The last row needs to be tagged - for cell in Cells.GetRowEnumerator(rng^.Row2, rng^.Col1, rng^.Col2) do - Include(cell^.Flags, cfMerged); - end else - // The new row goes through this cell block --> Shift only the bottom row - // of the range down by 1 - if (ARow >= rng^.Row1) and (ARow <= rng^.Row2) then - MergeCells(rng^.Row1, rng^.Col1, rng^.Row2+1, rng^.Col2); - end; - - ChangedCell(ARow, 0); + InsertRowOrCol(ARow, true); end; -procedure TsWorksheet.InsertRowCallback(data, arg: Pointer); +{@@ ---------------------------------------------------------------------------- + Inserts a row or column BEFORE the row/column specified by AIndex. Depending + on IsRow this is either the row or column index. + + Cells with greater row/column indexes are moved one row down/right. + Merged cell blocks and cell references in formulas are considered as well. + + @param AIndex Index of the row or column before which a new row or + column is inserted. + @param IsRow Determines whether AIndex refers to a row index (TRUE) or + column index (FALSE). +-------------------------------------------------------------------------------} +procedure TsWorksheet.InsertRowOrCol(AIndex: Integer; IsRow: Boolean); var cell: PCell; - row: Cardinal; + row: PRow; + col: PCol; i: Integer; - formula: TsRPNFormula; + rng: PsCellRange; begin - row := LongInt({%H-}PtrInt(arg)); - cell := PCell(data); - if cell = nil then // This should not happen. Just to make sure... - exit; + // Update row indexes of cell comments + FComments.InsertRowOrCol(AIndex, IsRow); - // Update row index of moved cells - if cell^.Row >= row then - inc(cell^.Row); + // Update row indexes of cell hyperlinks + FHyperlinks.InsertRowOrCol(AIndex, IsRow); - // Update formulas - if HasFormula(cell) then - begin - // (1) create an rpn formula - formula := BuildRPNFormula(cell); - // (2) update cell addresses affected by the insertion of a column - for i:=0 to Length(formula)-1 do begin - case formula[i].ElementKind of - fekCell, fekCellRef: - if formula[i].Row >= row then inc(formula[i].Row); - fekCellRange: - begin - if formula[i].Row >= row then inc(formula[i].Row); - if formula[i].Row2 >= row then inc(formula[i].Row2); - end; - end; + // Update row indexes of cell formulas + FFormulas.InsertRowOrCol(AIndex, IsRow); + + // Update cell indexes of cell records + FCells.InsertRowOrCol(AIndex, IsRow); + + if IsRow then begin + // Update row index of row records + for i:=0 to FRows.Count-1 do begin + row := PRow(FRows.Items[i]); + if row^.Row >= AIndex then inc(row^.Row); end; - // (3) convert rpn formula back to string formula - cell^.FormulaValue := ConvertRPNFormulaToStringFormula(formula); + end else + begin + // Update column index of column records + for i:=0 to FCols.Count-1 do begin + col := PCol(FCols.Items[i]); + if col^.Col >= AIndex then inc(col^.Col); + end; + end; + + // Update first and last row/column index + UpdateCaches; + + if IsRow then + begin + // Fix merged cells + for rng in FMergedCells do + begin + // The new row is ABOVE the merged block --> Shift entire range down by 1 row + if (AIndex < rng^.Row1) then + begin + // The formerly first row is no longer merged --> un-tag its cells + for cell in Cells.GetRowEnumerator(rng^.Row1, rng^.Col1, rng^.Col2) do + Exclude(cell^.Flags, cfMerged); + + // Shift merged block down + // (Don't call "MergeCells" here - this would add a new merged block + // because of the new merge base! --> infinite loop!) + inc(rng^.Row1); + inc(rng^.Row2); + // The last row needs to be tagged + for cell in Cells.GetRowEnumerator(rng^.Row2, rng^.Col1, rng^.Col2) do + Include(cell^.Flags, cfMerged); + end else + // The new row goes through this cell block --> Shift only the bottom row + // of the range down by 1 + if (AIndex >= rng^.Row1) and (AIndex <= rng^.Row2) then + MergeCells(rng^.Row1, rng^.Col1, rng^.Row2+1, rng^.Col2); + end; + + ChangedCell(AIndex, 0); + end else + begin + // Fix merged cells + for rng in FMergedCells do + begin + // The new column is at the LEFT of the merged block + // --> Shift entire range to the right by 1 column + if (AIndex < rng^.Col1) then + begin + // The former first column is no longer merged --> un-tag its cells + for cell in Cells.GetColEnumerator(rng^.Col1, rng^.Row1, rng^.Row2) do + Exclude(cell^.Flags, cfMerged); + + // Shift merged block to the right + // Don't call "MergeCells" here - this would add a new merged block + // because of the new merge base! --> infinite loop! + inc(rng^.Col1); + inc(rng^.Col2); + // The right column needs to be tagged + for cell in Cells.GetColEnumerator(rng^.Col2, rng^.Row1, rng^.Row2) do + Include(cell^.Flags, cfMerged); + end else + // The new column goes through this cell block --> Shift only the right + // column of the range to the right by 1 + if (AIndex >= rng^.Col1) and (AIndex <= rng^.Col2) then + MergeCells(rng^.Row1, rng^.Col1, rng^.Row2, rng^.Col2+1); + end; + + ChangedCell(0, AIndex); end; end; {@@ ---------------------------------------------------------------------------- Moves a column from a specified column index to another column index. The operation includes everything associated with the column (cell values, - cell properties, formats, formulas, column formats, column widths). Formulas - are automatically adjusted for the new position. + cell properties, formats, formulas, column formats, column widths). + Formulas are automatically adjusted for the new position. -------------------------------------------------------------------------------} procedure TsWorksheet.MoveCol(AFromCol, AToCol: Cardinal); var @@ -7714,6 +7591,34 @@ begin FCells.MoveAlongRow(r, AFromCol, AToCol); FComments.MoveAlongRow(r, AFromCol, AToCol); FHyperlinks.MoveAlongRow(r, AFromCol, AToCol); + FFormulas.MoveAlongRow(r, AFromCol, AToCol); + end; + finally + Workbook.EnableNotifications; + end; +end; + +{@@ ---------------------------------------------------------------------------- + Moves a row from a specified row index to another row index. + The operation includes everything associated with the row (cell values, + cell properties, formats, formulas, column formats, column widths). + Formulas are automatically adjusted for the new position. +-------------------------------------------------------------------------------} +procedure TsWorksheet.MoveRow(AFromRow, AToRow: Cardinal); +var + c: Integer; +begin + if AFromRow = AToRow then + // Nothing to do + exit; + + Workbook.DisableNotifications; + try + for c := 0 to GetLastColIndex do begin + FCells.MoveAlongCol(AFromRow, c, AToRow); + FComments.MoveAlongCol(AFromRow, c, AToRow); + FHyperlinks.MoveAlongCol(AFromRow, c, AToRow); + FFormulas.MoveAlongCol(AFromRow, c, AToRow); end; finally Workbook.EnableNotifications; @@ -8041,19 +7946,6 @@ begin end; end; -{@@ ---------------------------------------------------------------------------- - Recalculates rpn formulas in all worksheets --------------------------------------------------------------------------------} -(* -procedure TsWorkbook.Recalc; -var - sheet: pointer; -begin - for sheet in FWorksheets do - TsWorksheet(sheet).CalcFormulas; -end; -*) - {@@ ---------------------------------------------------------------------------- Conversion of length values between units -------------------------------------------------------------------------------} @@ -9594,41 +9486,39 @@ end; Since formulas may reference not-yet-calculated cells, this occurs in two steps: - 1. All formula cells are marked as "not calculated". - 2. Cells are calculated. If referenced cells are found as being + 1. All formulas are marked as "not calculated". + 2. Formulas are calculated. If referenced formulas are found as being "not calculated" they are calculated and then tagged as "calculated". - This results in an iterative calculation procedure. In the end, all cells + This results in an iterative calculation procedure. In the end, all formulas are calculated. -------------------------------------------------------------------------------} procedure TsWorkbook.CalcFormulas; var - cell: PCell; - p: Pointer; + formula: PsFormula; sheet: TsWorksheet; - i: Integer; + p: Pointer; begin if (boIgnoreFormulas in Options) then exit; - // prevent infinite loop due to triggering of formula calculation whenever - // a cell changes during execution of CalcFormulas. inc(FCalculationLock); try - // Step 1 - mark all formula cells as "not calculated" + // Step1 - mark all formulas as "not calculated" for p in FWorksheets do begin sheet := TsWorksheet(p); - for cell in sheet.Cells do - if HasFormula(cell) then - sheet.SetCalcState(cell, csNotCalculated); + for formula in sheet.Formulas do + formula^.CalcState := csNotCalculated; end; - // Step 2 - calculate cells. If a not-yet-calculated cell is found it is + // Step 2 - calculate formulas. If the formula calculted requires another + // the result of another formula not yet calculated this formula is + // calculated immediately. for p in FWorksheets do begin sheet := TsWorksheet(p); - for cell in TsWorksheet(sheet).Cells do - if HasFormula(cell) and (cell^.ContentType <> cctError) then - sheet.CalcFormula(cell); + for formula in sheet.Formulas do + sheet.CalcFormula(formula); end; + finally dec(FCalculationLock); end; @@ -9686,33 +9576,6 @@ var begin // Skeleton only - to be updated when new formula handling is finished. - (* - sheet := TsWorksheet(ACell^.Worksheet); - - case ACorrection of - fcWorksheetrenamed: - if (cf3dFormula in ACell^.Flags) then - begin - // The rpn formula contains the worksheet index which does not - // change upon sheet renaming. Simple rebuilding the string formula - // from rpn will insert the new sheet name. - rpn := sheet.BuildRPNFormula(ACell); - ACell^.FormulaValue := sheet.ConvertRPNFormulaToStringFormula(rpn); - exit; - end; - end; - - rpn := sheet.BuildRPNFormula(ACell); - for i:=0 to High(rpn) do begin - elem := rpn[i]; - { - case ACorrection of - ... // do specifice rpn corrections here - end; - } - end; - ACell^.FormulaValue := sheet.ConvertRPNFormulaToStringFormula(rpn); - *) end; {@@ ---------------------------------------------------------------------------- diff --git a/components/fpspreadsheet/source/common/fpstypes.pas b/components/fpspreadsheet/source/common/fpstypes.pas index f607f4272..ce0bdfc92 100644 --- a/components/fpspreadsheet/source/common/fpstypes.pas +++ b/components/fpspreadsheet/source/common/fpstypes.pas @@ -205,6 +205,9 @@ const SHEETSEPARATOR = '!'; type + TsFormulaFlag = (ffCalculating, ffCalculated); + TsFormulaFlags = set of TsFormulaFlag; + {@@ Elements of an expanded formula. Note: If ElementKind is fekCellOffset, "Row" and "Col" have to be cast to signed integers! } TsFormulaElement = record @@ -589,8 +592,8 @@ type TsCalcState = (csNotCalculated, csCalculating, csCalculated); {@@ Cell flag } - TsCellFlag = (cfCalculating, cfCalculated, cfHasComment, cfHyperlink, cfMerged, - cf3dFormula); + TsCellFlag = ({cfCalculating, cfCalculated, }cfHasComment, cfHyperlink, cfMerged, + cfHasFormula, cf3dFormula); {@@ Set of cell flags } TsCellFlags = set of TsCellFlag; @@ -744,7 +747,7 @@ type { Cell content } UTF8StringValue: String; // Strings cannot be part of a variant record RichTextParams: TsRichTextParams; // Formatting of individual text ranges - FormulaValue: String; // Formula for calculation of cell content +// FormulaValue: String; // Formula for calculation of cell content case ContentType: TCellContentType of // variant part must be at the end cctEmpty : (); // has no data at all cctFormula : (); // FormulaValue is outside the variant record diff --git a/components/fpspreadsheet/source/common/fpsutils.pas b/components/fpspreadsheet/source/common/fpsutils.pas index b7f235d38..db1b71c2f 100644 --- a/components/fpspreadsheet/source/common/fpsutils.pas +++ b/components/fpspreadsheet/source/common/fpsutils.pas @@ -202,8 +202,9 @@ procedure InitImageRecord(out AValue: TsImage; ARow, ACol: Cardinal; AOffsetX, AOffsetY, AScaleX, AScaleY: Double); procedure InitHeaderFooterImageRecord(out AImage: TsHeaderFooterImage); -procedure CopyCellValue(AFromCell, AToCell: PCell); +//procedure CopyCellValue(AFromCell, AToCell: PCell); function HasFormula(ACell: PCell): Boolean; +function Has3dFormula(ACell: PCell): Boolean; function SameCellBorders(AFormat1, AFormat2: PsCellFormat): Boolean; function SameFont(AFont1, AFont2: TsFont): Boolean; overload; function SameFont(AFont: TsFont; AFontName: String; AFontSize: Single; @@ -2334,7 +2335,6 @@ end; -------------------------------------------------------------------------------} procedure InitCell(out ACell: TCell); begin - ACell.FormulaValue := ''; ACell.UTF8StringValue := ''; FillChar(ACell, SizeOf(ACell), 0); end; @@ -2418,7 +2418,7 @@ begin Index := -1; end; end; - + (* {@@ ---------------------------------------------------------------------------- Copies the value of a cell to another one. Does not copy the formula, erases the formula of the destination cell if there is one! @@ -2428,18 +2428,20 @@ end; -------------------------------------------------------------------------------} procedure CopyCellValue(AFromCell, AToCell: PCell); begin - Assert(AFromCell <> nil); Assert(AToCell <> nil); - AToCell^.ContentType := AFromCell^.ContentType; - AToCell^.NumberValue := AFromCell^.NumberValue; - AToCell^.DateTimeValue := AFromCell^.DateTimeValue; - AToCell^.BoolValue := AFromCell^.BoolValue; - AToCell^.ErrorValue := AFromCell^.ErrorValue; - AToCell^.UTF8StringValue := AFromCell^.UTF8StringValue; - AToCell^.FormulaValue := ''; // This is confirmed with Excel + if AFromCell <> nil then begin + AToCell^.ContentType := AFromCell^.ContentType; + AToCell^.NumberValue := AFromCell^.NumberValue; + AToCell^.DateTimeValue := AFromCell^.DateTimeValue; + AToCell^.BoolValue := AFromCell^.BoolValue; + AToCell^.ErrorValue := AFromCell^.ErrorValue; + AToCell^.UTF8StringValue := AFromCell^.UTF8StringValue; + // Note: As confirmed with Excel, the formula is not to be copied here. + // Note: The calling routine must erase the formula if the destination cell has one. + end; end; - +*) {@@ ---------------------------------------------------------------------------- Returns TRUE if the cell contains a formula. @@ -2447,7 +2449,15 @@ end; -------------------------------------------------------------------------------} function HasFormula(ACell: PCell): Boolean; begin - Result := Assigned(ACell) and (Length(ACell^.FormulaValue) > 0); + Result := Assigned(ACell) and (cfHasFormula in ACell^.Flags); +end; + +{@@ ---------------------------------------------------------------------------- + Returns TRUE if the cell has a 3D formula (i.e. reference to another sheet) +-------------------------------------------------------------------------------} +function Has3dFormula(ACell: PCell): Boolean; +begin + Result := HasFormula(ACell) and (cf3dFormula in ACell^.Flags); end; {@@ ---------------------------------------------------------------------------- diff --git a/components/fpspreadsheet/source/common/xlsbiff8.pas b/components/fpspreadsheet/source/common/xlsbiff8.pas index f687ab337..2ca742d03 100644 --- a/components/fpspreadsheet/source/common/xlsbiff8.pas +++ b/components/fpspreadsheet/source/common/xlsbiff8.pas @@ -2554,6 +2554,29 @@ begin AStream.WriteWord(0); end; +function DoCollectSheetsWith3dRefs(ANode: TsExprNode; AData: Pointer): Boolean; +var + sheetlist: TsBIFF8ExternSheetList; + sheetIdx, sheetIdx1, sheetIdx2: Integer; + workbook: TsWorkbook; +begin + sheetlist := TsBIFF8ExternSheetList(AData); + if (ANode is TsCellExprNode) and TsCellExprNode(ANode).Has3DLink then + begin + sheetIdx := TsCellExprNode(ANode).GetSheetIndex; + sheetList.AddSheets('', nil, sheetIdx, sheetIdx); + end else + if (ANode is TsCellRangeExprNode) and TsCellRangeExprNode(ANode).Has3DLink then + begin + workbook := TsCellRangeExprNode(ANode).Workbook as TsWorkbook; + sheetIdx1 := TsCellRangeExprNode(ANode).GetSheetIndex(1); + sheetIdx2 := TsCellRangeExprNode(ANode).GetSheetIndex(2); + for sheetIdx := sheetIdx1 to sheetIdx2 do + sheetList.AddSheets('', nil, sheetIdx1, sheetIdx2); + end; + Result := false; +end; + {@@ ---------------------------------------------------------------------------- Collects the data for out-of-sheet links found in the specified worksheet (or all worksheets if the parameter is omitted). @@ -2561,6 +2584,14 @@ end; -------------------------------------------------------------------------------} procedure TsSpreadBIFF8Writer.CollectExternData; + procedure DoCollectForSheet(ASheet: TsWorksheet); + var + formula: PsFormula; + begin + for formula in ASheet.Formulas do + formula^.Parser.IterateNodes(@DoCollectSheetsWith3dRefs, FBiff8ExternSheets); + end; +{ procedure DoCollectForSheet(ASheet: TsWorksheet); var cell: PCell; @@ -2593,7 +2624,7 @@ procedure TsSpreadBIFF8Writer.CollectExternData; end; end; end; - + } var book: TsWorkbook; sheet: TsWorksheet; diff --git a/components/fpspreadsheet/source/common/xlscommon.pas b/components/fpspreadsheet/source/common/xlscommon.pas index 44ddbe569..02c3c5521 100644 --- a/components/fpspreadsheet/source/common/xlscommon.pas +++ b/components/fpspreadsheet/source/common/xlscommon.pas @@ -2872,13 +2872,35 @@ var n: Word; rpnFormula: TsRPNformula; strFormula: String; + formula: PsFormula; begin n := ReadRPNTokenArraySize(AStream); + if n = 0 then + exit(false); + Result := ReadRPNTokenArray(AStream, n, rpnFormula, ACell, ASharedFormulaBase); if Result then begin + formula := TsWorksheet(FWorksheet).Formulas.FindFormula(ACell); + if formula = nil then begin + formula := TsWorksheet(FWorksheet).Formulas.AddFormula(ACell^.Row, ACell^.Col); + formula^.Parser := TsSpreadsheetParser.Create(FWorksheet); + end; + formula^.Parser.RPNFormula := rpnFormula; + formula^.Text := formula^.Parser.Expression; + if formula^.Parser.Has3dLinks then + ACell^.Flags := ACell^.Flags + [cfHasFormula, cf3dFormula] + else + ACell^.Flags := ACell^.Flags + [cfHasFormula]; +{ strFormula := tsWorksheet(FWorksheet).ConvertRPNFormulaToStringFormula(rpnFormula); - if strFormula <> '' then - ACell^.FormulaValue := strFormula; + if strFormula <> '' then begin + formula := TsWorksheet(FWorksheet).Formulas.AddFormula(ACell^.Row, ACell^.Col, strFormula); + if formula^.Parsed.Has3dLinks then + ACell^.Flags := ACell^.Flags + [cfHasFormula, cf3dFormula] + else + ACell^.Flags := ACell^.Flags + [cfHasFormula]; + end; + } end; end; @@ -3441,6 +3463,27 @@ begin end; end; +function DoCollectSheetsWith3dRefs(ANode: TsExprNode; AData: Pointer): Boolean; +var + sheetlist: TsBIFFExternSheetList; + sheetIdx, sheetIdx1, sheetIdx2: Integer; + workbook: TsWorkbook; +begin + sheetlist := TsBIFFExternSheetList(AData); + if (ANode is TsCellExprNode) and TsCellExprNode(ANode).Has3DLink then + sheetList.AddSheet(TsCellExprNode(ANode).GetSheetName, ebkInternal) + else + if (ANode is TsCellRangeExprNode) and TsCellRangeExprNode(ANode).Has3DLink then + begin + workbook := TsCellRangeExprNode(ANode).Workbook as TsWorkbook; + sheetIdx1 := TsCellRangeExprNode(ANode).GetSheetIndex(1); + sheetIdx2 := TsCellRangeExprNode(ANode).GetSheetIndex(2); + for sheetIdx := sheetIdx1 to sheetIdx2 do + sheetList.AddSheet(workbook.GetWorksheetByIndex(sheetIdx).Name, ebkInternal); + end; + Result := false; // No need to rebuild the text formula +end; + {@@ ---------------------------------------------------------------------------- Collects the data for out-of-sheet links found in the specified worksheet (or all worksheets if the parameter is omitted). @@ -3450,6 +3493,13 @@ end; function TsSpreadBIFFWriter.CollectExternData(AWorksheet: TsBasicWorksheet = nil): Integer; procedure DoCollectForSheet(ASheet: TsWorksheet; ASheetList: TsBIFFExternSheetList); + var + formula: PsFormula; + begin + for formula in ASheet.Formulas do + formula^.Parser.IterateNodes(@DoCollectSheetsWith3dRefs, ASheetList); + end; +{ var cell: PCell; workbook: TsWorkbook; @@ -3496,6 +3546,7 @@ function TsSpreadBIFFWriter.CollectExternData(AWorksheet: TsBasicWorksheet = nil end; end; end; + } var sheet: TsWorksheet; diff --git a/components/fpspreadsheet/source/common/xlsxooxml.pas b/components/fpspreadsheet/source/common/xlsxooxml.pas index 982af9b2a..aa7038301 100644 --- a/components/fpspreadsheet/source/common/xlsxooxml.pas +++ b/components/fpspreadsheet/source/common/xlsxooxml.pas @@ -641,6 +641,7 @@ var datanode, tnode: TDOMNode; dataStr: String; formulaStr: String; + formula: PsFormula; nodeName: String; sstIndex: Integer; number: Double; @@ -731,8 +732,11 @@ begin sharedformulabase := TSharedFormulaData(FSharedFormulaBaseList[StrToInt(s)]); // ... and copy shared formula to destination cell InitCell(FWorksheet, sharedformulabase.Row, sharedformulabase.Col, lCell); - lCell.Formulavalue := sharedformulabase.Formula; - lCell.Worksheet := sharedformulabase.Worksheet; + formula := sharedFormulaBase.Worksheet.Formulas.AddFormula( + sharedFormulabase.Row, sharedFormulaBase.Col, sharedformulabase.Formula + ); +// lCell.Formulavalue := sharedformulabase.Formula; +// lCell.Worksheet := sharedformulabase.Worksheet; sheet.CopyFormula(@lCell, cell); cell^.ContentType := cctFormula; end; @@ -740,8 +744,8 @@ begin end else // "Normal" formula - cell^.FormulaValue := formulaStr; -// AWorksheet.WriteFormula(cell, formulaStr); + sheet.WriteFormula(cell, formulaStr); +// cell^.FormulaValue := formulaStr; except on E:EExprParser do begin FWorkbook.AddErrorMsg(E.Message); @@ -759,11 +763,8 @@ begin // get data type s := GetAttrValue(ANode, 't'); // "t" = data type if (s = '') and (dataStr = '') then - begin - formulaStr := cell^.FormulaValue; - sheet.WriteBlank(cell); // this erases the formula!!! - cell^.FormulaValue := formulaStr; - end else + sheet.WriteBlank(cell, true) // true --> do not erase the formula!!! + else if (s = '') or (s = 'n') then begin // Number or date/time, depending on format number := StrToFloat(dataStr, FPointSeparatorSettings); @@ -795,9 +796,9 @@ begin end else if (s = 'str') or (s = 'inlineStr') then begin // literal string - formulaStr := cell^.FormulaValue; +// formulaStr := cell^.FormulaValue; sheet.WriteText(cell, datastr); - cell^.FormulaValue := formulaStr; +// cell^.FormulaValue := formulaStr; end else if s = 'b' then // boolean @@ -2068,6 +2069,8 @@ begin sheetData.ID := GetAttrvalue(node, 'sheetID'); sheetData.Hidden := GetAttrValue(node, 'state') = 'hidden'; FSheetList.Add(sheetData); + // Create worksheet - needed because of 3d references + (FWorkbook as TsWorkbook).AddWorksheet(sheetData.Name, true); end; node := node.NextSibling; end; @@ -2520,8 +2523,12 @@ begin // read worksheets for i:=0 to FSheetList.Count-1 do begin + { // Create worksheet FWorksheet := (FWorkbook as TsWorkbook).AddWorksheet(TSheetData(FSheetList[i]).Name, true); + } + // Worksheets are already created... + FWorksheet := (FWorkbook as TsWorkbook).GetWorksheetByName(TSheetData(FSheetList[i]).Name); if TSheetData(FSheetList[i]).Hidden then FWorksheet.Options := FWorksheet.Options + [soHidden]; @@ -5219,10 +5226,15 @@ var cellPosText: String; lStyleIndex: Integer; t, v: String; + formula: PsFormula; + formulaStr: String; begin cellPosText := TsWorksheet.CellPosToText(ARow, ACol); lStyleIndex := GetStyleIndex(ACell); + formula := TsWorksheet(FWorksheet).Formulas.FindFormula(ARow, ACol); + formulaStr := PrepareFormula(formula^.Text); + case ACell^.ContentType of cctFormula: begin @@ -5265,7 +5277,7 @@ begin '%s' + '', [ CellPosText, lStyleIndex, t, - PrepareFormula(ACell^.FormulaValue), + formulaStr, v ])); end; diff --git a/components/fpspreadsheet/source/visual/fpspreadsheetctrls.pas b/components/fpspreadsheet/source/visual/fpspreadsheetctrls.pas index 1a8398fe1..a72032583 100644 --- a/components/fpspreadsheet/source/visual/fpspreadsheetctrls.pas +++ b/components/fpspreadsheet/source/visual/fpspreadsheetctrls.pas @@ -162,12 +162,12 @@ type published {@@ Automatically detects the fileformat when loading the spreadsheet file specified by FileName } - property AutoDetectFormat: Boolean read FAutoDetectFormat write FAutoDetectFormat; + property AutoDetectFormat: Boolean read FAutoDetectFormat write FAutoDetectFormat default true; {@@ File format of the next spreadsheet file to be loaded by means of the Filename property. Not used when AutoDetectFormat is TRUE. Note that if FileFormat is sfUser then the format ID must be specified at runtime. } - property FileFormat: TsSpreadsheetFormat read GetFileFormat write SetFileFormat; + property FileFormat: TsSpreadsheetFormat read GetFileFormat write SetFileFormat default sfOOXML; {@@ Name of the loaded spreadsheet file which is loaded by assigning a file name to this property. Format detection is determined by the properties AutoDetectFormat and FileFormat. Using this property loads the file at @@ -752,7 +752,8 @@ constructor TsWorkbookSource.Create(AOwner: TComponent); begin inherited Create(AOwner); FListeners := TFPList.Create; - FFileFormatID := ord(sfExcel8); + FFileFormatID := ord(sfOOXML); + FAutoDetectFormat := True; CreateNewWorkbook; end; diff --git a/components/fpspreadsheet/source/visual/fpspreadsheetgrid.pas b/components/fpspreadsheet/source/visual/fpspreadsheetgrid.pas index ad7c1e147..434b2bc4d 100644 --- a/components/fpspreadsheet/source/visual/fpspreadsheetgrid.pas +++ b/components/fpspreadsheet/source/visual/fpspreadsheetgrid.pas @@ -6587,7 +6587,8 @@ begin // If the cell already exists and contains a formula then the formula must be // removed. The formula would dominate over the data value. cell := Worksheet.FindCell(r, c); - if HasFormula(cell) then cell^.FormulaValue := ''; + if HasFormula(cell) then + Worksheet.UseformulaInCell(cell, nil); //cell^.FormulaValue := ''; if VarIsNull(AValue) then Worksheet.WriteBlank(r, c) diff --git a/components/fpspreadsheet/tests/copytests.pas b/components/fpspreadsheet/tests/copytests.pas index cd8cd813b..15199fd65 100644 --- a/components/fpspreadsheet/tests/copytests.pas +++ b/components/fpspreadsheet/tests/copytests.pas @@ -161,13 +161,14 @@ var i, row, col: Integer; cell: PCell; expectedFormula: String; + expectedStr, actualStr: String; begin TempFile := GetTempFileName; MyWorkbook := TsWorkbook.Create; try -// MyWorkbook.Options := MyWorkbook.Options + [boCalcBeforeSaving]; //boAutoCalc]; + MyWorkbook.Options := MyWorkbook.Options + [boCalcBeforeSaving]; //boAutoCalc]; MyWorkSheet:= MyWorkBook.AddWorksheet(CopyTestSheet); @@ -212,6 +213,7 @@ begin begin cell := Myworksheet.FindCell(row, 0); case ATestKind of + // 0: ; // don't copy, just write the original file for debugging 1: MyWorksheet.CopyValue(cell, row, 2); 2: MyWorksheet.CopyValue(cell, row, 1); 3: MyWorksheet.CopyFormat(cell, row, 2); @@ -233,6 +235,7 @@ begin // Read spreadsheet file... MyWorkbook.ReadFromFile(TempFile, AFormat); MyWorksheet := MyWorkbook.GetFirstWorksheet; + MyWorksheet.CalcFormulas; if odd(ATestKind) then col := 2 else col := 1; @@ -335,9 +338,12 @@ begin ) else begin + expectedStr := SetToString(PTypeInfo(TypeInfo(TsUsedFormattingFields)), + integer(Sourcecells[i+col-2].UsedformattingFields), true); + actualStr := SetToString(PTypeInfo(TypeInfo(TsUsedFormattingFields)), + integer(MyWorksheet.ReadUsedFormatting(cell)), true); CheckEquals( - true, - SourceCells[i+(col-2)].UsedFormattingFields = MyWorksheet.ReadUsedFormatting(cell), + expectedStr, actualStr, 'Used formatting fields mismatch, cell ' + CellNotation(myWorksheet, row, col) ); if (uffBackground in SourceCells[i+(col-2)].UsedFormattingFields) then @@ -390,7 +396,8 @@ begin else CheckEquals( SourceCells[i+col-2].FormulaValue, - cell^.Formulavalue, + MyWorksheet.ReadFormula(cell), +// cell^.Formulavalue, 'Formula mismatch, cell ' + CellNotation(MyWorksheet, row, col) ); 5: @@ -403,7 +410,8 @@ begin end; CheckEquals( expectedFormula, - cell^.FormulaValue, + MyWorksheet.ReadFormula(cell), +// cell^.FormulaValue, 'Formula mismatch, cell ' + Cellnotation(Myworksheet, row, col) ); end; @@ -421,7 +429,8 @@ begin end; CheckEquals( expectedFormula, - cell^.FormulaValue, + MyWorksheet.ReadFormula(cell), +// cell^.FormulaValue, 'Formula mismatch, cell ' + Cellnotation(Myworksheet, row, col) ); end; diff --git a/components/fpspreadsheet/tests/formulatests.pas b/components/fpspreadsheet/tests/formulatests.pas index d97e229ce..c170d32ca 100644 --- a/components/fpspreadsheet/tests/formulatests.pas +++ b/components/fpspreadsheet/tests/formulatests.pas @@ -763,7 +763,7 @@ begin cctEmpty : actual := EmptyResult; else fail('ContentType not supported'); end; - actualformula := cell^.FormulaValue; + actualformula := sheet1.Formulas.FindFormula(cell)^.Text; //cell^.FormulaValue; expected := SollValues[row]; // Cell does not store integers! diff --git a/components/fpspreadsheet/tests/insertdeletetests.pas b/components/fpspreadsheet/tests/insertdeletetests.pas index d16c61cdd..bba8f6759 100644 --- a/components/fpspreadsheet/tests/insertdeletetests.pas +++ b/components/fpspreadsheet/tests/insertdeletetests.pas @@ -565,10 +565,10 @@ begin '67890123'; DeleteCol := 2; Formula := 'C3'; - SollFormula := '#REF!'; // col index unchanged due to deletion after cell + SollFormula := '#REF!'; // cell needec by formula does not exist any more SollLayout := '1245678|'+ '2356789|'+ - '346E890|'+ // "E" = error + '346E890|'+ // "E" = error '4578901|'+ '5689012|'+ '6790123'; @@ -617,7 +617,7 @@ begin Layout := '12345678|'+ '23456789|'+ '3456F890|'+ // "F" = Formula in row 2, col 4 - '45678901|'+ + '45678901|'+ // delete this row '56789012|'+ '67890123'; DeleteRow := 3; @@ -1010,6 +1010,7 @@ begin if InsDelTestData[ATestIndex].DeleteRow >= 0 then MyWorksheet.DeleteRow(InsDelTestData[ATestIndex].DeleteRow); + MyWorkbook.CalcFormulas; MyWorkBook.WriteToFile(TempFile, AFormat, true); finally MyWorkbook.Free; diff --git a/components/fpspreadsheet/tests/singleformulatests.pas b/components/fpspreadsheet/tests/singleformulatests.pas index 72a2a1dd8..112bd4260 100644 --- a/components/fpspreadsheet/tests/singleformulatests.pas +++ b/components/fpspreadsheet/tests/singleformulatests.pas @@ -140,8 +140,8 @@ begin cell := worksheet.WriteFormula(TESTCELL_ROW, TESTCELL_COL, AFormula); // Read formula before saving - actualFormula := cell^.Formulavalue; - CheckEquals(AFormula, actualFormula, 'Unsaved formula text mismatch'); + actualFormula := worksheet.ReadFormula(cell); + CheckEquals(AExpectedFormula, actualFormula, 'Unsaved formula text mismatch'); // Read calculated value before saving actualvalue := worksheet.ReadAsNumber(TESTCELL_ROW, TESTCELL_COL); @@ -165,7 +165,8 @@ begin CheckEquals(AExpected, actualValue, 'Saved calculated value mismatch'); cell := worksheet.FindCell(TESTCELL_ROW, TESTCELL_COL); - actualformula := cell^.FormulaValue; + actualformula := worksheet.Formulas.FindFormula(cell)^.Text; +// actualformula := cell^.FormulaValue; // When writing ranges are reconstructed in correct order. CheckEquals(AExpectedFormula, actualformula, 'Saved formula text mismatch.'); finally @@ -302,15 +303,16 @@ end; { --- } +{ Range formulas in which the parts are not ordered. They will be put into the + correct order when then formula is written to the worksheet. --> the + expected range must be in correct order. } procedure TSpreadSingleFormulaTests.SumMultiSheetRange_FlippedSheetsAndCells_OOXML; begin - // In OOXML the range is written literally. - TestFloatFormula('SUM(Sheet3:Sheet2!C5:C3)', 55.0, ftkCellRangeSheetRange, sfOOXML); + TestFloatFormula('SUM(Sheet3:Sheet2!C5:C3)', 55.0, ftkCellRangeSheetRange, sfOOXML, 'SUM(Sheet2:Sheet3!C3:C5)'); end; procedure TSpreadSingleFormulaTests.SumMultiSheetRange_FlippedSheetsAndCells_ODS; begin - // ODS requires conversion of the formula which results in reordering of ranges. TestFloatFormula('SUM(Sheet3:Sheet2!C5:C3)', 55.0, ftkCellRangeSheetRange, sfOpenDocument, 'SUM(Sheet2:Sheet3!C3:C5)'); end; @@ -322,14 +324,12 @@ end; procedure TSpreadSingleFormulaTests.SumMultiSheetRange_FlippedCells_OOXML; begin - // In OOXML the range is written literally. - TestFloatFormula('SUM(Sheet2:Sheet3!C5:C3)', 55.0, ftkCellRangeSheetRange, sfOOXML); + TestFloatFormula('SUM(Sheet2:Sheet3!C5:C3)', 55.0, ftkCellRangeSheetRange, sfOOXML, 'SUM(Sheet2:Sheet3!C3:C5)'); end; procedure TSpreadSingleFormulaTests.SumMultiSheetRange_FlippedSheets_OOXML; begin - // In OOXML the range is written literally. - TestFloatFormula('SUM(Sheet3:Sheet2!C3:C5)', 55.0, ftkCellRangeSheetRange, sfOOXML); + TestFloatFormula('SUM(Sheet3:Sheet2!C3:C5)', 55.0, ftkCellRangeSheetRange, sfOOXML, 'SUM(Sheet2:Sheet3!C3:C5)'); end; diff --git a/components/fpspreadsheet/tests/testcases_calcrpnformula.inc b/components/fpspreadsheet/tests/testcases_calcrpnformula.inc index 291baa513..7bc0f27dc 100644 --- a/components/fpspreadsheet/tests/testcases_calcrpnformula.inc +++ b/components/fpspreadsheet/tests/testcases_calcrpnformula.inc @@ -1026,7 +1026,10 @@ else Myworksheet.WriteFormula(Row, 1, formula); SetLength(sollValues, Row+1); - sollValues[Row] := FloatResult(arccosh(number)); + if AFormat = sfExcel2 then + sollValues[Row] := ErrorResult(errFormulaNotSupported) + else + sollValues[Row] := FloatResult(arccosh(number)); MyWorksheet.WriteNumber(Row, 2, sollValues[Row].ResFloat); // ACOSH - error result (arccos is not defined below 1 @@ -1043,6 +1046,9 @@ SetLength(sollValues, Row+1); if AFormat = sfOpenDocument then sollValues[Row] := FloatResult(0) + else + if AFormat = sfExcel2 then + sollValues[Row] := ErrorResult(errFormulaNotSupported) else sollValues[Row] := ErrorResult(errOverFlow); @@ -1090,7 +1096,10 @@ else MyWorksheet.WriteFormula(Row, 1, formula); SetLength(sollValues, Row+1); - sollValues[Row] := FloatResult(arcsinh(number)); + if AFormat = sfExcel2 then + sollValues[Row] := ErrorResult(errFormulaNotSupported) + else + sollValues[Row] := FloatResult(arcsinh(number)); MyWorksheet.WriteNumber(Row, 2, sollValues[Row].ResFloat); // ATAN @@ -1120,7 +1129,10 @@ else MyWorksheet.WriteFormula(Row, 1, formula); SetLength(sollValues, Row+1); - sollValues[Row] := FloatResult(arctanh(number)); + if AFormat = sfExcel2 then + sollValues[Row] := ErrorResult(errFormulaNotSupported) + else + sollValues[Row] := FloatResult(arctanh(number)); MyWorksheet.WriteNumber(Row, 2, sollValues[Row].ResFloat); // ATANH - error result (arctan is only defined within ]-1,1[ @@ -1137,6 +1149,9 @@ SetLength(sollValues, Row+1); if AFormat = sfOpenDocument then sollValues[Row] := FloatResult(0) + else + if AFormat = sfExcel2 then + sollValues[Row] := ErrorResult(errFormulaNotSupported) else sollValues[Row] := ErrorResult(errOverFlow); @@ -1185,8 +1200,12 @@ else MyWorksheet.WriteFormula(Row, 1, formula); SetLength(sollValues, Row+1); - sollValues[Row] := FloatResult(cosh(number)); - MyWorksheet.WriteNumber(Row, 2, sollValues[Row].ResFloat); + if AFormat = sfExcel2 then + sollValues[Row] := ErrorResult(errFormulaNotSupported) + else begin + sollValues[Row] := FloatResult(cosh(number)); + MyWorksheet.WriteNumber(Row, 2, sollValues[Row].ResFloat); + end; // DEGREES if AFormat <> sfExcel2 then @@ -1678,7 +1697,10 @@ MyWorksheet.WriteFormula(Row, 1, formula); MyWorksheet.WriteNumber(Row, 2, sinh(number)); SetLength(sollValues, Row+1); - sollValues[Row] := FloatResult(sinh(number)); + if AFormat = sfExcel2 then + sollValues[Row] := ErrorResult(errFormulaNotSupported) + else + sollValues[Row] := FloatResult(sinh(number)); // SINH of cell value inc(Row); @@ -1692,7 +1714,10 @@ MyWorksheet.WriteFormula(Row, 1, formula); MyWorksheet.WriteNumber(Row, 2, sinh(cellB1)); SetLength(sollValues, Row+1); - sollValues[Row] := FloatResult(sinh(cellB1)); + if AFormat = sfExcel2 then + sollValues[Row] := ErrorResult(errFormulaNotSupported) + else + sollValues[Row] := FloatResult(sinh(cellB1)); // SQRT - valid result inc(Row); @@ -1785,7 +1810,10 @@ MyWorksheet.WriteFormula(Row, 1, formula); MyWorksheet.WriteNumber(Row, 2, tanh(number)); SetLength(sollValues, Row+1); - sollValues[Row] := FloatResult(tanh(number)); + if AFormat = sfExcel2 then + sollValues[Row] := ErrorResult(errFormulaNotSupported) + else + sollValues[Row] := FloatResult(tanh(number)); // TANH of cell value inc(Row); @@ -1799,7 +1827,11 @@ MyWorksheet.WriteFormula(Row, 1, formula); MyWorksheet.WriteNumber(Row, 2, tanh(cellB1)); SetLength(sollValues, Row+1); - sollValues[Row] := FloatResult(tanh(cellB1)); + if AFormat = sfExcel2 then + sollValues[Row] := ErrorResult(errFormulaNotSupported) + else + sollValues[Row] := FloatResult(tanh(cellB1)); + {------------------------------------------------------------------------------} { Date/time functions } @@ -2087,7 +2119,10 @@ Myworksheet.WriteDateTimeFormat(Row, 1, nfShortDate); Myworksheet.WriteDateTime(Row, 2, Date(), nfShortDate); SetLength(sollValues, Row+1); - sollValues[Row] := DateTimeResult(Date()); + if AFormat = sfExcel2 then + sollValues[Row] := ErrorResult(errFormulaNotSupported) + else + sollValues[Row] := DateTimeResult(Date()); // WEEKDAY / argument number inc(Row);