unit fpsNumFormatParser; {$ifdef fpc} {$mode delphi} {$endif} interface uses SysUtils, fpspreadsheet; const psOK = 0; psErrNoValidColorIndex = 1; psErrNoValidCompareNumber = 2; psErrUnknownInfoInBrackets = 3; psErrConditionalFormattingNotSupported = 4; psErrNoUsableFormat = 5; psErrNoValidNumberFormat = 6; psErrNoValidDateTimeFormat = 7; { TsNumFormatParser } type TsCompareOperation = (coNotUsed, coEqual, coNotEqual, coLess, coGreater, coLessEqual, coGreaterEqual ); TsNumFormatSection = record FormatString: String; CompareOperation: TsCompareOperation; CompareValue: Double; Color: TsColor; CountryCode: String; CurrencySymbol: String; Decimals: Byte; NumFormat: TsNumberFormat; end; TsNumFormatParser = class private FWorkbook: TsWorkbook; FCurrent: PChar; FStart: PChar; FEnd: PChar; FCurrSection: Integer; FSections: array of TsNumFormatSection; FFormatSettings: TFormatSettings; FFormatString: String; FStatus: Integer; function GetParsedSectionCount: Integer; function GetParsedSections(AIndex: Integer): TsNumFormatSection; protected procedure AddChar(AChar: Char); procedure AddSection; procedure AnalyzeBracket(const AValue: String); procedure AnalyzeText(const AValue: String); procedure CheckSections; procedure Parse(const AFormatString: String); procedure ScanAMPM(var s: String); procedure ScanBrackets; procedure ScanDateTime; procedure ScanDateTimeParts(TestToken, Replacement: Char; var s: String); procedure ScanFormat; procedure ScanNumber; procedure ScanText; public constructor Create(AWorkbook: TsWorkbook; const AFormatString: String); destructor Destroy; override; property FormatString: String read FFormatString; property ParsedSectionCount: Integer read GetParsedSectionCount; property ParsedSections[AIndex: Integer]: TsNumFormatSection read GetParsedSections; property Status: Integer read FStatus; end; implementation uses fpsutils; const COMPARE_STR: array[TsCompareOperation] of string = ( '', '=', '<>', '<', '>', '<=', '>' ); { TsNumFormatParser } constructor TsNumFormatParser.Create(AWorkbook: TsWorkbook; const AFormatString: String); begin inherited Create; FWorkbook := AWorkbook; FFormatSettings := DefaultFormatSettings; FFormatSettings.DecimalSeparator := '.'; FFormatSettings.ThousandSeparator := ','; Parse(AFormatString); end; destructor TsNumFormatParser.Destroy; begin FSections := nil; inherited Destroy; end; procedure TsNumFormatParser.AddChar(AChar: Char); begin with FSections[FCurrSection] do FormatString := FormatString + AChar; end; procedure TsNumFormatParser.AddSection; begin FCurrSection := Length(FSections); SetLength(FSections, FCurrSection + 1); with FSections[FCurrSection] do begin FormatString := ''; CompareOperation := coNotUsed; CompareValue := 0.0; Color := scBlack; CountryCode := ''; CurrencySymbol := ''; Decimals := 0; NumFormat := nfGeneral; end; end; procedure TsNumFormatParser.AnalyzeBracket(const AValue: String); var lValue: String; n: Integer; begin lValue := lowercase(AValue); // Colors if lValue = 'red' then FSections[FCurrSection].Color := scRed else if lValue = 'black' then FSections[FCurrSection].Color := scBlack else if lValue = 'blue' then FSections[FCurrSection].Color := scBlue else if lValue = 'white' then FSections[FCurrSection].Color := scWhite else if lValue = 'green' then FSections[FCurrSection].Color := scGreen else if lValue = 'cyan' then FSections[FCurrSection].Color := scCyan else if lValue = 'magenta' then FSections[FCurrSection].Color := scMagenta else if copy(lValue, 1, 5) = 'color' then begin lValue := copy(lValue, 6, Length(lValue)); if not TryStrToInt(trim(lValue), n) then begin FStatus := psErrNoValidColorIndex; exit; end; FSections[FCurrSection].Color := n; end else // Conditions if lValue[1] in ['=', '<', '>'] then begin n := 1; case lValue[1] of '=': FSections[FCurrSection].CompareOperation := coEqual; '<': case lValue[2] of '>': begin FSections[FCurrSection].CompareOperation := coNotEqual; inc(n); end; '=': begin FSections[FCurrSection].CompareOperation := coLessEqual; inc(n); end; else FSections[FCurrSection].CompareOperation := coLess; end; '>': case lValue[2] of '=': begin FSections[FCurrSection].CompareOperation := coGreaterEqual; inc(n); end; else FSections[FCurrSection].CompareOperation := coGreater; end; end; Delete(lValue, 1, n); if not TryStrToFloat(trim(lValue), FSections[FCurrSection].CompareValue) then FStatus := psErrNoValidCompareNumber; end else // Locale information if lValue[1] = '$' then begin FSections[FCurrSection].CountryCode := Copy(AValue, 2, Length(AValue)); end else FStatus := psErrUnknownInfoInBrackets; end; procedure TsNumFormatParser.AnalyzeText(const AValue: String); var uValue: String; begin uValue := Uppercase(AValue); if (uValue = '$') or (uValue = 'USD') or (uValue = '€') or (uValue = 'EUR') or (uValue = '£') or (uValue = 'GBP') or (uValue = '¥') or (uValue = 'JPY') then FSections[FCurrSection].CurrencySymbol := AValue; end; procedure TsNumFormatParser.CheckSections; var i: Integer; ns: Integer; s: String; begin ns := Length(FSections); for i:=0 to ns-1 do begin if FSections[i].FormatString = '' then FSections[i].NumFormat := nfGeneral; if (FSections[i].CurrencySymbol <> '') and (FSections[i].NumFormat = nfFixedTh) then FSections[i].NumFormat := nfCurrency; if FSections[i].CompareOperation <> coNotUsed then begin FStatus := psErrConditionalFormattingNotSupported; exit; end; case FSections[i].NumFormat of nfGeneral, nfFixed, nfFixedTh, nfPercentage, nfExp, nfSci, nfCurrency: try s := FormatFloat(FSections[i].FormatString, 1.0, FWorkBook.FormatSettings); except FStatus := psErrNoValidNumberFormat; exit; end; nfShortDateTime, nfShortDate, nfShortTime, nfShortTimeAM, nfLongDate, nfLongTime, nfLongTimeAM, nfFmtDateTime: try s := FormatDateTimeEx(FSections[i].FormatString, now(), FWorkbook.FormatSettings); except FStatus := psErrNoValidDateTimeFormat; exit; end; end; end; if ns = 2 then FFormatString := Format('%s;%s;%s', [ FSections[0].FormatString, FSections[1].FormatString, FSections[0].FormatString // make sure that fpc understands the "zero" ]) else if ns > 0 then begin FFormatString := FSections[0].FormatString; for i:=1 to ns-1 do FFormatString := Format('%s;%s', [FFormatString, FSections[i].FormatString]); end else FStatus := psErrNoUsableFormat; end; { function TsNumFormatParser.GetNumFormat: TsNumberFormat; var i: Integer; begin if FStatus <> psOK then Result := nfGeneral else if (FSections[0].NumFormat = nfCurrency) and (FSections[1].NumFormat = nfCurrency) and (FSections[2].NumFormat = nfCurrency) then begin if (FSections[1].Color = scNotDefined) then begin if (FSections[2].FormatString = '-') then Result := nfCurrencyDash else Result := nfCurrency; end else if FSections[1].Color = scRed then begin if (FSections[2].Formatstring = '-') then Result := nfCurrencyDashRed else Result := nfCurrencyRed; end; end else Result := FSections[0].NumFormat; end; } function TsNumFormatParser.GetParsedSectionCount: Integer; begin Result := Length(FSections); end; function TsNumFormatParser.GetParsedSections(AIndex: Integer): TsNumFormatSection; begin Result := FSections[AIndex]; end; procedure TsNumFormatParser.Parse(const AFormatString: String); var token: Char; begin FStatus := psOK; AddSection; FStart := @AFormatString[1]; FEnd := FStart + Length(AFormatString) - 1; FCurrent := FStart; while (FCurrent <= FEnd) and (FStatus = psOK) do begin token := FCurrent^; case token of '[': ScanBrackets; ';': AddSection; else ScanFormat; end; inc(FCurrent); end; CheckSections; end; { Extracts the text between square brackets --> AnalyzeBracket } procedure TsNumFormatParser.ScanBrackets; var s: String; token: Char; begin inc(FCurrent); // cursor stands at '[' while (FCurrent <= FEnd) and (FStatus = psOK) do begin token := FCurrent^; case token of ']': begin AnalyzeBracket(s); break; end; else s := s + token; end; inc(FCurrent); end; end; procedure TsNumFormatParser.ScanDateTime; var token: Char; done: Boolean; s: String; i: Integer; nf: TsNumberFormat; partStr: String; isTime: Boolean; isAMPM: Boolean; begin done := false; s := ''; isTime := false; isAMPM := false; while (FCurrent <= FEnd) and (FStatus = psOK) and (not done) do begin token := FCurrent^; case token of '\' : begin inc(FCurrent); token := FCurrent^; s := s + token; end; 'Y', 'y' : begin ScanDateTimeParts(token, token, s); isTime := false; end; 'M', 'm' : if isTime then // help fpc to separate "month" and "minute" ScanDateTimeParts(token, 'n', s) else // both "month" and "minute" work in fpc to some degree ScanDateTimeParts(token, token, s); 'D', 'd' : begin ScanDateTimeParts(token, token, s); isTime := false; end; 'H', 'h' : begin ScanDateTimeParts(token, token, s); isTime := true; end; 'S', 's' : begin ScanDateTimeParts(token, token, s); isTime := true; end; '/', ':', '.', ']', '[', ' ' : s := s + token; '0' : ScanDateTimeParts(token, 'z', s); 'A', 'a' : begin ScanAMPM(s); isAMPM := true; end; else begin done := true; dec(FCurrent); // char pointer must be at end of date/time mask. end; end; if not done then inc(FCurrent); end; FSections[FCurrSection].FormatString := FSections[FCurrSection].FormatString + s; s := FSections[FCurrSection].FormatString; // Check format try if s <> '' then begin FormatDateTime(s, now); // !!!! MODIFY TO USE EXTENDED SYNTAX !!!!! if s = FWorkbook.FormatSettings.LongDateFormat then nf := nfLongDate else if s = FWorkbook.FormatSettings.ShortDateFormat then nf := nfShortDate else if s = FWorkbook.FormatSettings.LongTimeFormat then nf := nfLongTime else if s = FWorkbook.FormatSettings.ShortTimeFormat then nf := nfShortTime else nf := nfFmtDateTime; FSections[FCurrSection].NumFormat := nf; end; except FStatus := psErrNoValidDateTimeFormat; end; end; procedure TsNumFormatParser.ScanAMPM(var s: String); var token: Char; begin while (FCurrent <= FEnd) do begin token := FCurrent^; if token in ['A', 'a', 'P', 'p', 'm', 'M', '/'] then s := s + token else begin dec(FCurrent); exit; end; inc(FCurrent); end; end; procedure TsNumFormatParser.ScanDateTimeParts(TestToken, Replacement: Char; var s: String); var token: Char; begin s := s + Replacement; while (FCurrent <= FEnd) do begin inc(FCurrent); token := FCurrent^; if token = TestToken then s := s + Replacement else begin dec(FCurrent); break; end; end; end; procedure TsNumFormatParser.ScanFormat; var token: Char; done: Boolean; begin done := false; while (FCurrent <= FEnd) and (FStatus = psOK) and (not done) do begin token := FCurrent^; case token of // Strip Excel's formatting symbols '\', '*' : ; '_' : inc(FCurrent); '"' : begin inc(FCurrent); ScanText; end; '0', '#', '.', ',', '-': ScanNumber; 'y', 'Y', 'm', 'M', 'd', 'D', 'h', 's', '[': ScanDateTime; ' ' : AddChar(token); ';' : begin done := true; dec(FCurrent); // Cursor must stay on the ";" end; end; if not done then inc(FCurrent); end; end; procedure TsNumFormatParser.ScanNumber; var token: Char; done: Boolean; countdecs: Boolean; s: String; hasThSep: Boolean; isExp: Boolean; isSci: Boolean; hasHash: Boolean; hasPerc: Boolean; nf: TsNumberFormat; begin countdecs := false; done := false; hasThSep := false; hasHash := false; hasPerc := false; isExp := false; isSci := false; s := ''; while (FCurrent <= FEnd) and (FStatus = psOK) and (not done) do begin token := FCurrent^; case token of ',': begin hasThSep := true; s := s + token; end; '.': begin countdecs := true; FSections[FCurrSection].Decimals := 0; s := s + token; end; '0': begin if countdecs then inc(FSections[FCurrSection].Decimals); s := s + token; end; 'E', 'e': begin if hasHash and countdecs then isSci := true else isExp := true; countdecs := false; s := s + token; end; '+', '-': s := s + token; '#': begin hasHash := true; countdecs := false; s := s + token; end; '%': begin hasPerc := true; s := s + token; end; else begin done := true; dec(FCurrent); end; end; if not done then inc(FCurrent); end; if s <> '' then begin if isExp then nf := nfExp else if isSci then nf := nfSci else if hasPerc then nf := nfPercentage else if hasThSep then nf := nfFixedTh else nf := nfFixed; end else nf := nfGeneral; FSections[FCurrSection].NumFormat := nf; FSections[FCurrSection].FormatString := FSections[FCurrSection].FormatString + s; end; { Scans a text in quotation marks. Tries to interpret the text as a currency symbol (--> AnalyzeText) } procedure TsNumFormatParser.ScanText; var token: Char; done: Boolean; s: String; begin done := false; s := ''; while (FCurrent <= FEnd) and (FStatus = psOK) and not done do begin token := FCurrent^; if token = '"' then begin done := true; AnalyzeText(s); end else begin s := s + token; inc(FCurrent); end; end; FSections[FCurrSection].FormatString := Format('%s"%s"', [FSections[FCurrSection].FormatString, s]); end; end.