How to select from DataTable with join to same table? - c#

I have DataTable object, which holds some "tree data structure". Data is not stored in any database, I just use DataTable to manipulate data without SQL server.
My data looks like this (indents are only for better reading here):
DataTable dtCategories = GetCategoriesAsDataTable();
id name parentId
int string int
----------------------
1 One 0
2 OneA 1
3 OneB 1
4 Two 0
5 TwoA 4
6 TwoB 4
7 TwoAA 5
8 TwoAB 5
So far - I was thinking about selecting first level with "where parentId = 0" and putting this to separate DataTable, like this:
DataTable dtFirstLevel = dtCategories.Select("[parentId] = 0");
// and after this - create DataTable for second level
// but I don't know how can I use "IN" clause here
DataTable dtSecondLevel = dtCategories.Select(?????????);
How can I select only first 2 levels of tree?
How can I select this without SQL server (by using only data objects)?

Maybe this helps:
var rows = table.AsEnumerable();
var parents = rows.Where(r => !r.Field<int?>("parentId").HasValue);
var children = rows.Where(r => r.Field<int?>("parentId").HasValue);
var secondLevel = from parent in parents
join child in children
on parent.Field<int>("id") equals child.Field<int?>("parentId").Value
select child;
var both = parents.Concat(secondLevel).CopyToDataTable();
Note that i've used Nullable<int> instead of 0 for a parent since that is more readable and less prone of errors. Here is your sample data:
var table = new DataTable();
table.Columns.Add("id", typeof(int));
table.Columns.Add("name", typeof(string));
table.Columns.Add("parentId", typeof(int));
table.Rows.Add(1, "One", (int?)null);
table.Rows.Add(2, "OneA", 1);
table.Rows.Add(3, "OneB", 1);
table.Rows.Add(4, "Two", (int?)null);
table.Rows.Add(5, "TwoA", 4);
table.Rows.Add(6, "TwoB", 4);
table.Rows.Add(7, "TwoAA", 5);
table.Rows.Add(8, "TwoAB", 5);
Result:
1 One
4 Two
2 OneA 1
3 OneB 1
5 TwoA 4
6 TwoB 4
Since you want to stay with 0 instead of int?:
var parents = rows.Where(r => r.Field<int>("parentId") == 0);
var children = rows.Where(r => r.Field<int>("parentId") != 0);
var secondLevel = from parent in parents
join child in children
on parent.Field<int>("id") equals child.Field<int>("parentId")
select child;

I think this function might help you figure out the level of tree of each entry so you can use it in your selection:
public int level(DataTable dt, DataRow row)
{
int parentid = int.Parse(row[2].ToString());
if (parentid == 0)
return 1;
else
return 1 + level(dt, GetDataRow(dt,parentid ));
}
public DataRow GetDataRow(DataTable dt, int id)
{
foreach (DataRow r in dt.Rows)
{
if (int.Parse(r[0].ToString()) == id) return r;
}
return null;
}

You have a couple of options to your problem. As proposed by #Ali, you could use recursion like this:
public int level(DataTable dt, DataRow row)
{
int parentid = int.Parse(row[2].ToString());
if (parentid == 0)
return 1;
else
return 1 + level(dt, GetDataRow(dt,parentid ));
}
public DataRow GetDataRow(DataTable dt, int id)
{
foreach (DataRow r in dt.Rows)
{
if (int.Parse(r[0].ToString()) == id) return r;
}
return null;
}
But the problem is that you'll end up iterating though every element and then using recursion on every iteration. If you have absolutely no data relationship between your columns and their level in the tree, besides a parentId, then this is your only solution.
On the other hand, if you do have a relationship, where you have name[level of tree] like Name[A] is tree level 1 and Name[AB] is tree level two with the right node, then iteration through each like:
foreach (DataRow r in dt.Rows)
{
//Pull out the element
//Check the element's level
//Add it to the result set if level <= 2
}
I'd personally prefer to solve the problem by actually building a tree structure or using a SQL WHERE clause, but it's hard to justify the time on it. Depending on where you get this data from, you may also be able to add an additional column which tells you which level the node is in depending on where it's inserted. If it has a grandparent (i.e. two parent nodes) you don't include it in the result set.

DataTable level1 = (from t in dtCategories.AsEnumerable()
where t.Field<int>("parentId") == 0
select t).CopyToDataTable();
DataTable level2 =(from t1 in dtCategories.AsEnumerable()
join t2 in dtCategories.AsEnumerable()
on t1.Field<int>("id") equals t2.Field<int>("parentId")
where t1.Field<int>("parentId") == 0
select t2).CopyToDataTable();

Another way to do it, this will give you a new object which contains the level and the row item itself. This will work for n number of levels...
var nodes = table.AsEnumerable();
//var nodes = new List<TreeNode>();
var parentId = 0;
var countLevel = 0;
var allNods = new List<dynamic>();
while (nodes.Any(p => p.Field<int>("parentId") == parentId))// && countLevel < 2)
// countlevel< 2 only to give you the first 2 levels only...
{
var nodesWithLevel = nodes.Where(p => p.Field<int>("parentId") == parentId)
.Select(p => new { Level = parentId, Node = p });
allNods = allNods.Concat<dynamic>(nodesWithLevel).ToList();
parentId++;
countLevel++;
}
The code currently expects that the root nodes have parentId = 0. Could be changed to null, too of cause...

Related

Count the number of occurrences of a string in a row

I have a combobox which contains different values:
public static DataTable GetStates()
{
DataTable myStates = new DataTable();
myStates.Columns.Add("Name", typeof(string));
myStates.Columns.Add("Location", typeof(string));
myStates.Rows.Add("1", "USA");
myStates.Rows.Add("2", "USA");
myStates.Rows.Add("3", "Canada");
return myStates;
}
I want this to be in BindStates function which will count the number of occurences of "USA" and so on.
public void BindStates(DataTable states)
{
int numberUsa = 0;
foreach (DataRow row in states.Rows)
{
if (row[1].ToString() == "USA")
{
numberUsa++;
}
}
Console.WriteLine(numberUsa.ToString());
}
You can do this by using LINQ against the datatable. So what I'm doing is creating an IGrouping that is grouped by the second object in the DataRow ItemArray. The second step is to just count how many items are in the "USA" Group.
// Get datatable with data
var states = GetStates();
// Create grouping of rows
var grouping = states.AsEnumerable().GroupBy(row => row.ItemArray[1]).ToList();
// Count how many rows are in the "USA" group
var numOfUSA = grouping.First(group => group.Key == "USA").Count();
Obviously this code can be improved with null checking etc, this is just to get you started. Good Luck!

Re-arrange rows of Datatable in runtime

I have multiple rows in a datatable, see a sample below:
Existing Table
Name Date Value Type
ABC(I) 11/11/2013 12.36 I
DEF(I) 11/11/2013 1 I
GHI(I) -do- -do- I
JKL(P) P
MNO(P) P
PQR(D) D
STU(D) -d0- -do- D
Required Table
Name Date Value Type
JKL(P) P
MNO(P) P
PQR(D) D
STU(D) -d0- -do- D
ABC(I) 11/11/2013 12.36 I
DEF(I) 11/11/2013 1 I
GHI(I) -do- -do- I
COndition to use
Sorting should be as per the column Type. Now I need a small change in order of the rows to be shown in the gridview. That is rows of Payment will come first then all Dues and at last all Interests types will come.
What I tried:
Sorting of column but it was not what I need.
Custom Grouping suggested by Tim Schmelter here
Code was:
public DataTable GroupBy(string i_sGroupByColumn, string i_sAggregateColumn, DataTable i_dSourceTable)
{
DataView dv = new DataView(i_dSourceTable);
//getting distinct values for group column
DataTable dtGroup = dv.ToTable(true, new string[] { i_sGroupByColumn });
//adding column for the row count
dtGroup.Columns.Add("Count", typeof(int));
//looping thru distinct values for the group, counting
foreach (DataRow dr in dtGroup.Rows) {
dr["Count"] = i_dSourceTable.Compute("Count(" + i_sAggregateColumn + ")", i_sGroupByColumn + " = '" + dr[i_sGroupByColumn] + "'");
}
//returning grouped/counted result
return dtGroup;
}
I dont know where and what I am lacking/missing. Kindly help.
try linq to order your table:
var query = dtGroup.AsEnumerable()
.OrderBy(c=> c.Field<DateTime?>("Date"))
.ThenByDescending(c=> c.Field<string>("Name"));
DataView dv2 = query.AsDataView();
If I understand correctly you want first sorting on P, D, I and then on date
Dictionary<string, int> sortDictionary = new Dictionary<string, int>();
sortDictionary.Add("P", 1);
sortDictionary.Add("D", 2);
sortDictionary.Add("I", 3);
var q = from row in dtGroup.AsEnumerable()
let type = sortDictionary[row.Field<string>("Name").Substring(4, 1)]
orderby type, row.Field<string>("Name")
select row;
foreach (var r in q)
{
string x = r["Name"].ToString() + r["Date"].ToString();
}

Recursive self join

I have table as below. I need to get all the manager id's for the user id provided.
userid managerid
10 1
9 10
6 9
2 6
4 1
If i pass 2 to my method I need to get 1,10,9 and 6. I have written the below query which will return only first level parent. I.e it will return only 6 & 9.
public List<int?> mymethod (int userId){
return (from e in mycontext.EmployeeManagers
join e1 in m_context.EmployeeManagers
on e.UserId equals e1.ManagerId
where e1.UserId == userId
select e1.ManagerId).AsQueryable().ToList()
}
How can I modify the query to return all the manager hirerachy?
please help.
You can not do this in a sinqle LINQ expression. You have to run this in a loop.
A better option is to do this in the database and then return the results to LINQ.
See:
Hierarchical data in Linq - options and performance
Fill a Recursive Data Structure from a Self-Referential Database Table
Linq-to-Sql: recursively get children
I would simply run a short loop like this (sorry for invalid capitalization, coded from scratch):
public List<Int> GetAllManagers(int userID)
{
var result = new List<int>();
int index = 0;
result.add(userID); // start with user (it will be removed later)
while (index < result.count)
{
var moreData = from e in mycontext.EmployeeManagers
where e.UserId == result[index];
select e.ManagerId;
foreach (int id in moreData)
if (result.indexOf(id)==-1)
result.add(id);
index++;
}
result.delete(0);
return result;
}
or recursevly
private void AddUniqueIds (List<int> elements, ref List<int> list)
{
foreach (int id in elements)
if (list.indexOf(id)==-1)
list.add(id);
}
public List<int> GetAllManagers(int userID)
{
var result = new List<int>();
var moreData = from e in mycontext.EmployeeManagers
where e.UserId == result[index];
select e.ManagerId;
foreach (int id in moreData)
AddUniqueIds(result, GetAllManagers(id));
return result;
}
You need to use different pattern.
Lets see that you get your query.
var query = myContext.EmployeeManagers
Then, you could join it however you want to
for(int = 0; i < 5; i++)
{
query = query.Join( ... ..., i, ... ); // Can't recall all
// the parameters right now.
}
And then just execute it:
var result = query.ToList();

Using LINQ to find three or more matching records

First, I'll describe my table structure.
I have table, with 2 columns (ID and Root). This table is converted to a List of Nodes where the simple node structure is:
struct Node
{
public int id;
public int root;
}
I need to find all entries in this List where there's 3 or more roots equals.
Example:
struct TeleDBData
{
public int ID;
public int? RootID;
}
private void InitList()
{
var eqList = new List<TeleDBData>();
TeleDBData root = new TeleDBData();
root.ID = 1;
TeleDBData node1 = new TeleDBData();
node1.ID = 2;
node1.RootID = 1;
TeleDBData node2 = new TeleDBData();
node2.ID = 3;
node2.RootID = 1;
TeleDBData node3 = new TeleDBData();
node3.ID = 4;
node3.RootID = 1;
TeleDBData node4 = new TeleDBData();
node4.ID = 5;
node4.RootID = 2;
eqList.Add(root);
eqList.Add(node1);
eqList.Add(node2);
eqList.Add(node3);
eqList.Add(node4);
}
After running the query, it will return node1, node2 and node3.
How can I find them using LINQ?
You just need to GroupBy accordingly:
var groups = eqList.GroupBy(n => n.RootID).Where(g => g.Count() >= 3);
foreach (var g in groups) {
Console.Out.WriteLine("There are {0} nodes which share RootId = {1}",
g.Count(), g.Key);
foreach (var node in g) {
Console.Out.WriteLine(" node id = " + node.ID);
}
}
See it in action.
Additional info:
In the code above, g is an IGrouping<int?, TeleDBData> so, by the documentation page definition, it's a collection of TeleDBData items that share a common key (which is an int?). groups is an IEnumerable<IGrouping<int?, TeleDBData>>, all of this is standard procedure for the Enumerable.GroupBy method.
The two things you would want to do with an IGrouping<,> is access its Key property to find the key and enumerate over it to process the grouped elements. We 're doing both of this in the above code.
As for the n in the GroupBy lambda, it simply represents each one of the items in eqList in turn; it follows that its type is TeleDBData. I picked n as the parameter name as an abbreviation of "node".

how to access columns by index in LINQ

I have a table like this in my Linq to sql class :
ID CL1 CL2 CL3 ... CL20
-- ---- ---- ----- ------
1 12 35 54 .... 44
2 11 35 78 ..... 75
data is not important in this example.
I need to access to each column with their index.
for example to reach data in CL3 like this:
var x = db.myTable.single(a=>a.ID==1)[3];
can anyone help me please?
You could convert your result to a DataTable like this
public static DataTable ConvertToDataTable<T>(IList<T> list)
{
var dt = new DataTable();
var properties = typeof(T).GetProperties();
foreach (var pi in properties)
dt.Columns.Add(pi.Name, pi.PropertyType);
foreach (T element in list) {
var row = dt.NewRow();
foreach (var pi in properties)
row[pi.Name] = pi.GetValue(element, null);
dt.Rows.Add(row);
}
return dt;
}
and then you can access the columns by name or by index.
var dt = ConvertToDataTable<test>(list);
var CL5 = dt.Rows[0][5];
var CL5_by_name = dt.Rows[1]["CL5"];
Properties in the object are not necessarily in the same order the columns are in the database.
You could do reflection to select a property by index but that wouldn't make sense. You should use names of the columns instead.
Based on your comment that the columns have name ending with a digit here is what you can do.
int columnIndex = 3;
var property = (from p in db.myTable.GetType().GetProperties()
where p.Name.EndsWith(columnIndex.ToString())
select p).First();
var record = db.myTable.single(a=>a.ID==1);
var x = property.GetValue(record, null)

Categories