Left join and count based on multiple values using linq c# - c#

I have 2 lists. List A consists of this value,
status | level
-----------------
open | low
open | medium
open | high
closed | low
closed | medium
closed | high
List B consists of this value,
task | status | level
------------------------
A | open | low
B | open | medium
C | closed | high
D | closed | low
E | open | low
I want to do left join (all value inside list A must be in the new list), and count the number of tasks with related to status. I want the level value as well since it will be used later in my code. The expected output:
status | level | count
-------------------------
open | low | 2
open | medium | 1
open | high | 0
closed | low | 1
closed | medium | 0
closed | high | 1
I know there are many answers here which provides the ways to code, but I'm still stuck, because my code doesnt work, it seems like it does not do the group by method because when I count, the value shown is one for all status.
var joined3 = (from id1 in joined
join id2 in tr
on new { lev = id1.Key.ToString(), stat = id1.Value.ToString() } equals new { lev = id2.Level.ToString(), stat = id2.Status.ToString() } into grouped
from id2 in grouped.DefaultIfEmpty()
group id2 by new {level = id1.Key, status = id1.Value } into grouped
select new
{
level = grouped.Key.level,
status = grouped.Key.status,
count = grouped.Count()
}).ToList();

The problem is that because of the left-join semantics of DefaultIfEmpty(), you always have at least one row. So you need to add a predicate to the Count()
var joined3 = (
from id1 in joined
join id2 in tr
on new { lev = id1.Key, stat = id1.Value } equals new { lev = id2.Level, stat = id2.Status } into grouped
from id2 in grouped.DefaultIfEmpty()
group id2 by new {level = id1.Key, status = id1.Value } into grouped
select new
{
level = grouped.Key.level,
status = grouped.Key.status,
count = grouped.Count(id2 => id2.Key != null)
}).ToList();
Alternatively, a simpler method is: don't group, but instead use a correlated count of the other list
var joined3 = (
from id1 in joined
select new
{
level = id1.level,
status = id1.status,
count = tr.Count(id2 => id2.Key == id1.Key && id2.Value == id1.Value)
}).ToList();
I see no reason to use ToString here, and it is likely to impact performance. Key and Value should be the same type on each list/table respectively.

var list1 = new List<Type1>
{
new Type1() {Status = StatusEnum.Open, Level = LevelEnum.Low},
new Type1() {Status = StatusEnum.Open, Level = LevelEnum.Medium},
new Type1() {Status = StatusEnum.Open, Level = LevelEnum.High},
new Type1() {Status = StatusEnum.Closed, Level = LevelEnum.Low},
new Type1() {Status = StatusEnum.Closed, Level = LevelEnum.Medium},
new Type1() {Status = StatusEnum.Closed, Level = LevelEnum.High}
};
var list2 = new List<Type2>
{
new Type2() {TaskDescription = "A", Status = StatusEnum.Open, Level = LevelEnum.Low},
new Type2() {TaskDescription = "B", Status = StatusEnum.Open, Level = LevelEnum.Medium},
new Type2() {TaskDescription = "C", Status = StatusEnum.Closed, Level = LevelEnum.High},
new Type2() {TaskDescription = "D", Status = StatusEnum.Closed, Level = LevelEnum.Low},
new Type2() {TaskDescription = "E", Status = StatusEnum.Open, Level = LevelEnum.Low}
};
var list3 = new List<Type3>();
foreach (var t in list1)
{
list3.Add(new Type3()
{Level = t.Level, Status = t.Status, Count = list2.Count(x => x.Level == t.Level && x.Status == t.Status)});
}
foreach (var t in list3)
{
Console.WriteLine($"{t.Status}/{t.Level}/{t.Count}");
}
class Type1
{
public StatusEnum Status { get; set; }
public LevelEnum Level { get; set; }
}
class Type2 : Type1
{
public string TaskDescription { get; set; }
}
class Type3 : Type2
{
public int Count { get; set; }
}
public enum StatusEnum
{
Open,
Closed
}
public enum LevelEnum
{
Low,
Medium,
High
}

Related

Linq query to group by based on special condition

My table is:
id | globalId | taskStatus |
1 | 10 | New |
2 | 11 | New |
3 | 10 | InProgress |
4 | 12 | New |
I would like to have a linq query that returns me result with row 2.
Conditions to check in query
Want to ignore those records which have same globalId and if task status of any record is InProgress. So in this case as record with 1, 3 have same global id 10 but task status of record with id 3 is InProgress, so don't want any of the two records.
Also a check in where condition Id < 4
I have tried the below query
var result = (from meetings in db.Meetings
join taskStatus in db.TaskStatus on meeting.TaskStatusId equals taskStatus.TaskStatusId
where (taskStatus.Name == InternalTaskStatus.New || taskStatus.Name == InternalTaskStatus.ToBePlannedInFuture || taskStatus.Name == InternalTaskStatus.Failed)
&& meeting.CalendarEvent != CalendarEvents.Delete
&& meeting.StartDateTime >= planningPeriodStartDate && meeting.EndDateTime <= planningPeriodEndDate
group meeting by meeting.GlobalAppointmentId into m
select new
{
MeetingResult = m.FirstOrDefault()
}).FirstOrDefault();
In the above query I have added check for task status, want only records with taskStatus-New,Failed,ToBePlannedInFuture. But here I am getting wrong result in this case as per above table I am getting result with id 1.
The ideal way to approach this is to, split the requirement.
Requirement 1 : Ignore Items where id < 4
var step1 = testList.Where(x=>x.id<4);
Requirement 2: Ignore Groups of items with same globalId is same, and none of the elements in group has status in "InProgress"
var step2 = step1.GroupBy(x=>x.globalId)
.Where(x=>!x.Any(c=>c.taskStatus.Equals("InProgress")));
Now you need to flatten the group to get the result as IEnumerabble
var step3= step2.SelectMany(x=>x);
Putting it all together
var result = testList.Where(x=>x.id<4).GroupBy(x=>x.globalId)
.Where(x=>!x.Any(c=>c.taskStatus.Equals("InProgress")))
.SelectMany(x=>x);
public class test
{
public int id { get; set; }
public int globalId { get; set; }
public string taskStatus { get; set; }
}
public void SampleName()
{
List<test> testList = new List<test>()
{
new test() { id = 1, globalId = 10, taskStatus = "New"},
new test() { id = 2 , globalId = 11 , taskStatus = "New"},
new test() { id = 3 , globalId = 10 , taskStatus = "InProgress"},
new test() { id = 4 , globalId = 12 , taskStatus = "New"}
};
var result = testList.Where(q => testList.Count(a => a.globalId == q.globalId) == 1 && q.taskStatus != "InProgress" && q.id < 4).ToList();
}

Creating a Nested List from a Flat List in C#

I currently have the following classes:
public class NavigationItem
{
public int ID { get; set; }
public string Title { get; set; }
public int ParentID { get; set; }
public List<NavigationItem> Children { get; set; }
}
public class FlatItem
{
public int ID { get; set; }
public string Title { get; set; }
public int ParentID { get; set; }
}
I have a sample data as follows:
+====+============+==========+
| ID | Title | ParentID |
+====+============+==========+
| 1 | Google | |
+----+------------+----------+
| 2 | Microsoft | |
+----+------------+----------+
| 3 | Oracle | |
+----+------------+----------+
| 4 | Gmail | 1 |
+----+------------+----------+
| 5 | Sheets | 1 |
+----+------------+----------+
| 6 | Adsense | 1 |
+----+------------+----------+
| 7 | Azure | 2 |
+----+------------+----------+
| 8 | SharePoint | 2 |
+----+------------+----------+
| 9 | Office | 2 |
+----+------------+----------+
| 10 | Java | 3 |
+----+------------+----------+
| 11 | Word | 9 |
+----+------------+----------+
| 12 | Excel | 9 |
+----+------------+----------+
| 13 | PowerPoint | 9 |
+----+------------+----------+
I already have the code to pull all the information from the sample data above and turn it into a List<FlatItem> object.
What's the best approach so that I can have a List<NavigationItem> object which will look like something below:
Google
Gmail
Sheets
AdSense
Microsoft
Azure
SharePoint
Office
Word
Excel
PowerPoint
Oracle
Java
I'm thinking of creating a recursive method to loop through my List<FlatItem> then structure it in a way to be a nested list of NavigationItem.
No need for recursion. You could use LINQ to build the structure easily:
List<FlatItem> flatItems = ...;
var navigationItems = flatItems.Select(
i => new NavigationItem { ID = i.ID, Title = i.Title, ParentID = i.ParentID }
).ToList();
foreach (var i in navigationItems)
i.Children = navigationItems.Where(n => n.ParentID == i.ID).ToList();
// get Google, Microsoft, Oracle items
var rootNavigationItems = navigationItems.Where(n => n.ParentID == 0);
Try this:
List<FlatItem> source = new List<UserQuery.FlatItem>()
{
new FlatItem() { ID = 1, Title = "Google", ParentID = null },
new FlatItem() { ID = 2, Title = "Microsoft", ParentID = null },
new FlatItem() { ID = 3, Title = "Oracle", ParentID = null },
new FlatItem() { ID = 4, Title = "Gmail", ParentID = 1 },
new FlatItem() { ID = 5, Title = "Sheets", ParentID = 1 },
new FlatItem() { ID = 6, Title = "Adsense", ParentID = 1 },
new FlatItem() { ID = 7, Title = "Azure", ParentID = 2 },
new FlatItem() { ID = 8, Title = "SharePoint", ParentID = 2 },
new FlatItem() { ID = 9, Title = "Office", ParentID = 2 },
new FlatItem() { ID = 10, Title = "Java", ParentID = 3 },
new FlatItem() { ID = 11, Title = "Word", ParentID = 9 },
new FlatItem() { ID = 12, Title = "Excel", ParentID = 9 },
new FlatItem() { ID = 13, Title = "PowerPoint", ParentID = 9 },
};
var lookup = source.ToLookup(x => x.ParentID);
Func<int?, List<NavigationItem>> build = null;
build = pid =>
lookup[pid]
.Select(x => new NavigationItem()
{
ID = x.ID,
Title = x.Title,
ParentID = x.ParentID,
Children = build(x.ID)
})
.ToList();
To start the process call build(null). That gives me this:
This does assume that the ParentId property is a int? - which your data table does suggest.
If you are ok with using recursion, you can create a function like this:
public List<NavigationItem> ChildrenOf(List<FlatItem> flatItems, int parentId)
{
var childrenFlatItems = flatItems.Where(i => i.ParentID == parentId);
return childrenFlatItems.Select(i => new NavigationItem {
ID = i.ID,
Title = i.Title,
ParentID = i.ParentID,
Children = ChildrenOf(flatItems, i.ID)})
.ToList();
}
Then, assuming that your root items have a parent id of 0 (since you aren't using nullable types), you generate the full list with:
ChildrenOf(flatsItems, 0)
Untested, however you could try this, should be fairly fast as well
var list = new List<FlatItem>();
var result = new List<NavigationItem>();
// just a helper to remember ids
var dict = new Dictionary<int, NavigationItem>();
foreach (var item in list)
{
var nav = new NavigationItem()
{
ID = item.ID,
ParentID = item.ParentID,
Title = item.Title,
Children = new List<NavigationItem>()
};
if (!dict.ContainsKey(nav.ParentID))
result.Add(nav);
else
dict[nav.ParentID].Children.Add(nav);
dict.Add(item.ID, nav);
}
no recursive, just GroupBy.
List<NavigationItem> list = ... // map from List<FlatItem>
// and init Children = new List<NavigationItem>();
var groups = list.GroupBy(x => x.ParentID).ToList();
foreach (var g in groups)
{
var items = list.Find(x => x.ID == g.Key);
if (items != null)
items.Children = g.ToList();
}
// tops is [Google, Microsoft, Oracle]
var tops = list.Where(x => x.ParentID == null).ToList();

Linq Table Group By Get Perecentage in each column

I have a datatable with 2 columns:
GuitarBrand | Status
---------------------
Fender | Sold
Fender | In Stock
Gibson | In Stock
Gibson | In Stock
I want to write a linq query to output
GuitarBrand | PercentSold | Sold/Total
---------------------------------------
Fender | 50% | 1/2
Gibson | 100% | 2/2
Here's what I have so far:
var groupedtable = from b in table.AsEnumerable()
group b by b.Field<"GuitarBrand"> into g
select new ( GuitarBrand = g.Key, Perecent = (float)g.Count()/(float)g.key)
Which I got from another post but it isn't even close to working, I get a Cannot Convert string to float. I've tried looking at other posts but I can't find anything.
Thanks!
You can use the following (hopefully self explanatory) query:
var groupedtable =
from b in table.AsEnumerable()
group b by b.Field<string>("GuitarBrand") into g
let Total = g.Count()
let Sold = g.Count(e => e.Field<string>("Status") == "Sold")
let SoldPercent = (float)Sold / (float)Total
select new
{
GuitarBrand = g.Key,
PercentSold = SoldPercent.ToString("p"),
TotalSold = Sold + "/" + Total
};
maybe something like this!
var groupedtable = from b in table.AsEnumerable()
group b by b.Field<"GuitarBrand"> into g
select new {
GuitarBrand = g.Key,
Perecent = g.Count(x=>x.Status.Eguals("Sold")/(float)g.Count()
}
Something like this should get you started. In your question, your output table is contradicting. I've assumed you want the data as labeled.
public static void Main()
{
var guitars = new List<Guitar>()
{
new Guitar(){ Brand = "Fender", Status = Status.Sold },
new Guitar(){ Brand = "Fender", Status = Status.InStock },
new Guitar(){ Brand = "Gibson", Status = Status.InStock },
new Guitar(){ Brand = "Gibson", Status = Status.InStock }
};
var query = guitars
.GroupBy(guitar => guitar.Brand)
.Select(group => new
{
GuitarBrand = group.Key,
Sold = group.Where(guitar => guitar.Status == Status.Sold).Count(),
Total = group.Count()
})
.Select(_ => new
{
_.GuitarBrand,
PercentSold = ((decimal)_.Sold / (decimal)_.Total) * 100,
SoldAndTotal = string.Format("{0}/{1}", _.Sold, _.Total)
});
}
class Guitar {
public string Brand { get; set; }
public Status Status { get; set; }
}
enum Status {
Sold,
InStock
}

How to combine many rows into single text?

I want to grouping phone number to the same section like this :
section | phone
1 | 1001
1 | 1002
2 | 1201
2 | 1202
and grouping them in like this :
section | phone
1 | 1001, 1002
2 | 1201, 1202
but i don't know syntax to groping them to that.
This code i do
var entgr = (from fx in MainOnline.MA_TelUsers
where fx.TE_SectionID != null
group fx by fx.TE_SectionID into id
from ids in id.DefaultIfEmpty()
select new
{
Section = ids.TE_SectionID,
TelPhone = ids.TE_Phone
});
How I grouping that and use it to join other table?
var entgr = (from fx in ph
group fx by fx.SectionId.ToString() into id
select new
{
Section = id.Key,
TelPhone = string.Join(", ",id.Select(s => s.PhoneNumber.ToString()))
});
try this query
var entgr = (from fx in MainOnline.MA_TelUsers
where fx.TE_SectionID != null
group fx by fx.TE_SectionID into ids
select new
{
Section = ids.TE_SectionID,
TelPhone =ids.Aggregate((a, b) =>
new {TelPhone = (a.TelPhone + ", " + b.TelPhone ) }).TelPhone
});
https://msdn.microsoft.com/en-us/library/vstudio/bb397696.aspx
See this link. If you want to it do it in single linq query then that I hope not possible.
But at evaluation time you can do like this
var ph = new List<Phone>();
ph.Add(new Phone() { SectionId = 1, PhoneNumber = 1001 });
ph.Add(new Phone() { SectionId = 1, PhoneNumber = 1002 });
ph.Add(new Phone() { SectionId = 2, PhoneNumber = 1201 });
ph.Add(new Phone() { SectionId = 2, PhoneNumber = 1202 });
var results = ph.GroupBy(i => i.SectionId).Select(i=> new {i.Key, i});
foreach (var phone in results)
{
int section = phone.Key;
string phoneNos = string.Join(",",phone.i.Select(i=>i.PhoneNumber));
}

Linq Full Outer Join on Two Objects

I have two objects called CountryMobility that I believe I need to combine with a full outer join. How can I do this using linq?
public class CountryMobility
{
public string countryCode { get; set; }
public int inbound { get; set; }
public int outbound { get; set; }
}
I want to combine two of these objects like so:
inboundStudents:
countryCode | inbound | outbound
EG | 2 | 0
CA | 3 | 0
CH | 5 | 0
outboundStudents:
countryCode | inbound | outbound
PE | 0 | 1
CA | 0 | 4
CH | 0 | 5
-
-
-
-
V
combinedStudents:
countryCode | inbound | outbound
PE | 0 | 1
CA | 3 | 4
CH | 5 | 5
EG | 2 | 0
I have tried the following linq statements but have not been able to figure out the correct syntax. I am currently getting a syntax error near
temp.DefaultIfEmpty(new { first.ID, inbound = 0, outbound=0 }) in both statements.
var leftOuterJoin =
from first in inboundActivities
join last in outboundActivities
on first.countryCode equals last.countryCode
into temp
from last in temp.DefaultIfEmpty
(new { first.countryCode, inbound = 0, outbound=0 })
select new CountryMobility
{
countryCode = first.countryCode,
inbound = first.inbound,
outbound = last.outbound,
};
var rightOuterJoin =
from last in outboundActivities
join first in inboundActivities
on last.countryCode equals first.countryCode
into temp
from first in temp.DefaultIfEmpty
(new { last.countryCode, inbound = 0, outbound = 0 })
select new CountryMobility
{
countryCode = last.countryCode,
inbound = first.inbound,
outbound = last.outbound,
};
var fullOuterJoin = leftOuterJoin.Union(rightOuterJoin);
After your latest information. It seems to me that you can do something much simpler. Namely a UNION ALL that you subsequently group by country code.
A UNION ALL can be created using the Concat method.
The sample below works for me (using in memory collections). The query is shown in the Run method.
public class CountryMobility
{
public string countryCode { get; set; }
public int inbound { get; set; }
public int outbound { get; set; }
}
public static class JoinedMobilityQuery
{
static CountryMobility[] inbound = {
new CountryMobility() { countryCode = "EG", inbound = 2 },
new CountryMobility() { countryCode = "CA", inbound = 3 },
new CountryMobility() { countryCode = "CH", inbound = 5 },
};
static CountryMobility[] outbound = {
new CountryMobility() { countryCode = "PE", outbound = 1 },
new CountryMobility() { countryCode = "CA", outbound = 4 },
new CountryMobility() { countryCode = "CH", outbound = 6 },
};
static IQueryable<CountryMobility> Inbound()
{
return inbound.AsQueryable();
}
static IQueryable<CountryMobility> Outbound()
{
return outbound.AsQueryable();
}
public static void Run()
{
var transfers = from t in Inbound().Concat(Outbound())
group t by t.countryCode into g
select new CountryMobility() {
countryCode = g.Key,
inbound = g.Sum(x => x.inbound),
outbound = g.Sum(x => x.outbound),
};
foreach (var transfer in transfers)
Console.WriteLine("{0}\t{1}\t{2}", transfer.countryCode, transfer.inbound, transfer.outbound);
}
}
Your DefaultIfEmpty is thropwing an error, because you are defining an anonymous object, but you are creating stronly typed objects in your select statement. They both have to be of the same type.
So define a default object like this:
var defaultActivity = new CountryMobility() { countryCode = String.Empty, outbound = 0, inbound = 0 };
After this, use it in your DefaultIfEmpty() method:
from last in temp.DefaultIfEmpty(defaultActivity)
select new CountryMobility
{
//...
};
last but not least, you have to do a groupby to get the desired results:
var fullOuterJoin = leftOuterJoin.Union(rightOuterJoin)
.GroupBy (oj => oj.countryCode)
.Select (oj => oj.FirstOrDefault());
Output:
Full Demo Code:
http://share.linqpad.net/u46gar.linq
You can do like this
List<CountryMobility> inboundStudents = new List<CountryMobility>{
new CountryMobility { countryCode="EG", inbound=2, outbound = 0},
new CountryMobility { countryCode="CA", inbound=3, outbound = 0},
new CountryMobility { countryCode="CH", inbound=5, outbound = 0}};
List<CountryMobility> outboundStudents = new List<CountryMobility>{
new CountryMobility { countryCode="PE", inbound=0, outbound = 1},
new CountryMobility { countryCode="CA", inbound=0, outbound = 4},
new CountryMobility { countryCode="CH", inbound=0, outbound = 5}};
var joinedList = inboundStudents.Concat(outboundStudents).GroupBy(item => new { item.countryCode});
var result = joinedList.Select(x => new
{
countryCode = x.Key.countryCode,
inbound = x.Sum(i => i.inbound),
outbound = x.Sum(i => i.outbound)
});

Categories