Getting coordinates of an ExcelRange object - delphi

In Excel's COM API:
Given an ExcelRange object, how would I determine which rows and columns are contained within it?
I do not want the contents of the range, just the "coordinates" of the range, preferably as integers.
I did notice that ExcelRange has both a Row and a Column property, however these only indicate the row and column of the upper left corner of the range.
Note: I am using Delphi, however this question could be relevant to any language using Excel though COM, so answering using Delphi is not necessary.

Assuming a simple rectangular range then you use the Rows and Columns properties of the ExcelRange object. The top-left of the selection is determined by Range.Row and Range.Column. The number of selected rows and columns is given by Range.Rows.Count and Range.Columns.Count.
In complete generality an Excel range can be made up of multiple non-contiguous areas. In this case you use the Areas property of ExcelRange to iterate through the simple rectangular ranges that make up the complex range.
To illustrate consider the following code:
procedure DescribeExcelRange(const Range: ExcelRange);
var
AreaIndex: Integer;
Area: ExcelRange;
begin
for AreaIndex := 1 to Range.Areas.Count do
begin
Area := Range.Areas[i];
Writeln(Format(
'Area %d: R%dC%d:R%dC%d',
[AreaIndex, Area.Row, Area.Column,
Area.Row+Area.Rows.Count-1, Area.Column+Area.Columns.Count-1]
));
end;
end;
I have not actually tested this code so I hope I have remembered correctly that indexing is 1-based.

Related

Trim the string data fetched from the table in delphi using ado dataset

I have got a table column of datatype char and by making use of ADO dataset I am fetching the data. But because of type char, it displays the data with trailing extra spaces. And the form on which this ADO dataset lies is inherited by many forms. I want to trim the extra spaces by making some change in the Delphi code on parent form by using some ADO dataset property
I have made use of FixedChar property of the data field by making it to false but that did not work
Select Cast(mycharcolumn as varchar(10)) as mynewcharcolumn from tablename
If you can change the query that retrieves the data, like I've shown here, then you effectively convert your char columns to varchar columns at the dataset level.
If you are unable to make changes to the query, then another way is to add a calculated column to the Tfield list of the dataset, and in the OnCalcFields event handler, you can Trim(mycharcolumn) to 'calculate' the value of the calculated field, which you then use in the code, in place of the original field.
You can also set the OnGetText event of the field and trim the text:
procedure TMyForm.AdoDataSet1GetText(Sender: TField; var Text: string; DisplayText: Boolean);
begin
Text := Trim(Text);
end;

Indirect Addresses in Array Formula

I have the following formula
=average(arrayformula(indirect(split(A1,","))))
Where A1 contains a list of cell addresses, such as E4,E6,E12. I expect this to be equivalent to =AVERAGE(E4,E6,E12), but this does not behave as expected, yielding 4 no matter what the data in the cells are. Preliminary research indicates that the INDIRECT() function doesn't pass through ARRAYFORMULA() correctly. Attempting SUM() on the outside yields precisely the same results.
Any ideas on how to average the values of cells obtained indirectly by a list of cell addresses?
I do have a list of columns and the row doesn't ever change for this average calculation, so I'm wondering if I could do some kind of subset instead, such as
=AVERAGE(RANGE){LIST_TO_SUBSET_BY}
I'm not sure about a built-in formula to do this so I've written a custom function to do it for you.
Go to Tools -> Script editor and replace the existing function with the code below and then save the project.
Now in your spreadsheet in any cell =CUSTOMFUNCTION(A1) where A1 contains a list of comma-separated cell references.
NOTE:
Updating values in the referenced cells won't force a recalculation of this formula, only updating cell A1 will.
I suggest you also go to File -> Spreadsheet settings -> Calculation and change 'Recalculation' to 'On change and every minute' that will force a recalculation of this function every minute.
/**
* Returns the average value of a dataset.
* #param {"A1"} cell The cell containing the list of cell references.
* #return The input repeated a specified nunmber of times.
* #customfunction
*/
function CUSTOMAVERAGE(cell){
var ss = SpreadsheetApp.getActiveSheet();
var array = [];
var cellRefs = cell.split(",");
for(var i in cellRefs){
array.push(ss.getRange(cellRefs[i]).getValue());
}
var sum = 0;
for(var i in array){
sum += array[i]
}
var avg = sum/array.length;
return avg;
}
Though this is a very specific application in response to this question, for the sake of the knowledge base, I'd like to show how this can be done without a script.
To give this context, imagine the LIST_CELL is a list of question numbers
(which are entered in as a header row, call the range QUESTIONS) on a test that correspond to certain standards, and the goal is to average only the questions that correspond to the standard next to which the list is written, and for each student. Using
=iferror(join(",",ArrayFormula(match(split(LIST_CELL,","),QUESTIONS,FALSE))),"")
The split function splits the a hand-entered list of questions on commas, the match function returns the column number of that particular question in QUESTIONS, and the join function joins the data back together. ArrayFormula allows the match to be performed on an array instead of just the first value.
Another single row heading lists the standards to which each question has been matched (possibly to more than one standard) by the comma separated list in LIST_CELL. For a column list of students in A:A, each standard needs to average the scores of every question that is listed next to the standard. This is accomplished by the nifty (if clunky):
average(ArrayFormula(hlookup(split(vlookup(LOOKUP_VAL,SEARCH_RANGE,COL_W_LIST),","),DATA_SOURCE,row(CURRENT_CELL))))
Breakdown from center outward:
LOOKUP_VAL is the value being looked up (the one that has multiple matches); in the example context, it's the standard.
SEARCH_RANGE is a range of cells containing both the list of lookup value (the standards in context) and the comma separated lists of column numbers generated by the first function. COL_W_LIST is the column number in the array SEARCH_RANGE that contains the list of row numbers matched from LIST_CELL.
Split takes the elements apart and placed them in a temporary array so that hlookup can be performed on each element. Via ArrayFormula the hlookup grabs each value on the same row in the appropriate QUESTIONS column - in context, it grabs the point scores for each question matched to the standard.
Finally, average is self-explanatory, and does take an array as input apparently.
These two functions in combination allow of use of indirect cell references in an array formula, and solves the much asked, "how do I include multiple matches in a calculation" question. At least in this specific context.
EDIT
There is an example "template" with this implemented here. You'll need to make your own copy to edit it.

How to compare 4 column values of TAdvStringGrid with one another and label them?

If i have colA, ColB, Colc, ColD and there are 1000 rows in each column in TAdvStringGrid . I would like check the number of double measurements of values in the 1000 rows in colA, ColB, Colc, ColD of TAdvStringGrid.
I am doing some thing like At first reading ColA, ColB, ColC, ColD values into multidimensional array and looping each element in multi dimensional array and comparing with each row element TAdvStringGrid and when found same using OnDrawcell function, I am labelling and displaying the row with a colour.
However it takes a lot of time. Is there a shorter way to do it. As the rows keep on increasing. Thanks for the answer in advance.
Every row is one measurement and one measurement consist of 4 values in ColA,B, C,D.
List : array of array of double;
SetLength (List,AdvStringGrid.RowCount,4);
for i := 0 to AdvStringGrid.RowCount -1 do begin
j:=0;
List[i,j] := strtofloat(AdvStringGrid.Cells[4,i+1]);
List[i,j+1] := strtofloat(AdvStringGrid.Cells[5,i+1]);
List[i,j+2] := strtofloat(AdvStringGrid.Cells[8,i+1]);
List[i,j+3] := strtofloat(AdvStringGrid.Cells[9,i+1]);
end;{for i}
How do i compare each element with neighbour and mark the duplicate??
Am I correct that every row is one meassurement? So one meassurement consist out of 4 values?
First thing is you shouldn't modify the visual StringGrid in in a loop. In the worst case the StringGrid invalidates and draws again after each action.
So it's good to read all data in an multidimention array.
To eliminate doubles i would sort everything and then compare neigbors. This is a pretty common pattern.
Define any order like ColA accending, then ColB accending, ColC accending and ColD accending and implenent a sort algorithm (like quicksort or mergesort).
After everything is sortet you can traverse the array from highes element to 0 and check if two neighbours are the same.
If you want to mark the double values instead of deleting them consider adding a 5th colum for a value when it is a duplicate.
After all the calcilation i would search for any Function like BeginUpdate() and Endupdate() to make sure that the StringGrid will only draw once.
Do all changes to StringGrid between the call of BeginUpdate() and Endupdate()
Update: your code could look something like this:
var
i:integer;
sortedList: array of array of double;
begin
setlength(List, 1000, 5); // use a fifth row for marking doublicates, set this value to 0
// Fill List like you mentioned here
sortedList = YourSortAlgorithm(List); // sort your List here
for i := high(sortedList) downto 0 do
begin
// compare if entries are duplicates
if sortedList[i,1] = sortedList[i-1,1] and sortedList[i,2] = sortedList[i-1,2] and sortedList[i,3] = sortedList[i-1,3] and sortedList[i,4] = sortedList[i-1,4] then
begin
sortedList[i-1,5] = 1; // 1 means duplicate, 0 means not duplicate
end;
end;
AdvStringGrid1.BeginUpdate;
// update your Stringgrid here
AdvStringGrid1.EndUpdate();
end;
Bye the way, instead of using a two dimentionaly array i would recoment to use a array of record.
Saying for example, that your ColA is a height, ColB is a length, ColC is a Temperature and ColD is a age you could define a record like this
type
TMeasurement = record
height: double;
length: double;
temperature: double;
age: double;
isBoolean: boolean;
end;
var
list: array of TMeasurement;
begin
//...
end;

calculated field-running total

I'm used to calculate the balance of the calculated field., But when I connect the dbgrid. Calculated by moving the scroll is wrong.
Please get help
var
Form1: TForm1;
i : Integer;
procedure TForm1.FormShow(Sender: TObject);
begin
i := 0;
DataSource1.DataSet := ADOTable1;
DBGrid1.DataSource := DataSource1;
end;
procedure TForm1.ADOTable1CalcFields(DataSet: TDataSet);
begin
i := (ADOTable1Debtor.AsInteger - ADOTable1creditor.AsInteger) + i;
ADOTable1Total.AsInteger := i;
end;
Now run the application and move the scroll in dbgrid column numbers (total) will change.
I'd like to know how to stop the change.
The calculated fields is designed to show value calculations at the row level and is not designed to make aggregations (calculations based on a set of rows).
For example, the database layer will fire the OnCalc event in no particular order and every time is needed to obtain the value of the field (for display pruposes, for example), since that value is not stored and may (and usually do) depend on the values of other fields.
In a set of 10 rows, you can get it called for rows 1, 2, 3, 4, 5 and then in 1 again...
You can use it, for example, in a line_total column which is the product of quantity and unit_price, but you can't use it to show e.g. the sum(line_total) of all the lines, as I infer you're trying to do in the shown code.
How to perform aggregations then?
You may want to link your DataSet to a ClientDataSet, which have AggregateFields, in which you can perform calculations like SUM(Quantity * Price) on the entire row-set or sub-sets based on a Index.
To learn more about AggregateFields read ClientDataSet Aggregates and GroupState by Cary Jensen in EDN.
What you are trying can't work, since any scroll will change the result.
Take a AdoDataset instead with following Commandtext
select ID, creditor, Debtor,(Select sum (Debtor-Creditor) from Table1 t where t.ID<=Table1.ID) as Total
from Table1 order by ID

How can create summary footer on runtime?

I use TcxGrid I have create fields on execution time because I have a pivot query and columns are variable
I filled my grid like theese codes
grdCevapDBTableView2.BeginUpdate;
grdCevapDBTableView2.ClearItems;
fillGridView(grdCevapDBTableView2,command);
grdCevapDBTableView2.DataController.CreateAllItems;
grdCevapDBTableView2.EndUpdate;
Now I want to get sum values from these columns. How can create summary footer on runtime?
Say for example you had a field called cost and you wanted to summarise the total:
index := grdCevapDBTableView2.GetColumnByFieldName('cost').index;
grdCevapDBTableView2.Columns[index].Summary.Footerkind:=skSum;
grdCevapDBTableView2.Columns[index].Summary.FooterFormat:='£ #.##';
I would also stick the beginupdate and endupdate between try..finally block, ie:
grdCevapDBTableView2.BeginUpdate;
try
grdCevapDBTableView2.ClearItems;
fillGridView(grdCevapDBTableView2,command);
grdCevapDBTableView2.DataController.CreateAllItems;
finally
grdCevapDBTableView2.EndUpdate;
end;
this just ensures that your tableview will eventually end the update and redraw.

Resources