I am writing column and cell classes for FMX TGrid that will contain TCalendarEdit and TTimeEdit instances in every cell. Everything works fine except the proper processing of changes done in these child controls.
type
TFMTValue<T> = record
FieldValue: T;
Modified: boolean;
Appended: boolean;
Deleted: boolean;
end;
TDateTimeCell = class(TStyledControl)
private
FDate_Time: TFMTValue<TDateTime>;
procedure SetDateTime(const Value: TFMTValue<TDateTime>);
function GetDateTime: TFMTValue<TDateTime>;
protected
procedure SetData(const Value: TValue); override;
public
property Date_Time: TFMTValue<TDateTime> read GetDateTime write SetDateTime;
...
end;
...
function TDateTimeCell.GetDateTime: TFMTValue<TDateTime>;
begin
FDate_Time.Modified := (FDate_Time.Modified) or
(FDate_Time.FieldValue <> FCalendarEdit.Date +
+ FTimeEdit.Time);
FDate_Time.FieldValue := FCalendarEdit.Date + FTimeEdit.Time;
Result := FDate_Time;
end;
procedure TDateTimeCell.SetData(const Value: TValue);
begin
Date_Time := Value.AsType<TFMTValue<TDateTime>>;
inherited SetData(TValue.From<TDateTime>(FDate_Time.FieldValue));
ApplyStyling;
end;
procedure TDateTimeCell.SetDateTime(const Value: TFMTValue<TDateTime>);
begin
FDate_Time := Value;
FCalendarEdit.Date := DateOf(FDate_Time.FieldValue);
FTimeEdit.Time := TimeOF(FDate_Time.FieldValue);
FDate_Time.FieldValue:=FCalendarEdit.Date + FTimeEdit.Time; //this line helps but not in all cases
end;
The idea is that data is assigned via TGrid OnGetValue event handler. Both date and time are displayed. The user activity is catched and Modified flag is set. The problem is that this flag is set to true sometimes even without any user activities. I suspect it is due to the rounding of time part of TDateTime. There are no other ways the code assignes values to FCalendarEdit.Date and FTimeEdit.Time.
How can I properly compare the data stored in FCalendarEdit.Date and FTimeEdit.Time with that stored in FDate_Time.FieldValue?
Appended
Setting the flag in this way does not resolve the issue.
FDate_Time.Modified := (FDate_Time.Modified) or
(DateOf(FDate_Time.FieldValue) <> FCalendarEdit.Date) or
(TimeOf(FDate_Time.FieldValue)<> FTimeEdit.Time);
Appended 2. On a valued advice of #Ken-White.
If we replace the comparison line by
FDate_Time.Modified := (FDate_Time.Modified) or
(not SameDateTime(FDate_Time.FieldValue,
FCalendarEdit.Date + FTimeEdit.Time));
It works fine. So the TDataTime comparison must be done by this function only.
TDateTime is of type Double, which means it's a floating point value, and therefore is subject to the usual issues of binary representation when doing comparisons for equality without specifying an acceptable delta (difference)..
Specifically for TDateTime values, you can use DateUtils.SameDateTime to compare equality down to less than one millisecond:
FDate_Time.Modified := (FDate_Time.Modified) or
(not SameDateTime(FDate_Time.FieldValue,
FCalendarEdit.Date + FTimeEdit.Time));
There is a bug in TCalendarEdit (a few actually) which is the underlying cause of your problem, but you can fix it with only a small change to your code.
The Problem
The TCalendarEdit makes a number of crucial errors when it applies a new Date value.
A TDate type is actually just an ordinary TDateTime in which you are supposed to ignore the time portion. Similarly a TTime is a TDateTime where you are supposed to ignore the date portion.
But you have to use these types correctly in your code - there is nothing that magically makes a TTime ignore the date or a TDate ignore the time.
For example, if you examine the constructor of the TCalendarEdit, you will see that it initialises the internal date/time to the current system date and time using Now, but truncates this to eliminate the time element:
Date := Trunc(Now);
So far so good.
But when you apply a new value via the Date property, it performs the following (simplified):
if Date <> Value then
FDateTime := Value + Time;
Both of these lines of code contain serious bugs:
It compares the Date (property returning the Date value of the control) with the Value being assigned - including any time value in that date/time. It should instead compare only the date part of the Value.
When assigning the new value to the internal date/time it adds Time to the Value you specified.
The first bug results in unnecessary changes to the internal property but is otherwise relatively innocuous. The second bug however is far more serious and is what is causing your problem.
I presume that the intention of the author of the control was to leave the time portion of the internal date/time value unchanged. However, the Value is not truncated, so it retains the time value specified in the assignment to the property. To make matters even worse, there is no Time property on this control, so this in fact adds the current system time to whatever time is specified in Value.
How This Affects Your Code and Test Case
Since your test case involved a time of midday - 12 hours - the result is that when you run this code in the afternoon, the Date of your TCalendarEdit is actually set to 25-Sep-2015 + 12 hours + the time when the control was initialised.
If you run the code in the morning, it seems to work because the time added results in a value that is still on the 25th Sep.
But when you run the code in the afternoon, the 12 hours are added to the current time and so the date rolls over to the next day!
With a more helpful diagnostic error message, or if you had inspected the properties in your code via the debugger, you would have seen this occurring.
DT := EncodeDate(2015, 9, 25) + EncodeTime(12, 0, 0, 0);
CalendarEdit1.Date := DT;
ShowMessage(DateTimeToString(CalendarEdit1.Date));
// When executed at e.g. 9am, displays: 25 Sep 2015
// When executed at e.g. 1pm, displays: 26 Sep 2015
So the reason your comparison then fails is because the date is actually completely different!
If you had tried simply using SameDateTime() for the comparison it may have appeared to have worked if you tested it in the morning but your problem would have returned in the afternoon !!
The Solution
You can work around these bugs in TCalendarEdit by ensuring that you respect the intended use of the property values yourself, assigning only those parts of the DT date/time value as appropriate in each case:
TimeEdit1.Time := TimeOf(DT);
CalendarEdit1.Date := DateOf(DT);
Although not strictly necessary in the case of the TTimeEdit, this will prevent these bugs in TCalendarEdit from causing these problems and makes it clear in your code that you are aware of what is required (consider it self documenting code if you like). :)
If you do not have TimeOf() and DateOf() functions in your version of Delphi, then the following is equivalent:
TimeEdit1.Time := DT - Trunc(DT);
CalendarEdit1.Date := Trunc(DT);
You could of course write your own versions of TimeOf() and DateOf() based on this, to make the intention clearer.
NOTE
There are precision complications arising from the floating point nature of date/time values in Delphi that could cause problems with direct comparisons with some specific values of date and time and for that reason it is highly recommended that you use the SameDateTime() function for performing such comparisons.
But this was absolutely not the cause of your problem in this case and SameDateTime() does not solve your problem.
SameDateTime() eliminates problems arising from differences in date/time values of less than 1 millisecond. The difference in this case was 24 hours!
Worth noting is that the TCalendarEdit control was deprecated in XE7 and has been removed entirely from XE8.
Related
I want to sort a ClientDataset by a StringField which contains Integers. These Integers are used as item numbers on invoices. When I sort the ClientDataset by that field using a simple index the result is this:
1
10
100
101
11
110
111
12
120
However I'd like them to be sorted like an IntegerField:
1
10
11
12
100
101
110
111
120
Is there a quick way to do this?
You don't need to change your existing field to a different type. Instead, a way to do what you want is to define an ftInteger field on the CDS which is of FieldFind fkInternalCalc. Then in the CDS's OnCalcFields event, set the field's value by converting the value of your string field to an integer.
The point of setting the calculated field's find tp fkInternalCalc is that the CDS can be indexed on, and hence sorted by, an fkInternalCalc field, unlike an fkCalculated field. So, by indexing your CDS on the added fkInternalCalc field, the records should be appear in their numeric order rather that the alphanumeric order of your string field.
Code:
procedure TForm1.CDS1CalcFields(DataSet: TDataSet);
begin
if CDS1IntField.IsNull then
CDS1IntField.AsInteger := StrToInt(CDS1StringField.AsString);
end;
procedure TForm1.FormCreate(Sender: TObject);
var
i : Integer;
begin
CDS1.CreateDataSet;
for i := 1 to 100 do
CDS1.InsertRecord([IntToStr(i)]);
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
CDS1.IndexFieldNames := 'IntField';
end;
If you want to sort as integers: by far the best approach is to: change the type to integer.
Yes, it's more work, but hear me out.
Currently you have 2 possibilities: either all your values are currently canonical1 integers, or they are not.
If they are, then you do have a little work to do. You need to change:
The column in the database
Any foreign key columns containing the same data
The types of any persisted fields for those columns
Any code that is expecting string item numbers
And you'll gain the following benefits:
Better performance due to: single op comparisons, less data in memory, narrower keys for B-Tree indexes and reduced heap memory allocations in Delphi for the previous long string objects.
Less concern about the nuances of strings and string comparison. E.g. unexpected padding.
And a strong typing guarantee that you'll not unexpectedly have to deal with 'problematic strings'. You raised a concern whether you would "have any negative consequences" due to making the change. I'm not aware of any. However, not making the change does risk the consequence of unexpected non-integer item numbers.
However, if your item numbers are not canonical integers: then trying to sort as integers is already problematic. How would you sort the following item numbers as integers?
'03', '13', '3', 'B2', ' 13 ', '3bad'
Martyn's answer would be the best way to go in this situation. Because you retain the original string value, you still have a safe and reliable key. (NB only use the internal calc field for sorting).
Note, you would have to tweak the: CDS1IntField.AsInteger := StrToInt(CDS1StringField.AsString); line to ensure you can set sensible sorting values. Any exceptions raised due to failure to convert some string would lead to nasty problems.
As a bonus, Martyn's answer is safe in the short term before you've investigated the knock-on effects of the larger change. But you may want to schedule time for the 'big job'.
1 The point about 'canonical' integers is subtle but important. Prefixing any number of zero's or spaces to an integer produces a non-canonical duplicate. Consider "3" and "03". As integers they are the same, but as strings they are different.
i have create a coding to set a 20 millisecond faster than windows system. i'm using encodetime.
here's the code
procedure TForm1.Button1Click(Sender: TObject);
Var
delphi_datetime :tDateTime;
t_date : tdatetime ;
windows_datetime : tSystemTime;
begin
t_date := dATE;
delphi_datetime := encodetime(8,44,59,980);
delphi_datetime := incmillisecond(delphi_datetime, 20);
replacedate(t_date , delphi_datetime);
datetimetosystemtime( delphi_datetime , windows_datetime );
setlocaltime( windows_datetime );
showmessage('time now = ' + timetostr(delphi_datetime));
end;
aftr i run it, show the correct time. but the date goes to 30 dec 1899.. but i want to the current today date but with the time faster 20 milliseconds. any help.. please...
You have the arguments of ReplaceDate backward. It reads the date of the second parameter and assigns the date portion of the first parameter. The date portion of delphi_datetime is 0 because that's how EncodeTime works. You take that zero value and assign it to t_date, but then you continue working with delphi_datetime.
Reverse the arguments of ReplaceDate, and you should see that your current system time gets set to 8:45:00.000 with the current date.
ReplaceDate(delphi_datetime, t_date);
You could have noticed the mistake sooner if you hadn't used a separate t_date variable. If you'd called Date directly, your code would have failed to compile:
ReplaceDate(Date, delphi_datetime); // can't pass function result as "var" parameter
This works:
ReplaceDate(delphi_datetime, Date);
Rob has identified problems with your existing code. However your existing approach is needlessly complex. If you chose a simpler approach you would find it easier to get the code right.
If you want a date time that is 20 milliseconds greater than now, do it like this:
MyDateTime := IncMillisecond(Now, 20);
If you want a date time representing 0845 today, then you write:
MyDateTime := Date + EncodeTime(8, 45, 0, 0);
Who can to advice in my problem.
I set date format as 'JUL/12 - 12 15:35', but when using StrToDateTime then give EConvertError.
What can I do with this format which contains 2 - date separator ?
Use next code
function LocaleFormatStrToDateTime(const S: string): TDateTime;
var
LFormatSettings: TFormatSettings;
begin
LFormatSettings := GetLocaleFormatSettings(LOCALE_SYSTEM_DEFAULT);
LFormatSettings.ShortTimeFormat := FormatSettings.ShortTimeFormat;
LFormatSettings.TimeSeparator := FormatSettings.TimeSeparator;
Result := StrToDateTime(S, LFormatSettings);
end;
----------
**
the best solution is use jvDateUtil.StrToDate*
**
Your format is completely non-standard (and almost incomprehensible), so can't be handled by the built-in Date/Time formatters.
You've designed your own format, so you need to write your own code to convert to and from it.
This is nature's way of telling you not to use wacky date and time formats!
Probably the string you're trying to convert is not compatible with the default system format. Taking a look at the method signature and description reveals that you can override it to suit your needs, see an example here.
It would be helpful if you posted a piece of the code you have so far, maybe you overlooked something.
EDIT
I've missed the fact that your're using a complex format, including multiple separators for the date, which I'm not sure that are supported in delphi.
I guess that in this case you could split your string into pieces and then encode them into a TDateTime. To convert your month name to a month number you can iterate through the LFormatSettings.ShortMonthNames array, something like:
String longMonth:= copy(S, 0, 3);
for i := Low(LFormatSettings.ShortMonthNames) to High(LFormatSettings.ShortMonthNames) do
if SameText(longMonth, LFormatSettings.ShortMonthNames[i]) then begin
shortMonth:=FormatFloat('00', i);
Break;
end;
I got the TDateTime thing fixed by using Floating vars to store them in a file. However, now I face a new problem: Invalid Floating Point - Most likely because of the Comma Separator.
How can I set the default separator in my program? Or is there any other way around?
You can use a TFormatSettings record to specify the decimal separator when you call StrToFloat and FloatToStr. You have to decide what to use and stick to that. Here is sample code with a .
var
d: TDateTime;
s: string;
fs: TFormatSettings;
begin
d := Now();
fs.DecimalSeparator := '.';
s := FloatToStr(d, fs);
end;
Another option would be to use the XML standard date time format. Delphi has some functions in XSBuiltIns to do the conversion from TDateTime to string and back. You will have a time zone offset in the value so if you move your persisted TDateTime from one time zone to another you may have some unwanted behavior. It depends on the usage of the value.
var
d: TDateTime;
s: string;
begin
d := Now();
s := DateTimeToXMLTime(d);
d := XMLTimeToDateTime(s);
end;
As Mikael suggested, there are a many ways to do this. To re-cap you wish to store a TDateTime to a file in textual format and be able to restore this value successfully irrespective of the locale on which the restoration happens.
Option 1
When storing, call FloatToStr, say, but force a '.' for the decimal separator through the TFormatSettings parameter. On restore, use StrToFloat with the same TFormatSettings.
Option 2
Encode the 8 byte TDateTime value using base 64. This has the downside that it renders the value unreadable.
Option 3
Similar to option 1, but encode the TDateTime by calling DateTimeToStr and explicitly passing a TFormatSettings that does not rely on anything in the locale – so do not rely on the locale's date or time separators, instead force your own. To reverse call StrToDateTime with an identical TFormatSettings record.
I am having a problem getting a list of fields from a query defined at run time by the users of my program. I let my users enter a SQL query into a memo control and then I want to let them go through the fields that will return and do such things as format the output, sum column values and so forth. So, I have to get the column names so they have a place to enter the additional information.
I would do fine if there were no parameters, but I also have to let them define filter parameters for the query. So, if I want to set the parameters to null, I have to know what the parameter's datatype is.
I am using Delphi 2006. I connect to a Firebird 2.1 database using the DBExpress component TSQLConnection and TSQLQuery. Previously, I was successful using:
for i := 0 to Qry.Params.Count - 1 do Qry.Params[i].value := varNull;
I discovered I had a problem when I tried to use a date parameter. It was just a coincidence that all my parameters up until then had been integers (record IDs). It turns out that varNull is just an enumerated constant with a value of 1 so I was getting acceptable results (no records) was working okay.
I only need a list of the fields. Maybe I should just parse the SELECT clause of the SQL statement. I thought setting Qry.Prepared to True would get me a list of the fields but no such luck. It wants values for the parameters.
If you have an idea, I would sure like to hear it. Thanks for any help.
Replied again 'coz I'm interested. My methods works (with my queries) because they have been pre-defined with the params' datatypes preset to the correct type:)
I'm not sure how you are expecting the query to know or derive the datatype of the param given that you are not even selecting the field that it operates against.
So I think your query setup and user input method will need more attention. I've just looked up how I did this a while ago. I do not use a parameterised query - I just get the "parameter values" from the user and put them directly into the SQL. So your sql would then read:
SELECT s.hEmployee, e.sLastName
FROM PR_Paystub s
INNER JOIN PR_Employee e ON e.hKey = s.hEmployee
WHERE s.dtPaydate > '01/01/2008'
therefore no parameter type knowledge is necessary. Does not stop your users entering garbage but that goes back to input control :)
Although a slightly different dataset type this is what I use with TClientDataset simple and effective :)
for i := 0 to FilterDataSet.Params.Count -1 do
begin
Case FilterDataSet.Params.Items[i].Datatype of
ftString:
ftSmallint, ftInteger, ftWord:
ftFloat, ftCurrency, ftBCD:
ftDate:
ftTime:
ftDateTime:
.
.
.
end;
end;
can you not do something similar with the query?
You guys are making this way too hard:
for i := 0 to Qry.Params.Count - 1 do begin
Qry.Params[i].Clear;
Qry.Params[i].Bound := True;
end;
I'm not sure what version of Delphi you are using. In the Delphi 2006 help under Variant Types, it says:
Special conversion rules apply to the
Borland.Delphi.System.TDateTime type
declared in the System unit. When a
Borland.Delphi.System.TDateTime is
converted to any other type, it
treated as a normal Double. When an
integer, real, or Boolean is converted
to a Borland.Delphi.System.TDateTime,
it is first converted to a Double,
then read as a date-time value. When a
string is converted to a
Borland.Delphi.System.TDateTime, it is
interpreted as a date-time value using
the regional settings. When an
Unassigned value is converted to
Borland.Delphi.System.TDateTime, it is
treated like the real or integer value
0. Converting a Null value to Borland.Delphi.System.TDateTime raises
an exception.
The last sentence seems important to me. I would read that as varNull cannot be converted to a TDateTime to put into the field, and hence you get the exception that you're experiencing.
It also implies that this is the only special case.
Couldn't you do something like:
for i := 0 to Qry.Params.Count - 1 do
begin
if VarType(Qry.Params[i].value) and varTypeMask = varDate then
begin
Qry.Params[i].value := Now; //or whatever you choose as your default
end
else
begin
Qry.Params[i].value := varNull;
end;
end;
What I ended up doing was this:
sNull := 'NULL';
Qry.SQL.Add(sSQL);
for i := 0 to Qry.Params.Count - 1 do begin
sParamName := Qry.Params[i].Name;
sSQL := SearchAndReplace (sSQL, ':' + sParamName, sNull, DELIMITERS);
end;
I had to write SearchAndReplace but that was easy. Delimiters are just the characters that signal the end of a word.
TmpQuery.ParamByName('MyDateTimeParam').DataType := ftDate;
TmpQuery.ParamByName('MyDateTimeParam').Clear;
TmpQuery.ParamByName('MyDateTimeParam').Bound := True;