diff --git a/components/fpspreadsheet/fpspreadsheet.pas b/components/fpspreadsheet/fpspreadsheet.pas index bbb924600..a104ee11a 100755 --- a/components/fpspreadsheet/fpspreadsheet.pas +++ b/components/fpspreadsheet/fpspreadsheet.pas @@ -678,7 +678,8 @@ type { Formulas } procedure CalcFormulas; function ReadRPNFormulaAsString(ACell: PCell): String; - function UseSharedFormula(ARow, ACol: Cardinal; ASharedFormulaBase: PCell): PCell; + function UseSharedFormula(ARow, ACol: Cardinal; ASharedFormulaBase: PCell): PCell; overload; + procedure UseSharedFormula(ACellRangeStr: String; ASharedFormulaBase: PCell); overload; { Data manipulation methods - For Cells } procedure CopyCell(AFromRow, AFromCol, AToRow, AToCol: Cardinal; AFromWorksheet: TsWorksheet); @@ -2956,6 +2957,33 @@ begin [GetCellString(ARow, ACol)]); end; +{@@ + Uses the formula defined in cell "ASharedFormulaBase" as a shared formula in + all cells of the given cell range. + + @param ACellRangeStr Range of cells which will use the shared formula. + The range is given as a string in Excel notation, + such as A1:B5, or A1 + @param ASharedFormulaBase Cell containing the formula to be shared +} +procedure TsWorksheet.UseSharedFormula(ACellRangeStr: String; ASharedFormulaBase: PCell); +var + r, c, r1, c1, r2, c2: Cardinal; + ok: Boolean; +begin + if pos(':', ACellRangeStr) = 0 then + begin + ok := ParseCellString(ACellRangeStr, r1, c1); + r2 := r1; + c2 := c1; + end else + ok := ParseCellRangeString(ACellRangeStr, r1, c1, r2, c2); + if ok then + for r := r1 to r2 do + for c := c1 to c2 do + UseSharedFormula(r, c, ASharedFormulaBase); +end; + {@@ Writes UTF-8 encoded text to a cell. diff --git a/components/fpspreadsheet/fpsutils.pas b/components/fpspreadsheet/fpsutils.pas index e11f63887..a1fc9f826 100644 --- a/components/fpspreadsheet/fpsutils.pas +++ b/components/fpspreadsheet/fpsutils.pas @@ -64,7 +64,9 @@ function ParseIntervalString(const AStr: string; out ADirection: TsSelectionDirection): Boolean; function ParseCellRangeString(const AStr: string; out AFirstCellRow, AFirstCellCol, ALastCellRow, ALastCellCol: Cardinal; - out AFlags: TsRelFlags): Boolean; + out AFlags: TsRelFlags): Boolean; overload; +function ParseCellRangeString(const AStr: string; + out AFirstCellRow, AFirstCellCol, ALastCellRow, ALastCellCol: Cardinal): Boolean; overload; function ParseCellString(const AStr: string; out ACellRow, ACellCol: Cardinal; out AFlags: TsRelFlags): Boolean; overload; function ParseCellString(const AStr: string; @@ -76,7 +78,8 @@ function ParseCellColString(const AStr: string; function GetColString(AColIndex: Integer): String; function GetCellString(ARow,ACol: Cardinal; AFlags: TsRelFlags = [rfRelRow, rfRelCol]): String; -function GetCellRangeString(ARow1, ACol1, ARow2, ACol2: Cardinal; AFlags: TsRelFlags): String; +function GetCellRangeString(ARow1, ACol1, ARow2, ACol2: Cardinal; + AFlags: TsRelFlags = [rfRelRow, rfRelCol, rfRelRow2, rfRelCol2]): String; function GetErrorValueStr(AErrorValue: TsErrorValue): String; @@ -427,6 +430,26 @@ begin end; +{@@ + Parses strings like A5:C10 into a range selection information. + Information on relative/absolute cells is ignored. + + @param AStr Cell range string, such as A5:C10 + @param AFirstCellRow Row index of the top/left cell of the range (output) + @param AFirstCellCol Column index of the top/left cell of the range (output) + @param ALastCellRow Row index of the bottom/right cell of the range (output) + @param ALastCellCol Column index of the bottom/right cell of the range (output) + @return false if the string is not a valid cell range +} +function ParseCellRangeString(const AStr: string; + out AFirstCellRow, AFirstCellCol, ALastCellRow, ALastCellCol: Cardinal): Boolean; +var + flags: TsRelFlags; +begin + Result := ParseCellRangeString(AStr, AFirstCellRow, AFirstCellCol, ALastCellRow, ALastCellCol, flags); +end; + + {@@ Parses a cell string, like 'A1' into zero-based column and row numbers Note that there can be several letters to address for more than 26 columns. @@ -647,7 +670,8 @@ end; @example ARow1 = 0, ACol1 = 0, ARow = 2, ACol = 1, AFlags = [rfRelRow, rfRelRow2] --> $A1:$B3 } -function GetCellRangeString(ARow1, ACol1, ARow2, ACol2: Cardinal; AFlags: TsRelFlags): String; +function GetCellRangeString(ARow1, ACol1, ARow2, ACol2: Cardinal; + AFlags: TsRelFlags = [rfRelRow, rfRelCol, rfRelRow2, rfRelCol2]): String; begin Result := Format('%s%s%s%d:%s%s%s%d', [ RELCHAR[rfRelCol in AFlags], GetColString(ACol1), diff --git a/components/fpspreadsheet/xlsbiff2.pas b/components/fpspreadsheet/xlsbiff2.pas index 11fdca270..bd16f4e5b 100755 --- a/components/fpspreadsheet/xlsbiff2.pas +++ b/components/fpspreadsheet/xlsbiff2.pas @@ -790,7 +790,9 @@ end; { Reads the cell coordiantes of the top/left cell of a range using a shared formula. This cell contains the rpn token sequence of the formula. - Is overridden because BIFF2 has 1 byte for column. } + Is overridden because BIFF2 has 1 byte for column. + Code is not called for shared formulas (which are not supported by BIFF2), but + maybe for array formulas. } procedure TsSpreadBIFF2Reader.ReadRPNSharedFormulaBase(AStream: TStream; out ARow, ACol: Cardinal); begin diff --git a/components/fpspreadsheet/xlscommon.pas b/components/fpspreadsheet/xlscommon.pas index e35f62069..56764960c 100644 --- a/components/fpspreadsheet/xlscommon.pas +++ b/components/fpspreadsheet/xlscommon.pas @@ -535,7 +535,8 @@ type procedure WriteSelections(AStream: TStream; ASheet: TsWorksheet); // Writes out a shared formula procedure WriteSharedFormula(AStream: TStream; ACell: PCell); virtual; - procedure WriteSharedFormulaRange(AStream: TStream; const ARange: TRect); virtual; + procedure WriteSharedFormulaRange(AStream: TStream; + AFirstRow, AFirstCol, ALastRow, ALastCol: Cardinal); virtual; procedure WriteSheetPR(AStream: TStream); procedure WriteStringRecord(AStream: TStream; AString: String); virtual; // Writes cell content received by workbook in OnNeedCellData event @@ -2882,8 +2883,7 @@ end; the formulas in each cell. In BIFF2 WriteSharedFormula must not do anything. } procedure TsSpreadBIFFWriter.WriteSharedFormula(AStream: TStream; ACell: PCell); var - range: TRect; - node: TAVLTreeNode; + r, c, r1, r2, c1, c2: Cardinal; cell: PCell; RPNLength: word; recordSizePos: Int64; @@ -2892,32 +2892,29 @@ var i: Integer; begin // Determine cell range covered by the shared formula in ACell. - range := Rect(-1, -1, -1, -1); - node := FWorksheet.Cells.FindLowest; - while Assigned(node) do begin - cell := PCell(node.Data); - if cell.SharedFormulaBase = ACell then begin - // Nodes are ordered along rows --> the first cell met must be the left border of the range - if range.Left = -1 then - range.Left := cell.Col - else - if cell.Col < range.Left then begin - FWorkbook.AddErrorMsg('Non-rectangular cell range covered by shared formula in cell %s', - [GetCellString(ACell^.Row, ACell^.Col)]); - exit; - end; - // The right border of the range must have the max col index - if range.Right = -1 then - range.Right := cell.Col - else if cell.Col > range.Right then - range.Right := cell.Col; - // The first cell met must be the top border of the range - if range.Top = -1 then - range.Top := Cell.Row; - // dto. with bottom border - range.Bottom := Cell.Row; - end; - node := FWorksheet.Cells.FindSuccessor(node); + // Find range of cells using this shared formula + r1 := ACell^.Row; r2 := r1; + c1 := ACell^.Col; c2 := c1; + r := r1; + c := c1; + while c <= FWorksheet.GetLastColIndex do + begin + cell := FWorksheet.FindCell(r, c); + if (cell <> nil) and (cell^.SharedFormulaBase = ACell^.SharedFormulaBase) then + c2 := c + else + break; + inc(c); + end; + c := c1; + while r <= FWorksheet.GetLastRowIndex do + begin + cell := FWorksheet.FindCell(r, c); + if (cell <> nil) and (cell^.SharedFormulaBase <> ACell^.SharedFormulaBase) then + r2 := r + else + break; + inc(r); end; // Write BIFF record ID and size @@ -2927,13 +2924,13 @@ begin startPos := AStream.Position; // Write borders of cell range covered by the formula - WriteSharedFormulaRange(AStream, range); + WriteSharedFormulaRange(AStream, r1, c1, r2, c2); // Not used AStream.WriteByte(0); // Number of existing formula records - AStream.WriteByte((range.Right-range.Left+1)*(range.Bottom-range.Top+1)); + AStream.WriteByte((r2-r1+1) * (c2-c1+1)); // Copy the formula (we don't want to overwrite the cell formulas) // and adjust relative references @@ -2956,16 +2953,20 @@ end; Valid for BIFF5 and BIFF8 - BIFF8 writes 8-bit column index as well. No need for BIFF2 which does not support shared formulas. } procedure TsSpreadBIFFWriter.WriteSharedFormulaRange(AStream: TStream; - const ARange: TRect); + AFirstRow, AFirstCol, ALastRow, ALastCol: Cardinal); +var + c: Word; begin // Index to first row - AStream.WriteWord(WordToLE(ARange.Top)); + AStream.WriteWord(WordToLE(AFirstRow)); // Index to last row - AStream.WriteWord(WordToLE(ARange.Bottom)); + AStream.WriteWord(WordToLE(ALastRow)); // Index to first column - AStream.WriteByte(Lo(ARange.Left)); + c := Lo(AFirstCol); + AStream.WriteByte(Lo(c)); // Index to last rcolumn - AStream.WriteByte(Lo(ARange.Right)); + c := Lo(ALastCol); + AStream.WriteByte(Lo(c)); end; diff --git a/components/fpspreadsheet/xlsxooxml.pas b/components/fpspreadsheet/xlsxooxml.pas index 14028b90d..d8a7390b5 100755 --- a/components/fpspreadsheet/xlsxooxml.pas +++ b/components/fpspreadsheet/xlsxooxml.pas @@ -2266,6 +2266,8 @@ end; procedure TsSpreadOOXMLWriter.WriteWorksheet(AWorksheet: TsWorksheet); begin + FWorksheet := AWorksheet; + FCurSheetNum := Length(FSSheets); SetLength(FSSheets, FCurSheetNum + 1); @@ -2499,16 +2501,69 @@ procedure TsSpreadOOXMLWriter.WriteFormula(AStream: TStream; var cellPosText: String; lStyleIndex: Integer; + r, c, r2, c2: Cardinal; + cell: PCell; + id: Cardinal; begin cellPosText := TsWorksheet.CellPosToText(ARow, ACol); lStyleIndex := GetStyleIndex(ACell); - AppendToStream(AStream, Format( - '' + - '%s' + - '', [ - CellPosText, lStyleIndex, - PrepareFormula(ACell^.FormulaValue) + // Cell uses a shared formula + if Assigned(ACell^.SharedFormulaBase) then begin + // Cell is base of the shared formula, i.e. contains the shared formula + if (ACell = ACell^.SharedFormulaBase) then + begin + // Find range of cells using this shared formula + r2 := ACell^.Row; + c2 := ACell^.Col; + c := c2; + r := r2; + while c <= FWorksheet.GetLastColIndex do + begin + cell := FWorksheet.FindCell(r, c); + if (cell <> nil) and (cell^.SharedFormulaBase = ACell^.SharedFormulaBase) then + c2 := c + else + break; + inc(c); + end; + c := ACell^.Col; + while r <= FWorksheet.GetLastRowIndex do + begin + cell := FWorksheet.FindCell(r, c); + if (cell <> nil) and (cell^.SharedFormulaBase <> ACell^.SharedFormulaBase) then + r2 := r + else + break; + inc(r); + end; + + AppendToStream(AStream, Format( + '' + + '%s' + + '', [ + CellPosText, lStyleIndex, + GetCellRangeString(ACell^.Row, ACell^.Col, r2, c2), + PtrInt(ACell), // Use the cell pointer as ID of the shared formula + PrepareFormula(ACell^.FormulaValue) + ])); + end else + // Cell uses the shared formula + AppendToStream(AStream, Format( + '' + + '' + + '', [ + CellPosText, lStyleIndex, + PtrInt(ACell^.SharedFormulaBase) // ID of the shared formula + ])); + end else + // "normal" formula + AppendToStream(AStream, Format( + '' + + '%s' + + '', [ + CellPosText, lStyleIndex, + PrepareFormula(ACell^.FormulaValue) ])); end;