fpspreadsheet: More improvements for reading numbers in csv files and automatic format detection.

git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@3672 8e941d3f-bd1b-0410-a28a-d453659cc2b4
This commit is contained in:
wp_xxyyzz
2014-10-20 21:04:20 +00:00
parent 43acbe2908
commit 4473d3b4a5
3 changed files with 155 additions and 75 deletions

View File

@ -111,12 +111,10 @@
<ComponentName Value="CSVParamsForm"/>
<HasResources Value="True"/>
<ResourceBaseClass Value="Form"/>
<UnitName Value="sCSVParamsForm"/>
</Unit2>
<Unit3>
<Filename Value="sctrls.pas"/>
<IsPartOfProject Value="True"/>
<UnitName Value="sCtrls"/>
</Unit3>
<Unit4>
<Filename Value="sformatsettingsform.pas"/>
@ -124,7 +122,6 @@
<ComponentName Value="FormatSettingsForm"/>
<HasResources Value="True"/>
<ResourceBaseClass Value="Form"/>
<UnitName Value="sFormatsettingsForm"/>
</Unit4>
</Units>
</ProjectOptions>

View File

@ -14,8 +14,8 @@ type
FWorksheetName: String;
function IsBool(AText: String; out AValue: Boolean): Boolean;
function IsDateTime(AText: String; out ADateTime: TDateTime): Boolean;
function IsNumber(AText: String; out ANumber: Double;
out ACurrencySymbol, AWarning: String): Boolean;
function IsNumber(AText: String; out ANumber: Double; out ANumFormat: TsNumberFormat;
out ADecimals: Integer; out ACurrencySymbol, AWarning: String): Boolean;
function IsQuotedText(var AText: String): Boolean;
procedure ReadCellValue(ARow, ACol: Cardinal; AText: String);
protected
@ -207,10 +207,13 @@ begin
end;
function TsCSVReader.IsNumber(AText: String; out ANumber: Double;
out ANumFormat: TsNumberFormat; out ADecimals: Integer;
out ACurrencySymbol, AWarning: String): Boolean;
var
p: Integer;
decsep, thsep: Char;
begin
Result := false;
AWarning := '';
// To detect whether the text is a currency value we look for the currency
@ -223,11 +226,8 @@ begin
if p > 0 then begin
Delete(AText, p, Length(ACurrencySymbol));
AText := Trim(AText);
if AText = '' then begin
Result := false;
ACurrencySymbol := '';
if AText = '' then
exit;
end;
// Negative financial values are often enclosed by parenthesis
if ((AText[1] = '(') and (AText[Length(AText)] = ')')) then
AText := '-' + Trim(Copy(AText, 2, Length(AText)-2));
@ -235,11 +235,57 @@ begin
ACurrencySymbol := '';
if CSVParams.AutoDetectNumberFormat then
Result := TryStrToFloatAuto(AText, ANumber, AWarning)
else
Result := TryStrToFloatAuto(AText, ANumber, decsep, thsep, AWarning)
else begin
Result := TryStrToFloat(AText, ANumber, CSVParams.FormatSettings);
if Result then
begin
if pos(CSVParams.FormatSettings.DecimalSeparator, AText) = 0
then decsep := #0
else decsep := CSVParams.FormatSettings.DecimalSeparator;
if pos(CSVParams.FormatSettings.ThousandSeparator, AText) = 0
then thsep := #0
else thsep := CSVParams.FormatSettings.ThousandSeparator;
end;
end;
if not Result then ACurrencySymbol := '';
// Try to determine the number format
if Result then
begin
if thsep <> #0 then
ANumFormat := nfFixedTh
else
ANumFormat := nfGeneral;
// count number of decimal places and try to catch special formats
ADecimals := 0;
if decsep <> #0 then
begin
// Go to the decimal separator and search towards the end of the string
p := pos(decsep, AText) + 1;
while (p <= Length(AText)) do begin
// exponential format
if AText[p] in ['+', '-', 'E', 'e'] then
begin
ANumFormat := nfExp;
break;
end else
// percent format
if AText[p] = '%' then
begin
ANumFormat := nfPercentage;
break;
end else
begin
inc(p);
inc(ADecimals);
end;
end;
if (ADecimals > 0) and (ADecimals < 9) and (ANumFormat = nfGeneral) then
// "no formatting" assumed if there are "many" decimals
ANumFormat := nfFixed;
end;
end else
ACurrencySymbol := '';
end;
function TsCSVReader.IsQuotedText(var AText: String): Boolean;
@ -268,6 +314,7 @@ var
currSym: string;
warning: String;
nf: TsNumberFormat;
decs: Integer;
begin
// Empty strings are blank cells -- nothing to do
if AText = '' then
@ -282,7 +329,10 @@ begin
// Remove quotes
if (AText[1] = CSVParams.QuoteChar) and (AText[Length(AText)] = CSVParams.QuoteChar) then
Delete(AText, 2, Length(AText)-2);
begin
Delete(AText, Length(AText), 1);
Delete(AText, 1, 1);
end;
{
// Quoted text is a TEXT cell
@ -294,12 +344,12 @@ begin
}
// Check for a NUMBER or CURRENCY cell
if IsNumber(AText, dblValue, currSym, warning) then
if IsNumber(AText, dblValue, nf, decs, currSym, warning) then
begin
if currSym <> '' then
FWorksheet.WriteCurrency(ARow, ACol, dblValue, nfCurrency, 2, currSym)
FWorksheet.WriteCurrency(ARow, ACol, dblValue, nfCurrency, decs, currSym)
else
FWorksheet.WriteNumber(ARow, ACol, dblValue);
FWorksheet.WriteNumber(ARow, ACol, dblValue, nf, decs);
if warning <> '' then
FWorkbook.AddErrorMsg('Cell %s: %s', [GetCellString(ARow, ACol), warning]);
exit;

View File

@ -130,7 +130,7 @@ function FormatDateTime(const FormatStr: string; DateTime: TDateTime;
const FormatSettings: TFormatSettings; Options : TFormatDateTimeOptions = []): string;
function TryStrToFloatAuto(AText: String; out ANumber: Double;
out AWarning: String): Boolean;
out ADecimalSeparator, AThousandSeparator: Char; out AWarning: String): Boolean;
function TwipsToPts(AValue: Integer): Single;
function PtsToTwips(AValue: Single): Integer;
@ -1481,17 +1481,21 @@ end;
Is needed for reading CSV files.
-------------------------------------------------------------------------------}
function TryStrToFloatAuto(AText: String; out ANumber: Double;
out AWarning: String): Boolean;
out ADecimalSeparator, AThousandSeparator: Char; out AWarning: String): Boolean;
var
i: Integer;
testSep: Char;
testSepPos: Integer;
lastDigitPos: Integer;
isPercent: Boolean;
isExp: Boolean;
fs: TFormatSettings;
done: Boolean;
begin
Result := false;
AWarning := '';
ADecimalSeparator := #0;
AThousandSeparator := #0;
if AText = '' then
exit;
@ -1506,65 +1510,89 @@ begin
// but no decimal separator misinterprets the thousand separator as a
// decimal separator.
done := false; // Indicates that both decimal and thousand separators are found
testSep := #0; // Separator candidate to be tested
testSepPos := 0; // Position of this separator chandidate in the string
done := false; // Indicates that both decimal and thousand separators are found
testSep := #0; // Separator candidate to be tested
testSepPos := 0; // Position of this separator candidate in the string
lastDigitPos := 0; // Position of the last numerical digit
isExp := false; // Flag for exponential format
isPercent := false; // Flag for percentage format
i := Length(AText); // Start at end...
while i >= 1 do // ...and search towards start
begin
if AText[i] in ['.', ','] then
begin
if testSep = #0 then begin
testSep := AText[i];
testSepPos := i;
end;
// This is the right-most separator candidate in the text
// It can be a decimal or a thousand separator.
dec(i);
while i >= 1 do
begin
if not (AText[i] in ['0'..'9']) then begin
Result := false;
exit;
end;
// If we find the testSep character again it must be a thousand separator.
if (AText[i] = testSep) then
case AText[i] of
'0'..'9':
if (lastDigitPos = 0) and (AText[i] in ['0'..'9']) then
lastDigitPos := i;
'e', 'E':
isExp := true;
'%':
isPercent := true;
'+', '-':
;
'.', ',':
begin
// ... but only if there are 3 numerical digits in between
if (testSepPos - i = 4) then
begin
fs.ThousandSeparator := testSep;
// The decimal separator is the "other" character.
if testSep = '.' then
fs.DecimalSeparator := ','
else
fs.DecimalSeparator := '.';
done := true;
i := 0;
end else
begin
Result := false;
exit;
if testSep = #0 then begin
testSep := AText[i];
testSepPos := i;
end;
// This is the right-most separator candidate in the text
// It can be a decimal or a thousand separator.
// Therefore, we continue searching from here.
dec(i);
while i >= 1 do
begin
if not (AText[i] in ['0'..'9', '+', '-']) then
exit;
// If we find the testSep character again it must be a thousand separator.
if (AText[i] = testSep) then
begin
// ... but only if there are 3 numerical digits in between
if (testSepPos - i = 4) then
begin
fs.ThousandSeparator := testSep;
// The decimal separator is the "other" character.
if testSep = '.' then
fs.DecimalSeparator := ','
else
fs.DecimalSeparator := '.';
ADecimalSeparator := fs.DecimalSeparator;
AThousandSeparator := fs.ThousandSeparator;
done := true;
i := 0;
end else
begin
Result := false;
exit;
end;
end
else
// If we find the "other" separator character, then testSep was a
// decimal separator and the current character is a thousand separator.
// But there must be 3 digits in between.
if AText[i] in ['.', ','] then
begin
if testSepPos - i <> 4 then // no 3 digits in between --> no number, maybe a date.
exit;
fs.DecimalSeparator := testSep;
fs.ThousandSeparator := AText[i];
ADecimalSeparator := fs.DecimalSeparator;
AThousandSeparator := fs.ThousandSeparator;
done := true;
i := 0;
end;
dec(i);
end;
end
else
// If we find the "other" separator character, then testSep was a
// decimal separator and the current character is a thousand separator.
if AText[i] in ['.',','] then
begin
fs.DecimalSeparator := testSep;
fs.ThousandSeparator := AText[i];
done := true;
i := 0;
end;
dec(i);
end;
end else
if not (AText[i] in ['0'..'9', '+', '-', 'e', 'E', '%']) then
begin
Result := false;
AWarning := '';
exit;
else
exit; // Non-numeric character found, no need to continue
end;
dec(i);
end;
@ -1576,9 +1604,10 @@ begin
// type is found and it is at the third position from the string's end it
// might by a thousand separator or a decimal separator. We assume the
// latter case, but create a warning.
if Length(AText) - testSepPos = 3 then
if (lastDigitPos - testSepPos = 3) and not isPercent then
AWarning := Format(rsAmbiguousDecThouSeparator, [AText]);
fs.DecimalSeparator := testSep;
ADecimalSeparator := fs.DecimalSeparator;
// Make sure that the thousand separator is different from the decimal sep.
if testSep = '.' then fs.ThousandSeparator := ',' else fs.ThousandSeparator := '.';
end;
@ -1595,9 +1624,13 @@ begin
// Try string-to-number conversion
Result := TryStrToFloat(AText, ANumber, fs);
// If successful take care of the percentage sign
if Result and isPercent then
ANumber := ANumber * 0.01;
// If successful ...
if Result then
begin
// ... take care of the percentage sign
if isPercent then
ANumber := ANumber * 0.01;
end;
end;