Is there any way in Delphi 5 to convert a string to a TDateTime where you can specify the actual format to use?
I'm working on a jobprocessor, which accepts tasks from various workstations. The tasks have a range of parameters, some of which are dates, but (unfortunately, and out of my control) they're passed as strings. Since the jobs can come from different workstations, the actual datetime format used to format the dates as a string might (and, of course, actual do) differ.
Googling around, the only quick solutions I found was to sneakily change the ShortDateFormat variable, and restore it to its original value afterwards. Since ShortDateFormat is a global variable, and I'm working in a threaded environment the only way this would work is by synchronizing every access to it, which is completely unacceptable (and undoable).
I could copy the library code from the SysUtils unit into my own methods, and tweak them to work with a specified format instead of the global variables, but I'm just wondering if there's something more adequate out there that I missed.
UPDATE
To put it more succinctly:
I need something like StrToDate (or StrToDateTime), with the added option of specifying the exact format it should use to convert the string to a TDateTime.
Use VarToDateTime instead. It supports many more date formats in the string and converts them automatically.
var
DateVal: TDateTime;
begin
DateVal := VarToDateTime('23 Sep 2010');
ShowMessage(DateToStr(DateVal));
end;
I see you're using Delphi 5. Some versions of Delphi will need to add Variants to the uses clause; most later versions add it for you. I don't remember which category Delphi 5 fell into.
I created such routine for FreePascal's dateutils unit, and it should easy to port, if porting is needed at all.
Code:
http://svn.freepascal.org/cgi-bin/viewvc.cgi/trunk/packages/rtl-objpas/src/inc/dateutil.inc?revision=30628&view=co
(code is the last (huge) procedure at the end of the file)
documentation:
http://www.freepascal.org/docs-html/rtl/dateutils/scandatetime.html
Note that it is not a complete inverse of formatdatetime, and it has some extensions:
An inverse of FormatDateTime is not 100% an inverse, simply because one can put e.g. time tokens twice in the format string,and scandatetime wouldn't know which time to pick.
Strings like hn can't be reversed safely. E.g. 1:2 (2 minutes after 1) delivers 12 which is parsed as 12:00 and then
misses chars for the "n" part.
trailing characters are ignored.
no support for Eastern Asian formatting characters since they are windows only.
no MBCS support.
Extensions
#9 eats whitespace.
whitespace at the end of a pattern is optional.
? matches any char.
Quote the above chars to really match the char.
(I believe these comments are slightly outdated in the sense hat some Asian support was added later but I am not sure)
Later versions of Delphi can take an extra TFormatSettings argument to the string conversion functions. TFormatSettings is a structure containing the various format global variables (ShortDateFormat, LongDateFormat, etc). So you can override those value in a thread-safe manner and even for a single call.
I don't remember in which version of Delphi this was introduced, but I'm pretty sure it was after Delphi 5.
So yeah, as far as I know, you either need to synchronize every access to ShortDateFormat, or use a different function.
Here's the function, its two helpers, and all the code, I wrote to parse a string using an exact datetime format:
class function TDateTimeUtils.TryStrToDateExact(const S, DateFormat: string; PivotYear: Integer;
out Value: TDateTime): Boolean;
var
Month, Day, Year: Integer;
Tokens: TStringDynArray;
CurrentToken: string;
i, n: Integer;
Partial: string;
MaxValue: Integer;
nCurrentYear: Integer;
function GetCurrentYear: Word;
var
y, m, d: Word;
begin
DecodeDate(Now, y, m, d);
Result := y;
end;
begin
Result := False;
{
M/dd/yy
Valid pictures codes are
d Day of the month as digits without leading zeros for single-digit days.
dd Day of the month as digits with leading zeros for single-digit days.
ddd Abbreviated day of the week as specified by a LOCALE_SABBREVDAYNAME* value, for example, "Mon" in English (United States).
Windows Vista and later: If a short version of the day of the week is required, your application should use the LOCALE_SSHORTESTDAYNAME* constants.
dddd Day of the week as specified by a LOCALE_SDAYNAME* value.
M Month as digits without leading zeros for single-digit months.
MM Month as digits with leading zeros for single-digit months.
MMM Abbreviated month as specified by a LOCALE_SABBREVMONTHNAME* value, for example, "Nov" in English (United States).
MMMM Month as specified by a LOCALE_SMONTHNAME* value, for example, "November" for English (United States), and "Noviembre" for Spanish (Spain).
y Year represented only by the last digit.
yy Year represented only by the last two digits. A leading zero is added for single-digit years.
yyyy Year represented by a full four or five digits, depending on the calendar used. Thai Buddhist and Korean calendars have five-digit years. The "yyyy" pattern shows five digits for these two calendars, and four digits for all other supported calendars. Calendars that have single-digit or two-digit years, such as for the Japanese Emperor era, are represented differently. A single-digit year is represented with a leading zero, for example, "03". A two-digit year is represented with two digits, for example, "13". No additional leading zeros are displayed.
yyyyy Behaves identically to "yyyy".
g, gg Period/era string formatted as specified by the CAL_SERASTRING value.
The "g" and "gg" format pictures in a date string are ignored if there is no associated era or period string.
PivotYear
The maximum year that a 1 or 2 digit year is assumed to be.
The Microsoft de-factor standard for y2k is 2029. Any value greater
than 29 is assumed to be 1930 or higher.
e.g. 2029:
1930, ..., 2000, 2001,..., 2029
If the PivotYear is between 0 and 99, then PivotYear is assumed to be
a date range in the future. e.g. (assuming this is currently 2010):
Pivot Range
0 1911..2010 (no future years)
1 1912..2011
...
98 2009..2108
99 2010..2099 (no past years)
0 ==> no years in the future
99 ==> no years in the past
}
if Length(S) = 0 then
Exit;
if Length(DateFormat) = 0 then
Exit;
Month := -1;
Day := -1;
Year := -1;
Tokens := TDateTimeUtils.TokenizeFormat(DateFormat);
n := 1; //input string index
for i := Low(Tokens) to High(Tokens) do
begin
CurrentToken := Tokens[i];
if CurrentToken = 'MMMM' then
begin
//Long month names, we don't support yet (you're free to write it)
Exit;
end
else if CurrentToken = 'MMM' then
begin
//Short month names, we don't support yet (you're free to write it)
Exit;
end
else if CurrentToken = 'MM' then
begin
//Month, with leading zero if needed
if not ReadDigitString(S, n, 2{MinDigits}, 2{MaxDigits}, 1{MinValue}, 12{MaxValue}, {var}Month) then Exit;
end
else if CurrentToken = 'M' then
begin
//months
if not ReadDigitString(S, n, 1{MinDigits}, 2{MaxDigits}, 1{MinValue}, 12{MaxValue}, {var}Month) then Exit;
end
else if CurrentToken = 'dddd' then
begin
Exit; //Long day names, we don't support yet (you're free to write it)
end
else if CurrentToken = 'ddd' then
begin
Exit; //Short day names, we don't support yet (you're free to write it);
end
else if CurrentToken = 'dd' then
begin
//If we know what month it is, and even better if we know what year it is, limit the number of valid days to that
if (Month >= 1) and (Month <= 12) then
begin
if Year > 0 then
MaxValue := MonthDays[IsLeapYear(Year), Month]
else
MaxValue := MonthDays[True, Month]; //we don't know the year, assume it's a leap year to be more generous
end
else
MaxValue := 31; //we don't know the month, so assume it's the largest
if not ReadDigitString(S, n, 2{MinDigits}, 2{MaxDigits}, 1{MinValue}, MaxValue{MaxValue}, {var}Day) then Exit;
end
else if CurrentToken = 'd' then
begin
//days
//If we know what month it is, and even better if we know what year it is, limit the number of valid days to that
if (Month >= 1) and (Month <= 12) then
begin
if Year > 0 then
MaxValue := MonthDays[IsLeapYear(Year), Month]
else
MaxValue := MonthDays[True, Month]; //we don't know the year, assume it's a leap year to be more generous
end
else
MaxValue := 31; //we don't know the month, so assume it's the largest
if not ReadDigitString(S, n, 1{MinDigits}, 2{MaxDigits}, 1{MinValue}, MaxValue{MaxValue}, {var}Day) then Exit;
end
else if (CurrentToken = 'yyyy') or (CurrentToken = 'yyyyy') then
begin
//Year represented by a full four or five digits, depending on the calendar used.
{
Thai Buddhist and Korean calendars have five-digit years.
The "yyyy" pattern shows five digits for these two calendars,
and four digits for all other supported calendars.
Calendars that have single-digit or two-digit years, such as for
the Japanese Emperor era, are represented differently.
A single-digit year is represented with a leading zero, for
example, "03". A two-digit year is represented with two digits,
for example, "13". No additional leading zeros are displayed.
}
if not ReadDigitString(S, n, 4{MinDigits}, 4{MaxDigits}, 0{MinValue}, 9999{MaxValue}, {var}Year) then Exit;
end
else if CurrentToken = 'yyy' then
begin
//i'm not sure what this would look like, so i'll ignore it
Exit;
end
else if CurrentToken = 'yy' then
begin
//Year represented only by the last two digits. A leading zero is added for single-digit years.
if not ReadDigitString(S, n, 2{MinDigits}, 2{MaxDigits}, 0{MinValue}, 99{MaxValue}, {var}Year) then Exit;
nCurrentYear := GetCurrentYear;
Year := (nCurrentYear div 100 * 100)+Year;
if (PivotYear < 100) and (PivotYear >= 0) then
begin
//assume pivotyear is a delta from this year, not an absolute value
PivotYear := nCurrentYear+PivotYear;
end;
//Check the pivot year value
if Year > PivotYear then
Year := Year - 100;
end
else if CurrentToken = 'y' then
begin
//Year represented only by the last digit.
if not ReadDigitString(S, n, 1{MinDigits}, 1{MaxDigits}, 0{MinValue}, 9{MaxValue}, {var}Year) then Exit;
nCurrentYear := GetCurrentYear;
Year := (nCurrentYear div 10 * 10)+Year;
if (PivotYear < 100) and (PivotYear >= 0) then
begin
//assume pivotyear is a delta from this year, not an absolute value
PivotYear := nCurrentYear+PivotYear;
end;
//Check the pivot year value
if Year > PivotYear then
Year := Year - 100;
end
else
begin
//The input string should contains CurrentToken starting at n
Partial := Copy(S, n, Length(CurrentToken));
Inc(n, Length(CurrentToken));
if Partial <> CurrentToken then
Exit;
end;
end;
//If there's still stuff left over in the string, then it's not valid
if n <> Length(s)+1 then
begin
Result := False;
Exit;
end;
if Day > MonthDays[IsLeapYear(Year), Month] then
begin
Result := False;
Exit;
end;
try
Value := EncodeDate(Year, Month, Day);
except
Result := False;
Exit;
end;
Result := True;
end;
class function TDateTimeUtils.TokenizeFormat(fmt: string): TStringDynArray;
var
i: Integer;
partial: string;
function IsDateFormatPicture(ch: AnsiChar): Boolean;
begin
case ch of
'M','d','y': Result := True;
else Result := False;
end;
end;
begin
SetLength(Result, 0);
if Length(fmt) = 0 then
Exit;
//format is only one character long? If so then that's the tokenized entry
if Length(fmt)=1 then
begin
SetLength(Result, 1);
Result[0] := fmt;
end;
partial := fmt[1];
i := 2;
while i <= Length(fmt) do
begin
//If the characters in partial are a format picture, and the character in fmt is not the same picture code then write partial to result, and reset partial
if IsDateFormatPicture(partial[1]) then
begin
//if the current fmt character is different than the running partial picture
if (partial[1] <> fmt[i]) then
begin
//Move the current partial to the output
//and start a new partial
SetLength(Result, Length(Result)+1);
Result[High(Result)] := partial;
Partial := fmt[i];
end
else
begin
//the current fmt character is more of the same format picture in partial
//Add it to the partial
Partial := Partial + fmt[i];
end;
end
else
begin
//The running partial is not a format picture.
//If the current fmt character is a picture code, then write out the partial and start a new partial
if IsDateFormatPicture(fmt[i]) then
begin
//Move the current partial to the output
//and start a new partial
SetLength(Result, Length(Result)+1);
Result[High(Result)] := partial;
Partial := fmt[i];
end
else
begin
//The current fmt character is another non-picture code. Add it to the running partial
Partial := Partial + fmt[i];
end;
end;
Inc(i);
Continue;
end;
//If we have a running partial, then add it to the output
if partial <> '' then
begin
SetLength(Result, Length(Result)+1);
Result[High(Result)] := partial;
end;
end;
class function TDateTimeUtils.ReadDigitString(const S: string; var Pos: Integer;
MinDigits, MaxDigits: Integer; MinValue, MaxValue: Integer;
var Number: Integer): Boolean;
var
Digits: Integer;
Value: Integer;
Partial: string;
CandidateNumber: Integer;
CandidateDigits: Integer;
begin
Result := False;
CandidateNumber := -1;
CandidateDigits := 0;
Digits := MinDigits;
while Digits <= MaxDigits do
begin
Partial := Copy(S, Pos, Digits);
if Length(Partial) < Digits then
begin
//we couldn't get all we wanted. We're done; use whatever we've gotten already
Break;
end;
//Check that it's still a number
if not TryStrToInt(Partial, Value) then
Break;
//Check that it's not too big - meaning that getting anymore wouldn't work
if (Value > MaxValue) then
Break;
if (Value >= MinValue) then
begin
//Hmm, looks good. Keep it as our best possibility
CandidateNumber := Value;
CandidateDigits := Digits;
end;
Inc(Digits); //try to be greedy, grabbing even *MORE* digits
end;
if (CandidateNumber >= 0) or (CandidateDigits > 0) then
begin
Inc(Pos, CandidateDigits);
Number := CandidateNumber;
Result := True;
end;
end;
If you want to know how this was solved in later Delphi's, you can take a look at the source of a slightly more modern (looks like Delphi 6) sysutils.pas here:
http://anygen.googlecome.com/.../SysUtils.pas
Check out the overloaded versions of StrToDateTime that take a TFormatSettings parameter.
function StrToDateTime(const S: string;
const FormatSettings: TFormatSettings): TDateTime; overload;
Use RegExpr library (https://github.com/masterandrey/TRegExpr)
var
RE: TRegExpr;
begin
RE := TRegExpr.Create;
try
RE.Expression := '^(\d\d\d\d)/(\d\d)/(\d\d)T(\d\d):(\d\d):(\d\d)$';
if RE.Exec( Value ) then
begin
try
Result := EncodeDate( StrToInt( RE.Match[1] ),
StrToInt( RE.Match[2] ),
StrToInt( RE.Match[3] ) ) +
EncodeTime( StrToInt( RE.Match[4] ),
StrToInt( RE.Match[5] ),
StrToInt( RE.Match[6] ),
0 )
except
raise EConvertError.Create( 'Invalid date-time: ' + Value )
end
end
else
raise EConvertError.Create( 'Bad format: ' + Value )
finally
RE.Free
end
end;
I am not sure about what you want. I don't use Delphi 5 any more but I am pretty sure the function StrToDateTime exists in it. Using it you can convert a string to TDateTime with format settings. Then you can convert such TDateTime to any format using FormatDateTime which enables you to use any date format you wish.
I would go the other way around it. As I see it, you have about two options of wich you mentioned one yourself
Adjust the ShortDateFormat and keep every acces to it synchronized.
If you know the format of the strings you are recieving (somehow, you'll have to), just do some string juggling to first get your strings in your current shortdateformat. After that, convert the (juggled) string to a TDateTime.
I do wonder how you are going to determine the format for say 04/05/2010.
program DateTimeConvert;
{$APPTYPE CONSOLE}
uses
SysUtils;
function GetPart(const part, input, format: string): string;
var
I: Integer;
begin
for I := 1 to Length(format) do
if Uppercase(format[I]) = Uppercase(part) then
Result := Result + input[I];
end;
function GetDay(const input, format: string): string;
begin
Result := GetPart('d', input, format);
if Length(Result) = 1 then Result := SysUtils.Format('0%0:s', [Result]);
end;
function GetMonth(const input, format: string): string;
begin
Result := GetPart('m', input, format);
if Length(Result) = 1 then Result := SysUtils.Format('0%0:s', [Result]);
end;
function GetYear(const input, format: string): string;
begin
Result := GetPart('y', input, format);
end;
function ConvertToMyLocalSettings(const input, format: string): string;
begin
Result := SysUtils.Format('%0:s/%1:s/%2:s', [GetDay(input, format), GetMonth(input, format), GetYear(input, format)]);
end;
begin
Writeln(ConvertToMyLocalSettings('05/04/2010', 'dd/mm/yyyy'));
Writeln(ConvertToMyLocalSettings('05-04-2010', 'dd-mm-yyyy'));
Writeln(ConvertToMyLocalSettings('5-4-2010', 'd-m-yyyy'));
Writeln(ConvertToMyLocalSettings('4-5-2010', 'm-d-yyyy'));
Writeln(ConvertToMyLocalSettings('4-05-2010', 'M-dd-yyyy'));
Writeln(ConvertToMyLocalSettings('05/04/2010', 'dd/MM/yyyy'));
Readln;
end.
I have a Form TForm1 having 5 TEdit and 2 TBitBtn.
I also need the Program so that after inputting Numeric Data in Edit1 and Edit2 on BitBtn1Click, Edit1 and Edit2 value will summed and will be displayed in Edit3.
You want to do something like this:
var
val1, val2, sum: Integer;
...
val1 := StrToInt(Edit1.Text);
val2 := StrToInt(Edit2.Text);
sum := val1 + val2;
Edit3.Text := IntToStr(sum);
If you want floating point arithmetic do it like this
var
val1, val2, sum: Double;
...
val1 := StrToFloat(Edit1.Text);
val2 := StrToFloat(Edit2.Text);
sum := val1 + val2;
Edit3.Text := FloatToStr(sum);
To read and set the value of a TEdit control, you simply reference the Text property of the control. The Text property is of type String.
Since Text is a String property, you can treat it in your code as a String variable. You can pass it into a function that expects a String constant:
// Edit1 is the name of the TEdit control
// Display the value in the edit control to the user
ShowMessage(Edit1.Text);
You can assign it to a String variable with a simple assignment:
var
// My string variable
myString: String;
begin
// Edit1 is the Name of the control
myString := Edit1.Text;
end;
To set the value of a TEdit control, you simply assign a string to the Text property. This could be a String constant:
Edit1.Text := 'hello';
Or it could be from a String variable:
Edit1.Text := myString;
Math is done on numeric types, so for arithmetic, you'll need to use a function to convert the string values into numbers.
For Integer arithmetic, you can use StrToInt() or StrToIntDef():
var
myInteger: Integer;
begin
// Convert Edit1.Text string to a number and assign to numeric type for math
// If the value in Edit1.Text cannot be converted, an exception will be raised
myInteger := StrToInt(Edit1.Text);
end;
Using StrToIntDef():
var
myInteger: Integer;
begin
// If Edit1.Text cannot be converted, the default value of 0 will be used
myInteger := StrToIntDef(Edit1.Text, 0);
end;
For floating point arithmetic, use StrToFloat() or StrToFloatDef() instead.
To assign an Integer back to the Text property, you'll need to convert the Integer to a String before you assign it:
var
myInteger: Integer;
begin
myInteger := 12;
Edit1.Text := IntToStr(myInteger);
end;
For floating points, use FloatToStr().
Finally, to put everything together, to get the numeric values of two edit boxes and display the sum in a third edit box, simply do this:
var
// Floating point variables
value1: Real;
value2: Real;
sum: Real;
begin
// Get the values from the edit boxes, converting them to floating point types
value1 := StrToFloat(Edit1.Text);
value2 := StrToFloat(Edit2.Text);
// Sum them
sum := value1 + value2;
// Convert the sum to string and assign back to edit box
Edit3.Text := FloatToStr(sum);
end;
Or in one step:
Edit3.Text := FloatToStr(StrToFloat(Edit1.Text) + StrToFloat(Edit2.Text));
I noticed the following snippet:
so that after inputting Numeric Data in Edit1 and Edit2
If you want to only allow Numeric data, it's a good idea to disallow non numeric data in the edit boxes.
Here's how to do that.
const
TabKey = #9;
Backspace = #8;
Enter = #13;
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
if not (Key in ['0'..'9','-',TabKey,Enter,Backspace]) then Key:= #0; //integers
//realnumbers: if not (Key in ['0'..'9','-','e','E','.',TabKey,Enter,Backspace]) then Key:= #0;
end;
If you only have integer data, this will do, if you have scientific numbers, you need to do some testing for the letter e and the decimal point as well to allow for irrational numbers.
Regardless of what you do it's a good idea to check to see the input is a valid number and let the user know.
procedure TForm1.Edit1Change(Sender: TObject);
var
MyEdit: TEdit;
OtherEdit: TEdit;
TryNumber: double;
OtherNumber: double;
Success: boolean;
begin
Success:= true;
if (Sender is TEdit) then begin
MyEdit:= TEdit(Sender);
try
if MyEdit.Text = '' then TryNumber:= 0
else TryNumber:= StrToFloat(MyEdit.Text);
MyEdit.Color:= clWindow; //all is OK make edit standard white.
MyEdit.Hint:= '';
except
MyEdit.Color:= clRed; //Let the user know the output will not compute.
MyEdit.Hint:= MyEdit.Text + ' is not a valid number ';
Success:= false;
end;
end;
if (MyEdit = Edit1) then OtherEdit:= Edit2
else OtherEdit:= Edit1;
try
if OtherText.Text = '' then OtherNumber:= 0
else OtherNumber:= StrToFloat(OtherEdit.Text);
except
Success:= false;
end;
if Success then Edit3.Text:= FloatToStr(TryNumber + OtherNumber);
end;
Note that you can attach this Event to both Edit1 and Edit2, so you do not have to write the code twice. (But I'm sure you already knew that).
(Both edit's share the same event).
Important things to remember
Always use try..except to catch errors so that your program does not break with an error, see: http://www.delphibasics.co.uk/Article.asp?Name=Exceptions
If you have an editbox that only allows numeric data, consider using a maskedit which only allows valid chars, or code your own filter (if it's trivial to do so).
Try and use a single routine for multiple controls, so you don't end up with multiple very similar routines that all do almost the same thing. That way if you change something, you'll only have to change it in one place and it will work in all the controls that make use of that routine.