fpspreadsheet: Add BIFF5 writer for 3d references. Add corresponding unit test. Some related bug fixes. Less hints.

git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@6401 8e941d3f-bd1b-0410-a28a-d453659cc2b4
This commit is contained in:
wp_xxyyzz
2018-05-10 14:31:37 +00:00
parent 74c64b7258
commit e98bda1717
8 changed files with 190 additions and 74 deletions

View File

@ -599,7 +599,6 @@ type
FIsRef: Boolean;
FOtherSheet: Boolean;
protected
procedure Check; override;
function GetCol: Cardinal;
function GetRow: Cardinal;
function GetSheetIndex: Integer;
@ -611,6 +610,7 @@ type
ACellString: String; Othersheet: Boolean); overload;
function AsRPNItem(ANext: PRPNItem): PRPNItem; override;
function AsString: string; override;
procedure Check; override;
function NodeType: TsResultType; override;
property Worksheet: TsWorksheet read FWorksheet;
end;
@ -1020,12 +1020,10 @@ var
S: String;
row, row2: Cardinal;
col, col2: Cardinal;
sheetName: String;
flags: TsRelFlags;
begin
C := CurrentChar;
if C = FSheetNameTerminator then C := NextPos;
sheetName := '';
while (not IsWordDelim(C)) and (C <> cNull) and (C <> FSheetNameTerminator) do
begin
if ((FParser.Dialect = fdOpenDocument) and (C = ']')) then begin

View File

@ -3760,7 +3760,7 @@ begin
'_': // Excel: Leave width of next character empty
begin
FToken := NextToken;
uch := UTF8CharacterToUnicode(FCurrent, n);
uch := UTF8CharacterToUnicode(FCurrent, n); // wp: Why Unicode ???
if n > 1 then
begin
AddElement(nftEmptyCharWidth, UnicodeToUTF8(uch));

View File

@ -2384,7 +2384,6 @@ var
valueType, calcExtValueType: String;
valueStr: String;
node: TDOMNode;
parser: TsSpreadsheetParser;
p: Integer;
fmt: PsCellFormat;
ns: String;
@ -7949,7 +7948,6 @@ var
fmt: TsCellFormat;
numFmtParams: TsNumFormatParams;
h,m,s,ms: Word;
mask: String;
begin
Unused(ARow, ACol);

View File

@ -111,9 +111,13 @@ type
procedure WriteFonts(AStream: TStream);
procedure WriteFORMAT(AStream: TStream; ANumFormatStr: String;
ANumFormatIndex: Integer); override;
procedure WriteGlobalLinkTable(AStream: TStream);
procedure WriteIndex(AStream: TStream);
procedure WriteLABEL(AStream: TStream; const ARow, ACol: Cardinal;
const AValue: string; ACell: PCell); override;
procedure WriteLocalLinkTable(AStream: TStream);
function WriteRPNCellAddress3D(AStream: TStream; ASheet, ARow, ACol: Cardinal;
AFlags: TsRelFlags): Word; override;
procedure WriteStringRecord(AStream: TStream; AString: String); override;
procedure WriteStyle(AStream: TStream);
procedure WriteWindow2(AStream: TStream; ASheet: TsWorksheet);
@ -394,13 +398,6 @@ begin
sheet := FWorkbook.AddWorksheet(ConvertEncoding(s, FCodePage, EncodingUTF8), true);
if sheetState <> 0 then
sheet.Options := sheet.Options + [soHidden];
(*
{ Temporarily store parameters for worksheet in FSheetList }
sheetData := TsSheetData.Create;
sheetData.Name := ConvertEncoding(s, FCodePage, EncodingUTF8);
sheetData.Hidden := sheetState <> 0;
FSheetList.Add(sheetData);
*)
end;
{@@ ----------------------------------------------------------------------------
@ -512,13 +509,7 @@ var
SectionEOF: Boolean = False;
RecordType: Word;
CurStreamPos: Int64;
// sheetData: TsSheetData;
begin (*
sheetData := TsSheetData(FSheetList[FCurSheetIndex]);
FWorksheet := FWorkbook.AddWorksheet(sheetData.Name, true);
if sheetData.Hidden then
FWorksheet.Options := FWorksheet.Options + [soHidden];
*)
begin
FWorksheet := FWorkbook.GetWorksheetByIndex(FCurSheetIndex);
while (not SectionEOF) do
begin
@ -1188,8 +1179,9 @@ begin
WriteWindowProtect(AStream, bpLockWindows in Workbook.Protection);
WritePROTECT(AStream, bpLockStructure in Workbook.Protection);
WritePASSWORD(AStream, Workbook.CryptoInfo);
WriteEXTERNCOUNT(AStream);
WriteEXTERNSHEET(AStream);
WriteGlobalLinkTable(AStream);
// WriteEXTERNCOUNT(AStream);
// WriteEXTERNSHEET(AStream);
WriteDefinedNames(AStream);
WriteWINDOW1(AStream);
WriteFonts(AStream);
@ -1236,6 +1228,9 @@ begin
WriteMargin(AStream, 3); // 3 = bottom margin
WritePageSetup(AStream);
// Local link table
WriteLocalLinkTable(AStream);
// Protection
if FWorksheet.IsProtected then begin
WritePROTECT(AStream, true);
@ -1731,6 +1726,36 @@ begin
SetLength(buf, 0);
end;
{@@ ----------------------------------------------------------------------------
Writes the global link table: an EXTERNCOUNT record and several EXTERNSHEET
records
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF5Writer.WriteGlobalLinkTable(AStream: TStream);
var
L: TStringList;
i: Integer;
sheet: TsWorksheet;
begin
L := TStringList.Create;
try
for i := 0 to FWorkbook.GetWorksheetCount-1 do
begin
sheet := FWorkbook.GetWorksheetByIndex(i);
with sheet.PageLayout do
if (NumPrintRanges > 0) or HasRepeatedCols or HasRepeatedRows then
L.Add(sheet.Name);
end;
if L.Count = 0 then
exit;
WriteEXTERNCOUNT(AStream, L.Count);
for i:=0 to L.Count-1 do
WriteEXTERNSHEET(AStream, L[i]);
finally
L.Free;
end;
end;
{@@ ----------------------------------------------------------------------------
Writes an Excel 5 INDEX record
@ -1854,6 +1879,75 @@ begin
SetLength(buf, 0);
end;
{@@ ----------------------------------------------------------------------------
Writes a local Link Table with EXTERNCOUNT and EXTERSHEET records for
internal 3D references to other sheets
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF5Writer.WriteLocalLinkTable(AStream: TStream);
var
L: TStringList;
cell: PCell;
found: Boolean;
i, n: Integer;
begin
L := TStringList.Create;
try
// Check whether there is any cell with a formula with 3D reference
found := false;
for cell in FWorksheet.Cells do
if HasFormula(cell) and (pos('!', cell^.FormulaValue) <> 0) then begin
found := true;
break;
end;
// Write every sheet to local link table. This is too much - it would be
// enough to write only those sheets involved in a 3d reference - but it
// simplifies processing of 3d references a lot.
if found then begin
n := FWorkbook.GetWorksheetCount;
if n > 0 then begin
WriteEXTERNCOUNT(AStream, word(n));
for i := 0 to n-1 do
WriteEXTERNSHEET(AStream, FWorkbook.GetWorksheetByIndex(i).Name);
end;
end;
finally
L.Free;
end;
end;
{@@ ----------------------------------------------------------------------------
Writes a 3D cell address consisting of worksheet, row and column indexes.
Needed for references to other worksheets within the same workbook.
Returns the number of bytes written.
-------------------------------------------------------------------------------}
function TsSpreadBIFF5Writer.WriteRPNCellAddress3D(AStream: TStream;
ASheet, ARow, ACol: Cardinal; AFlags: TsRelFlags): Word;
var
p: Int64;
begin
p := AStream.Position;
// One-based index to EXTERNSHEET record in the Local Link Table
// containing the name of the first referenced sheet.
// This is always a negative value to indicate a 3D reference.
AStream.WriteWord(WordToLE(-(ASheet+1)));
// 8 unused bytes
AStream.WriteDWord(0);
AStream.WriteDWord(0);
// Zero-based index to first referenced sheet (FFFFH = deleted sheet)
AStream.WriteWord(WordToLE(ASheet));
// Zero-based index to last referenced sheet (FFFFH = deleted sheet)
AStream.WriteWord(WordToLE(ASheet));
// Write row/column address
WriteRPNCellAddress(AStream, ARow, ACol, AFlags);
Result := AStream.Position - p;
end;
{@@ ----------------------------------------------------------------------------
Writes an Excel 5 STRING record which immediately follows a FORMULA record
when the formula result is a string.

View File

@ -177,7 +177,7 @@ type
procedure WriteDimensions(AStream: TStream; AWorksheet: TsWorksheet);
procedure WriteEOF(AStream: TStream);
procedure WriteEXTERNBOOK(AStream: TStream);
procedure WriteEXTERNSHEET(AStream: TStream); override;
procedure WriteEXTERNSHEET(AStream: TStream);
procedure WriteFONT(AStream: TStream; AFont: TsFont);
procedure WriteFonts(AStream: TStream);
procedure WriteFORMAT(AStream: TStream; ANumFormatStr: String;
@ -915,14 +915,7 @@ var
SectionEOF: Boolean = False;
RecordType: Word;
CurStreamPos: Int64;
// sheetData: TsSheetData;
begin
(*
sheetData := TsSheetData(FSheetList[FCurSheetIndex]);
FWorksheet := FWorkbook.AddWorksheet(sheetData.Name, true);
if sheetData.Hidden then
FWorksheet.Options := FWorksheet.Options + [soHidden];
*)
FWorksheet := FWorkbook.GetWorksheetByIndex(FCurSheetIndex);
while (not SectionEOF) do
begin
@ -1016,7 +1009,6 @@ var
rtParams: TsRichTextParams;
sheet: TsWorksheet;
sheetstate: Byte;
sheetdata: TsSheetData;
begin
{ Absolute stream position of the BOF record of the sheet represented
by this record }
@ -1038,12 +1030,6 @@ begin
sheet := FWorkbook.AddWorksheet(UTF8Encode(widename), true);
if sheetState <> 0 then
sheet.Options := sheet.Options + [soHidden];
(*
sheetData := TsSheetData.Create;
sheetData.Name := UTF8Encode(wideName);
sheetData.Hidden := sheetState <> 0;
FSheetList.Add(sheetdata);
*)
end;
function TsSpreadBIFF8Reader.ReadString(const AStream: TStream;
@ -1886,7 +1872,7 @@ var
sheetnames: widestring;
externbook: TBiff8Externbook;
p: Int64;
t: array[0..1] of byte;
t: array[0..1] of byte = (0, 0);
begin
if FBiff8ExternBooks = nil then
FBiff8ExternBooks := TFPObjectList.Create(true);
@ -1904,8 +1890,7 @@ begin
else
if (t[0] = 1) and (t[1] = $3A) then
externbook.Kind := ebkAddInFunc
else
if n = 0 then
else if n = 0 then
externbook.Kind := ebkDDE_OLE
else
externbook.Kind := ebkExternal;
@ -1922,11 +1907,11 @@ begin
sheetnames := ''
else begin
// Sheet names (Unicode strings with 16bit string length)
sheetnames := UTF8Encode(ReadWideString(AStream, false));
sheetnames := ReadWideString(AStream, false);
for i := 2 to n do
sheetnames := sheetnames + #1 + UTF8Encode(ReadWideString(AStream, false));
sheetnames := sheetnames + widechar(#1) + ReadWideString(AStream, false);
end;
externbook.SheetNames := sheetNames;
externbook.SheetNames := UTF8Encode(sheetNames);
end;
FBiff8ExternBooks.Add(externbook);
@ -3759,16 +3744,14 @@ end;
function TsSpreadBIFF8Writer.WriteRPNCellAddress3D(AStream: TStream;
ASheet, ARow, ACol: Cardinal; AFlags: TsRelFlags): Word;
var
c: Cardinal;
begin
// Next line is a simplification: We should write the index of the sheet
// in the REF record here, but these are arranged in the same order as the
// sheets. --> MUST BE RE-DONE ONCE SHEET RANGES ARE ALLOWED.
AStream.WriteWord(WordToLE(ASheet));
// The row/col address is written in relative notation!
Result := 2 + WriteRPNCellAddress(AStream, ARow, ACol, [rfRelRow, rfRelCol]);
// Write row/column address
Result := 2 + WriteRPNCellAddress(AStream, ARow, ACol, AFlags);
end;

View File

@ -395,7 +395,6 @@ type
FCurSheetIndex: Integer;
FActivePane: Integer;
FExternSheets: TStrings;
// FSheetList: TFPList;
procedure AddBuiltinNumFormats; override;
procedure ApplyCellFormatting(ACell: PCell; XFIndex: Word); virtual;
@ -573,9 +572,9 @@ type
procedure WriteError(AStream: TStream; const ARow, ACol: Cardinal;
const AValue: TsErrorValue; ACell: PCell); override;
// Writes out an EXTERNCOUNT record
procedure WriteEXTERNCOUNT(AStream: TStream);
procedure WriteEXTERNCOUNT(AStream: TStream; ACount: Word);
// Writes out an EXTERNSHEET record
procedure WriteEXTERNSHEET(AStream: TStream); virtual;
procedure WriteEXTERNSHEET(AStream: TStream; ASheetName: String);
// Writes out a FORMAT record
procedure WriteFORMAT(AStream: TStream; ANumFormatStr: String;
ANumFormatIndex: Integer); virtual;
@ -613,7 +612,7 @@ type
function WriteRPNCellAddress(AStream: TStream; ARow, ACol: Cardinal;
AFlags: TsRelFlags): Word; virtual;
function WriteRPNCellAddress3D(AStream: TStream; ASheet, ARow, ACol: Cardinal;
AFlags: TsRelFlags): Word; virtual;
{%H-}AFlags: TsRelFlags): Word; virtual;
function WriteRPNCellOffset(AStream: TStream; ARowOffset, AColOffset: Integer;
AFlags: TsRelFlags): Word; virtual;
function WriteRPNCellRangeAddress(AStream: TStream; ARow1, ACol1, ARow2, ACol2: Cardinal;
@ -978,8 +977,6 @@ constructor TsSpreadBIFFReader.Create(AWorkbook: TsWorkbook);
begin
inherited Create(AWorkbook);
// FSheetList := TFPList.Create;
FPalette := TsPalette.Create;
PopulatePalette;
@ -1008,9 +1005,6 @@ var
begin
for j:=0 to FDefinedNames.Count-1 do TObject(FDefinedNames[j]).Free;
FDefinedNames.Free;
{
for j:= 0 to FSheetList.Count-1 do TObject(FSheetList[j]).Free;
FSheetList.Free; }
FExternSheets.Free;
FPalette.Free;
@ -2483,6 +2477,7 @@ end;
procedure TsSpreadBIFFReader.ReadRPNSheetIndex(AStream: TStream;
out ASheet1, ASheet2: Integer);
begin
Unused(AStream);
ASheet1 := -1;
ASheet2 := -1;
end;
@ -2733,7 +2728,13 @@ begin
INT_EXCEL_TOKEN_TREFN_R: rpnItem := RPNCellRef(r, c, flags, rpnItem);
end;
end;
INT_EXCEL_TOKEN_TREF3D_R, INT_EXCEL_TOKEN_TREF3d_V:
INT_EXCEL_TOKEN_TREF3D_V:
begin
ReadRPNSheetIndex(AStream, sheet1, sheet2);
ReadRPNCellAddress(AStream, r, c, flags);
rpnItem := RpnCellValue3D(sheet1, r, c, flags, rpnItem);
end;
INT_EXCEL_TOKEN_TREF3D_R: //, INT_EXCEL_TOKEN_TREF3d_V:
begin
ReadRPNSheetIndex(AStream, sheet1, sheet2);
ReadRPNCellAddressOffset(AStream, dr, dc, flags);
@ -3711,6 +3712,7 @@ end;
Writes a BIFF EXTERNCOUNT record.
Valid for BIFF2-BIFF5.
-------------------------------------------------------------------------------}
(*
procedure TsSpreadBIFFWriter.WriteEXTERNCOUNT(AStream: TStream);
var
i: Integer;
@ -3734,11 +3736,42 @@ begin
{ Count of EXTERNSHEET records following }
AStream.WriteWord(WordToLE(n));
end;
*)
procedure TsSpreadBIFFWriter.WriteEXTERNCOUNT(AStream: TStream; ACount: Word);
begin
{ BIFF record header }
WriteBIFFHeader(AStream, INT_EXCEL_ID_EXTERNCOUNT, 2);
{ Count of EXTERNSHEET records following }
AStream.WriteWord(WordToLE(ACount));
end;
{@@ ----------------------------------------------------------------------------
Writes a BIFF EXTERNSHEET record.
Valid for BIFF2-BIFF5.
-------------------------------------------------------------------------------}
procedure TsSpreadBIFFWriter.WriteEXTERNSHEET(AStream: TStream;
ASheetName: String);
var
s: ansistring;
begin
// Convert to ANSI
s := ConvertEncoding(ASheetName, encodingUTF8, FCodePage);
{ BIFF record header }
WriteBIFFHeader(AStream, INT_EXCEL_ID_EXTERNSHEET, 2 + Length(s));
{ Character count in worksheet name - don't count the following flag! }
AStream.WriteByte(Length(s));
{ Flag for identification as own sheet }
AStream.WriteByte($03);
{ Sheet name }
AStream.WriteBuffer(s[1], Length(s));
end;
(*
procedure TsSpreadBIFFWriter.WriteEXTERNSHEET(AStream: TStream);
var
sheet: TsWorksheet;
@ -3766,7 +3799,7 @@ begin
end;
end;
end;
*)
{@@ ----------------------------------------------------------------------------
Writes the a margin record for printing (margin is in inches).
The margin is identified by the parameter AMargin:
@ -4330,12 +4363,15 @@ end;
{@ -----------------------------------------------------------------------------
Writes the address of a cell as used in an RPN formula and returns the
count of bytes written.
count of bytes written. The return value is $FFFF if 3D addresses are not
supported (BIFF 2).
Placeholder. To be overridden by BIFF5 and BIFF8.
-------------------------------------------------------------------------------}
function TsSpreadBIFFWriter.WriteRPNCellAddress3D(AStream: TStream;
ASheet, ARow, ACol: Cardinal; AFlags: TsRelFlags): Word;
begin
Unused(AStream, ASheet);
Unused(ARow, ACol);
Result := 0;
end;
@ -4626,7 +4662,10 @@ begin
AFormula[i].Row, AFormula[i].Col,
AFormula[i].RelFlags
);
inc(RPNLength, n);
if n = $FFFF then
FWorkbook.AddErrorMsg('3D cell addresses are not supported.')
else
inc(RPNLength, n);
end;
INT_EXCEL_TOKEN_TAREA_R: { fekCellRange }

View File

@ -76,6 +76,7 @@ type
procedure Test_Write_Read_CalcStringFormula_ODS;
{ Formulas with 3D references to other sheets }
procedure Test_Write_Read_Calc3DFormula_BIFF5;
procedure Test_Write_Read_Calc3DFormula_BIFF8;
procedure Test_Write_Read_Calc3DFormula_OOXML;
procedure Test_Write_Read_Calc3DFormula_ODS;
@ -698,20 +699,11 @@ var
workbook: TsWorkbook;
row: Integer;
tempFile: string; //write xls/xml to this file and read back from it
actual: TsExpressionResult;
expected: TsExpressionResult;
actual, expected: TsExpressionResult;
cell: PCell;
sollValues: array of TsExpressionResult;
formula: String;
ErrorMargin: Double;
formula, actualformula: String;
begin
ErrorMargin:=0; //1.44E-7;
//1.44E-7 for SUMSQ formula
//6.0E-8 for SUM formula
//4.8E-8 for MAX formula
//2.4E-8 for now formula
//about 1E-15 is needed for some trig functions
// Create test workbook
workbook := TsWorkbook.Create;
try
@ -746,10 +738,14 @@ begin
for row := 0 to sheet1.GetLastRowIndex do
begin
cell := sheet1.FindCell(Row, 0);
if (Cell = nil) then
Fail('Error in test code: failed to get cell ' + CellNotation(sheet1, Row, 0));
formula := sheet1.ReadAsText(cell);
cell := sheet1.FindCell(Row, 1);
if (cell = nil) then
fail('Error in test code: Failed to get cell ' + CellNotation(sheet1, Row, 1));
case cell^.ContentType of
cctBool : actual := BooleanResult(cell^.BoolValue);
cctNumber : actual := FloatResult(cell^.NumberValue);
@ -759,6 +755,7 @@ begin
cctEmpty : actual := EmptyResult;
else fail('ContentType not supported');
end;
actualformula := cell^.FormulaValue;
expected := SollValues[row];
// Cell does not store integers!
@ -784,7 +781,7 @@ begin
rtFloat:
{$if (defined(mswindows)) or (FPC_FULLVERSION>=20701)}
// FPC 2.6.x and trunk on Windows need this, also FPC trunk on Linux x64
CheckEquals(expected.ResFloat, actual.ResFloat, ErrorMargin,
CheckEquals(expected.ResFloat, actual.ResFloat,
'Test read calculated formula result mismatch, cell '+CellNotation(sheet1, Row, 1));
{$else}
// Non-Windows: test without error margin
@ -800,6 +797,9 @@ begin
GetEnumname(TypeInfo(TsErrorValue), ord(actual.ResError)),
'Test read calculated formula error value mismatch, cell '+CellNotation(sheet1, Row, 1));
end;
CheckEquals(
formula, actualformula, 'Read formula string mismatch, cell ' +CellNotation(sheet1, Row, 1));
end;
finally
@ -809,6 +809,11 @@ begin
end;
procedure TSpreadWriteReadFormulaTests.Test_Write_Read_Calc3DFormula_BIFF5;
begin
Test_Write_Read_Calc3DFormulas(sfExcel5);
end;
procedure TSpreadWriteReadFormulaTests.Test_Write_Read_Calc3DFormula_BIFF8;
begin
Test_Write_Read_Calc3DFormulas(sfExcel8);

View File

@ -69,7 +69,6 @@
SetLength(SollValues, Row+1);
SollValues[Row] := StringResult('AB');
{
inc(Row);
formula := 'D1&Sheet2!B3%"BC"';