fpspreadsheet: Allow number formats with no leading digits. Add corresponding unit test cases.

git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@6218 8e941d3f-bd1b-0410-a28a-d453659cc2b4
This commit is contained in:
wp_xxyyzz
2018-02-28 12:14:19 +00:00
parent 022b837553
commit f78d780b5f
6 changed files with 179 additions and 48 deletions

View File

@ -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;

View File

@ -853,7 +853,8 @@ begin
if (el+3 < nel) and (Elements[el+1].Token = nftExpChar) then
begin
Result := Result + '<number:scientific-number number:decimal-places="0"';
n := IfThen(Elements[el].Token = nftIntZeroDigit, Elements[el].IntValue, 1);
// n := IfThen(Elements[el].Token = nftIntZeroDigit, Elements[el].IntValue, 1);
n := FSections[ASection].MinIntDigits;
Result := Result + ' number:min-integer-digits="' + IntToStr(n) + '"';
n := Elements[el+3].IntValue;
Result := Result + ' number:min-exponent-digits="' + IntToStr(n) + '"';
@ -865,7 +866,8 @@ begin
if (el+5 < nel) and (Elements[el+1].Token = nftDecSep) and (Elements[el+3].Token = nftExpChar)
then begin
Result := Result + '<number:scientific-number';
n := IfThen(Elements[el].Token = nftIntZeroDigit, Elements[el].IntValue, 1);
// n := IfThen(Elements[el].Token = nftIntZeroDigit, Elements[el].IntValue, 1);
n := FSections[ASection].MinIntDigits;
Result := Result + ' number:min-integer-digits="' + IntToStr(n) + '"';
n := IfThen(Elements[el+2].Token = nftZeroDecs, Elements[el+2].IntValue, 1);
Result := Result + ' number:decimal-places="' + IntToStr(n) + '"';
@ -878,7 +880,8 @@ begin
if (el+2 < nel) and (Elements[el+1].Token = nftDecSep) then
begin
Result := Result + '<number:number';
n := IfThen(Elements[el].Token = nftIntZeroDigit, Elements[el].IntValue, 1);
// n := IfThen(Elements[el].Token = nftIntZeroDigit, Elements[el].IntValue, 1);
n := FSections[ASection].MinIntDigits;
Result := Result + ' number:min-integer-digits="' + IntToStr(n) + '"';
n := IfThen(Elements[el+2].Token = nftZeroDecs, Elements[el+2].IntValue, 1);
Result := Result + ' number:decimal-places="' + IntToStr(n) + '"';
@ -888,11 +891,18 @@ begin
inc(el, 2);
end
else
(*
// Standard integer, format '#'
if (el = 0) and (nel = 1) and (Elements[el].Token = nftIntOptDigit) then
Result := Result + '<number:number number:min-integer-digits="0" number:decimal-places="0" />'
else
*)
// Standard integer
if (el = nel-1) or (Elements[el+1].Token <> nftDecSep) then
begin
Result := Result + '<number:number number:decimal-places="0"';
n := IfThen(Elements[el].Token = nftIntZeroDigit, Elements[el].IntValue, 1);
// n := IfThen(Elements[el].Token = nftIntZeroDigit, Elements[el].IntValue, 1);
n := FSections[ASection].MinIntDigits;
Result := Result + ' number:min-integer-digits="' + IntToStr(n) + '"';
if (nfkHasFactor in Kind) and (Factor <> 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

View File

@ -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);

View File

@ -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;

View File

@ -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')
);

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<CONFIG>
<ProjectOptions>
<Version Value="10"/>
<Version Value="11"/>
<PathDelim Value="\"/>
<General>
<SessionStorage Value="InProjectDir"/>
@ -19,9 +19,10 @@
<Version Value="2"/>
</PublishOptions>
<RunParams>
<local>
<FormatVersion Value="1"/>
</local>
<FormatVersion Value="2"/>
<Modes Count="1">
<Mode0 Name="default"/>
</Modes>
</RunParams>
<RequiredPackages Count="4">
<Item1>