Does anyone know how to make a distinct count in fastreport?
Example
I have the report:
Name sex
João m
João m
Maria f
In the normal count, the result would be 3, but I want one that takes only the number of rows that do not repeat the field name .
In this case, the result would be 2.
Can anyone help me? That's just an example.
I can not do a group by in SQL because I have several fields.
I'm not skilled in using FastReport but I've found this page on the FastReport's official forum.
I think you can change the example by adapting it to your scenario (Note that the syntax could require some adjustments).
Bands:
GroupHeader1 <Sex>
MasterData1 [Name, Sex, ...]
GroupFooter1 [GetDistinctCount]
Script (Only working with dataset sorted by the field to count):
var
LastValue : string;
DistinctCount : integer;
//create this event by double-clicking the event from the Object Inspector
procedure OnGroupHeader1.OnBeforePrint;
begin
if LastValue <> (<Datasetname."Sex">) then
Inc(DinstinctCount);
LastValue := <Datasetname."Sex">
end;
function GetDistinctCount: string;
begin
Result := IntToStr(DistinctCount);
end;
The base idea is that the DistinctCount variable is incremented each time the field value changes.
Script (Should works also with unsorted dataset):
var
FoundValues : array of string;
(* !!IMPORTANT!!
You need to initialize FoundValues array before to start counting: *)
SetLength(FoundValues, 0);
function IndexOf(AArray : array of string; const AValue : string) : integer;
begin
Result := 0;
while(Result < Length(AArray)) do
begin
if(AArray[Result] = AValue) then
Exit;
Inc(Result);
end;
Result := -1;
end;
//create this event by double-clicking the event from the Object Inspector
procedure OnGroupHeader1.OnBeforePrint;
begin
if(IndexOf(FoundValues, <Datasetname."Sex">) = -1) then
begin
SetLength(FoundValues, Length(FoundValues) + 1);
FoundValues[Length(FoundValues) - 1] := <Datasetname."Sex">;
end;
end;
function GetDistinctCount: string;
begin
Result := IntToStr(Length(FoundValues));
end;
The base idea is that each different value found is added to the FoundValues array.
You can do it in Firebird without GROUP BY as:
DECLARE #T TABLE (ID INT IDENTITY (1,1), Name NVARCHAR(25) , Sex CHAR(1));
INSERT INTO #T VALUES
('Sami','M'),
('Sami','M'),
('Maria','F');
SELECT DISTINCT Name , Sex FROM #T
You can also create a View , and then use it in your report.
If you really need to do that in FastReport, you have to use a GroupHeader and a GroupFooter to that.
How ?
You have to write your script in OnBeforePrint event.
procedure OnGroupHeader1.OnBeforePrint;
Create this one by double-clicking in the event in the object inspector.
Related
In cxGrid,I have a column that is boolean (properties : checkbox).
How can I do a footer summary (SUM) of such a column i.e to sum how many records are checked.
Right now, if I set it to SUM, my footer summary displays negative numbers for the items checked.How can I avoid these negative numbers?
edit :
I have found a would be solution on their site with :
procedure TForm1.cxGrid1DBTableView1DataControllerSummaryFooterSummaryItemsSummary(
ASender: TcxDataSummaryItems; Arguments: TcxSummaryEventArguments;
var OutArguments: TcxSummaryEventOutArguments);
var
si: TcxGridDBTableSummaryItem;
begin
si := Arguments.SummaryItem as TcxGridDBTableSummaryItem;
if si.Column = cxGrid1DBTableView1Sonda then
OutArguments.Done := not OutArguments.Value;
end;
However I am getting the error :
Could not convert variant of type (Null) into type (Boolean).
Dont understand this. Field is boolean type (bit).
edit2:
The problem is that sql server by default sets boolean type to NULL.
That is why the conversion error.
You can also just set your grid to calculate that summary using a different field , for example a calculated field where you assign the exact value you want to add each time.
Add a calculated field to your dataset, with the desired value.
MyHiddenField.Value := -1 * YourCheckingField.AsInteger;
Go to the Summaries Tab on the CxGrid dialog, and add a new Summary:
Set the Column property to the Grid Column where you want it to appear
Set the FieldName to your calculated field
And finally set Kind to skSum
It is better to send such questions to DevExpress support team.
You can customize footer:
assign Kind=skNone to footer summary item
use OnGetText event to show what you want
Quick example (shows number of chars in all records as footer value):
procedure TForm54.cxGrid1DBTableView1TcxGridDBDataControllerTcxDataSummaryFooterSummaryItems0GetText(
Sender: TcxDataSummaryItem; const AValue: Variant; AIsFooter: Boolean;
var AText: string);
var i,j: integer;
begin
j := 0;
for i := 0 to cxGrid1DBTableView1.DataController.RecordCount-1 do
j := j + Length(String(cxGrid1DBTableView1.DataController.Values[i, cxGrid1DBTableView1c.Index]));
AText := IntToStr(j);
end;
I think that you can resolve that problem in SQL component. Use typecasting and your cxGrid will work with Integer values.
SELECT CAS(Bit_Column AS int) AS Int_Column
FROM YourTable
You can try this :
procedure TForm1.cxGrid1DBTableView1DataControllerSummaryAfterSummary(
ASender: TcxDataSummary);
var i, j, Imp:integer;
Item: TcxGridDBTableSummaryItem;
begin
DBTableView1.DataController.BeginLocate;
for j:=0 to ASender.FooterSummaryItems.Count-1 do ASender.FooterSummaryValues[j]:=0;
try
for i:=0 to DBTableView1.DataController.RowCount-1 do
begin
if (DBTableView1.DataController.Values[i,cxGrid1DBTableView1Sonda.Index]<>null) then
for j:=0 to ASender.FooterSummaryItems.Count-1 do
begin
Item:=TcxGridDBTableSummaryItem(ASender.FooterSummaryItems[j]);
Imp:= DBTableView1.DataController.Values[i, cxGrid1DBTableView1Sonda.Index];
if (Imp= 1) or ((Imp= 2)
then ASender.FooterSummaryValues[j]:= ASender.FooterSummaryValues[j]+1;
end;
end;
finally
DBTableView1.DataController.EndLocate;
end;
end;
procedure cxGrid1DBTableView1DataControllerSummaryFooterSummaryItemsSummary(
ASender: TcxDataSummaryItems; Arguments: TcxSummaryEventArguments;
var OutArguments: TcxSummaryEventOutArguments);
var
AValue: Variant;
AItem: TcxGridTableSummaryItem;
begin
AItem := TcxGridTableSummaryItem(Arguments.SummaryItem);
// считаем проверенные
if (AItem.Column = tvCompareisCorrect) and (AItem.Kind = skCount) and (AItem.Position = spFooter) then begin
AValue := tvCompare.DataController.Values[ Arguments.RecordIndex, tvCompareisCorrect.Index];
if not VarIsNull(AValue) then
if not VarAsType(AValue, varBoolean) then Dec(OutArguments.CountValue);
end;
How can I access a TDBGrid column by name instead of Index?
For example, now I use:
grdInvoiceItems.Columns[2].Visible := False;
but it would be much better to write something like:
grdInvoiceItems.Columns['UnitPrice'].Visible := False;
In the mean time I use a for cycle like in:
for idx := 0 to grdInvoiceItems.Columns.Count - 1 do
begin
if (
(grdInvoiceItems.Columns[idx].FieldName = 'UnitPrice') or
(grdInvoiceItems.Columns[idx].FieldName = 'Discount') or
(grdInvoiceItems.Columns[idx].FieldName = 'SecretCode')
) then
grdInvoiceItems.Columns[idx].Visible := False;
end;
Using colum name is IMO much better tham column index since index is subject to change more often than name.
Any idea on how to encapsulate it better?
You could try something like this:
function ColumnByName(Grid : TDBGrid; const AName : String) : TColumn;
var
i : Integer;
begin
Result := Nil;
for i := 0 to Grid.Columns.Count - 1 do begin
if (Grid.Columns[i].Field <> Nil) and (CompareText(Grid.Columns[i].FieldName, AName) = 0) then begin
Result := Grid.Columns[i];
exit;
end;
end;
end;
Of course, if you are using a version of Delphi which is recent enough to support Class Helpers, you could wrap this function into a Class Helper for TDBGrid, like this
type
TGridHelper = class helper for TDBGrid
function ColumnByName(const AName : String) : TColumn;
end;
[...]
function TGridHelper.ColumnByName(const AName: String): TColumn;
var
i : Integer;
begin
Result := Nil;
for i := 0 to Columns.Count - 1 do begin
if (Columns[i].Field <> Nil) and (CompareText(Columns[i].FieldName, AName) = 0) then begin
Result := Columns[i];
exit;
end;
end;
end;
Then, you could do this
Col := DBGrid1.ColumnByName('SomeName');
Obviously, you could write a similar function which searches by the column's title, rather than the associated Field's FieldName.
You could create a mapping between column name and grid index e.g. as a dictionary and use that. Note that not every column in a dataset is necessarily visible in a dbgrid. In addition there might be calculated fields in the dataset, so don't forget these. The safest way to create the mapping would be to iterate trough the columns of the dbgrid and store their field names together with the column index. This way you won't get any invalid entries and any field that's not in the mapping does not have a dbgrid column.
I wanted to to the similar thing, but i was thinking if the dbgrid doesn't know the colmuns by name (for this case), maybe one else (speaking of components) does know it allready.
In my case i use a fdquery -> Datasource -> DBgrid connection.
The FDQuery knows the fields by name and by id.
So considering you use similar components you can do
dbgrid1.Columns[fdquery1.FieldByName('UnitPrice').Index].visible:=false;
I have a string grid, from which i can delete columns. I defined a CustomStringGrid type that allows me to use DeleteColumn method.
This is how it looks:
TCustomStringGrid = class(TStringGrid)
[...]
With tCustomStringGrid(mygrid) do
DeleteColumn(col)
end;
IS there something similar to add a column? I've tried InsertColumn but it doesn't seem to exist. I want to add a column at a particular position. In fact, if a user deletes a column i have an undo button which i want to reinsert the deleted column (i'm keeping the data in an array so i can recreate the column but i don't know how to insert one in a particular position).
Thank you!
It's not built in but easy to emulate, with ColCount = ColCount + 1 and MoveColumn from a HackClass.
type
THackGrid=Class(Grids.TCustomGrid)
End;
Procedure InsertColumn(G:TStringGrid;Position:Integer);
begin
if Position<G.ColCount then
begin
G.ColCount := G.ColCount + 1;
THackGrid(g).MoveColumn(G.ColCount - 1,Position);
end;
end;
procedure TMyForm.Button1Click(Sender: TObject);
begin
InsertColumn(StringGrid1,1);
end;
THack grid is not working, maybe it is ok when both cols are visible, but that works always :
Procedure MoveColumn(G:TStringGrid;OldPosition : integer;NewPosition:Integer);
var
i : integer;
temp : string;
begin
for i := 0 to g.rowcount - 1 do
begin
temp := g.cells[OldPosition,i];
g.cells[OldPosition,i] := g.cells[NewPosition,i];
g.cells[NewPosition,i] := temp;
end;
end;
I have just an adoquery and when i try
adoquery1.sort := 'Cost';
it does not sort the items in the query.
{Gets starting cards and put them into the correct rows}
//***************************************************************************
procedure TFGame.GetStartingCards;
//***************************************************************************
const
ManaTypes : array [0..3] of string = ('Lava','Water','Dark','Nature');
var
i: integer;
z:integer;
Cards: TObjectList<Tcard>;
begin
Cards := TObjectList<TCard>.Create(false);
z:=0;
{add all tcards (Desgin ) to this list in order Lava,water,dark,nature }
cards.Add(cardLava1);
cards.Add(cardlava2);
cards.Add(cardlava3);
for i := 0 to Length(ManaTypes) - 1 do
begin
with adoquery1 do
begin
close;
sql.Clear;
sql.Add('SELECT TOP 4 * FROM Cards WHERE Color = "'+ManaTypes[i]+'" ORDER BY Rnd(-(1000*ID)*Time())');
open;
end;
{return the result of everything for giving mana type.. }
if adoquery1.RecordCount = 0 then
Showmessage('Error no cards in db');
adoquery1.Sort := 'Cost';
adoquery1.First;
while not adoquery1.Eof do
begin
cards[z].Ccost := adoquery1.Fieldbyname('Cost').AsInteger;
//based on color change background
cards[z].Background.LoadFromFile(format('%s\pics\%s.png',[maindir,cards[z].Ccolor]));
adoquery1.Next;
cards[z].repaint;
z:=z+1;
end;
end;
cards.Free;
end;
Adoquery.Sort should work if you set CursorLocation to clUseClient.
The alternative could be changing your query to:
Select * from
(
SELECT TOP 4 * FROM Cards WHERE Color = "'+ManaTypes[i]+'" ORDER BY Rnd(-(1000*ID)*Time())
) x
ORDER by Cost
which will select 4 random rows and sort these by Cost.
EDIT
as follow up to #kobik 's comment:
If you are already using clUseClient and your sort does not seem to work you will have to make sure the sorting can be interpreted in your intent. If you are using an Wide(String)field it will be sorted as any string (10,8,9). You might either change the field type to int or float, or add an casted field to you query for sorting purpose (CINT(TextFiled) as IntForSort ,CDBL(Textfield) as FloatForSort for Access).
Since this might lead to converting errors if the content of the field can not be casted, so I'd recommend to use the intended field type on design of the table.
i have this problem: starting from an empty list (0 elements) i want check if an element is present or not present in this list. In case this record not is present in list then i add this record to list, otherwise update element in list.
I have tried writing this code:
program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils, System.Generics.Collections, System.Generics.Defaults;
type
TDBStats = record
Comb: Integer;
Freq: Integer;
end;
TDBStatsList = TList<TDBStats>;
procedure Add(ODBStats: TDBStatsList; const Item: TDBStats);
var
rItem: TDBStats;
begin
rItem := Item;
rItem.Freq := 1;
oDBStats.Add(rItem);
end;
procedure Update(ODBStats: TDBStatsList; const Item: TDBStats; const Index: Integer);
var
rItem: TDBStats;
begin
rItem := Item;
Inc(rItem.Freq);
oDBStats[Index] := rItem;
end;
var
oDBStats: TDBStatsList;
rDBStats: TDBStats;
myArr: array [0..4] of integer;
iIndex1: Integer;
begin
try
myArr[0] := 10;
myArr[1] := 20;
myArr[2] := 30;
myArr[3] := 40;
myArr[4] := 10;
oDBStats := TList<TDBStats>.Create;
try
for iIndex1 := 0 to 4 do
begin
rDBStats.Comb := myArr[iIndex1];
if oDBStats.Contains(rDBStats) then
Update(oDBStats, rDBStats, oDBStats.IndexOf(rDBStats))
else
Add(oDBStats, rDBStats);
end;
// Check List
for iIndex1 := 0 to Pred(oDBStats.Count) do
Writeln(oDBStats[iIndex1].Comb:3, oDBStats[iIndex1].Freq:10);
finally
oDBStats.Free;
end;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
Readln;
end.
and should return this result:
10 2
20 1
30 1
40 1
50 1
but return this result:
10 1
20 1
30 1
40 1
50 1
10 1
I have understood about problem: when i use oDBStats.Contains(rDBStats) it control if rDBStats element is contained in list; the first time not found it and add in list; but when it is added in list i update freq field to 1; so second time when i check again being rdbstats with freq = 0 not found it.
As i can solve this problem? I need to have a counter, where i get from input a "comb" and i want check if this "comb" is present in list, indipendetely from value of the other field of the record. In case i find "comb" in list, then i update, increasing freq field.
Thanks for help.
When you call Contains on a generic list, it looks if the given value is already inside the list. The value in your case is a record which consists of two fields. As you didn't specify a custom comparer, Delphi will use a default comparer which in case of a record does a binary compare. So only when two records are binary equal they will be treated as equal.
To make your example work you have to specify a custom comparer that compares only the comb field of the records. This is an example:
oDBStats := TList<TDBStats>.Create(TDelegatedComparer<TDBStats>.Create(
function(const Left, Right: TDBStats): Integer
begin
result := CompareValue(Left.comb, Right.comb);
end));
In addition you have an error in your update routine. Instead of incrementing the existing value, you are incrementing the undefined value of the item parameter. The change in the first line should make it work:
rItem := oDBStats[Index];
Inc(rItem.Freq);
oDBStats[Index] := rItem;
You have the wrong data structure since what you really need is a dictionary.
The fundamental problem with using a list is that you want to search on a subset of the stored record. But lists are not set up for that. Solve the problem by re-writing using TDictionary<Integer, Integer>.
I can recommend that you have a thorough read of the dictionary code example at the Embarcadero docwiki.
The key to the dictionary is what you call comb and the value is freq. To add an item you do this:
if Dict.TryGetValue(Comb, Freq) then
Dict[Comb] := Freq+1
else
Dict.Add(Comb, 1);
I'm assuming your dictionary is declared like this:
var
Dict: TDictionary<Integer, Integer>;
and created like this:
Dict := TDictionary<Integer, Integer>;
You can enumerate the dictionary with a simple for in loop.
var
Item: TPair<Integer, Integer>;
...
for Item in Dict do
Writeln(Item.Key:3, Item.Value:10);
Although be warned that the dictionary will enumerate in an odd order. You may wish to sort before printing.
If you wish to store more information associated with each entry in the dictionary then put the additional fields in a record.
type
TDictValue = record
Freq: Integer;
Field1: string;
Field2: TDateTime;
//etc.
end;
Then your dictionary becomes TDictionary<Integer, TDictValue>.