From 36765d4f9b15e15da46451dfa2f523937424a344 Mon Sep 17 00:00:00 2001 From: wp_xxyyzz Date: Fri, 13 Jun 2014 13:33:02 +0000 Subject: [PATCH] fpspreadsheet: Fix issues related to biff2 expecting localized formatting strings. git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@3164 8e941d3f-bd1b-0410-a28a-d453659cc2b4 --- .../examples/excel2demo/excel2write.lpr | 48 +++---- .../fpspreadsheet/fpsnumformatparser.pas | 43 +++++- components/fpspreadsheet/fpspreadsheet.pas | 14 +- components/fpspreadsheet/xlsbiff2.pas | 126 ++++++++++-------- components/fpspreadsheet/xlscommon.pas | 8 +- 5 files changed, 149 insertions(+), 90 deletions(-) diff --git a/components/fpspreadsheet/examples/excel2demo/excel2write.lpr b/components/fpspreadsheet/examples/excel2demo/excel2write.lpr index 16c40cded..fd8a0da32 100644 --- a/components/fpspreadsheet/examples/excel2demo/excel2write.lpr +++ b/components/fpspreadsheet/examples/excel2demo/excel2write.lpr @@ -45,6 +45,8 @@ begin } // Write some number cells + MyWorksheet.WriteNumber(0, 0, 0.0, nfFixed, 2); + (* MyWorksheet.WriteNumber(0, 0, 1.0); MyWorksheet.WriteUsedFormatting(0, 0, [uffBold, uffNumberFormat]); MyWorksheet.WriteNumber(0, 1, 2.0); @@ -138,11 +140,11 @@ begin MyWorksheet.WriteUTF8Text(r, 0, 'nfShortDateTime'); MyWorksheet.WriteDateTime(r, 1, now, nfShortDateTime); inc(r); - MyWorksheet.WriteUTF8Text(r, 0, 'nfFmtDateTime, DM'); - MyWorksheet.WriteDateTime(r, 1, now, nfFmtDateTime, 'DM'); + MyWorksheet.WriteUTF8Text(r, 0, 'nfCustom, ''dd/mmm'''); + MyWorksheet.WriteDateTime(r, 1, now, nfCustom, 'dd/mmm'''); inc(r); - MyWorksheet.WriteUTF8Text(r, 0, 'nfFmtDateTime, MY'); - MyWorksheet.WriteDateTime(r, 1, now, nfFmtDateTime, 'MY'); + MyWorksheet.WriteUTF8Text(r, 0, 'nfCustom, ''mmm/yy'''); + MyWorksheet.WriteDateTime(r, 1, now, nfCustom, 'mmm/yy'); inc(r); MyWorksheet.WriteUTF8Text(r, 0, 'nfShortTimeAM'); MyWorksheet.WriteDateTime(r, 1, now, nfShortTimeAM); @@ -150,16 +152,16 @@ begin MyWorksheet.WriteUTF8Text(r, 0, 'nfLongTimeAM'); MyWorksheet.WriteDateTime(r, 1, now, nfLongTimeAM); inc(r); - MyWorksheet.WriteUTF8Text(r, 0, 'nfFmtDateTime, MS'); - MyWorksheet.WriteDateTime(r, 1, now, nfFmtDateTime, 'MS'); + MyWorksheet.WriteUTF8Text(r, 0, 'nfCustom, nn:ss'); + MyWorksheet.WriteDateTime(r, 1, now, nfCustom, 'nn:ss'); MyWorksheet.WriteFontColor(r, 1, scGray); inc(r); - MyWorksheet.WriteUTF8Text(r, 0, 'nfFmtDateTime, MSZ'); - MyWorksheet.WriteDateTime(r, 1, now, nfFmtDateTime, 'MSZ'); + MyWorksheet.WriteUTF8Text(r, 0, 'nfCustom, nn:ss.z'); + MyWorksheet.WriteDateTime(r, 1, now, nfCustom, 'nn:ss.z'); MyWorksheet.WriteFontColor(r, 1, scGray); inc(r); - MyWorksheet.WriteUTF8Text(r, 0, 'nfFmtDateTime, mm:ss.zzz'); - MyWorksheet.WriteDateTime(r, 1, now, nfFmtDateTime, 'mm:ss.zzz'); + MyWorksheet.WriteUTF8Text(r, 0, 'nfCustom, mm:ss.zzz'); + MyWorksheet.WriteDateTime(r, 1, now, nfCustom, 'mm:ss.zzz'); MyWorksheet.WriteFontColor(r, 1, scGray); // Write formatted numbers @@ -269,24 +271,24 @@ begin MyWorksheet.WriteFontColor(r, 4, scGray); inc(r,2); MyWorksheet.WriteUTF8Text(r, 0, 'nfCurrency, 0 decs'); - MyWorksheet.WriteNumber(r, 1, number, nfCurrency, 0, '$'); - MyWorksheet.WriteNumber(r, 2, -number, nfCurrency, 0, '$'); - MyWorksheet.WriteNumber(r, 3, 0.0, nfCurrency, 0, '$'); + MyWorksheet.WriteCurrency(r, 1, number, nfCurrency, 0, '$'); + MyWorksheet.WriteCurrency(r, 2, -number, nfCurrency, 0, '$'); + MyWorksheet.WriteCurrency(r, 3, 0.0, nfCurrency, 0, '$'); inc(r); MyWorksheet.WriteUTF8Text(r, 0, 'nfCurrencyRed, 0 decs'); - MyWorksheet.WriteNumber(r, 1, number, nfCurrencyRed, 0, 'USD'); - MyWorksheet.WriteNumber(r, 2, -number, nfCurrencyRed, 0, 'USD'); - MyWorksheet.WriteNumber(r, 3, 0.0, nfCurrencyRed, 0, 'USD'); + MyWorksheet.WriteCurrency(r, 1, number, nfCurrencyRed, 0, 'USD'); + MyWorksheet.WriteCurrency(r, 2, -number, nfCurrencyRed, 0, 'USD'); + MyWorksheet.WriteCurrency(r, 3, 0.0, nfCurrencyRed, 0, 'USD'); inc(r); MyWorksheet.WriteUTF8Text(r, 0, 'nfAccounting, 0 decs'); - MyWorksheet.WriteNumber(r, 1, number, nfAccounting, 0, 'USD'); - MyWorksheet.WriteNumber(r, 2, -number, nfAccounting, 0, 'USD'); - MyWorksheet.WriteNumber(r, 3, 0.0, nfAccounting, 0, 'USD'); + MyWorksheet.WriteCurrency(r, 1, number, nfAccounting, 0, 'USD'); + MyWorksheet.WriteCurrency(r, 2, -number, nfAccounting, 0, 'USD'); + MyWorksheet.WriteCurrency(r, 3, 0.0, nfAccounting, 0, 'USD'); inc(r); MyWorksheet.WriteUTF8Text(r, 0, 'nfAccountingRed, 0 decs'); - MyWorksheet.WriteNumber(r, 1, number, nfAccountingRed, 0, 'USD'); - MyWorksheet.WriteNumber(r, 2, -number, nfAccountingRed, 0, 'USD'); - MyWorksheet.WriteNumber(r, 3, 0.0, nfAccountingRed, 0, 'USD'); + MyWorksheet.WriteCurrency(r, 1, number, nfAccountingRed, 0, 'USD'); + MyWorksheet.WriteCurrency(r, 2, -number, nfAccountingRed, 0, 'USD'); + MyWorksheet.WriteCurrency(r, 3, 0.0, nfAccountingRed, 0, 'USD'); inc(r, 2); MyWorksheet.WriteUTF8Text(r, 0, 'nfCustom, "$"#,##0_);("$"#,##0)'); MyWorksheet.WriteNumber(r, 1, number); @@ -378,7 +380,7 @@ begin MyWorksheet.WriteRowInfo(5, lRow); lRow.Height := 2; // 2 lines 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/fpsnumformatparser.pas b/components/fpspreadsheet/fpsnumformatparser.pas index 55ed9c8ea..475141d7c 100644 --- a/components/fpspreadsheet/fpsnumformatparser.pas +++ b/components/fpspreadsheet/fpsnumformatparser.pas @@ -155,6 +155,8 @@ type *) function GetDateTimeCode(ASection: Integer): String; function IsDateTimeFormat: Boolean; + procedure LimitDecimals; + procedure Localize; property CurrencySymbol: String read GetCurrencySymbol; property Decimals: Byte read GetDecimals write SetDecimals; @@ -358,10 +360,8 @@ begin case element.Token of nftText: if element.TextValue <> '' then result := Result + '"' + element.TextValue + '"'; - nftThSep: - Result := Result + ','; - nftDecSep: - Result := Result + '.'; + nftThSep, nftDecSep: + Result := Result + element.TextValue; nftDigit: Result := Result + '0'; nftOptDigit, nftOptDec: @@ -812,6 +812,7 @@ begin nftDay : Result := Result + 'd'; nftHour : Result := Result + 'h'; nftMinute : Result := Result + 'n'; + nftSecond : Result := Result + 's'; nftMilliSeconds: Result := Result + 'z'; end; inc(i); @@ -1274,6 +1275,40 @@ begin (FSections[ASection].Elements[AIndex].Token = AToken); end; +{ Limits the decimals to 0 or 2, as required by Excel } +procedure TsNumFormatParser.LimitDecimals; +var + i, j: Integer; +begin + for j:=0 to High(FSections) do + for i:=0 to High(FSections[j].Elements) do + if FSections[j].Elements[i].Token = nftDecs then + if FSections[j].Elements[i].IntValue > 0 then + FSections[j].Elements[i].IntValue := 2; +end; + +{ Localizes the thousand- and decimal separator symbols by replacing them with + the FormatSettings value of the workbook. A recreated format string will be + localized as required by Excel2 } +procedure TsNumFormatParser.Localize; +var + i, j: Integer; + fs: TFormatSettings; + txt: String; +begin + fs := FWorkbook.FormatSettings; + for j:=0 to High(FSections) do + for i:=0 to High(FSections[j].Elements) do begin + txt := FSections[j].Elements[i].TextValue; + case FSections[j].Elements[i].Token of + nftThSep : txt := fs.ThousandSeparator; + nftDecSep : txt := fs.DecimalSeparator; + nftCurrSymbol: txt := UTF8ToAnsi(txt); + end; + FSections[j].Elements[i].TextValue := txt; + end; +end; + function TsNumFormatParser.NextToken: Char; var delta: Integer; diff --git a/components/fpspreadsheet/fpspreadsheet.pas b/components/fpspreadsheet/fpspreadsheet.pas index b5a1f3f8e..788ee5b7e 100755 --- a/components/fpspreadsheet/fpspreadsheet.pas +++ b/components/fpspreadsheet/fpspreadsheet.pas @@ -700,8 +700,7 @@ type procedure ConvertAfterReading(AFormatIndex: Integer; var AFormatString: String; var ANumFormat: TsNumberFormat); virtual; procedure ConvertBeforeWriting(var AFormatString: String; - var ANumFormat: TsNumberFormat; var ADecimals: Byte; - var ACurrencySymbol: String); virtual; + var ANumFormat: TsNumberFormat); virtual; procedure Delete(AIndex: Integer); function Find(ANumFormat: TsNumberFormat; AFormatString: String): Integer; overload; function Find(AFormatString: String): Integer; overload; @@ -3479,7 +3478,7 @@ end; to a format compatible with the spreadsheet file format. Nothing is changed here. The method needs to be overridden. } procedure TsCustomNumFormatList.ConvertBeforeWriting(var AFormatString: String; - var ANumFormat: TsNumberFormat; var ADecimals: Byte; var ACurrencySymbol: String); + var ANumFormat: TsNumberFormat); begin // nothing to do here. But see, e.g., xlscommon.TsBIFFNumFormatList end; @@ -3600,13 +3599,12 @@ function TsCustomNumFormatList.FormatStringForWriting(AIndex: Integer): String; var item: TsNumFormatdata; nf: TsNumberFormat; - decs: Byte; - cs: String; begin item := Items[AIndex]; if item <> nil then begin Result := item.FormatString; - ConvertBeforeWriting(Result, nf, decs, cs); + nf := item.NumFormat; + ConvertBeforeWriting(Result, nf); end else Result := ''; end; @@ -3652,7 +3650,7 @@ begin inherited Create; FWorkbook := AWorkbook; CreateNumFormatList; - FNumFormatList.FWorkbook := AWorkbook; +// FNumFormatList.FWorkbook := AWorkbook; end; destructor TsCustomSpreadReader.Destroy; @@ -3723,7 +3721,7 @@ begin inherited Create; FWorkbook := AWorkbook; CreateNumFormatList; - FNumFormatList.FWorkbook := AWorkbook; +// FNumFormatList.FWorkbook := AWorkbook; end; destructor TsCustomSpreadWriter.Destroy; diff --git a/components/fpspreadsheet/xlsbiff2.pas b/components/fpspreadsheet/xlsbiff2.pas index 215f6ac2d..9cf90ce16 100755 --- a/components/fpspreadsheet/xlsbiff2.pas +++ b/components/fpspreadsheet/xlsbiff2.pas @@ -43,7 +43,7 @@ type protected procedure AddBuiltinFormats; override; procedure ConvertBeforeWriting(var AFormatString: String; - var ANumFormat: TsNumberFormat; var ADecimals: Byte; var ACurrencySymbol: String); override; + var ANumFormat: TsNumberFormat); override; function FindFormatOf(AFormatCell: PCell): Integer; override; public constructor Create(AWorkbook: TsWorkbook); @@ -57,6 +57,7 @@ type WorkBookEncoding: TsEncoding; FWorksheet: TsWorksheet; FFont: TsFont; + FFmtIndex: Integer; protected procedure ApplyCellFormatting(ARow, ACol: Cardinal; XFIndex: Word); override; procedure CreateNumFormatList; override; @@ -66,6 +67,7 @@ type procedure ReadColWidth(AStream: TStream); procedure ReadFont(AStream: TStream); procedure ReadFontColor(AStream: TStream); + procedure ReadFormat(AStream: TStream); override; procedure ReadFormula(AStream: TStream); override; procedure ReadInteger(AStream: TStream); procedure ReadLabel(AStream: TStream); override; @@ -170,11 +172,41 @@ begin inherited Create(AWorkbook); end; +{ Prepares the list of built-in number formats. They are created in the default + dialect for FPC, they have to be converted to Excel syntax before writing. + Note that Excel2 expects them to be localized. This is something which has to + be taken account of in ConverBeforeWriting.} procedure TsBIFF2NumFormatList.AddBuiltinFormats; var fs: TFormatSettings; ds, ts, cs: string; begin + fs := FWorkbook.FormatSettings; + AddFormat( 0, '', nfGeneral); + AddFormat( 1, '0', nfFixed); + AddFormat( 2, '0.00', nfFixed); + AddFormat( 3, '#,##0', nfFixedTh); + AddFormat( 4, '#,##0.00', nfFixedTh); + AddFormat( 5, '"'+cs+'"#,##0_);("'+cs+'"#,##0)', nfCurrency); + AddFormat( 6, '"'+cs+'"#,##0_);[Red]("'+cs+'"#,##0)', nfCurrencyRed); + AddFormat( 7, '"'+cs+'"#,##0.00_);("'+cs+'"#,##0.00)', nfCurrency); + AddFormat( 8, '"'+cs+'"#,##0.00_);[Red]("'+cs+'"#,##0.00)', nfCurrency); + AddFormat( 9, '0%', nfPercentage); + AddFormat(10, '0.00%', nfPercentage); + AddFormat(11, '0.00E+00', nfExp); + AddFormat(12, fs.ShortDateFormat, nfShortDate); + AddFormat(13, fs.LongDateFormat, nfLongDate); + AddFormat(14, 'd/mmm', nfCustom); + AddFormat(15, 'mmm/yy', nfCustom); + //AddFormat(14, SpecialDateTimeFormat('dm', fs, true), nfFmtDateTime); + //AddFormat(15, SpecialDateTimeFormat('my', fs, true), nfFmtDateTime); + AddFormat(16, AddAMPM(fs.ShortTimeFormat, fs), nfShortTimeAM); + AddFormat(17, AddAMPM(fs.LongTimeFormat, fs), nfLongTimeAM); + AddFormat(18, fs.ShortTimeFormat, nfShortTime); + AddFormat(19, fs.LongTimeFormat, nfLongTime); + AddFormat(20, fs.ShortDateFormat + ' ' + fs.ShortTimeFormat, nfShortDateTime); + + (* fs := Workbook.FormatSettings; ds := fs.DecimalSeparator; ts := fs.ThousandSeparator; @@ -202,61 +234,29 @@ begin AddFormat(18, fs.ShortTimeFormat, nfShortTime); AddFormat(19, fs.LongTimeFormat, nfLongTime); AddFormat(20, fs.ShortDateFormat + ' ' + fs.ShortTimeFormat, nfShortDateTime); - + *) FFirstFormatIndexInFile := 0; // BIFF2 stores built-in formats to file. FNextFormatIndex := 21; // not needed - there are not user-defined formats end; procedure TsBIFF2NumFormatList.ConvertBeforeWriting(var AFormatString: String; - var ANumFormat: TsNumberFormat; var ADecimals: Byte; var ACurrencySymbol: String); + var ANumFormat: TsNumberFormat); var fmt: String; + parser: TsNumFormatParser; begin - case ANumFormat of - nfGeneral: - ; - nfFixed, nfFixedTh, nfPercentage, nfExp, - nfCurrency, nfCurrencyRed, nfAccounting, nfAccountingRed: - if ADecimals > 0 then ADecimals := 2; - nfSci: - begin - if ADecimals > 0 then ADecimals := 2; - ANumFormat := nfExp; - end; - { - nfFmtDateTime: - begin - fmt := lowercase(AFormatString); - if (fmt = 'd-mm') or (fmt = 'd/mm') or - (fmt = 'dd-mm') or (fmt = 'dd/mm') or - (fmt = 'dd-mmm') or (fmt = 'dd/mmm') - then - AFormatString := SpecialDateTimeFormat('dm', Workbook.FormatSettings, true) - else - if (fmt = 'm-yy') or (fmt = 'm/yy') or - (fmt = 'mm-yy') or (fmt = 'mm/yy') or - (fmt = 'mmm-yy') or (fmt = 'mmm/yy') or - (fmt = 'm-yyyy') or (fmt = 'm/yyyy') or - (fmt = 'mm-yyyy') or (fmt = 'mm/yyyy') or - (fmt = 'mmm-yyyy') or (fmt = 'mmm-yyyy') - then - AFormatString := SpecialDateTimeFormat('my', Workbook.FormatSettings, true) - else - if (copy(fmt, 1, 5) = 'nn:ss') or (copy(fmt, 1, 5) = 'mm:ss') or - (copy(fmt, 1, 4) = 'n:ss') or (copy(fmt, 1, 4) = 'm:ss') - then - ANumFormat := nfLongTime - else - ANumFormat := nfShortDateTime; - end; - } - nfCustom, nfTimeInterval: - begin - ANumFormat := nfGeneral; - AFormatString := ''; - ADecimals := 0; - end; + if AFormatString = '' then + AFormatString := 'General' + else begin + parser := TsNumFormatParser.Create(FWorkbook, AFormatString); + try + parser.Localize; + parser.LimitDecimals; + AFormatString := parser.FormatString[nfdExcel]; + finally + parser.Free; + end; end; end; @@ -276,9 +276,8 @@ begin parser.Free; end; + Result := 0; case AFormatCell^.NumberFormat of - nfGeneral, - nfTimeInterval : Result := 0; nfFixed : Result := IfThen(decs = 0, 1, 2); nfFixedTh : Result := IfThen(decs = 0, 3, 4); nfCurrency, @@ -302,11 +301,21 @@ end; { Creates formatting strings that are written into the file. These are the strings in the format list. The only exception is the nfGeneral entry which is written as "General". } - function TsBIFF2NumFormatList.FormatStringForWriting(AIndex: Integer): String; +function TsBIFF2NumFormatList.FormatStringForWriting(AIndex: Integer): String; begin Result := inherited FormatStringForWriting(AIndex); + { if Result = '' then - Result := 'General'; + Result := 'General' + else begin + parser := TsNumFormatParser.Create(Workbook, Result); + try + parser.Localize; + Result := Parser.FormatString[nfdExcel]; + finally + end; + end; + } end; @@ -445,6 +454,13 @@ begin FFont.Color := WordLEToN(AStream.ReadWord); end; +// Read the FORMAT record for formatting numerical data +procedure TsSpreadBIFF2Reader.ReadFormat(AStream: TStream); +begin + // We ignore the formats in the file, everything is known + // (Using the formats in the file would require de-localizing them). +end; + procedure TsSpreadBIFF2Reader.ReadFromStream(AStream: TStream; AData: TsWorkbook); var BIFF2EOF: Boolean; @@ -475,6 +491,7 @@ begin INT_EXCEL_ID_BLANK : ReadBlank(AStream); INT_EXCEL_ID_FONT : ReadFont(AStream); INT_EXCEL_ID_FONTCOLOR : ReadFontColor(AStream); + INT_EXCEL_ID_FORMAT : ReadFormat(AStream); INT_EXCEL_ID_INTEGER : ReadInteger(AStream); INT_EXCEL_ID_NUMBER : ReadNumber(AStream); INT_EXCEL_ID_LABEL : ReadLabel(AStream); @@ -917,7 +934,12 @@ end; { Builds up the list of number formats to be written to the biff2 file. Unlike biff5+ no formats are added here because biff2 supports only 21 standard formats; these formats have been added by the NumFormatList's - AddBuiltInFormats. } + AddBuiltInFormats. + + NOT CLEAR IF THIS IS TRUE ???? + + } + // ToDo: check if the BIFF2 format is really restricted to 21 formats. procedure TsSpreadBIFF2Writer.ListAllNumFormats; begin // Nothing to do here. diff --git a/components/fpspreadsheet/xlscommon.pas b/components/fpspreadsheet/xlscommon.pas index c36f35374..fa3ab4ac0 100644 --- a/components/fpspreadsheet/xlscommon.pas +++ b/components/fpspreadsheet/xlscommon.pas @@ -358,8 +358,7 @@ type protected procedure AddBuiltinFormats; override; procedure ConvertBeforeWriting(var AFormatString: String; - var ANumFormat: TsNumberFormat; var ADecimals: Byte; - var ACurrencySymbol: String); override; + var ANumFormat: TsNumberFormat); override; public end; @@ -766,7 +765,8 @@ begin end; procedure TsBIFFNumFormatList.ConvertBeforeWriting(var AFormatString: String; - var ANumFormat: TsNumberFormat; var ADecimals: Byte; var ACurrencySymbol: String); + var ANumFormat: TsNumberFormat); +{var ADecimals: Byte; var ACurrencySymbol: String); } var parser: TsNumFormatParser; fmt: String; @@ -777,8 +777,10 @@ begin // We convert the fpc format string to Excel dialect AFormatString := parser.FormatString[nfdExcel]; ANumFormat := parser.NumFormat; + { ADecimals := parser.Decimals; ACurrencySymbol := parser.CurrencySymbol; + } end; finally parser.Free;