You've already forked lazarus-ccr
git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@3049 8e941d3f-bd1b-0410-a28a-d453659cc2b4
1187 lines
33 KiB
ObjectPascal
1187 lines
33 KiB
ObjectPascal
{
|
|
Utility functions and constants from FPSpreadsheet
|
|
}
|
|
|
|
// to do: Remove the patched FormatDateTime when the feature of square brackets
|
|
// in time format codes is in the rtl
|
|
|
|
unit fpsutils;
|
|
|
|
{$mode objfpc}{$H+}
|
|
|
|
interface
|
|
|
|
uses
|
|
Classes, SysUtils, StrUtils, fpspreadsheet;
|
|
|
|
// Exported types
|
|
type
|
|
TsSelectionDirection = (fpsVerticalSelection, fpsHorizontalSelection);
|
|
|
|
const
|
|
// Date formatting string for unambiguous date/time display as strings
|
|
// Can be used for text output when date/time cell support is not available
|
|
ISO8601Format='yyyymmdd"T"hhmmss';
|
|
// Extended ISO 8601 date/time format, used in e.g. ODF/opendocument
|
|
ISO8601FormatExtended='yyyy"-"mm"-"dd"T"hh":"mm":"ss';
|
|
|
|
// Endianess helper functions
|
|
function WordToLE(AValue: Word): Word;
|
|
function DWordToLE(AValue: Cardinal): Cardinal;
|
|
function IntegerToLE(AValue: Integer): Integer;
|
|
function WideStringToLE(const AValue: WideString): WideString;
|
|
|
|
function WordLEtoN(AValue: Word): Word;
|
|
function DWordLEtoN(AValue: Cardinal): Cardinal;
|
|
function WideStringLEToN(const AValue: WideString): WideString;
|
|
|
|
function LongRGBToExcelPhysical(const RGB: DWord): DWord;
|
|
|
|
// Other routines
|
|
function ParseIntervalString(const AStr: string;
|
|
var AFirstCellRow, AFirstCellCol, ACount: Integer;
|
|
var ADirection: TsSelectionDirection): Boolean;
|
|
function ParseCellRangeString(const AStr: string;
|
|
var AFirstCellRow, AFirstCellCol, ALastCellRow, ALastCellCol: Integer;
|
|
var AFlags: TsRelFlags): Boolean;
|
|
function ParseCellString(const AStr: string;
|
|
var ACellRow, ACellCol: Integer; var AFlags: TsRelFlags): Boolean; overload;
|
|
function ParseCellString(const AStr: string;
|
|
var ACellRow, ACellCol: Integer): Boolean; overload;
|
|
function ParseCellRowString(const AStr: string;
|
|
var AResult: Integer): Boolean;
|
|
function ParseCellColString(const AStr: string;
|
|
var AResult: Integer): Boolean;
|
|
|
|
function GetColString(AColIndex: Integer): String;
|
|
|
|
function UTF8TextToXMLText(AText: ansistring): ansistring;
|
|
|
|
function TwipsToMillimeters(AValue: Integer): Single;
|
|
function MillimetersToTwips(AValue: Single): Integer;
|
|
|
|
function IsExpNumberFormat(s: String; out Decimals: Word; out IsSci: Boolean): Boolean;
|
|
function IsFixedNumberFormat(s: String; out Decimals: Word): Boolean;
|
|
function IsPercentNumberFormat(s: String; out Decimals: Word): Boolean;
|
|
function IsThousandSepNumberFormat(s: String; out Decimals: Word): Boolean;
|
|
function IsDateFormat(s: String; out IsLong: Boolean): Boolean;
|
|
function IsTimeFormat(s: String; out isLong, isAMPM, isInterval: Boolean;
|
|
out SecDecimals: Word): Boolean;
|
|
|
|
function BuildNumFormatString(ANumberFormat: TsNumberFormat; ADecimals: Byte): String;
|
|
|
|
function SciFloat(AValue: Double; ADecimals: Word): String;
|
|
//function TimeIntervalToString(AValue: TDateTime; AFormatStr: String): String;
|
|
procedure MakeTimeIntervalMask(Src: String; var Dest: String);
|
|
|
|
function FormatDateTime(const FormatStr: string; DateTime: TDateTime): string;
|
|
|
|
implementation
|
|
|
|
uses
|
|
Math;
|
|
|
|
{
|
|
Endianess helper functions
|
|
|
|
Excel files are all written with Little Endian byte order,
|
|
so it's necessary to swap the data to be able to build a
|
|
correct file on big endian systems.
|
|
|
|
These routines are preferable to System unit routines because they
|
|
ensure that the correct overloaded version of the conversion routines
|
|
will be used, avoiding typecasts which are less readable.
|
|
|
|
They also guarantee delphi compatibility. For Delphi we just support
|
|
big-endian isn't support, because Delphi doesn't support it.
|
|
}
|
|
|
|
function WordToLE(AValue: Word): Word;
|
|
begin
|
|
{$IFDEF FPC}
|
|
Result := NtoLE(AValue);
|
|
{$ELSE}
|
|
Result := AValue;
|
|
{$ENDIF}
|
|
end;
|
|
|
|
function DWordToLE(AValue: Cardinal): Cardinal;
|
|
begin
|
|
{$IFDEF FPC}
|
|
Result := NtoLE(AValue);
|
|
{$ELSE}
|
|
Result := AValue;
|
|
{$ENDIF}
|
|
end;
|
|
|
|
function IntegerToLE(AValue: Integer): Integer;
|
|
begin
|
|
{$IFDEF FPC}
|
|
Result := NtoLE(AValue);
|
|
{$ELSE}
|
|
Result := AValue;
|
|
{$ENDIF}
|
|
end;
|
|
|
|
function WordLEtoN(AValue: Word): Word;
|
|
begin
|
|
{$IFDEF FPC}
|
|
Result := LEtoN(AValue);
|
|
{$ELSE}
|
|
Result := AValue;
|
|
{$ENDIF}
|
|
end;
|
|
|
|
function DWordLEtoN(AValue: Cardinal): Cardinal;
|
|
begin
|
|
{$IFDEF FPC}
|
|
Result := LEtoN(AValue);
|
|
{$ELSE}
|
|
Result := AValue;
|
|
{$ENDIF}
|
|
end;
|
|
|
|
function WideStringToLE(const AValue: WideString): WideString;
|
|
var
|
|
j: integer;
|
|
begin
|
|
{$IFDEF FPC}
|
|
{$IFDEF FPC_LITTLE_ENDIAN}
|
|
Result:=AValue;
|
|
{$ELSE}
|
|
Result:=AValue;
|
|
for j := 1 to Length(AValue) do begin
|
|
PWORD(@Result[j])^:=NToLE(PWORD(@Result[j])^);
|
|
end;
|
|
{$ENDIF}
|
|
{$ELSE}
|
|
Result:=AValue;
|
|
{$ENDIF}
|
|
end;
|
|
|
|
function WideStringLEToN(const AValue: WideString): WideString;
|
|
var
|
|
j: integer;
|
|
begin
|
|
{$IFDEF FPC}
|
|
{$IFDEF FPC_LITTLE_ENDIAN}
|
|
Result:=AValue;
|
|
{$ELSE}
|
|
Result:=AValue;
|
|
for j := 1 to Length(AValue) do begin
|
|
PWORD(@Result[j])^:=LEToN(PWORD(@Result[j])^);
|
|
end;
|
|
{$ENDIF}
|
|
{$ELSE}
|
|
Result:=AValue;
|
|
{$ENDIF}
|
|
end;
|
|
|
|
{ Converts RGB part of a LongRGB logical structure to its physical representation
|
|
IOW: RGBA (where A is 0 and omitted in the function call) => ABGR
|
|
Needed for conversion of palette colors. }
|
|
function LongRGBToExcelPhysical(const RGB: DWord): DWord;
|
|
begin
|
|
{$IFDEF FPC}
|
|
{$IFDEF ENDIAN_LITTLE}
|
|
result := RGB shl 8; //tags $00 at end for the A byte
|
|
result := SwapEndian(result); //flip byte order
|
|
{$ELSE}
|
|
//Big endian
|
|
result := RGB; //leave value as is //todo: verify if this turns out ok
|
|
{$ENDIF}
|
|
{$ELSE}
|
|
// messed up result
|
|
{$ENDIF}
|
|
end;
|
|
|
|
{@@
|
|
Parses strings like A5:A10 into an selection interval information
|
|
}
|
|
function ParseIntervalString(const AStr: string;
|
|
var AFirstCellRow, AFirstCellCol, ACount: Integer;
|
|
var ADirection: TsSelectionDirection): Boolean;
|
|
var
|
|
//Cells: TStringList;
|
|
LastCellRow, LastCellCol: Integer;
|
|
p: Integer;
|
|
s1, s2: String;
|
|
begin
|
|
Result := True;
|
|
|
|
{ Simpler:
|
|
use "pos" instead of the TStringList overhead.
|
|
And: the StringList is not free'ed here
|
|
|
|
// First get the cells
|
|
Cells := TStringList.Create;
|
|
ExtractStrings([':'],[], PChar(AStr), Cells);
|
|
|
|
// Then parse each of them
|
|
Result := ParseCellString(Cells[0], AFirstCellRow, AFirstCellCol);
|
|
if not Result then Exit;
|
|
Result := ParseCellString(Cells[1], LastCellRow, LastCellCol);
|
|
if not Result then Exit;
|
|
}
|
|
|
|
// First find the position of the colon and split into parts
|
|
p := pos(':', AStr);
|
|
if p = 0 then exit(false);
|
|
s1 := copy(AStr, 1, p-1);
|
|
s2 := copy(AStr, p+1, Length(AStr));
|
|
|
|
// Then parse each of them
|
|
Result := ParseCellString(s1, AFirstCellRow, AFirstCellCol);
|
|
if not Result then Exit;
|
|
Result := ParseCellString(s2, LastCellRow, LastCellCol);
|
|
if not Result then Exit;
|
|
|
|
if AFirstCellRow = LastCellRow then
|
|
begin
|
|
ADirection := fpsHorizontalSelection;
|
|
ACount := LastCellCol - AFirstCellCol + 1;
|
|
end
|
|
else if AFirstCellCol = LastCellCol then
|
|
begin
|
|
ADirection := fpsVerticalSelection;
|
|
ACount := LastCellRow - AFirstCellRow + 1;
|
|
end
|
|
else Exit(False);
|
|
end;
|
|
|
|
{@@
|
|
Parses strings like A5:C10 into a range selection information.
|
|
Return also information on relative/absolute cells.
|
|
}
|
|
function ParseCellRangeString(const AStr: string;
|
|
var AFirstCellRow, AFirstCellCol, ALastCellRow, ALastCellCol: Integer;
|
|
var AFlags: TsRelFlags): Boolean;
|
|
var
|
|
p: Integer;
|
|
s: String;
|
|
begin
|
|
Result := True;
|
|
|
|
// First find the colon
|
|
p := pos(':', AStr);
|
|
if p = 0 then exit(false);
|
|
|
|
// Analyze part after the colon
|
|
s := copy(AStr, p+1, Length(AStr));
|
|
Result := ParseCellString(s, ALastCellRow, ALastCellCol, AFlags);
|
|
if not Result then exit;
|
|
if (rfRelRow in AFlags) then begin
|
|
Include(AFlags, rfRelRow2);
|
|
Exclude(AFlags, rfRelRow);
|
|
end;
|
|
if (rfRelCol in AFlags) then begin
|
|
Include(AFlags, rfRelCol2);
|
|
Exclude(AFlags, rfRelCol);
|
|
end;
|
|
|
|
// Analyze part before the colon
|
|
s := copy(AStr, 1, p-1);
|
|
Result := ParseCellString(s, AFirstCellRow, AFirstCellCol, AFlags);
|
|
end;
|
|
|
|
|
|
{@@
|
|
Parses a cell string, like 'A1' into zero-based column and row numbers
|
|
|
|
The parser is a simple state machine, with the following states:
|
|
|
|
0 - Reading Column part 1 (necesserely needs a letter)
|
|
1 - Reading Column part 2, but could be the first number as well
|
|
2 - Reading Row
|
|
|
|
'AFlags' indicates relative addresses.
|
|
}
|
|
function ParseCellString(const AStr: string; var ACellRow, ACellCol: Integer;
|
|
var AFlags: TsRelFlags): Boolean;
|
|
var
|
|
i: Integer;
|
|
state: Integer;
|
|
Col, Row: string;
|
|
lChar: Char;
|
|
isAbs: Boolean;
|
|
const
|
|
cLetters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
|
|
'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'W', 'X', 'Y', 'Z'];
|
|
cDigits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
|
begin
|
|
// Starting state
|
|
Result := True;
|
|
state := 0;
|
|
Col := '';
|
|
Row := '';
|
|
AFlags := [rfRelCol, rfRelRow];
|
|
isAbs := false;
|
|
|
|
// Separates the string into a row and a col
|
|
for i := 1 to Length(AStr) do
|
|
begin
|
|
lChar := AStr[i];
|
|
|
|
if lChar = '$' then begin
|
|
if isAbs then
|
|
exit(false);
|
|
isAbs := true;
|
|
continue;
|
|
end;
|
|
|
|
case state of
|
|
|
|
0:
|
|
begin
|
|
if lChar in cLetters then
|
|
begin
|
|
Col := lChar;
|
|
if isAbs then
|
|
Exclude(AFlags, rfRelCol);
|
|
isAbs := false;
|
|
state := 1;
|
|
end
|
|
else Exit(False);
|
|
end;
|
|
|
|
1:
|
|
begin
|
|
if lChar in cLetters then
|
|
Col := Col + lChar
|
|
else if lChar in cDigits then
|
|
begin
|
|
Row := lChar;
|
|
if isAbs then
|
|
Exclude(AFlags, rfRelRow);
|
|
isAbs := false;
|
|
state := 2;
|
|
end
|
|
else Exit(False);
|
|
end;
|
|
|
|
2:
|
|
begin
|
|
if lChar in cDigits then Row := Row + lChar
|
|
else Exit(False);
|
|
end;
|
|
|
|
end;
|
|
end;
|
|
|
|
// Now parses each separetely
|
|
ParseCellRowString(Row, ACellRow);
|
|
ParseCellColString(Col, ACellCol);
|
|
end;
|
|
|
|
{ for compatibility with old version which does not return flags for relative
|
|
cell addresses }
|
|
function ParseCellString(const AStr: string;
|
|
var ACellRow, ACellCol: Integer): Boolean;
|
|
var
|
|
flags: TsRelFlags;
|
|
begin
|
|
ParseCellString(AStr, ACellRow, ACellCol, flags);
|
|
end;
|
|
|
|
function ParseCellRowString(const AStr: string; var AResult: Integer): Boolean;
|
|
begin
|
|
try
|
|
AResult := StrToInt(AStr) - 1;
|
|
except
|
|
Result := False;
|
|
end;
|
|
Result := True;
|
|
end;
|
|
|
|
function ParseCellColString(const AStr: string; var AResult: Integer): Boolean;
|
|
const
|
|
INT_NUM_LETTERS = 26;
|
|
begin
|
|
Result := False;
|
|
AResult := 0;
|
|
|
|
if Length(AStr) = 1 then AResult := Ord(AStr[1]) - Ord('A')
|
|
else if Length(AStr) = 2 then
|
|
begin
|
|
AResult := (Ord(AStr[1]) - Ord('A') + 1) * INT_NUM_LETTERS
|
|
+ Ord(AStr[2]) - Ord('A');
|
|
end
|
|
else if Length(AStr) = 3 then
|
|
begin
|
|
AResult := (Ord(AStr[1]) - Ord('A') + 1) * INT_NUM_LETTERS * INT_NUM_LETTERS
|
|
+ (Ord(AStr[2]) - Ord('A') + 1) * INT_NUM_LETTERS
|
|
+ Ord(AStr[3]) - Ord('A');
|
|
end
|
|
else Exit(False);
|
|
|
|
Result := True;
|
|
end;
|
|
|
|
function Letter(AValue: Integer): char;
|
|
begin
|
|
Result := Char(AValue + ord('A'));
|
|
end;
|
|
|
|
function GetColString(AColIndex: Integer): String;
|
|
begin
|
|
if AColIndex < 26 then
|
|
Result := Letter(AColIndex)
|
|
else
|
|
if AColIndex < 26*26 then
|
|
Result := Letter(AColIndex div 26) + Letter(AColIndex mod 26)
|
|
else
|
|
if AColIndex < 26*26*26 then
|
|
Result := Letter(AColIndex div (26*26)) + Letter((AColIndex mod (26*26)) div 26)
|
|
+ Letter(AColIndex mod (26*26*26))
|
|
else
|
|
Result := 'too big';
|
|
end;
|
|
|
|
{In XML files some chars must be translated}
|
|
function UTF8TextToXMLText(AText: ansistring): ansistring;
|
|
var
|
|
Idx:Integer;
|
|
WrkStr, AppoSt:ansistring;
|
|
begin
|
|
WrkStr:='';
|
|
|
|
for Idx:=1 to Length(AText) do
|
|
begin
|
|
case AText[Idx] of
|
|
'&': begin
|
|
AppoSt:=Copy(AText, Idx, 6);
|
|
|
|
if (Pos('&', AppoSt) = 1) or
|
|
(Pos('<', AppoSt) = 1) or
|
|
(Pos('>', AppoSt) = 1) or
|
|
(Pos('"', AppoSt) = 1) or
|
|
(Pos(''', AppoSt) = 1) then begin
|
|
//'&' is the first char of a special chat, it must not be converted
|
|
WrkStr:=WrkStr + AText[Idx];
|
|
end else begin
|
|
WrkStr:=WrkStr + '&';
|
|
end;
|
|
end;
|
|
'<': WrkStr:=WrkStr + '<';
|
|
'>': WrkStr:=WrkStr + '>';
|
|
'"': WrkStr:=WrkStr + '"';
|
|
'''':WrkStr:=WrkStr + ''';
|
|
else
|
|
WrkStr:=WrkStr + AText[Idx];
|
|
end;
|
|
end;
|
|
|
|
Result:=WrkStr;
|
|
end;
|
|
|
|
{ Excel's unit of row heights is "twips", i.e. 1/20 point. 72 pts = 1 inch = 25.4 mm
|
|
The procedure TwipsToMillimeters performs the conversion to millimeters. }
|
|
function TwipsToMillimeters(AValue: Integer): Single;
|
|
begin
|
|
Result := 25.4 * AValue / (20 * 72);
|
|
end;
|
|
|
|
{ Converts Millimeters to Twips, i.e. 1/20 pt }
|
|
function MillimetersToTwips(AValue: Single): Integer;
|
|
begin
|
|
Result := Round((AValue * 20 * 72) / 25.4);
|
|
end;
|
|
|
|
|
|
{ Format checking procedures }
|
|
|
|
{ This simple parsing procedure of the Excel format string checks for a fixed
|
|
float format s, i.e. s can be '0', '0.00', '000', '0,000', and returns the
|
|
number of decimals, i.e. number of zeros behind the decimal point }
|
|
function IsFixedNumberFormat(s: String; out Decimals: Word): Boolean;
|
|
var
|
|
i: Integer;
|
|
p: Integer;
|
|
decs: String;
|
|
begin
|
|
Decimals := 0;
|
|
|
|
// Excel time formats with milliseconds ("mm:ss.000") can be incorrectly
|
|
// detected as fixed number formats. Check this case at first.
|
|
if pos('s.0', s) > 0 then begin
|
|
Result := false;
|
|
exit;
|
|
end;
|
|
|
|
// Check if s is a valid format mask.
|
|
try
|
|
FormatFloat(s, 1.0);
|
|
except
|
|
on EConvertError do begin
|
|
Result := false;
|
|
exit;
|
|
end;
|
|
end;
|
|
|
|
// If it is count the zeros - each one is a decimal.
|
|
if s = '0' then
|
|
Result := true
|
|
else begin
|
|
p := pos('.', s); // position of decimal point;
|
|
if p = 0 then begin
|
|
Result := false;
|
|
end else begin
|
|
Result := true;
|
|
for i:= p+1 to Length(s) do
|
|
if s[i] = '0' then begin
|
|
inc(Decimals)
|
|
end
|
|
else
|
|
exit; // ignore characters after the last 0
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
{ This function checks whether the format string corresponds to a thousand
|
|
separator format like "#,##0.000' and returns the number of fixed decimals
|
|
(i.e. zeros after the decimal point) }
|
|
function IsThousandSepNumberFormat(s: String; out Decimals: Word): Boolean;
|
|
var
|
|
i, p: Integer;
|
|
begin
|
|
Decimals := 0;
|
|
|
|
// Check if s is a valid format string
|
|
try
|
|
FormatFloat(s, 1.0);
|
|
except
|
|
on EConvertError do begin
|
|
Result := false;
|
|
exit;
|
|
end;
|
|
end;
|
|
|
|
// If it is look for the thousand separator. If found count decimals.
|
|
Result := (Pos(',', s) > 0);
|
|
if Result then begin
|
|
p := pos('.', s);
|
|
if p > 0 then
|
|
for i := p+1 to Length(s) do
|
|
if s[i] = '0' then
|
|
inc(Decimals)
|
|
else
|
|
exit; // ignore format characters after the last 0
|
|
end;
|
|
end;
|
|
|
|
{ This function checks whether the format string corresponds to percent
|
|
formatting and determines the number of decimals }
|
|
function IsPercentNumberFormat(s: String; out Decimals: Word): Boolean;
|
|
var
|
|
i, p: Integer;
|
|
begin
|
|
Decimals := 0;
|
|
// The signature of the percent format is a percent sign at the end of the
|
|
// format string.
|
|
Result := (s <> '') and (s[Length(s)] = '%');
|
|
if Result then begin
|
|
// Check for a valid format string
|
|
try
|
|
FormatDateTime(s, 1.0);
|
|
except
|
|
on EConvertError do begin
|
|
Result := false;
|
|
exit;
|
|
end;
|
|
end;
|
|
// Count decimals
|
|
p := pos('.', s);
|
|
if p > 0 then
|
|
for i := p+1 to Length(s)-1 do
|
|
if s[i] = '0' then
|
|
inc(Decimals)
|
|
else
|
|
exit; // ignore characters after last 0
|
|
end;
|
|
end;
|
|
|
|
{ This function checks whether the format string corresponds to exponential
|
|
formatting and determines the number of decimals. If it contains a # character
|
|
the function assumes a "scientific" format rounding the exponent to multiples
|
|
of 2. }
|
|
function IsExpNumberFormat(s: String; out Decimals: Word;
|
|
out IsSci: Boolean): Boolean;
|
|
var
|
|
i, pdp, pe, ph: Integer;
|
|
begin
|
|
Result := false;
|
|
Decimals := 0;
|
|
IsSci := false;
|
|
|
|
if SameText(s, 'General') then
|
|
exit;
|
|
|
|
// Check for a valid format string
|
|
try
|
|
FormatDateTime(s, 1.0);
|
|
except
|
|
on EConvertError do begin
|
|
exit;
|
|
end;
|
|
end;
|
|
|
|
pe := pos('e', lowercase(s));
|
|
result := pe > 0;
|
|
if Result then begin
|
|
// The next character must be a "+", "-", or "0"
|
|
if (pe = Length(s)) or not (s[pe+1] in ['+', '-', '0']) then begin
|
|
Result := false;
|
|
exit;
|
|
end;
|
|
// Count decimals
|
|
pdp := pos('.', s);
|
|
if (pdp > 0) then begin
|
|
if pdp < pe then
|
|
for i:=pdp+1 to pe-1 do
|
|
if s[i] = '0' then
|
|
inc(Decimals)
|
|
else
|
|
break; // ignore characters after last 0
|
|
end;
|
|
// Look for hash signs # as indicator of the "scientific" format
|
|
ph := pos('#', s);
|
|
if ph > 0 then IsSci := true;
|
|
end;
|
|
end;
|
|
|
|
{ IsDateFormat checks if the format string s corresponds to a date format }
|
|
function IsDateFormat(s: String; out IsLong: Boolean): Boolean;
|
|
begin
|
|
s := Lowercase(s);
|
|
// Day, month, year are separated by a slash
|
|
// We also check part of the year/month/day symbol because there may be
|
|
// other control code with a slash.
|
|
Result := (pos('y/', s) > 0) or (pos('m/', s) > 0) or (pos('/m', s) > 0) or (pos('/d', s) > 0);
|
|
if Result then
|
|
// Check validity of format string
|
|
try
|
|
FormatDateTime(s, now);
|
|
s := Lowercase(s);
|
|
isLong := (pos('mmm', s) <> 0) or (pos('mmmm', s) <> 0);
|
|
except on EConvertError do
|
|
Result := false;
|
|
end;
|
|
end;
|
|
|
|
{ IsTimeFormat checks if the format string s is a time format. isLong is
|
|
true if the string contains hours, minutes and seconds (two colons).
|
|
isAMPM is true if the string contains "AM/PM", "A/P" or "AMPM".
|
|
isInterval is true if the string contains square bracket codes for time intervals.
|
|
SecDecimals is the number of decimals for the seconds. }
|
|
function IsTimeFormat(s: String; out isLong, isAMPM, isInterval: Boolean;
|
|
out SecDecimals: Word): Boolean;
|
|
var
|
|
p, pdp, i, count: Integer;
|
|
begin
|
|
isLong := false;
|
|
isAMPM := false;
|
|
SecDecimals := 0;
|
|
|
|
// Time parts are separated by a colon
|
|
p := pos(':', s);
|
|
result := p > 0;
|
|
|
|
if Result then begin
|
|
count := 1;
|
|
s := Uppercase(s);
|
|
|
|
// Seek for "H:MM:SS" or "H:MM" to see if it is a long or short time format.
|
|
if pos('H:MM:SS', s) <> 0 then
|
|
isLong := true
|
|
else
|
|
if pos('H:MM', s) <> 0 then
|
|
isLong := false
|
|
else
|
|
// If there are is a second colon s is a "long" time format
|
|
for i:=p+1 to Length(s) do
|
|
if s[i] = ':' then begin
|
|
isLong := true;
|
|
break;
|
|
end;
|
|
|
|
// Seek for "AM/PM" etc to detect that specific format
|
|
isAMPM := (pos('AM/PM', s) > 0) or (pos('A/P', s) > 0) or (pos('AMPM', s) > 0);
|
|
|
|
// Look for special square bracket symbols indicating the interval format.
|
|
isInterval := (pos('[H]', s) <> 0) or (pos('[HH]', s) <> 0) or
|
|
(pos('[M]', s) <> 0) or (pos('[MM]', s) <> 0) or
|
|
(pos('[N]', s) <> 0) or (pos('[NN]', s) <> 0) or
|
|
(pos('[S]', s) <> 0) or (pos('[SS]', s) <> 0);
|
|
|
|
// Count decimals
|
|
pdp := pos('.', s);
|
|
if (pdp > 0) then
|
|
for i:=pdp+1 to Length(s) do
|
|
if (s[i] in ['0', 'z', 'Z']) then
|
|
inc(SecDecimals)
|
|
else
|
|
break; // ignore characters after last 0
|
|
|
|
// Check validity of format string
|
|
try
|
|
FormatDateTime(s, now);
|
|
except on EConvertError do
|
|
Result := false;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
{ Builds a number format string from the numberformat code and the count of
|
|
decimals. }
|
|
function BuildNumFormatString(ANumberFormat: TsNumberFormat;
|
|
ADecimals: Byte): String;
|
|
var
|
|
decs: String;
|
|
begin
|
|
decs := DupeString('0', ADecimals);
|
|
if ADecimals > 0 then decs := '.' + decs;
|
|
case ANumberFormat of
|
|
nfFixed:
|
|
Result := '0' + decs;
|
|
nfFixedTh:
|
|
Result := '#,##0' + decs;
|
|
nfExp:
|
|
Result := '0' + decs + 'E+00';
|
|
nfSci:
|
|
Result := '##0' + decs + 'E+0';
|
|
nfPercentage:
|
|
Result := '0' + decs + '%';
|
|
else
|
|
Result := '';
|
|
end;
|
|
end;
|
|
|
|
{ Formats the number AValue in "scientific" format with the given number of
|
|
decimals. "Scientific" is the same as "exponential", but with exponents rounded
|
|
to multiples of 3 (like for "kilo" - "Mega" - "Giga" etc.). }
|
|
function SciFloat(AValue: Double; ADecimals: Word): String;
|
|
var
|
|
m: Double;
|
|
ex: Integer;
|
|
begin
|
|
if AValue = 0 then
|
|
Result := '0.0'
|
|
else begin
|
|
ex := floor(log10(abs(AValue))); // exponent
|
|
// round exponent to multiples of 3
|
|
ex := (ex div 3) * 3;
|
|
if ex < 0 then dec(ex, 3);
|
|
m := AValue * Power(10, -ex); // mantisse
|
|
Result := Format('%.*fE%d', [ADecimals, m, ex]);
|
|
end;
|
|
end;
|
|
(*
|
|
{ Formats the number AValue as a time string according to the format string.
|
|
If the hour part is between square brackets it can be greater than 24 hours.
|
|
Dto for the minutes or seconds part, with the higher-value part being added
|
|
and no longer being shown explicitly.
|
|
Example:
|
|
AValue = 1:30:02, FormatStr = "[mm]:ss]" --> "90:02" }
|
|
function TimeIntervalToString(AValue: TDateTime; AFormatStr: String): String;
|
|
var
|
|
hrs, mins, secs: Integer;
|
|
diff: Double;
|
|
h,m,s,z: Word;
|
|
ts: String;
|
|
fmt: String;
|
|
p: Integer;
|
|
begin {
|
|
fmt := Lowercase(AFormatStr);
|
|
p := pos('h]', fmt);
|
|
if p > 0 then begin
|
|
System.Delete(fmt, 1, p+2);
|
|
Result := FormatDateTime(fmt, AValue);
|
|
DecodeTime(frac(abs(AValue)), h, m, s, z);
|
|
hrs := h + trunc(abs(AValue))*24;
|
|
Result := FormatDateTime(fmt, AValue);
|
|
end;
|
|
for i
|
|
p := pos('h
|
|
}
|
|
ts := DefaultFormatSettings.TimeSeparator;
|
|
DecodeTime(frac(abs(AValue)), h, m, s, z);
|
|
hrs := h + trunc(abs(AValue))*24;
|
|
if z > 499 then inc(s);
|
|
if hrs > 0 then
|
|
Result := Format('%d%s%.2d%s%.2d', [hrs, ts, m, ts, s])
|
|
else
|
|
Result := Format('%d%s%.2d', [m, ts, s]);
|
|
if AValue < 0.0 then Result := '-' + Result;
|
|
end;
|
|
*)
|
|
{ Creates a "time interval" format string having the first code identifier
|
|
in square brackets. }
|
|
procedure MakeTimeIntervalMask(Src: String; var Dest: String);
|
|
var
|
|
L: TStrings;
|
|
begin
|
|
L := TStringList.Create;
|
|
try
|
|
L.StrictDelimiter := true;
|
|
L.Delimiter := ':';
|
|
L.DelimitedText := Src;
|
|
if L[0][1] <> '[' then L[0] := '[' + L[0];
|
|
if L[0][Length(L[0])] <> ']' then L[0] := L[0] + ']';
|
|
Dest := L.DelimitedText;
|
|
finally
|
|
L.Free;
|
|
end;
|
|
end;
|
|
|
|
|
|
{******************************************************************************}
|
|
{******************************************************************************}
|
|
{ Patch for SysUtils.FormatDateTime }
|
|
{ Remove when the feature of square brackets in time format masks is in rtl }
|
|
{******************************************************************************}
|
|
{******************************************************************************}
|
|
|
|
// Copied from "fpc/rtl/objpas/sysutils/datei.inc"
|
|
procedure DateTimeToString(out Result: string; const FormatStr: string; const DateTime: TDateTime; const FormatSettings: TFormatSettings);
|
|
var
|
|
ResultLen: integer;
|
|
ResultBuffer: array[0..255] of char;
|
|
ResultCurrent: pchar;
|
|
|
|
{$IFDEF MSWindows}
|
|
isEnable_E_Format : Boolean;
|
|
isEnable_G_Format : Boolean;
|
|
eastasiainited : boolean;
|
|
{$ENDIF MSWindows}
|
|
|
|
(* This part is in the original code. It is not needed here and avoids a
|
|
dependency on the unit Windows.
|
|
|
|
{$IFDEF MSWindows}
|
|
procedure InitEastAsia;
|
|
var ALCID : LCID;
|
|
PriLangID , SubLangID : Word;
|
|
|
|
begin
|
|
ALCID := GetThreadLocale;
|
|
PriLangID := ALCID and $3FF;
|
|
if (PriLangID>0) then
|
|
SubLangID := (ALCID and $FFFF) shr 10
|
|
else
|
|
begin
|
|
PriLangID := SysLocale.PriLangID;
|
|
SubLangID := SysLocale.SubLangID;
|
|
end;
|
|
isEnable_E_Format := (PriLangID = LANG_JAPANESE)
|
|
or
|
|
(PriLangID = LANG_KOREAN)
|
|
or
|
|
((PriLangID = LANG_CHINESE)
|
|
and
|
|
(SubLangID = SUBLANG_CHINESE_TRADITIONAL)
|
|
);
|
|
isEnable_G_Format := (PriLangID = LANG_JAPANESE)
|
|
or
|
|
((PriLangID = LANG_CHINESE)
|
|
and
|
|
(SubLangID = SUBLANG_CHINESE_TRADITIONAL)
|
|
);
|
|
eastasiainited :=true;
|
|
end;
|
|
{$ENDIF MSWindows}
|
|
*)
|
|
procedure StoreStr(Str: PChar; Len: Integer);
|
|
begin
|
|
if ResultLen + Len < SizeOf(ResultBuffer) then
|
|
begin
|
|
StrMove(ResultCurrent, Str, Len);
|
|
ResultCurrent := ResultCurrent + Len;
|
|
ResultLen := ResultLen + Len;
|
|
end;
|
|
end;
|
|
|
|
procedure StoreString(const Str: string);
|
|
var Len: integer;
|
|
begin
|
|
Len := Length(Str);
|
|
if ResultLen + Len < SizeOf(ResultBuffer) then
|
|
begin
|
|
StrMove(ResultCurrent, pchar(Str), Len);
|
|
ResultCurrent := ResultCurrent + Len;
|
|
ResultLen := ResultLen + Len;
|
|
end;
|
|
end;
|
|
|
|
procedure StoreInt(Value, Digits: Integer);
|
|
var
|
|
S: string[16];
|
|
Len: integer;
|
|
begin
|
|
System.Str(Value:Digits, S);
|
|
for Len := 1 to Length(S) do
|
|
begin
|
|
if S[Len] = ' ' then
|
|
S[Len] := '0'
|
|
else
|
|
Break;
|
|
end;
|
|
StoreStr(pchar(@S[1]), Length(S));
|
|
end ;
|
|
|
|
var
|
|
Year, Month, Day, DayOfWeek, Hour, Minute, Second, MilliSecond: word;
|
|
|
|
|
|
procedure StoreFormat(const FormatStr: string; Nesting: Integer; TimeFlag: Boolean);
|
|
var
|
|
Token, lastformattoken, prevlasttoken: char;
|
|
FormatCurrent: pchar;
|
|
FormatEnd: pchar;
|
|
Count: integer;
|
|
Clock12: boolean;
|
|
P: pchar;
|
|
tmp: integer;
|
|
isInterval: Boolean;
|
|
|
|
begin
|
|
if Nesting > 1 then // 0 is original string, 1 is included FormatString
|
|
Exit;
|
|
|
|
FormatCurrent := PChar(FormatStr);
|
|
FormatEnd := FormatCurrent + Length(FormatStr);
|
|
Clock12 := false;
|
|
isInterval := false;
|
|
P := FormatCurrent;
|
|
// look for unquoted 12-hour clock token
|
|
while P < FormatEnd do
|
|
begin
|
|
Token := P^;
|
|
case Token of
|
|
'''', '"':
|
|
begin
|
|
Inc(P);
|
|
while (P < FormatEnd) and (P^ <> Token) do
|
|
Inc(P);
|
|
end;
|
|
'A', 'a':
|
|
begin
|
|
if (StrLIComp(P, 'A/P', 3) = 0) or
|
|
(StrLIComp(P, 'AMPM', 4) = 0) or
|
|
(StrLIComp(P, 'AM/PM', 5) = 0) then
|
|
begin
|
|
Clock12 := true;
|
|
break;
|
|
end;
|
|
end;
|
|
end; // case
|
|
Inc(P);
|
|
end ;
|
|
token := #255;
|
|
lastformattoken := ' ';
|
|
prevlasttoken := 'H';
|
|
while FormatCurrent < FormatEnd do
|
|
begin
|
|
Token := UpCase(FormatCurrent^);
|
|
Count := 1;
|
|
P := FormatCurrent + 1;
|
|
case Token of
|
|
'''', '"':
|
|
begin
|
|
while (P < FormatEnd) and (p^ <> Token) do
|
|
Inc(P);
|
|
Inc(P);
|
|
Count := P - FormatCurrent;
|
|
StoreStr(FormatCurrent + 1, Count - 2);
|
|
end ;
|
|
'A':
|
|
begin
|
|
if StrLIComp(FormatCurrent, 'AMPM', 4) = 0 then
|
|
begin
|
|
Count := 4;
|
|
if Hour < 12 then
|
|
StoreString(FormatSettings.TimeAMString)
|
|
else
|
|
StoreString(FormatSettings.TimePMString);
|
|
end
|
|
else if StrLIComp(FormatCurrent, 'AM/PM', 5) = 0 then
|
|
begin
|
|
Count := 5;
|
|
if Hour < 12 then StoreStr(FormatCurrent, 2)
|
|
else StoreStr(FormatCurrent+3, 2);
|
|
end
|
|
else if StrLIComp(FormatCurrent, 'A/P', 3) = 0 then
|
|
begin
|
|
Count := 3;
|
|
if Hour < 12 then StoreStr(FormatCurrent, 1)
|
|
else StoreStr(FormatCurrent+2, 1);
|
|
end
|
|
else
|
|
raise EConvertError.Create('Illegal character in format string');
|
|
end ;
|
|
'/': StoreStr(@FormatSettings.DateSeparator, 1);
|
|
':': StoreStr(@FormatSettings.TimeSeparator, 1);
|
|
'[': isInterval := true;
|
|
']': isInterval := false;
|
|
' ', 'C', 'D', 'H', 'M', 'N', 'S', 'T', 'Y','Z' :
|
|
begin
|
|
while (P < FormatEnd) and (UpCase(P^) = Token) do
|
|
Inc(P);
|
|
Count := P - FormatCurrent;
|
|
case Token of
|
|
' ': StoreStr(FormatCurrent, Count);
|
|
'Y': begin
|
|
if Count > 2 then
|
|
StoreInt(Year, 4)
|
|
else
|
|
StoreInt(Year mod 100, 2);
|
|
end;
|
|
'M': begin
|
|
if isInterval and ((prevlasttoken = 'H') or TimeFlag) then
|
|
StoreInt(Minute + Hour*60 + trunc(DateTime)*24*60, 0)
|
|
else
|
|
if (lastformattoken = 'H') or TimeFlag then
|
|
begin
|
|
if Count = 1 then
|
|
StoreInt(Minute, 0)
|
|
else
|
|
StoreInt(Minute, 2);
|
|
end
|
|
else
|
|
begin
|
|
case Count of
|
|
1: StoreInt(Month, 0);
|
|
2: StoreInt(Month, 2);
|
|
3: StoreString(FormatSettings.ShortMonthNames[Month]);
|
|
else
|
|
StoreString(FormatSettings.LongMonthNames[Month]);
|
|
end;
|
|
end;
|
|
end;
|
|
'D': begin
|
|
case Count of
|
|
1: StoreInt(Day, 0);
|
|
2: StoreInt(Day, 2);
|
|
3: StoreString(FormatSettings.ShortDayNames[DayOfWeek]);
|
|
4: StoreString(FormatSettings.LongDayNames[DayOfWeek]);
|
|
5: StoreFormat(FormatSettings.ShortDateFormat, Nesting+1, False);
|
|
else
|
|
StoreFormat(FormatSettings.LongDateFormat, Nesting+1, False);
|
|
end ;
|
|
end ;
|
|
'H':
|
|
if isInterval then
|
|
StoreInt(Hour + trunc(DateTime)*24, 0)
|
|
else
|
|
if Clock12 then
|
|
begin
|
|
tmp := hour mod 12;
|
|
if tmp=0 then tmp:=12;
|
|
if Count = 1 then
|
|
StoreInt(tmp, 0)
|
|
else
|
|
StoreInt(tmp, 2);
|
|
end
|
|
else begin
|
|
if Count = 1 then
|
|
StoreInt(Hour, 0)
|
|
else
|
|
StoreInt(Hour, 2);
|
|
end;
|
|
'N': if isInterval then
|
|
StoreInt(Minute + 60*Hour + 60*24*trunc(DateTime), 0)
|
|
else
|
|
if Count = 1 then
|
|
StoreInt(Minute, 0)
|
|
else
|
|
StoreInt(Minute, 2);
|
|
'S': if isInterval then
|
|
StoreInt(Second + Minute*60 + Hour*60*60 + trunc(DateTime)*24*60*60, 0)
|
|
else
|
|
if Count = 1 then
|
|
StoreInt(Second, 0)
|
|
else
|
|
StoreInt(Second, 2);
|
|
'Z': if Count = 1 then
|
|
StoreInt(MilliSecond, 0)
|
|
else
|
|
StoreInt(MilliSecond, 3);
|
|
'T': if Count = 1 then
|
|
StoreFormat(FormatSettings.ShortTimeFormat, Nesting+1, True)
|
|
else
|
|
StoreFormat(FormatSettings.LongTimeFormat, Nesting+1, True);
|
|
'C': begin
|
|
StoreFormat(FormatSettings.ShortDateFormat, Nesting+1, False);
|
|
if (Hour<>0) or (Minute<>0) or (Second<>0) then
|
|
begin
|
|
StoreString(' ');
|
|
StoreFormat(FormatSettings.LongTimeFormat, Nesting+1, True);
|
|
end;
|
|
end;
|
|
|
|
(* This part is in the original code. It is not needed here and avoids a
|
|
dependency on the unit Windows.
|
|
|
|
{$IFDEF MSWindows}
|
|
'E':
|
|
begin
|
|
if not Eastasiainited then InitEastAsia;
|
|
if Not(isEnable_E_Format) then StoreStr(@FormatCurrent^, 1)
|
|
else
|
|
begin
|
|
while (P < FormatEnd) and (UpCase(P^) = Token) do
|
|
P := P + 1;
|
|
Count := P - FormatCurrent;
|
|
StoreString(ConvertEraYearString(Count,Year,Month,Day));
|
|
end;
|
|
lastformattoken:=token;
|
|
end;
|
|
'G':
|
|
begin
|
|
if not Eastasiainited then InitEastAsia;
|
|
if Not(isEnable_G_Format) then StoreStr(@FormatCurrent^, 1)
|
|
else
|
|
begin
|
|
while (P < FormatEnd) and (UpCase(P^) = Token) do
|
|
P := P + 1;
|
|
Count := P - FormatCurrent;
|
|
StoreString(ConvertEraString(Count,Year,Month,Day));
|
|
end;
|
|
lastformattoken:=token;
|
|
end;
|
|
{$ENDIF MSWindows}
|
|
*)
|
|
end;
|
|
prevlasttoken := lastformattoken;
|
|
lastformattoken := token;
|
|
end;
|
|
else
|
|
StoreStr(@Token, 1);
|
|
end ;
|
|
Inc(FormatCurrent, Count);
|
|
end;
|
|
end;
|
|
|
|
begin
|
|
{$ifdef MSWindows}
|
|
eastasiainited:=false;
|
|
{$endif MSWindows}
|
|
DecodeDateFully(DateTime, Year, Month, Day, DayOfWeek);
|
|
DecodeTime(DateTime, Hour, Minute, Second, MilliSecond);
|
|
ResultLen := 0;
|
|
ResultCurrent := @ResultBuffer[0];
|
|
if FormatStr <> '' then
|
|
StoreFormat(FormatStr, 0, False)
|
|
else
|
|
StoreFormat('C', 0, False);
|
|
ResultBuffer[ResultLen] := #0;
|
|
result := StrPas(@ResultBuffer[0]);
|
|
end ;
|
|
|
|
function FormatDateTime(const FormatStr: string; DateTime: TDateTime): string;
|
|
begin
|
|
DateTimeToString(Result, FormatStr, DateTime, DefaultFormatSettings);
|
|
end;
|
|
|
|
end.
|
|
|