diff --git a/components/fpspreadsheet/source/common/fpsnumformat.pas b/components/fpspreadsheet/source/common/fpsnumformat.pas index 020beb095..3222168f1 100644 --- a/components/fpspreadsheet/source/common/fpsnumformat.pas +++ b/components/fpspreadsheet/source/common/fpsnumformat.pas @@ -132,6 +132,8 @@ type NumFormat: TsNumberFormat; {@@ Number of decimal places used by the format string } Decimals: Byte; + {@@ Minimum number of digits before the decimal separator } + MinIntDigits: Byte; {@@ Factor by which a number will be multiplied before converting to string } Factor: Double; {@@ Digits to be used for the integer part of a fraction } @@ -310,7 +312,8 @@ function BuildDateTimeFormatString(ANumberFormat: TsNumberFormat; function BuildFractionFormatString(AMixedFraction: Boolean; ANumeratorDigits, ADenominatorDigits: Integer): String; function BuildNumberFormatString(ANumberFormat: TsNumberFormat; - const AFormatSettings: TFormatSettings; ADecimals: Integer = -1): String; + const AFormatSettings: TFormatSettings; ADecimals: Integer = -1; + AMinIntDigits: Integer = 1): String; function BuildFormatStringFromSection(const ASection: TsNumFormatSection): String; @@ -1401,39 +1404,53 @@ end; value of the FormatSettings is used. In case of a fraction format "ADecimals" refers to the maximum count digits of the denominator. + @param AMinIntDigits minimum count of integer digits, i.e. count of '0' in + the format string before the decimal separator @return String of formatting codes @example ANumberFormat = nfFixedTh, ADecimals = 2 --> '#,##0.00' -------------------------------------------------------------------------------} function BuildNumberFormatString(ANumberFormat: TsNumberFormat; - const AFormatSettings: TFormatSettings; ADecimals: Integer = -1): String; + const AFormatSettings: TFormatSettings; ADecimals: Integer = -1; + AMinIntDigits: Integer = 1): String; var - decs: String; + decdigits: String; + intdigits: String; begin Result := ''; + if AMinIntDigits > 0 then + intdigits := DupeString('0', AMinIntDigits) + else + intdigits := '#'; if ADecimals = -1 then ADecimals := AFormatSettings.CurrencyDecimals; - decs := DupeString('0', ADecimals); - if ADecimals > 0 then decs := '.' + decs; + if ADecimals > 0 then + decdigits := '.' + DupeString('0', ADecimals) + else + decdigits := ''; case ANumberFormat of nfText: Result := '@'; nfFixed: - Result := '0' + decs; + Result := intdigits + decdigits; nfFixedTh: - Result := '#,##0' + decs; + begin + while Length(IntDigits) < 4 do intDigits := '#' + intdigits; + System.Insert(',', intdigits, Length(intdigits)-2); + Result := intdigits + decdigits; + end; nfExp: - Result := '0' + decs + 'E+00'; + Result := intdigits + decdigits + 'E+00'; nfPercentage: - Result := '0' + decs + '%'; + Result := intdigits + decdigits + '%'; nfFraction: if ADecimals = 0 then // "ADecimals" has a different meaning here... Result := '# ??/??' // This is the default fraction format else begin - decs := DupeString('?', ADecimals); - Result := '# ' + decs + '/' + decs; + decdigits := DupeString('?', ADecimals); + Result := '# ' + decdigits + '/' + decdigits; end; nfCurrency, nfCurrencyRed: Result := BuildCurrencyFormatString(ANumberFormat, AFormatSettings, @@ -2834,7 +2851,12 @@ begin case section^.Elements[el].Token of nftZeroDecs: section^.Decimals := section^.Elements[el].IntValue; - nftIntZeroDigit, nftIntOptDigit, nftIntSpaceDigit: + nftIntZeroDigit: + begin + section^.MinIntDigits := section^.Elements[el].IntValue; + i := section^.Elements[el].IntValue; + end; + nftIntOptDigit, nftIntSpaceDigit: i := section^.Elements[el].IntValue; nftFracNumSpaceDigit, nftFracNumZeroDigit: section^.FracNumerator := section^.Elements[el].IntValue; diff --git a/components/fpspreadsheet/source/common/fpsopendocument.pas b/components/fpspreadsheet/source/common/fpsopendocument.pas index 810bbde89..fbb7013fe 100644 --- a/components/fpspreadsheet/source/common/fpsopendocument.pas +++ b/components/fpspreadsheet/source/common/fpsopendocument.pas @@ -853,7 +853,8 @@ begin if (el+3 < nel) and (Elements[el+1].Token = nftExpChar) then begin Result := Result + '' + else + *) // Standard integer if (el = nel-1) or (Elements[el+1].Token <> nftDecSep) then begin Result := Result + ' 0) then Result := Result + Format(' number:display-factor="%.0f"', [1.0/Factor]); @@ -2962,13 +2972,13 @@ procedure TsSpreadOpenDocReader.ReadNumFormats(AStylesNode: TDOMNode); nodeName: String; nf: TsNumberFormat; nfs: String; - decs: Byte; - sint: String; s: String; f: Double; fracInt, fracNum, fracDenom: Integer; grouping: Boolean; nex: Integer; + nint: Integer; + ndecs: Integer; cs: String; color: TsColor; hasColor: Boolean; @@ -2987,9 +2997,8 @@ procedure TsSpreadOpenDocReader.ReadNumFormats(AStylesNode: TDOMNode); end else if nodeName = 'number:number' then begin - sint := GetAttrValue(node, 'number:min-integer-digits'); - if sint = '' then sint := '1'; - + s := GetAttrValue(node, 'number:min-integer-digits'); + if s <> '' then nint := StrToInt(s) else nint := 0; s := GetAttrValue(node, 'number:decimal-places'); if s = '' then s := GetAttrValue(node, 'decimal-places'); @@ -2999,12 +3008,12 @@ procedure TsSpreadOpenDocReader.ReadNumFormats(AStylesNode: TDOMNode); nfs := nfs + 'General'; end else begin - decs := StrToInt(s); + ndecs := StrToInt(s); grouping := GetAttrValue(node, 'number:grouping') = 'true'; s := GetAttrValue(node, 'number:display-factor'); if s <> '' then f := StrToFloat(s, FPointSeparatorSettings) else f := 1.0; nf := IfThen(grouping, nfFixedTh, nfFixed); - nfs := nfs + BuildNumberFormatString(nf, Workbook.FormatSettings, decs); //, StrToInt(sint)); + nfs := nfs + BuildNumberFormatString(nf, Workbook.FormatSettings, ndecs, nint); if f <> 1.0 then begin nf := nfCustom; while (f > 1.0) do @@ -3031,11 +3040,13 @@ procedure TsSpreadOpenDocReader.ReadNumFormats(AStylesNode: TDOMNode); if nodeName = 'number:scientific-number' then begin nf := nfExp; + s := GetAttrValue(node, 'number:min-integer-digits'); + if s <> '' then nint := StrToInt(s) else nint := 0; s := GetAttrValue(node, 'number:decimal-places'); - if s <> '' then decs := StrToInt(s) else decs := 0; + if s <> '' then ndecs := StrToInt(s) else ndecs := 0; s := GetAttrValue(node, 'number:min-exponent-digits'); if s <> '' then nex := StrToInt(s) else nex := 1; - nfs := nfs + BuildNumberFormatString(nfFixed, Workbook.FormatSettings, decs); + nfs := nfs + BuildNumberFormatString(nfFixed, Workbook.FormatSettings, ndecs, nint); nfs := nfs + 'E+' + DupeString('0', nex); end else if nodeName = 'number:currency-symbol' then diff --git a/components/fpspreadsheet/source/common/fpspreadsheet.pas b/components/fpspreadsheet/source/common/fpspreadsheet.pas index a6e32baeb..531401bf4 100644 --- a/components/fpspreadsheet/source/common/fpspreadsheet.pas +++ b/components/fpspreadsheet/source/common/fpspreadsheet.pas @@ -256,9 +256,11 @@ type function WriteNumber(ARow, ACol: Cardinal; ANumber: double): PCell; overload; procedure WriteNumber(ACell: PCell; ANumber: Double); overload; function WriteNumber(ARow, ACol: Cardinal; ANumber: double; - ANumFormat: TsNumberFormat; ADecimals: Byte = 2): PCell; overload; + ANumFormat: TsNumberFormat; ADecimals: Byte = 2; + AMinIntDigits: Integer = 1): PCell; overload; procedure WriteNumber(ACell: PCell; ANumber: Double; - ANumFormat: TsNumberFormat; ADecimals: Byte = 2); overload; + ANumFormat: TsNumberFormat; ADecimals: Byte = 2; + AMinIntDigits: Integer = 1); overload; function WriteNumber(ARow, ACol: Cardinal; ANumber: double; ANumFormat: TsNumberFormat; ANumFormatString: String): PCell; overload; procedure WriteNumber(ACell: PCell; ANumber: Double; @@ -4789,34 +4791,39 @@ end; {@@ ---------------------------------------------------------------------------- Writes a floating-point number to a cell - @param ARow Cell row index - @param ACol Cell column index - @param ANumber Number to be written - @param ANumFormat Identifier for a built-in number format, e.g. nfFixed (optional) - @param ADecimals Number of decimal places used for formatting (optional) + @param ARow Cell row index + @param ACol Cell column index + @param ANumber Number to be written + @param ANumFormat Identifier for a built-in number format, + e.g. nfFixed (optional) + @param ADecimals Number of decimal places used for formatting (optional) + @param AMinIntDigits Minimum count of digits before the decimal separator @return Pointer to cell created or used @see TsNumberFormat -------------------------------------------------------------------------------} function TsWorksheet.WriteNumber(ARow, ACol: Cardinal; ANumber: double; - ANumFormat: TsNumberFormat; ADecimals: Byte = 2): PCell; + ANumFormat: TsNumberFormat; ADecimals: Byte = 2; + AMinIntDigits: Integer = 1): PCell; begin Result := GetCell(ARow, ACol); - WriteNumber(Result, ANumber, ANumFormat, ADecimals); + WriteNumber(Result, ANumber, ANumFormat, ADecimals, AMinIntDigits); end; {@@ ---------------------------------------------------------------------------- Writes a floating-point number to a cell - @param ACell Pointer to the cell - @param ANumber Number to be written - @param ANumFormat Identifier for a built-in number format, e.g. nfFixed - @param ADecimals Optional number of decimal places used for formatting - If ANumFormat is nfFraction the ADecimals defines the - digits of Numerator and denominator. + @param ACell Pointer to the cell + @param ANumber Number to be written + @param ANumFormat Identifier for a built-in number format, e.g. nfFixed + @param ADecimals Optional number of decimal places used for formatting + If ANumFormat is nfFraction the ADecimals defines the + digits of Numerator and denominator. + @param AMinIntDigits Minimum count of digits before the decimal separator @see TsNumberFormat -------------------------------------------------------------------------------} procedure TsWorksheet.WriteNumber(ACell: PCell; ANumber: Double; - ANumFormat: TsNumberFormat; ADecimals: Byte = 2); + ANumFormat: TsNumberFormat; ADecimals: Byte = 2; + AMinIntDigits: Integer = 1); var fmt: TsCellFormat; nfs: String; @@ -4837,7 +4844,7 @@ begin if ADecimals = 0 then ADecimals := 1; nfs := '# ' + DupeString('?', ADecimals) + '/' + DupeString('?', ADecimals); end else - nfs := BuildNumberFormatString(fmt.NumberFormat, Workbook.FormatSettings, ADecimals); + nfs := BuildNumberFormatString(fmt.NumberFormat, Workbook.FormatSettings, ADecimals, AMinIntDigits); fmt.NumberFormatIndex := Workbook.AddNumberFormat(nfs); end else begin Exclude(fmt.UsedFormattingFields, uffNumberFormat); diff --git a/components/fpspreadsheet/tests/formattests.pas b/components/fpspreadsheet/tests/formattests.pas index 83fcf9369..5262e392a 100644 --- a/components/fpspreadsheet/tests/formattests.pas +++ b/components/fpspreadsheet/tests/formattests.pas @@ -75,6 +75,8 @@ type procedure TestWriteRead_MergedCells(AFormat: TsSpreadsheetFormat); // Many XF records procedure TestWriteRead_ManyXF(AFormat: TsSpreadsheetFormat); + // Format strings + procedure TestWriteRead_FormatStrings(AFormat: TsSpreadsheetFormat); published // Writes out numbers & reads back. @@ -89,6 +91,7 @@ type procedure TestWriteRead_BIFF2_MergedCells; procedure TestWriteRead_BIFF2_NumberFormats; procedure TestWriteRead_BIFF2_ManyXFRecords; + procedure TestWriteRead_BIFF2_FormatStrings; // These features are not supported by Excel2 --> no test cases required! // - Background // - BorderStyle @@ -107,6 +110,7 @@ type procedure TestWriteRead_BIFF5_NumberFormats; procedure TestWriteRead_BIFF5_TextRotation; procedure TestWriteRead_BIFF5_WordWrap; + procedure TestWriteRead_BIFF5_FormatStrings; { BIFF8 Tests } procedure TestWriteRead_BIFF8_Alignment; @@ -120,6 +124,7 @@ type procedure TestWriteRead_BIFF8_NumberFormats; procedure TestWriteRead_BIFF8_TextRotation; procedure TestWriteRead_BIFF8_WordWrap; + procedure TestWriteRead_BIFF8_FormatStrings; { ODS Tests } procedure TestWriteRead_ODS_Alignment; @@ -133,6 +138,7 @@ type procedure TestWriteRead_ODS_NumberFormats; procedure TestWriteRead_ODS_TextRotation; procedure TestWriteRead_ODS_WordWrap; + procedure TestWriteRead_ODS_FormatStrings; { OOXML Tests } procedure TestWriteRead_OOXML_Alignment; @@ -146,6 +152,7 @@ type procedure TestWriteRead_OOXML_NumberFormats; procedure TestWriteRead_OOXML_TextRotation; procedure TestWriteRead_OOXML_WordWrap; + procedure TestWriteRead_OOXML_FormatStrings; { CSV Tests } procedure TestWriteRead_CSV_DateTimeFormats; @@ -1677,6 +1684,84 @@ begin TestWriteRead_ManyXF(sfExcel2); end; + +procedure TSpreadWriteReadFormatTests.TestWriteRead_FormatStrings( + AFormat: TsSpreadsheetFormat); +const + FormatStrings: Array[0..5] of string = ( + '#', '#.000', '#.000E+00', '0', '0.000', '0.000E+00'); + Numbers: array[0..4] of double = ( + 0, 1.23456789, -1.23456789, 1234.56789, -1234.56789); +var + MyWorkBook: TsWorkbook; + MyWorkSheet: TsWorksheet; + sollStr: String; + currStr: String; + currVal: Double; + r, c: Cardinal; + TempFile: String; +begin + MyWorkbook := TsWorkbook.Create; + try + MyWorkSheet := MyWorkBook.AddWorksheet('Sheet'); + for r := 0 to High(Numbers) do + for c := 0 to High(FormatStrings) do + MyWorksheet.WriteNumber(r, c, Numbers[r], nfCustom, FormatStrings[c]); + TempFile := NewTempFile; + MyWorkBook.WriteToFile(TempFile, AFormat, true); + finally + MyWorkbook.Free; + end; + + MyWorkbook := TsWorkbook.Create; + try + MyWorkbook.ReadFromFile(TempFile, AFormat); + MyWorksheet := MyWorkbook.GetWorksheetByName('Sheet'); + CheckEquals(High(Numbers), MyWorksheet.GetLastRowIndex, 'Row count mismatch'); + CheckEquals(High(FormatStrings), MyWorksheet.GetLastColIndex, 'Col count mismatch'); + for r := 0 to MyWorksheet.GetLastRowIndex do + for c := 0 to MyWorksheet.GetLastColIndex do begin + currStr := MyWorksheet.ReadAsText(r, c); + currVal := MyWorksheet.ReadAsNumber(r, c); + sollStr := FormatFloat(FormatStrings[c], currVal); + // Quick & dirty fix for FPC's issue with #.00E+00 showing a leading zero + if (sollStr <> '') and (sollStr[1] = '0') and + (pos('#.', FormatStrings[c]) = 1) and (pos('E', FormatStrings[c]) > 0) + then + Delete(sollStr, 1, 1); + CheckEquals(sollStr, currStr, Format('Formatted cell mismatch, FormatStr "%s", Cell %s', [ + FormatStrings[c], GetCellString(r, c)])); + end; + finally + MyWorkbook.Free; + end; +end; + +procedure TSpreadWriteReadFormatTests.TestWriteRead_BIFF2_FormatStrings; +begin + TestWriteRead_FormatStrings(sfExcel2); +end; + +procedure TSpreadWriteReadFormatTests.TestWriteRead_BIFF5_FormatStrings; +begin + TestWriteRead_FormatStrings(sfExcel5); +end; + +procedure TSpreadWriteReadFormatTests.TestWriteRead_BIFF8_FormatStrings; +begin + TestWriteRead_FormatStrings(sfExcel8); +end; + +procedure TSpreadWriteReadFormatTests.TestWriteRead_OOXML_FormatStrings; +begin + TestWriteRead_FormatStrings(sfOOXML); +end; + +procedure TSpreadWriteReadFormatTests.TestWriteRead_ODS_FormatStrings; +begin + TestWriteRead_FormatStrings(sfOpenDocument); +end; + initialization RegisterTest(TSpreadWriteReadFormatTests); InitSollFmtData; diff --git a/components/fpspreadsheet/tests/numformatparsertests.pas b/components/fpspreadsheet/tests/numformatparsertests.pas index 63d06e5a9..53b712918 100644 --- a/components/fpspreadsheet/tests/numformatparsertests.pas +++ b/components/fpspreadsheet/tests/numformatparsertests.pas @@ -35,7 +35,7 @@ type var ParserTestData: Array[0..13] of TParserTestData; - RoundingTestData: Array[0..62] of TRoundingTestData = ( + RoundingTestData: Array[0..65] of TRoundingTestData = ( // 0 (FormatString: '0'; Number: 1.2; SollString: '1'), (FormatString: '0'; Number: 1.9; SollString: '2'), @@ -109,7 +109,12 @@ var (FormatString: '0.0##'; Number: 1.21; SollString: '1.21'), (FormatString: '0.0##'; Number: 1.212; SollString: '1.212'), (FormatString: '0.0##'; Number: 1.2134; SollString: '1.213'), - (FormatString: '0.0##'; Number: 1.2135; SollString: '1.214') + (FormatString: '0.0##'; Number: 1.2135; SollString: '1.214'), + + // 63 + (FormatString: '#'; Number: 0; SollString: ''), + (FormatString: '#'; Number: 1.2; SollString: '1'), + (FormatString: '#'; Number: -1.2; SollString: '-1') ); diff --git a/components/fpspreadsheet/tests/spreadtestgui.lpi b/components/fpspreadsheet/tests/spreadtestgui.lpi index 05b79648b..6bf720977 100644 --- a/components/fpspreadsheet/tests/spreadtestgui.lpi +++ b/components/fpspreadsheet/tests/spreadtestgui.lpi @@ -1,7 +1,7 @@ - + @@ -19,9 +19,10 @@ - - - + + + +