Search dates on a record - delphi

I'm storing dates on MS ACCESS database along with other information. But I can't seem to search them as I please. I'd like to search by: year, month. I'm using a datetimepicker
Currently I'm using this code to do so:
ADOTable1.Filter := 'Date > ' +
Edit1.Text;
ADOTable1.Filtered := True;
end;
Could anyone help me?

You have two options: your solution uses the less preferable one of passing string literals to a query. In this case, you have to 'escape' the value that you are passing, eg
ADOTable1.Filter := 'Date > ''' + Edit1.Text + '''';
This will result in a line like
ADOTable1.Filter := 'Date > ''27-Sep-69'''
The better solution is to use a parameterised query
select <whatever> from table
where date > :p1
You pass the parameter in the following manner
ADOTable.parambyname ('p1').asdate:= strtodate (edit1.text);
I admit that I don't use ADO components so that syntax may be slightly off, but it's the syntax used for Firebird.
Parameters are better than using raw text values because you don't have to worry about adding the correct number of quotation marks, and no one can pass in a bad value. Imagine what would happen if edit1.text contained '27-Sep-65;Drop table1'; - this could delete your table from the database!

Related

How do you access a memtable to pull to a record for edit in delphi?

I know how to pull information from ADO query based on a unique ID. Is there a way to do that with a memtable, but with no unique ID to establish the record to pull for edit. Can you use just the currently selected item in the memtable?
Edit:
It is a Tkbmmemtable it is a third part component, but I assume it works similar to whatever is embedded in Delphi. Long story short, there is a form that comes up that receives data from the user. Everytime you hit ok it adds it to a tkbmmemtable (which is displayed to the user in a grid) and is stored there until you the user hits "write to database" and then it is written to a backend with a unique ID. I am trying to enable the ability to double click on a record in the grid and have a screen come up where they can edit the data and then post it back to the memtable.
Can you use just the currently selected item in the memtable?
The short answer is "Yes", as #KenWhite has incisively explained.
The reason the answer is "Yes" is because of the way TDataSet and its descendants like TKbmMemtable work.
Is you probably know, the defined behaviour of TDataSet et al is that it models an internal "cursor" which points to exactly one record in the dataset, and that record is treated by TDataSet and all the db-aware controls as the "active" record: you navigate the dataset by moving the logical cursor by methods such as First, Last, Next, Locate etc.
Internally, when a TDataSet is open, it has an array of "buffers", which are pointers to dynamically allocated memory blocks which each store the data of one of a contiguous range of the records in the dataset. The number of buffers is fixed when the dataset is opened and is determined by what db-aware components are connected to the dataset; usually there are a sufficient number of them to hold the rows in a TDbGrid.
TDataSet has a method (function) ActiveBuffer which is defined in DB.Pas as
function TDataSet.ActiveBuffer: PChar;
begin
Result := FBuffers[FActiveRecord];
end;
Any editing operation on the dataset's data operates on the record data in the ActiveBuffer It is identified simply by the pointer which TDataSet.ActiveBuffer returns. That's all that's needed and is why editing operations on a record in an in-memory dataset work fine without any ID/PK/SequenceNumber or whatever field.
That's it, really.
the logic would be :
1. pull the data using adoquery
2. put the data to memtable
3. user will edit values
4. save changes
treat your memtable as container. so when saving to database your code will be more like this :
var
script : string;
begin
if insert then
begin
script :=
'insert into table(c1, c2, c3) values (' +
QuotedStr(memTable.FieldByName('c1').AsString) + ',' +
QuotedStr(memTable.FieldByName('c1').AsString) + ',' +
QuotedStr(memTable.FieldByName('c1').AsString) + ' ' +
')';
end
else
begin
script :=
'update table set ' +
'c1='+ QuotedStr(memTable.FieldByName('c1').AsString) + ',' +
'c1='+ QuotedStr(memTable.FieldByName('c1').AsString) + ',' +
'c1='+ QuotedStr(memTable.FieldByName('c1').AsString) + ',' +
'where id = ' + IntToStr(memTable.FieldByName('id').AsInteger);
end;
// adoQuery.Execute(script);
end;

Avoiding Duplicate Column Name Additions in a TDataSet

I am dynamically adding fields to a TDataSet using the following code:
while not ibSQL.Eof do
fieldname := Trim(ibSql.FieldByName('columnnameofchange').AsString);
TDataSet.FieldDefs.Add(fieldname , ftString, 255);
end
Problem is that I might get duplicate names so what is the easiest way to screen for duplicates and not add the duplicates that are already added.
I hope not to traverse through the TDataSet.FieldDefList for each column added as this would be tedious for every single column addition. And there can be many additions.
Please supply another solution if possible. If not then I am stuck using the FieldDefList iteration.
I will also add that screening out duplicates on the SQL query is an option but not a desired option.
Thanks
TFieldDefs has a method IndexOf that returns -1 when a field with the given name does not exist.
If I understand you correctly, the easiest way would probably be to put all of the existing field names in a TStringList. You could then check for the existence before adding a new field, and if you add it you simply add the name to the list:
var
FldList: TStringList;
i: Integer;
begin
FldList := TStringList.Create;
try
for i := 0 to DataSet.FieldCount - 1 do
FldList.Add(DataSet.Fields[i].FieldName);
while not ibSQL.Eof do
begin
fieldname := Trim(ibSql.FieldByName('columnnameofchange').AsString);
if FldList.IndexOf(fieldName) = -1 then
begin
FldList.Add(fieldName);
DataSet.FieldDefs.Add(fieldname , ftString, 255);
end;
ibSQL.Next;
end;
finally
FldList.Free;
end;
end;
I'm posting this anyway as I finished writing it, but clearly screening on the query was my preference for this problem.
I'm having a bit of trouble understanding what you're aiming for so forgive me if I'm not answering your question. Also, it has been years since I used Delphi regularly so this is definitely not a specific answer.
If you're using the TADOQuery (or whatever TDataSet you're using) in the way I expect my workaround was to do something like:
//SQL
SELECT
a.field1,
a.... ,
a.fieldN,
b.field1 as "AlternateName"
FROM
Table a INNER JOIN Table b
WHERE ...
As which point it automatically used AlternateName instead of field1 (thus the collision where you're forced to work by index or rename the columns.
Obviously if you're opening a table for writing this isn't a great solution. In my experience with Delphi most of the hardship could be stripped out with simple SQL tricks so that you did not need to waste time playing with the fields.
Essentially this is just doing what you're doing at the source instead of the destination and it is a heck of a lot easier to update.
What I'd do is keep a TStringList with Sorted := true and Duplicates := dupError set. For each field, do myStringList.Add(UpperCase(FieldName)); inside a try block, and if it throws an exception, you know it's a duplicate.
TStringList is really an incredibly versatile class. It's always a bit surprising all the uses you can find for it...

Filtering and comparing a date stored as a string in Delphi

Working with a legacy project that has a date stored as a string in the format
'6/1/2013'
Example of the date range filter I'm trying to do:
table.filter := 'stringdate >= ' + QuotedStr(adatepicker.text) + ' and ' +
'stringdate <= ' + QuotedStr(enddatepicker.text);
Obviously this doesn't work. When setting a date range filter the data is incorrect because we're string sorting a date.
What are some ways I can quickly hack this to make it work while planning a later migration to a proper date data type?
You don't indicate what the underlying DBMS is, so you can probably do this in the SQL instead of a filter.
If you can't do that and the dataset isn't too large, you can convert the database date values to real dates and use them in the OnFilterRecord event:
procedure TForm3.Table1FilterRecord(DataSet: TDataSet; var Accept: Boolean);
var
TableDate: TDateTime;
begin
TableDate := StrToDate(Table1DateField.AsString);
Accept := (TableDate >= ADatePicker.Date) and
(TableDate <= EndDatePicker.Date);
end;
I think, you can still compare a date stored as a string or varchar, but this is certainly a bad practice because you will need to convert this strings into dates data type to be able to compare between them. If you have indexes defined on the column, they cannot be used anymore since the column will be converted and it will cause slow performance on large database.
An example on comparing dates (from the SQL standpoint) is like this:
SELECT *
FROM tableName
WHERE CONVERT(DATETIME, dateSTRColumn, XXX) > GETDATE()
where XXX is the current format of the date stored as string.
or in other terms:-
SELECT convert(varchar(20), dateSTRColumn, 121) FROM tableName
this should give you some ideas.
Otherwise, you might have to write a simple custom function yourself.
Or change database.
on delphi and adotables and .filter property you can code:
mytable.filter:='mydatefield=#2018-12-31#';
mytable.fittered:=true;

How best to present my Delphi database table to FastReport for lists and aggregates

I have spent several days so far laying the ground work to use FastReport in my Application. The Application stores device test result data in the form of a DBF file comprising several fixed fields(DeviceID, Passed etc) plus a variable number of result fields, each of which correspond to the type of measurement data available. There can be as few as one of these fields and as many as 100. Each field has a letter code name such as OV and RV. Total record counts can be from zero up to some 10's of thousands.
A specific report template will have already included in its design the field names that it will display. Missing fields will be empty on the report.
My question involves the best way of designing the report and the data supplied to the report so that the report construction is as simple as possible - I'm going to allow my users to generate their own reports - and I need two kinds of report output - list of results and aggregates. It is the aggregates that are giving me the headache. I need not only MIN, MAX, COUNT etc (as provided internally in FastReport) but Standard Deviation as well. Further, I would like to use the FastReport 'drill down' feature where you can click on a group header and the data table is revealed or hidden. My aggregates should ideally be in the header, not the footer so that they appear all the time.
I have found that SQL in a TQuery gives me a lot of flexibility since it provides the 'StDev' aggregrate (FastREport does not) but as far as I can see I would need a fixed TQuery for each of my fields. So far, the nicest solution that I can come up with involves using a filter on the main table for 'Passed' (so that the user can view passe, failed or all) and then to build a separate 'stats' table with the same field name columns, but with MIN, MAX, COUNT, MEAN, STDEV as individual records. I would then use a TfrxDBDataSet to expose this table to FastReport. I see that I can also use FastReport's own ADODatabase and ADOQuery to directly access my DBF file. This works well but again I did not want to expose this access layer to my user in the report if possible.
This just seems so messy when aggregate functions must be a fundamental database requirement. Am I missing a much easier way of doing this? I've worked my way through the (excellent) demos supplied with FastReport (professional) and I'm using XE2. I'm also aware of the useful functions in the MATH unit if I need to calculate StDev myself.
I would appreciate any guidance, thanks.
For anything you could calculate in code, lists of array values, aggregate or functional calculation results, I prefer to use the TfrxUserDataSet and implement the TfrxReport.OnGetvalue event.
Although it might initially be confusing, the user datasets simply declare a data set name, and the list of fields available through that data set name and use events which fire to let you "navigate" (first, next record) and declare when you've reached the end of your calculated data. This allows you to build a "generator" or else, just a normal virtual-data-provider set of logic for your calculations.
Here's what my OnGetValue events look like:
procedure TfrmReport.frxReportGetValue(const VarName: string; var Value: Variant);
begin
Value := GetReportValue(VarName);
end;
// INPUT: VarName = '(<GlobalArea."hdReportTitle">)'
// OUTPUT: tableName = 'GlobalArea', fieldName = 'hdReportTitle'
function ParseVar(const VarName:String; var tableName,fieldName:String; var ParenFlag:Boolean):Boolean;
var
paVarName:String;
angleBracketFlag:Boolean;
dotPos:Integer;
fieldQuoteFlag:Boolean;
procedure RemoveOuter(var str:String; initialChar,finalChar:Char; var flag);
var
n:Integer;
begin
n := Length(str);
if n>2 then begin
ParenFlag := (str[1]=initialChar) and (str[n]=finalChar);
if ParenFlag then begin
str := Copy(str,2,n-2);
end;
end;
end;
begin
result := false;
fieldQuoteFlag := false;
paVarName := SysUtils.Trim(VarName);
ParenFlag := false;
tableName := '';
fieldName := '';
RemoveOuter(paVarName, '(',')',parenFlag);
RemoveOuter(paVarName,'<','>',angleBracketFlag);
dotPos := Pos('.',paVarName);
if dotPos >0 then begin
tableName := Copy(paVarName,1,dotPos-1);
fieldName := Copy(paVarName,dotPos+1,Length(paVarName));
RemoveOuter(fieldName, '"','"',fieldQuoteFlag);
result := true;
end else begin
tableName := '';
fieldName := paVarName;
end;
end;
function TfrmProfitAnalysisReport.GetReportValue(const VarName:String):Variant;
var
tableName:String;
fieldName:String;
parenFlag:Boolean;
begin
ParseVar(VarName,tableName,fieldName,parenFlag);
result := NULL;
{ Global Area - Header Values }
if sameText(tableName,'GlobalArea') then begin
if fieldName='hdReportTitle' then
result := GetTitle; { A function that calculates a title for the report }
else if fieldName='hdReportSubtitle' then
result := 'Report for Customer XYZ'
else if fieldName='....' then begin
...
end;
if Variants.VarIsNull( result) then
result := '?'+fieldName+'?';
end;
Well, a lot of questions with a lot of possible answers:
1) About the datasets, I really recommend put them in your application (DataModule or Form) instead of using them inside the report. It will give you more flexibility;
2) You can have one query for each aggregation, but this will affect performance if your data tables grows in tons of records. Some alternatives:
2.1) calculate the values in your FastReport script, but this will also expose the logic to the report;
2.2) Iterate through the record on the Delphi code, and pass the results as variables to your report. Example:
frxReport.Variables['MIN'] := YourMinVariableOrMethod;
frxReport.Variables['MAX'] := YourMaxVariableOrMethod;
2.3) Using a ClientDataSet associated with your query and implement TAggregateFields on the ClientDataSet.
I, personally, prefer the 2.2 approach, with all logic in the Delphi code, which is simple and powerful.

Dbgrid calculation

I have a DBGrid and I´m trying to do a billing sheet but sometimes it doesn't do the calculations. How can I avoid that??
procedure TOrcamentos.DBGridEh1ColExit(Sender: TObject);
var
percent: double;
Unid: double;
tot: currency;
vaz: string;
begin
if Dorcamen_SUB.DataSet.State in [dsEdit, dsInsert] then
try
Dorcamen_SUB.DataSet.Post;
finally
vaz := DBGridEh1.Columns[3].Field.text;
if (vaz<> '') then
try
Torcamen_SUB.Edit;
Unid := (Torcamen_SUB.FieldByName('QT').AsFloat);
tot := (Torcamen_SUB.FieldByName('Precovenda').AsFloat);
percent := (Torcamen_SUB.FieldByName('Desconto').AsFloat);
try
tot := tot+(tot * percent)/ 100;
finally
Torcamen_SUB.FieldByName('Total').AsFloat := unid*tot;
Torcamen_SUB.Post;
Orcamentos.TotalExecute(self);
end;
except
end;
end;
end;
The better way to implement calculations is actually to move the calculation to your TTable component that the grid is linked to. The Total field shouldn't actually be a field in the database since but rather a calculated field based on values from other fields. Simply add an extra field using the field editor of the table, type in the field name Total, select the correct datatype and then select the field type as Calculated. Click Ok and then add code similar to this for the OnCalcField event of the table:
Torcamen_SUB.FieldByName('Total').AsFloat := Torcamen_SUB.FieldByName('QT').AsFloat * (
Torcamen_SUB.FieldByName('Precovenda').AsFloat + (Torcamen_SUB.FieldByName('Precovenda').AsFloat * Torcamen_SUB.FieldByName('Desconto').AsFloat)/100) ;
A general rule of thumb is that calculated values shouldn't be saved to the database unless really really necessary. It's best to simply add them as calculated fields to the dataset and then link the grid to the dataset. All calculated fields will then be displayed in the grid and each row will show the correct calculated value based on the values for that row.
I think you're mixing a business logic (like calculating a total) with User Interaction logic (like the event on which some grid column loses the focus) and that's the source of the erratic behavior of your application.
Looks like not even you know where it happens and where it doesn't happen.
Consider using the Field's events (for example, OnChange event) to perform that kind of calculations.
Lucky you if you're using a dataset with aggregation capabilities (like TClientDataSet), because you can just declare what you want in a TAggregateField and forget about doing calculations "by hand".
Not your question but... be careful with the way you're using try/finally also... for example, in this bit of code:
try
tot := tot+(tot * percent)/ 100;
finally
Torcamen_SUB.FieldByName('Total').AsFloat := unid*tot;
//other things
end;
be aware that if for some reason an exception occurs on the line between the try and finally clauses, the variable tot will have an undefined value (in this case, the result of the previous assignment), so the Assignment to the Torcamen_SUB.total field will be wrong, after all. I'm not sure if it is really what you want.

Resources