From 1676d31702f5f4fa67f7a7d4fed80e3e9b498c75 Mon Sep 17 00:00:00 2001 From: wp_xxyyzz Date: Fri, 2 May 2014 18:49:05 +0000 Subject: [PATCH] fpspreadsheet: Add date/time reading/writing support to BIFF2 and the corresponding "write-read" unit test cases. Passed. git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@2988 8e941d3f-bd1b-0410-a28a-d453659cc2b4 --- .../examples/excel2demo/excel2write.lpr | 74 +++- .../examples/fpsgrid/fpsgrid.lpi | 162 +++++---- .../reference/BIFFExplorer/bebiffgrid.pas | 18 + .../reference/BIFFExplorer/bemain.pas | 9 +- components/fpspreadsheet/tests/datetests.pas | 6 + .../fpspreadsheet/tests/numberstests.pas | 6 + components/fpspreadsheet/xlsbiff2.pas | 324 ++++++++++++++++-- components/fpspreadsheet/xlscommon.pas | 34 +- 8 files changed, 504 insertions(+), 129 deletions(-) diff --git a/components/fpspreadsheet/examples/excel2demo/excel2write.lpr b/components/fpspreadsheet/examples/excel2demo/excel2write.lpr index 432d4c874..c11655985 100644 --- a/components/fpspreadsheet/examples/excel2demo/excel2write.lpr +++ b/components/fpspreadsheet/examples/excel2demo/excel2write.lpr @@ -17,6 +17,9 @@ var MyWorksheet: TsWorksheet; MyRPNFormula: TsRPNFormula; MyDir: string; + number: Double; + lCol: TCol; + lRow: TRow; begin // Open the output file MyDir := ExtractFilePath(ParamStr(0)); @@ -30,8 +33,7 @@ begin // Write some number cells MyWorksheet.WriteNumber(0, 0, 1.0); - MyWorksheet.WriteUsedFormatting(0, 0, [uffBold]); - + MyWorksheet.WriteUsedFormatting(0, 0, [uffBold, uffNumberFormat]); MyWorksheet.WriteNumber(0, 1, 2.0); MyWorksheet.WriteNumber(0, 2, 3.0); MyWorksheet.WriteNumber(0, 3, 4.0); @@ -95,6 +97,74 @@ begin MyWorksheet.WriteNumber(6, 3, 2017); MyWorksheet.WriteFont(6, 3, 'Arial', 18, [fssBold], scBlue); + // Write current date/time to cells B11:B16 + MyWorksheet.WriteUTF8Text(10, 0, 'nfShortDate'); + MyWorksheet.WriteDateTime(10, 1, now, nfShortDate); + MyWorksheet.WriteUTF8Text(11, 0, 'nfShortTime'); + MyWorksheet.WriteDateTime(11, 1, now, nfShortTime); + MyWorksheet.WriteUTF8Text(12, 0, 'nfLongTime'); + MyWorksheet.WriteDateTime(12, 1, now, nfLongTime); + MyWorksheet.WriteUTF8Text(13, 0, 'nfShortDateTime'); + MyWorksheet.WriteDateTime(13, 1, now, nfShortDateTime); + MyWorksheet.WriteUTF8Text(14, 0, 'nfFmtDateTime, DM'); + MyWorksheet.WriteDateTime(14, 1, now, nfFmtDateTime, 'DM'); + MyWorksheet.WriteUTF8Text(15, 0, 'nfFmtDateTime, MY'); + MyWorksheet.WriteDateTime(15, 1, now, nfFmtDateTime, 'MY'); + MyWorksheet.WriteUTF8Text(16, 0, 'nfShortTimeAM'); + MyWorksheet.WriteDateTime(16, 1, now, nfShortTimeAM); + MyWorksheet.WriteUTF8Text(17, 0, 'nfLongTimeAM'); + MyWorksheet.WriteDateTime(17, 1, now, nfLongTimeAM); + MyWorksheet.WriteUTF8Text(18, 0, 'nfFmtDateTime, MS'); + MyWorksheet.WriteDateTime(18, 1, now, nfFmtDateTime, 'MS'); + MyWorksheet.WriteUTF8Text(19, 0, 'nfFmtDateTime, MSZ'); + MyWorksheet.WriteDateTime(19, 1, now, nfFmtDateTime, 'MSZ'); + + // Write formatted numbers + number := 12345.67890123456789; + MyWorksheet.WriteUTF8Text(24, 1, '12345.67890123456789'); + MyWorksheet.WriteUTF8Text(24, 2, '-12345.67890123456789'); + MyWorksheet.WriteUTF8Text(25, 0, 'nfFixed, 0 decs'); + MyWorksheet.WriteNumber(25, 1, number, nfFixed, 0); + MyWorksheet.WriteNumber(25, 2, -number, nfFixed, 0); + MyWorksheet.WriteUTF8Text(26, 0, 'nfFixed, 2 decs'); + MyWorksheet.WriteNumber(26, 1, number, nfFixed, 2); + MyWorksheet.WriteNumber(26, 2, -number, nfFixed, 2); + MyWorksheet.WriteUTF8Text(27, 0, 'nfFixedTh, 0 decs'); + MyWorksheet.WriteNumber(27, 1, number, nfFixedTh, 0); + MyWorksheet.WriteNumber(27, 2, -number, nfFixedTh, 0); + MyWorksheet.WriteUTF8Text(28, 0, 'nfFixedTh, 2 decs'); + MyWorksheet.WriteNumber(28, 1, number, nfFixedTh, 2); + MyWorksheet.WriteNumber(28, 2, -number, nfFixedTh, 2); + MyWorksheet.WriteUTF8Text(29, 0, 'nfSci, 1 dec'); + MyWorksheet.WriteNumber(29, 1, number, nfSci); + MyWorksheet.WriteNumber(29, 2, -number, nfSci); + MyWorksheet.WriteNumber(29, 3, 1.0/number, nfSci); + MyWorksheet.WriteNumber(29, 4, -1.0/number, nfSci); + MyWorksheet.WriteUTF8Text(30, 0, 'nfExp, 2 decs'); + MyWorksheet.WriteNumber(30, 1, number, nfExp, 2); + MyWorksheet.WriteNumber(30, 2, -number, nfExp, 2); + MyWorksheet.WriteNumber(30, 3, 1.0/number, nfExp, 2); + MyWorksheet.WriteNumber(30, 4, -1.0/number, nfExp, 2); + + number := 1.333333333; + MyWorksheet.WriteUTF8Text(35, 0, 'nfPercentage, 0 decs'); + MyWorksheet.WriteNumber(35, 1, number, nfPercentage, 0); + MyWorksheet.WriteUTF8Text(36, 0, 'nfPercentage, 2 decs'); + MyWorksheet.WriteNumber(36, 1, number, nfPercentage, 2); + MyWorksheet.WriteUTF8Text(37, 0, 'nfTimeInterval'); + MyWorksheet.WriteDateTime(37, 1, number, nfTimeInterval); + + // Set width of columns 0 and 1 + MyWorksheet.WriteColWidth(0, 40); + lCol.Width := 35; + MyWorksheet.WriteColInfo(1, lCol); + + // Set height of rows 5 and 6 + lRow.Height := 10; + MyWorksheet.WriteRowInfo(5, lRow); + lRow.Height := 5; + MyWorksheet.WriteRowInfo(6, lRow); + // Save the spreadsheet to a file MyWorkbook.WriteToFile(MyDir + 'test' + STR_EXCEL_EXTENSION, sfExcel2, true); MyWorkbook.Free; diff --git a/components/fpspreadsheet/examples/fpsgrid/fpsgrid.lpi b/components/fpspreadsheet/examples/fpsgrid/fpsgrid.lpi index 224f22a88..10b127a17 100644 --- a/components/fpspreadsheet/examples/fpsgrid/fpsgrid.lpi +++ b/components/fpspreadsheet/examples/fpsgrid/fpsgrid.lpi @@ -108,7 +108,7 @@ - + @@ -117,7 +117,7 @@ - + @@ -131,21 +131,19 @@ - + + - - + + - - - @@ -264,11 +262,10 @@ - - - + + @@ -292,8 +289,8 @@ - - + + @@ -302,8 +299,8 @@ - - + + @@ -312,9 +309,12 @@ - - + + + + + @@ -348,127 +348,137 @@ + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + diff --git a/components/fpspreadsheet/reference/BIFFExplorer/bebiffgrid.pas b/components/fpspreadsheet/reference/BIFFExplorer/bebiffgrid.pas index be046aa96..8c6f37a3f 100644 --- a/components/fpspreadsheet/reference/BIFFExplorer/bebiffgrid.pas +++ b/components/fpspreadsheet/reference/BIFFExplorer/bebiffgrid.pas @@ -48,6 +48,7 @@ type procedure ShowFont; procedure ShowFooter; procedure ShowFormat; + procedure ShowFormatCount; procedure ShowFormula; procedure ShowHeader; procedure ShowHideObj; @@ -268,6 +269,8 @@ begin ShowSelection; $001E, $041E: ShowFormat; + $001F: + ShowFormatCount; $0022: ShowDateMode; $0024: @@ -1219,6 +1222,21 @@ begin end; +procedure TBIFFGrid.ShowFormatCount; +var + numBytes: Integer; + w: Word; +begin + if FFormat = sfExcel2 then begin + RowCount := 1 + FixedRows; + numBytes := 2; + Move(FBuffer[FBufferIndex], w, numBytes); + ShowInRow(FCurrRow, FBufferIndex, numbytes, IntToStr(WordLEToN(w)), + 'Number of FORMAT records'); + end; +end; + + procedure TBIFFGrid.ShowFormula; const ABS_REL: array[boolean] of string = ('abs', 'rel'); diff --git a/components/fpspreadsheet/reference/BIFFExplorer/bemain.pas b/components/fpspreadsheet/reference/BIFFExplorer/bemain.pas index 4387b5a9c..6eed35229 100644 --- a/components/fpspreadsheet/reference/BIFFExplorer/bemain.pas +++ b/components/fpspreadsheet/reference/BIFFExplorer/bemain.pas @@ -595,11 +595,6 @@ begin OnRecentFile := @MRUMenuManagerRecentFile; end; - FXFIndex := -1; - FFontIndex := -1; - FFormatIndex := -1; - FRowIndex := -1; - HexGrid.ColWidths[HexGrid.ColCount-1] := 5; HexGrid.DefaultRowHeight := HexGrid.Canvas.TextHeight('Tg') + 4; AlphaGrid.DefaultRowHeight := HexGrid.DefaultRowHeight; @@ -1059,6 +1054,10 @@ begin Screen.Cursor := crHourGlass; BiffTree.Clear; parentnode := nil; + FXFIndex := -1; + FFontIndex := -1; + FFormatIndex := -1; + FRowIndex := -1; AStream.Position := 0; while AStream.Position < AStream.Size do begin p := AStream.Position; diff --git a/components/fpspreadsheet/tests/datetests.pas b/components/fpspreadsheet/tests/datetests.pas index b73c591d7..a5586e8c6 100644 --- a/components/fpspreadsheet/tests/datetests.pas +++ b/components/fpspreadsheet/tests/datetests.pas @@ -208,6 +208,7 @@ type // One cell per test so some tests can fail and those further below may still work procedure TestWriteReadDates(AFormat: TsSpreadsheetFormat); published + procedure TestWriteReadDates_BIFF2; procedure TestWriteReadDates_BIFF5; procedure TestWriteReadDates_BIFF8; end; @@ -333,6 +334,11 @@ begin DeleteFile(TempFile); end; +procedure TSpreadWriteReadDateTests.TestWriteReadDates_BIFF2; +begin + TestWriteReadDates(sfExcel2); +end; + procedure TSpreadWriteReadDateTests.TestWriteReadDates_BIFF5; begin TestWriteReadDates(sfExcel5); diff --git a/components/fpspreadsheet/tests/numberstests.pas b/components/fpspreadsheet/tests/numberstests.pas index fce9d4eb8..de650c57e 100644 --- a/components/fpspreadsheet/tests/numberstests.pas +++ b/components/fpspreadsheet/tests/numberstests.pas @@ -95,6 +95,7 @@ type // One cell per test so some tests can fail and those further below may still work procedure TestWriteReadNumbers(AFormat: TsSpreadsheetFormat); published + procedure TestWriteReadNumbers_BIFF2; procedure TestWriteReadNumbers_BIFF5; procedure TestWriteReadNumbers_BIFF8; end; @@ -200,6 +201,11 @@ begin DeleteFile(TempFile); end; +procedure TSpreadWriteReadNumberTests.TestWriteReadNumbers_BIFF2; +begin + TestWriteReadNumbers(sfExcel2); +end; + procedure TSpreadWriteReadNumberTests.TestWriteReadNumbers_BIFF5; begin TestWriteReadNumbers(sfExcel5); diff --git a/components/fpspreadsheet/xlsbiff2.pas b/components/fpspreadsheet/xlsbiff2.pas index 83444c47a..fe2d071d2 100755 --- a/components/fpspreadsheet/xlsbiff2.pas +++ b/components/fpspreadsheet/xlsbiff2.pas @@ -48,6 +48,9 @@ type procedure ReadRowInfo(AStream: TStream); protected procedure ApplyCellFormatting(ARow, ACol: Cardinal; XFIndex: Word); override; + procedure ExtractNumberFormat(AXFIndex: WORD; + out ANumberFormat: TsNumberFormat; out ADecimals: Word; + out ANumberFormatStr: String); override; procedure ReadBlank(AStream: TStream); override; procedure ReadColWidth(AStream: TStream); procedure ReadFont(AStream: TStream); @@ -57,7 +60,6 @@ type procedure ReadLabel(AStream: TStream); override; procedure ReadNumber(AStream: TStream); override; procedure ReadRowColXF(AStream: TStream; out ARow, ACol: Cardinal; out AXF: Word); override; - procedure ReadXF(AStream: TStream); public { General reading methods } @@ -77,6 +79,9 @@ type procedure WriteEOF(AStream: TStream); procedure WriteFont(AStream: TStream; AFontIndex: Integer); procedure WriteFonts(AStream: TStream); + procedure WriteFormat(AStream: TStream; AFormatCode: String); + procedure WriteFormatCount(AStream: TStream); + procedure WriteFormats(AStream: TStream); procedure WriteIXFE(AStream: TStream; XFIndex: Word); procedure WriteXF(AStream: TStream; AFontIndex, AFormatIndex: byte; ABorders: TsCellBorders = []; AHorAlign: TsHorAlignment = haLeft; @@ -95,7 +100,7 @@ type end; var - // the palette of the default BIFF2 colors as "big-endian color" values + { the palette of the default BIFF2 colors as "big-endian color" values } PALETTE_BIFF2: array[$0..$07] of TsColorValue = ( $000000, // $00: black $FFFFFF, // $01: white @@ -107,6 +112,36 @@ var $00FFFF // $07: cyan ); + { These are the built-in number formats of BIFF 2. They are not stored in + the file. Note that, compared to the BUFF5+ built-in formats, two formats + are missing and the indexes are offset by 2 after #11. + It seems that BIFF2 can handle only these 21 formats. The other formats + available in fpspreadsheet are mapped to these 21 formats such that least + destruction is made. } + NUMFORMAT_BIFF2: array[0..20] of string = ( + 'General', // 0 + '0', + '0.00', + '#,##0', + '#,##0.00', + '"$"#,##0_);("$"#,##0)', // 5 + '"$"#,##0_);[Red]("$"#,##0)', + '"$"#,##0.00_);("$"#,##0.00)', + '"$"#,##0.00_);[Red]("$"#,##0.00)', + '0%', + '0.00%', // 10 + '0.00E+00', + 'M/D/YY', + 'D-MMM-YY', + 'D-MMM', + 'MMM-YY', // 15 + 'h:mm AM/PM', + 'h:mm:ss AM/PM', + 'h:mm', + 'h:mm:ss', + 'M/D/YY h:mm' // 20 + ); + implementation const @@ -120,6 +155,7 @@ const INT_EXCEL_ID_BOF = $0009; INT_EXCEL_ID_EOF = $000A; INT_EXCEL_ID_FORMAT = $001E; + INT_EXCEL_ID_FORMATCOUNT= $001F; INT_EXCEL_ID_COLWIDTH = $0024; INT_EXCEL_ID_XF = $0043; INT_EXCEL_ID_IXFE = $0044; @@ -135,6 +171,34 @@ const INT_EXCEL_CHART = $0020; INT_EXCEL_MACRO_SHEET = $0040; + { FORMAT record constants for BIFF2 } + // Subset of the built-in formats for US Excel, + // including those needed for date/time output + FORMAT_GENERAL = 0; //general/default format + FORMAT_FIXED_0_DECIMALS = 1; //fixed, 0 decimals + FORMAT_FIXED_2_DECIMALS = 2; //fixed, 2 decimals + FORMAT_FIXED_THOUSANDS_0_DECIMALS = 3; //fixed, w/ thousand separator, 0 decs + FORMAT_FIXED_THOUSANDS_2_DECIMALS = 4; //fixed, w/ thousand separator, 2 decs + FORMAT_CURRENCY_0_DECIMALS = 5; //currency (with currency symbol), 0 decs + FORMAT_CURRENCY_2_DECIMALS = 7; //currency (with currency symbol), 2 decs + FORMAT_PERCENT_0_DECIMALS = 9; //percent, 0 decimals + FORMAT_PERCENT_2_DECIMALS = 10; //percent, 2 decimals + FORMAT_EXP_2_DECIMALS = 11; //exponent, 2 decimals + FORMAT_SCI_1_DECIMAL = 11; //scientific, 1 decimal -- not present in BIFF2 -- mapped to EXP_2_DECIMALS + FORMAT_SHORT_DATE = 12; //short date + FORMAT_DATE_DM = 14; //date D-MMM + FORMAT_DATE_MY = 15; //date MMM-YYYY + FORMAT_SHORT_TIME_AM = 16; //short time H:MM with AM + FORMAT_LONG_TIME_AM = 17; //long time H:MM:SS with AM + FORMAT_SHORT_TIME = 18; //short time H:MM + FORMAT_LONG_TIME = 19; //long time H:MM:SS + FORMAT_SHORT_DATETIME = 20; //short date+time + { The next three formats are not available in BIFF2. They should not be used + when a file is to be saved in BIFF2. If it IS saved as BIFF2 the formats + are mapped to FORMAT_LONG_TIME} + FORMAT_TIME_MS = 19; //time MM:SS + FORMAT_TIME_MSZ = 19; //time MM:SS.0 + FORMAT_TIME_INTERVAL = 19; //time [hh]:mm:ss, hh can be >24 { TsSpreadBIFF2Reader } @@ -181,6 +245,81 @@ begin end; end; +{ Extracts the number format data from an XF record indexed by AXFIndex. + Note that BIFF2 supports only 21 formats. } +procedure TsSpreadBIFF2Reader.ExtractNumberFormat(AXFIndex: WORD; + out ANumberFormat: TsNumberFormat; out ADecimals: Word; + out ANumberFormatStr: String); +const + NOT_USED = nfGeneral; + fmts: array[1..20] of TsNumberFormat = ( + nfFixed, nfFixed, nfFixedTh, nfFixedTh, nfFixedTh, // 1..5 + nfFixedTh, nfFixedTh, nfFixedTh, nfPercentage, nfPercentage, // 6..10 + nfExp, nfShortDate, nfShortDate, nfFmtDateTime, nfFmtDateTime, // 11..15 + nfShortTimeAM, nfLongTimeAM, nfShortTime, nfLongTime, nfShortDateTime// 16..20 + ); + decs: array[1..20] of word = ( + 0, 2, 0, 2, 0, 0, 2, 2, 0, 2, // 1..10 + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0 // 11..20 + ); +var + lFormatData: TFormatListData; + lXFData: TXFListData; + isAMPM: Boolean; + isLongTime: Boolean; + isMilliSec: Boolean; + t,d: Boolean; +begin + ANumberFormat := nfGeneral; + ANumberFormatStr := ''; + ADecimals := 0; + + lFormatData := FindFormatDataForCell(AXFIndex); + if lFormatData = nil then begin + // no custom format, so first test for default formats + lXFData := TXFListData (FXFList.Items[AXFIndex]); + if (lXFData.FormatIndex > 0) and (lXFData.FormatIndex <= 20) then begin + ANumberFormat := fmts[lXFData.FormatIndex]; + ADecimals := decs[lXFData.FormatIndex]; + end; + end else + // The next is copied from xlscommon - I think it's not necessary here + if IsPercentNumberFormat(lFormatData.FormatString, ADecimals) then + ANumberFormat := nfPercentage + else + if IsExpNumberFormat(lFormatData.Formatstring, ADecimals) then + ANumberFormat := nfExp + else + if IsThousandSepNumberFormat(lFormatData.FormatString, ADecimals) then + ANumberFormat := nfFixedTh + else + if IsFixedNumberFormat(lFormatData.FormatString, ADecimals) then + ANumberFormat := nfFixed + else begin + t := IsTimeFormat(lFormatData.FormatString, isLongTime, isAMPM, isMilliSec); + d := IsDateFormat(lFormatData.FormatString); + if d and t then + ANumberFormat := nfShortDateTime + else + if d then + ANumberFormat := nfShortDate + else + if t then begin + if isAMPM then begin + if isLongTime then + ANumberFormat := nfLongTimeAM + else + ANumberFormat := nfShortTimeAM; + end else begin + if isLongTime then + ANumberFormat := nfLongTime + else + ANumberFormat := nfShortTime; + end; + end; + end; +end; + procedure TsSpreadBIFF2Reader.ReadBlank(AStream: TStream); var ARow, ACol: Cardinal; @@ -341,16 +480,24 @@ procedure TsSpreadBIFF2Reader.ReadNumber(AStream: TStream); var ARow, ACol: Cardinal; XF: Word; - AValue: Double; + value: Double; + dt: TDateTime; + nf: TsNumberFormat; + nd: Word; + nfs: String; begin { BIFF Record row/column/style } ReadRowColXF(AStream, ARow, ACol, XF); { IEE 754 floating-point value } - AStream.ReadBuffer(AValue, 8); + AStream.ReadBuffer(value, 8); - { Save the data } - FWorksheet.WriteNumber(ARow, ACol, AValue); + {Find out what cell type, set content type and value} + ExtractNumberFormat(XF, nf, nd, nfs); + if IsDateTime(value, nf, dt) then + FWorksheet.WriteDateTime(ARow, ACol, dt, nf, nfs) + else + FWorksheet.WriteNumber(ARow, ACol, value, nf, nd); { Apply formatting to cell } ApplyCellFormatting(ARow, ACol, XF); @@ -414,31 +561,29 @@ begin end; procedure TsSpreadBIFF2Reader.ReadXF(AStream: TStream); -{ -Offset Size Contents - 0 1 Index to FONT record (➜5.45) - 1 1 Not used - 2 1 Number format and cell flags: - Bit Mask Contents - 5-0 3FH Index to FORMAT record (➜5.49) - 6 40H 1 = Cell is locked - 7 80H 1 = Formula is hidden - 3 1 Horizontal alignment, border style, and background: - Bit Mask Contents - 2-0 07H XF_HOR_ALIGN – Horizontal alignment - 0 General, 1 Left, 2 Centred, 3 Right, 4 Filled - 3 08H 1 = Cell has left black border - 4 10H 1 = Cell has right black border - 5 20H 1 = Cell has top black border - 6 40H 1 = Cell has bottom black border - 7 80H 1 = Cell has shaded background -} +{ Offset Size Contents + 0 1 Index to FONT record (➜5.45) + 1 1 Not used + 2 1 Number format and cell flags: + Bit Mask Contents + 5-0 3FH Index to FORMAT record (➜5.49) + 6 40H 1 = Cell is locked + 7 80H 1 = Formula is hidden + 3 1 Horizontal alignment, border style, and background: + Bit Mask Contents + 2-0 07H XF_HOR_ALIGN – Horizontal alignment + 0 General, 1 Left, 2 Center, 3 Right, 4 Filled + 3 08H 1 = Cell has left black border + 4 10H 1 = Cell has right black border + 5 20H 1 = Cell has top black border + 6 40H 1 = Cell has bottom black border + 7 80H 1 = Cell has shaded background } type - TXFRecord = packed record // see p. 224 - FontIndex: byte; // Offset 0, Size 1 - NotUsed: byte; // Offset 1, Size 1 - NumFormat_Flags: byte; // Offset 2, Size 1 - HorAlign_Border_BackGround: Byte; // Offset 3, Size 1 + TXFRecord = packed record + FontIndex: byte; + NotUsed: byte; + NumFormat_Flags: byte; + HorAlign_Border_BackGround: Byte; end; var lData: TXFListData; @@ -453,7 +598,7 @@ begin lData.FontIndex := xf.FontIndex; // Format index - lData.FormatIndex := xf.NumFormat_Flags and $07; + lData.FormatIndex := xf.NumFormat_Flags and $3F; // Horizontal alignment b := xf.HorAlign_Border_Background and MASK_XF_HOR_ALIGN; @@ -490,6 +635,8 @@ begin // Add the decoded data to the list FXFList.Add(lData); + + ldata := TXFListData(FXFList.Items[FXFList.Count-1]); end; @@ -635,6 +782,7 @@ begin WriteBOF(AStream); WriteFonts(AStream); + WriteFormats(AStream); WriteXFRecords(AStream); WriteColWidths(AStream); WriteCellsToStream(AStream, Workbook.GetFirstWorksheet.Cells); @@ -658,7 +806,7 @@ begin { not used } AStream.WriteByte(0); - { number format and cell flags } + { Number format index and cell flags } b := AFormatIndex and $3F; AStream.WriteByte(b); @@ -689,7 +837,7 @@ begin lFormatIndex := 0; //General format (one of the built-in number formats) lBorders := []; lHorAlign := FFormattingStyles[i].HorAlignment; -(* + // Now apply the modifications. if uffNumberFormat in FFormattingStyles[i].UsedFormattingFields then case FFormattingStyles[i].NumberFormat of @@ -740,16 +888,18 @@ begin if (fmt = 'my') or (fmt = 'mmm-yy') or (fmt = 'mmm yy') or (fmt = 'mmm/yy') then lFormatIndex := FORMAT_DATE_MY else + { Because of limitations of BIFF2 the next two formats are mapped + to the same format index! } if (fmt = 'ms') or (fmt = 'nn:ss') or (fmt = 'mm:ss') then lFormatIndex := FORMAT_TIME_MS else if (fmt = 'msz') or (fmt = 'nn:ss.zzz') or (fmt = 'mm:ss.zzz') or (fmt = 'mm:ss.0') or (fmt = 'mm:ss.z') or (fmt = 'nn:ss.z') then - lFormatIndex := FORMAT_TIME_MSZ + lFormatIndex := FORMAT_TIME_MSZ; end; nfTimeInterval: lFormatIndex := FORMAT_TIME_INTERVAL; end; - *) + if uffBorder in FFormattingStyles[i].UsedFormattingFields then lBorders := FFormattingStyles[i].Border; @@ -880,6 +1030,110 @@ begin WriteFont(AStream, i); end; +procedure TsSpreadBIFF2Writer.WriteFormat(AStream: TStream; AFormatCode: String); +var + len: Integer; + s: AnsiString; +begin + if AFormatCode = '' then + exit; + + s := AFormatCode; + len := Length(s); + + { BIFF record header } + AStream.WriteWord(WordToLE(INT_EXCEL_ID_FORMAT)); + AStream.WriteWord(WordToLE(len + 1)); + + { Write format string } + AStream.WriteByte(len); + AStream.WriteBuffer(s[1], len); +end; + +procedure TsSpreadBIFF2Writer.WriteFormatCount(AStream: TStream); +begin + AStream.WriteWord(WordToLE(INT_EXCEL_ID_FORMATCOUNT)); + AStream.WriteWord(WordToLE(2)); + AStream.WriteWord(WordToLE(High(NUMFORMAT_BIFF2)+1)); +end; + +procedure TsSpreadBIFF2Writer.WriteFormats(AStream: TStream); +var + i: Integer; +begin + WriteFormatCount(AStream); + for i:=0 to High(NUMFORMAT_BIFF2) do + WriteFormat(AStream, NUMFORMAT_BIFF2[i]); +end; +(* +var + ds, ts: Char; //decimal separator, thousand separator +begin + ds := DefaultFormatSettings.DecimalSeparator; + ts := DefaultFormatSettings.ThousandSeparator; + { 0} WriteFormat(AStream, 'General'); // 0 + { 1} WriteFormat(AStream, '0'); + { 2} WriteFormat(AStream, '0'+ds+'00'); // 0.00 + { 3} WriteFormat(AStream, '#'+ts+'##0'); // #,##0 + { 4} WriteFormat(AStream, '#'+ts+'##0'+ds+'00'); // #,##0.00 + { 5} WriteFormat(AStream, '"$"#'+ts+'##0_);("$"#'+ts+'##0)'); + { 6} WriteFormat(AStream, '"$"#'+ts+'##0_);[Red]("$"#'+ts+'##0)'); + { 7} WriteFormat(AStream, '"$"#'+ts+'##0'+ds+'00_);("$"#'+ts+'##0'+ds+'00)'); + { 8} WriteFormat(AStream, '"$"#'+ts+'##0'+ds+'00_);[Red]("$"#'+ts+'##0'+ds+'00)'); + { 9} WriteFormat(AStream, '0%'); + {10} WriteFormat(AStream, '0'+ds+'00%'); // 0.00% + {11} WriteFormat(AStream, '0'+ds+'00E+00'); // 0.00E+00 + {12} WriteFormat(AStream, 'm/d/yy'); + {13} WriteFormat(AStream, 'd-mmm-yy'); + {14} WriteFormat(AStream, 'd-mmm'); + {15} WriteFormat(AStream, 'mmm-yy'); + {16} WriteFormat(AStream, 'h:mm AM/PM'); + {17} WriteFormat(AStream, 'h:mm:ss AM/PM'); + {18} WriteFormat(AStream, 'h:mm'); + {19} WriteFormat(AStream, 'h:mm:ss'); + {20} WriteFormat(AStream, 'm/d/yy h:mm'); + + { # TODO: locale support + 0 => 'GENERAL', + 1 => '0', + 2 => '0.00', + 3 => '#,##0', + 4 => '#,##0.00', + 5 => '"$"#,##0_);("$"#,##0)', + 6 => '"$"#,##0_);[Red]("$"#,##0)', + 7 => '"$"#,##0.00_);("$"#,##0.00)', + 8 => '"$"#,##0.00_);[Red]("$"#,##0.00)', + 9 => '0%', + 10 => '0.00%', + 11 => '0.00E+00', + 12 => '# ?/?', + 13 => '# ??/??', + 14 => 'M/D/YY', + 15 => 'D-MMM-YY', + 16 => 'D-MMM', + 17 => 'MMM-YY', + 18 => 'h:mm AM/PM', + 19 => 'h:mm:ss AM/PM', + 20 => 'h:mm', + 21 => 'h:mm:ss', + 22 => 'M/D/YY h:mm', + 37 => '_(#,##0_);(#,##0)', + 38 => '_(#,##0_);[Red](#,##0)', + 39 => '_(#,##0.00_);(#,##0.00)', + 40 => '_(#,##0.00_);[Red](#,##0.00)', + 41 => '_("$"* #,##0_);_("$"* (#,##0);_("$"* "-"_);_(@_)', + 42 => '_(* #,##0_);_(* (#,##0);_(* "-"_);_(@_)', + 43 => '_("$"* #,##0.00_);_("$"* (#,##0.00);_("$"* "-"??_);_(@_)', + 44 => '_(* #,##0.00_);_(* (#,##0.00);_(* "-"??_);_(@_)', + 45 => 'mm:ss', + 46 => '[h]:mm:ss', + 47 => 'mm:ss.0', + 48 => '##0.0E+0', + 49 => '@', + } +end; + *) + { Writes an Excel 2 FORMULA record diff --git a/components/fpspreadsheet/xlscommon.pas b/components/fpspreadsheet/xlscommon.pas index d80993d57..7045b860a 100644 --- a/components/fpspreadsheet/xlscommon.pas +++ b/components/fpspreadsheet/xlscommon.pas @@ -207,7 +207,7 @@ const DATEMODE_1900_BASE=1; //1/1/1900 minus 1 day in FPC TDateTime DATEMODE_1904_BASE=1462; //1/1/1904 in FPC TDateTime - { FORMAT record constants } + { FORMAT record constants for BIFF5-BIFF8} // Subset of the built-in formats for US Excel, // including those needed for date/time output FORMAT_GENERAL = 0; //general/default format @@ -330,7 +330,7 @@ type // Returns the numberformat for a given XF record procedure ExtractNumberFormat(AXFIndex: WORD; out ANumberFormat: TsNumberFormat; out ADecimals: Word; - out ANumberFormatStr: String); + out ANumberFormatStr: String); virtual; // Finds format record for XF record pointed to by cell // Will not return info for built-in formats function FindFormatDataForCell(const AXFIndex: Integer): TFormatListData; @@ -382,8 +382,10 @@ type const AValue: TDateTime; ACell: PCell); override; // Writes out a PALETTE record containing all colors defined in the workbook procedure WritePalette(AStream: TStream); + public constructor Create(AWorkbook: TsWorkbook); override; + destructor Destroy; override; end; function IsExpNumberFormat(s: String; out Decimals: Word): Boolean; @@ -470,7 +472,7 @@ begin inherited Destroy; end; -{ Applies the XF formatting given by the given index to the specified cell } +{ Applies the XF formatting referred to by XFIndex to the specified cell } procedure TsSpreadBIFFReader.ApplyCellFormatting(ARow, ACol: Cardinal; XFIndex: Word); var @@ -521,6 +523,8 @@ begin end; end; +{ Extracts number format data from an XF record index by AXFIndex. + Valid for BIFF5-BIFF8. Needs to be overridden for BIFF2 } procedure TsSpreadBIFFReader.ExtractNumberFormat(AXFIndex: WORD; out ANumberFormat: TsNumberFormat; out ADecimals: Word; out ANumberFormatStr: String); @@ -568,7 +572,7 @@ begin if lFormatData = nil then begin // no custom format, so first test for default formats - lXFData := TXFListData (FXFList.Items[AXFIndex]); + lXFData := TXFListData(FXFList.Items[AXFIndex]); case lXFData.FormatIndex of FORMAT_DATE_DM: begin ANumberFormat := nfFmtDateTime; ANumberFormatStr := 'DM'; end; @@ -716,6 +720,9 @@ begin end; end; +{ Read column info (column width) from the stream. + Valid for BIFF3-BIFF8. + For BIFF2 use the records COLWIDTH and COLUMNDEFAULT. } procedure TsSpreadBiffReader.ReadColInfo(const AStream: TStream); var c, c1, c2: Cardinal; @@ -768,8 +775,8 @@ procedure TsSpreadBIFFReader.ReadNumber(AStream: TStream); var ARow, ACol: Cardinal; XF: WORD; - AValue: Double; - lDateTime: TDateTime; + value: Double; + dt: TDateTime; nf: TsNumberFormat; nd: word; nfs: String; @@ -777,14 +784,14 @@ begin ReadRowColXF(AStream,ARow,ACol,XF); { IEE 754 floating-point value } - AStream.ReadBuffer(AValue, 8); + AStream.ReadBuffer(value, 8); {Find out what cell type, set content type and value} ExtractNumberFormat(XF, nf, nd, nfs); - if IsDateTime(AValue, nf, lDateTime) then - FWorksheet.WriteDateTime(ARow, ACol, lDateTime, nf, nfs) + if IsDateTime(value, nf, dt) then + FWorksheet.WriteDateTime(ARow, ACol, dt, nf, nfs) else - FWorksheet.WriteNumber(ARow, ACol, AValue, nf, nd); + FWorksheet.WriteNumber(ARow, ACol, value, nf, nd); { Add attributes to cell } ApplyCellFormatting(ARow, ACol, XF); @@ -853,6 +860,11 @@ begin FDateMode := dm1900; end; +destructor TsSpreadBIFFWriter.Destroy; +begin + inherited Destroy; +end; + function TsSpreadBIFFWriter.FormulaElementKindToExcelTokenID( AElementKind: TFEKind; out ASecondaryID: Word): Word; const @@ -1126,7 +1138,7 @@ procedure TsSpreadBIFFWriter.WriteDateTime(AStream: TStream; const ARow, var ExcelDateSerial: double; begin - ExcelDateSerial := ConvertDateTimeToExcelDateTime(AValue,FDateMode); + ExcelDateSerial := ConvertDateTimeToExcelDateTime(AValue, FDateMode); // fpspreadsheet must already have set formatting to a date/datetime format, so // this will get written out as a pointer to the relevant XF record. // In the end, dates in xls are just numbers with a format. Pass it on to WriteNumber: