From ef46ccd90d0ca2da066ce2723b90ed7a44a097fa Mon Sep 17 00:00:00 2001 From: wp_xxyyzz Date: Tue, 20 Feb 2018 23:02:45 +0000 Subject: [PATCH] fpspreadsheet: Add workbook option boIgnoreFormulas for writing unsupported formulas (See forum https://forum.lazarus.freepascal.org/index.php/topic,40146.0.html). git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@6209 8e941d3f-bd1b-0410-a28a-d453659cc2b4 --- .../examples/other/demo_ignore_formula.lpi | 64 ++++++++++ .../examples/other/demo_ignore_formula.lpr | 114 ++++++++++++++++++ .../source/common/fpsopendocument.pas | 112 +++++++++-------- .../source/common/fpspreadsheet.pas | 37 ++++-- 4 files changed, 262 insertions(+), 65 deletions(-) create mode 100644 components/fpspreadsheet/examples/other/demo_ignore_formula.lpi create mode 100644 components/fpspreadsheet/examples/other/demo_ignore_formula.lpr diff --git a/components/fpspreadsheet/examples/other/demo_ignore_formula.lpi b/components/fpspreadsheet/examples/other/demo_ignore_formula.lpi new file mode 100644 index 000000000..b1fb06da7 --- /dev/null +++ b/components/fpspreadsheet/examples/other/demo_ignore_formula.lpi @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + <UseAppBundle Value="False"/> + <ResourceType Value="res"/> + </General> + <BuildModes Count="1"> + <Item1 Name="Default" Default="True"/> + </BuildModes> + <PublishOptions> + <Version Value="2"/> + </PublishOptions> + <RunParams> + <FormatVersion Value="2"/> + <Modes Count="0"/> + </RunParams> + <RequiredPackages Count="1"> + <Item1> + <PackageName Value="laz_fpspreadsheet"/> + </Item1> + </RequiredPackages> + <Units Count="1"> + <Unit0> + <Filename Value="demo_ignore_formula.lpr"/> + <IsPartOfProject Value="True"/> + </Unit0> + </Units> + </ProjectOptions> + <CompilerOptions> + <Version Value="11"/> + <PathDelim Value="\"/> + <Target> + <Filename Value="demo_ignore_formula"/> + </Target> + <SearchPaths> + <IncludeFiles Value="$(ProjOutDir)"/> + <UnitOutputDirectory Value="lib\$(TargetCPU)-$(TargetOS)"/> + </SearchPaths> + </CompilerOptions> + <Debugging> + <Exceptions Count="3"> + <Item1> + <Name Value="EAbort"/> + </Item1> + <Item2> + <Name Value="ECodetoolError"/> + </Item2> + <Item3> + <Name Value="EFOpenError"/> + </Item3> + </Exceptions> + </Debugging> +</CONFIG> diff --git a/components/fpspreadsheet/examples/other/demo_ignore_formula.lpr b/components/fpspreadsheet/examples/other/demo_ignore_formula.lpr new file mode 100644 index 000000000..5c01752b2 --- /dev/null +++ b/components/fpspreadsheet/examples/other/demo_ignore_formula.lpr @@ -0,0 +1,114 @@ +{ This example uses the "ignoreFormula" workbook option to create an ods + file with an external reference. + + NOTE: The external reference is not calculated. This will happen when + LibreOffice Calc loads the file. When the file is closed in LOCalc + confirmation must be given to save the file because it has been changed + by LOCalc. + + This method does not work with Excel because it writes an additonal + folder and xml files for external links. } + +program demo_ignore_formula; + +{$mode objfpc}{$H+} + +{$DEFINE ODS} +{.$DEFINE XLSX} // <---- NOT WORKING + +uses + SysUtils, FileUtil, + fpsTypes, fpsUtils, fpSpreadsheet, fpsOpenDocument, xlsxOOXML; + +const + {$IFDEF ODS} + FILE_FORMAT = sfOpenDocument; + MASTER_FILE = 'master.ods'; + EXTERNAL_FILE = 'external.ods'; + {$ENDIF} + {$IFDEF XLSX} + FILE_FORMAT = sfOOXML; + MASTER_FILE = 'master.xlsx'; + EXTERNAL_FILE = 'external.xlsx'; + {$ENDIF} + EXTERNAL_SHEET = 'Sheet'; + CELL1 = 'A1'; + CELL2 = 'B1'; + + +var + book: TsWorkbook; + sheet: TsWorksheet; + cell: PCell; + + // example for an external ods reference: + // ='file:///D:/fpspreadsheet/examples/other/external.ods'#$Sheet.A1 + function ODS_ExtRef(AFilename, ASheetName, ACellAddr: String): String; + var + i: Integer; + begin + Result := ExpandFileName(AFileName); + for i:=1 to Length(Result) do + if Result[i] = '\' then Result[i] := '/'; + Result := Format('''file:///%s''#$%s.%s', [ + Result, ASheetName, ACellAddr + ]); + end; + + // example for an external xlsx reference: + // =[external.xlsx]Sheet!$A$1 + function XLSX_ExtRef(AFilename, ASheetName, ACellAddr: String): String; + var + r, c: Cardinal; + flags: TsRelFlags; + begin + ParseCellString(ACellAddr, r, c, flags); + Result := Format('[%s]%s!%s', [ + ExtractFileName(AFileName), ASheetName, GetCellString(r, c, []) + ]); + end; + + function ExtRef(AFileName, ASheetName, ACellAddr: String): String; + begin + {$IFDEF ODS} + Result := ODS_ExtRef(AFileName, ASheetName, ACellAddr); + {$ENDIF} + {$IFDEF XLSX} + Result := XLSX_ExtRef(AFilename, ASheetName, ACellAddr); + {$ENDIF} + end; + +begin + // Write external file + book := TsWorkbook.Create; + try + sheet := book.AddWorksheet(EXTERNAL_SHEET); + + cell := sheet.GetCell(CELL1); + sheet.WriteNumber(cell, 1000.0); + + cell := sheet.GetCell(CELL2); + sheet.WriteText(cell, 'Hallo'); + + book.WriteToFile(EXTERNAL_FILE, FILE_FORMAT, true); + finally + book.Free; + end; + + // Write ods and xlsx master files + book := TsWorkbook.Create; + try + // Instruct fpspreadsheet to leave the formula alone. + book.Options := book.Options + [boIgnoreFormulas]; + sheet := book.AddWorksheet('Sheet'); + + // Write external references + sheet.WriteFormula(0, 0, ExtRef(EXTERNAL_FILE, EXTERNAL_SHEET, CELL1)); + sheet.WriteFormula(1, 0, ExtRef(EXTERNAL_FILE, EXTERNAL_SHEET, CELL2)); + book.WriteToFile(MASTER_FILE, FILE_FORMAT, true); + + finally + book.Free; + end; +end. + diff --git a/components/fpspreadsheet/source/common/fpsopendocument.pas b/components/fpspreadsheet/source/common/fpsopendocument.pas index b900ec83e..9cdc44634 100644 --- a/components/fpspreadsheet/source/common/fpsopendocument.pas +++ b/components/fpspreadsheet/source/common/fpsopendocument.pas @@ -7446,8 +7446,10 @@ var comment: String; r1,c1,r2,c2: Cardinal; fmt: TsCellFormat; + ignoreFormulas: Boolean; begin Unused(ARow, ACol); + ignoreFormulas := (boIgnoreFormulas in FWorkbook.Options); // Style fmt := FWorkbook.GetCellFormat(ACell^.FormatIndex); @@ -7473,59 +7475,65 @@ begin if FWorksheet.HasHyperlink(ACell) then FWorkbook.AddErrorMsg(rsODSHyperlinksOfTextCellsOnly, [GetCellString(ARow, ACol)]); - // Convert string formula to the format needed by ods: semicolon list separators! - parser := TsSpreadsheetParser.Create(FWorksheet); - try - parser.Dialect := fdOpenDocument; - parser.Expression := ACell^.FormulaValue; - formula := Parser.LocalizedExpression[FPointSeparatorSettings]; - finally - parser.Free; - end; + if ignoreFormulas then begin + formula := ACell^.FormulaValue; + if (formula <> '') and (formula[1] = '=') then Delete(formula, 1, 1); + end else + begin + valueStr := ''; + // Convert string formula to the format needed by ods: semicolon list separators! + parser := TsSpreadsheetParser.Create(FWorksheet); + try + parser.Dialect := fdOpenDocument; + parser.Expression := ACell^.FormulaValue; + formula := Parser.LocalizedExpression[FPointSeparatorSettings]; + finally + parser.Free; + end; - valueStr := ''; - case ACell^.ContentType of - cctNumber: - begin - valuetype := 'float'; - value := ' office:value="' + Format('%g', [ACell^.NumberValue], FPointSeparatorSettings) + '"'; - end; - cctDateTime: - if trunc(ACell^.DateTimeValue) = 0 then - begin - valuetype := 'time'; - value := ' office:time-value="' + FormatDateTime(ISO8601FormatTimeOnly, ACell^.DateTimeValue) + '"'; - end - else - begin - valuetype := 'date'; - if frac(ACell^.DateTimeValue) = 0.0 then - value := ' office:date-value="' + FormatDateTime(ISO8601FormatDateOnly, ACell^.DateTimeValue) + '"' + case ACell^.ContentType of + cctNumber: + begin + valuetype := 'float'; + value := ' office:value="' + Format('%g', [ACell^.NumberValue], FPointSeparatorSettings) + '"'; + end; + cctDateTime: + if trunc(ACell^.DateTimeValue) = 0 then + begin + valuetype := 'time'; + value := ' office:time-value="' + FormatDateTime(ISO8601FormatTimeOnly, ACell^.DateTimeValue) + '"'; + end else - value := ' office:date-value="' + FormatDateTime(ISO8601FormatExtended, ACell^.DateTimeValue) + '"'; - end; - cctUTF8String: - begin - valuetype := 'string'; - value := ' office:string-value="' + ACell^.UTF8StringValue +'"'; - valueStr := '<text:p>' + ACell^.UTF8StringValue + '</text:p>'; - end; - cctBool: - begin - valuetype := 'boolean'; - value := ' office:boolean-value="' + BoolToStr(ACell^.BoolValue, 'true', 'false') + '"'; - end; - cctError: - if HasFormula(ACell) then - begin - // Open/LibreOffice always writes a float value 0 to the cell - valuetype := 'float'; // error as result of a formula - value := ' office:value="0"'; - end else - begin - valuetype := 'string" calcext:value-type="error'; // an error "constant" - value := ' office:value=""'; - end; + begin + valuetype := 'date'; + if frac(ACell^.DateTimeValue) = 0.0 then + value := ' office:date-value="' + FormatDateTime(ISO8601FormatDateOnly, ACell^.DateTimeValue) + '"' + else + value := ' office:date-value="' + FormatDateTime(ISO8601FormatExtended, ACell^.DateTimeValue) + '"'; + end; + cctUTF8String: + begin + valuetype := 'string'; + value := ' office:string-value="' + ACell^.UTF8StringValue +'"'; + valueStr := '<text:p>' + ACell^.UTF8StringValue + '</text:p>'; + end; + cctBool: + begin + valuetype := 'boolean'; + value := ' office:boolean-value="' + BoolToStr(ACell^.BoolValue, 'true', 'false') + '"'; + end; + cctError: + if HasFormula(ACell) then + begin + // Open/LibreOffice always writes a float value 0 to the cell + valuetype := 'float'; // error as result of a formula + value := ' office:value="0"'; + end else + begin + valuetype := 'string" calcext:value-type="error'; // an error "constant" + value := ' office:value=""'; + end; + end; end; { Fix special xml characters } @@ -7533,7 +7541,7 @@ begin { We are writing a very rudimentary formula here without result and result data type. Seems to work... } - if FWorksheet.GetCalcState(ACell) = csCalculated then + if not ignoreFormulas or (FWorksheet.GetCalcState(ACell) = csCalculated) then AppendToStream(AStream, Format( '<table:table-cell table:formula="=%s" office:value-type="%s"%s%s%s>' + comment + diff --git a/components/fpspreadsheet/source/common/fpspreadsheet.pas b/components/fpspreadsheet/source/common/fpspreadsheet.pas index d14109524..a6e32baeb 100644 --- a/components/fpspreadsheet/source/common/fpspreadsheet.pas +++ b/components/fpspreadsheet/source/common/fpspreadsheet.pas @@ -675,10 +675,12 @@ type @param boWriteZoomfactor Instructs the writer to write the current zoom factors of the worksheets to file. @param boAbortReadOnFormulaError Aborts reading if a formula error is - encountered } + encountered + @param boIgnoreFormulas Formulas are not checked and not calculated. + Cannot be used for biff formats. } TsWorkbookOption = (boVirtualMode, boBufStream, boFileStream, boAutoCalc, boCalcBeforeSaving, boReadFormulas, boWriteZoomFactor, - boAbortReadOnFormulaError); + boAbortReadOnFormulaError, boIgnoreFormulas); {@@ Set of option flags for the workbook } TsWorkbookOptions = set of TsWorkbookOption; @@ -1273,6 +1275,9 @@ var cell: PCell; formula: String; begin + if (boIgnoreFormulas in Workbook.Options) then + exit; + formula := ACell^.FormulaValue; ACell^.Flags := ACell^.Flags + [cfCalculating] - [cfCalculated]; @@ -1345,6 +1350,9 @@ procedure TsWorksheet.CalcFormulas; var cell: PCell; 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. inc(FWorkbook.FCalculationLock); @@ -5669,18 +5677,21 @@ begin if ACell = nil then exit; - // Remove '='; is not stored internally - if (AFormula <> '') and (AFormula[1] = '=') then - AFormula := Copy(AFormula, 2, Length(AFormula)); + if not (boIgnoreFormulas in Workbook.Options) then + begin + // Remove '='; is not stored internally + if (AFormula <> '') and (AFormula[1] = '=') then + AFormula := Copy(AFormula, 2, Length(AFormula)); - // Convert "localized" formula to standard format - if ALocalized then begin - parser := TsSpreadsheetParser.Create(self); - try - parser.LocalizedExpression[Workbook.FormatSettings] := AFormula; - AFormula := parser.Expression; - finally - parser.Free; + // Convert "localized" formula to standard format + if ALocalized then begin + parser := TsSpreadsheetParser.Create(self); + try + parser.LocalizedExpression[Workbook.FormatSettings] := AFormula; + AFormula := parser.Expression; + finally + parser.Free; + end; end; end;