Related
I have question about how I could use Linq grouping the same combination that the list has then relate to a two list.
Example:
I have theses classes.
public class PetCategoryOwner
{
public string PetCategory { get; set; }
public string Owner { get; set; }
}
public class PetCategoriesOwners
{
public IEnumerable<string> PetCategories { get; set; }
public IEnumerable<string> Owners { get; set; }
}
The example data.
Owner
Pet Category
Higa
Terry
Higa
Charlotte
Oliver
Terry
Oliver
Charlotte
Oliver
Chausie
Price
Chausie
Liam
Terry
Liam
Chartreux
var petCategoryOwner = new List<PetCategoryOwner>()
{
new PetCategoryOwner { Owner = "Higa", PetCategory = "Terry"},
new PetCategoryOwner { Owner = "Higa", PetCategory = "Charlotte"},
new PetCategoryOwner { Owner = "Oliver", PetCategory = "Terry"},
new PetCategoryOwner { Owner = "Oliver", PetCategory = "Charlotte"},
new PetCategoryOwner { Owner = "Oliver", PetCategory = "Chausie"},
new PetCategoryOwner { Owner = "Price", PetCategory = "Chausie"},
new PetCategoryOwner { Owner = "Liam", PetCategory = "Terry"},
new PetCategoryOwner { Owner = "Liam", PetCategory = "Chartreux"}
};
Expected values
Owner
Pet Category
Group
Higa
Terry
A
Higa
Charlotte
A
Oliver
Terry
A
Oliver
Charlotte
A
Oliver
Chausie
B
Price
Chausie
B
Liam
Terry
C
Liam
Chartreux
C
var petCategoriesOwners = new List<PetCategoriesOwners>()
{
new PetCategoriesOwners()
{
PetCategories = new List<string>() { "Terry", "Charlotte" },
Owners = new List<string>() { "Oliver", "Higa" }
},
new PetCategoriesOwners()
{
PetCategories = new List<string>() { "Chausie" },
Owners = new List<string>() { "Oliver", "Price" }
},
new PetCategoriesOwners()
{
PetCategories = new List<string>() { "Chartreux", "Terry" },
Owners = new List<string>() { "Liam" }
}
}
In order to solve your problem you need to do two steps: group by owners and merge owners based on the fact other group's set is a subset of current owner. You can try to achieve it by running below LINQ query:
public class PetCategoriesOwners
{
public List<string> PetCategories { get; set; }
public List<string> Owners { get; set; }
}
var petCategoriesOwners = petCategoryOwner
.GroupBy(x => x.Owner)
.Select(x => new
{
Owner = x.Key,
Categories = x.Select(y => y.PetCategory)
})
.OrderBy(x => x.Categories.Count())
.Aggregate(new List<PetCategoriesOwners>(), (acc, current) =>
{
var currentCategories = current.Categories.ToList();
var matches = acc.Where(group => group.PetCategories.All(x => currentCategories.Contains(x)));
foreach(var match in matches)
{
match.Owners.Add(current.Owner);
currentCategories = currentCategories.Except(match.PetCategories).ToList();
}
if (currentCategories.Any())
{
acc.Add(
new PetCategoriesOwners() {
Owners = new List<string>() { current.Owner },
PetCategories = currentCategories
});
}
return acc;
});
So it's important to group by Owner, process groups in ascending order in terms of length. The Aggregate method basically tries to find if previosly entered item overlaps with currently processed one. If it happens then we take those intersecting elements, add owner there and remove those from current element. If any element is left then we create own group for such owner.
Edit: .NET Fiddle
Note: This answer is based on a false interpretation of the question post. In it, I have assumed that petCategoriesOwners is an input and that the content of the table containing the Group column is the expected output.
You could achieve what you want to do by using a combination of SelectMany(), .Select(), .Where() and .Contains() from System.Linq.
Slightly simplified, by using ints for the group value rather than char, this is a possible implemenation:
public class GroupedOwnerAndPetCategory
{
public string Owner { get; set; }
public string PetCategory { get; set; }
public int Group { get; set; }
}
var grouped = petCategoriesOwners
.SelectMany((ownerPetMix, index) => petCategoryOwner
.Where(ownerPetPair =>
ownerPetMix.Owners.Contains(ownerPetPair.Owner) &&
ownerPetMix.PetCategories.Contains(ownerPetPair.PetCategory))
.Select(ownerPetPair => new GroupedOwnerAndPetCategory
{
Owner = ownerPetPair.Owner,
PetCategory = ownerPetPair.PetCategory,
Group = index
}))
.ToList();
Using your example input, grouped contains the following entries:
Higa Terry 0
Higa Charlotte 0
Oliver Terry 0
Oliver Charlotte 0
Oliver Chausie 1
Price Chausie 1
Liam Terry 2
Liam Chartreux 2
Example fiddle here.
I need to group items in a list and then pass each group to a function for further elaboration.
This is my code:
var list = new List<MyObj>(); // list is created ad populated elsewhere in code...
var query = list.AsEnumerable();
query = query.Where(x => x.MyProp == true).Select(x => x); // query definition is way more complicated
var grp = query.GroupBy(x => new { x.Name, x.Surname }).ToList();
Here grp is of type List<IGrouping<'a, MyObj>>.
I can easily iterate through my items with:
foreach (var g in grp)
{
foreach (var o in g)
{
// here "o" is of type MyObj
}
}
but I don't know how to create a function that receives a group and iterates through its items:
foreach (var g in grp)
{
DoSomethingWithGroup(g);
}
This is because I have an anonymous type (Key) in grp definition.
I tried to replace the Key anonymous type with a custom type:
private class GrpKey
{
public string Name { get; set; }
public string Surname { get; set; }
}
/* ... */
var grp = query.GroupBy(x => new GrpKey { Name = x.Name, Surname = x.Surname }).ToList();
This way grp is of type List<IGrouping<MyGrpKey, MyObj>> instead of List<IGrouping<'a, MyObj>>.
I could then create a function:
private void DoSomethingWithGroup(IGrouping<MyGrpKey, MyObj>) { /* ... */ }
Unfortunately, this way grouping doesn't work anymore: grp now contains as many groups as items in the source list, each one with a single item.
Instead of specifying your method like you did:
private void DoSomethingWithGroup(IGrouping<MyGrpKey, MyObj>)
separate the key and the elements into their on parameters
private void DoSomethingWithGroup<T>(T groupKey, IEnumerable<MyObj> entities)
With this change you can do the following:
//Populate list with some dummy data
var list = new List<MyObj>()
{
new MyObj { Id = 1, MyProp = true, Name = "A", Surname = "B"},
new MyObj { Id = 2, MyProp = false, Name = "A", Surname = "B"},
new MyObj { Id = 3, MyProp = true, Name = "B", Surname = "B"},
new MyObj { Id = 4, MyProp = true, Name = "B", Surname = "B"},
new MyObj { Id = 5, MyProp = true, Name = "C", Surname = "B"},
};
//Perform the grouping
var groups = list
.Where(x => x.MyProp)
.GroupBy(x => new { x.Name, x.Surname })
.ToList();
//Perform some arbitrary action on the group basis
foreach (var group in groups)
{
DoSomethingWithGroup(group.Key, group);
}
If the implementation of the DoSomethignWithGroup looks like this:
void DoSomethingWithGroup<T>(T groupKey, IEnumerable<MyObj> entities)
{
Console.WriteLine(groupKey);
foreach (var entity in entities)
{
Console.WriteLine($"- {entity.Id}");
}
}
then the output will be this:
{ Name = A, Surname = B }
- 1
{ Name = B, Surname = B }
- 3
- 4
{ Name = C, Surname = B }
- 5
DotnetFiddle link
If the key is not needed in your function, you can omit that in the method. A IGrouping<TKey, TElement> is an IEnumerable<TElement> so you could just define it as:
private void DoSomethingWithGroup(IEnumerable<MyObj> items)
{
///...
}
I have a table of WorldEvents. Each WorldEvent has a list of Presentations, that happened in some country, regarding that WorldEvent
public class WorldEvent
{
public int ID { get; set; }
public string Name { get; set; }
public List<Presentation> PresentationList { get; set; }
}
public class Presentation
{
public int ID { get; set; }
public string Name { get; set; }
public string Country { get; set; }
}
public class WorldEventService
{
public List<WorldEvent> GetWorldEvents()
{
List<WorldEvent> worldEventList = new List<WorldEvent>();
List<Presentation> presentationList = new List<Presentation>();
// Create list of Presentations for WorldEvent_1
presentationList = new List<Presentation>()
{
new Presentation() { ID = 1, Name = "Presentation_1", Country = "Germany",},
new Presentation() { ID = 2, Name = "Presentation_2", Country = "UK",},
new Presentation() { ID = 3, Name = "Presentation_3", Country = "UK",},
};
// Add WorldEvent_1 to the list of WorldEvents
worldEventList.Add(new WorldEvent()
{
ID = 1,
Name = "WorldEvent_1",
PresentationList = presentationList,
});
// Create list of Presentations for WorldEvent_2
presentationList = new List<Presentation>()
{
new Presentation() { ID = 4, Name = "Presentation_4", Country = "USA",},
new Presentation() { ID = 5, Name = "Presentation_5", Country = "UK",},
new Presentation() { ID = 6, Name = "Presentation_6", Country = "Japan",},
};
// Add WorldEvent_2 to the list of WorldEvents
worldEventList.Add(new WorldEvent()
{
ID = 2,
Name = "WorldEvent_2",
PresentationList = presentationList,
});
// Create list of Presentations for WorldEvent_3
presentationList = new List<Presentation>()
{
new Presentation() { ID = 7, Name = "Presentation_7", Country = "France",},
new Presentation() { ID = 8, Name = "Presentation_8", Country = "Germany",},
new Presentation() { ID = 9, Name = "Presentation_9", Country = "Japan",},
};
// Add WorldEvent_3 to the list of WorldEvents
worldEventList.Add(new WorldEvent()
{
ID = 3,
Name = "WorldEvent_3",
PresentationList = presentationList,
});
return worldEventList;
}
}
Now - how can I get a list of WorldEvents, whose Presentations took place in the UK.
And - in the list of my interest, WorldEvents should contain info about those UK Presentations only.
In other word, I need this as result:
WorldEvent_1(Presentation_2, Presentation_3)
WorldEvent_2(Presentation_5)
If I've understood what you want. There are many ways to do this, however you can filter first, then recreate your WorldEvents with the filtered list of Presentation
var country = "UK";
var result = worldEventList.Where(x => x.PresentationList.Any(y => y.Country == country))
.Select(x => new WorldEvent()
{
ID = x.ID,
Name = x.Name,
PresentationList = x.PresentationList
.Where(y => y.Country == country)
.ToList()
}).ToList();
or as noted by Gert Arnold in the comments you could filter after the fact
var result = worldEventList.Select(x => new WorldEvent()
{
ID = x.ID,
Name = x.Name,
PresentationList = x.PresentationList
.Where(y => y.Country == country).ToList()
}).Where(x => x.PresentationList.Any())
.ToList();
Note : Because this is not projecting (selecting) each Presentation, any change you make to a Presentation in the result will be reflected in the original data. If you don't want this, you will need to recreate each Presentation
var worldEvent = new WorldEventService.GetWorldEvents();
var filter = "";//userInput
var filteredResult = worldEvent.Select(r => new WorldEvent
{
PresentationList = r.PresentationList.Where(c => c.Country == filter).ToList(),
ID = r.Id,
Name = r.Name
}).ToList();
public static List<WorldEvent> Filter(string Country, List<WorldEvent> events) {
var evs = from ev in events.Where(x => x.PresentationList.Any(y => y.Country == Country))
let targetPres = from pres in ev.PresentationList
where pres.Country == Country
select pres
select new WorldEvent {
ID = ev.ID,
Name = ev.Name,
PresentationList = targetPres.ToList()
};
return evs.ToList();
}
Not sure if my understanding is correct, I guess there's a one to many relationship between your WorldEvent and Presentation table. So if you'd like to get all the WorldEvents and its related Presentations which take place in UK, by using EntityFramework, you can try this:
worldEventContext
.Include(PresentationContext)
.Select(
w => new
{
w.ID,
w.Name,
PresentationList = w.PresentationContext.Where(p => p.Country == "UK")
})
The best way I can describe what I'm trying to do is "Nested DistinctBy".
Let's say I have a collection of objects. Each object contains a collection of nicknames.
class Person
{
public string Name { get; set; }
public int Priority { get; set; }
public string[] Nicknames { get; set; }
}
public class Program
{
public static void Main()
{
var People = new List<Person>
{
new Person { Name = "Steve", Priority = 4, Nicknames = new string[] { "Stevo", "Lefty", "Slim" }},
new Person { Name = "Karen", Priority = 6, Nicknames = new string[] { "Kary", "Birdie", "Snookie" }},
new Person { Name = "Molly", Priority = 3, Nicknames = new string[] { "Mol", "Lefty", "Dixie" }},
new Person { Name = "Greg", Priority = 5, Nicknames = new string[] { "G-man", "Chubs", "Skippy" }}
};
}
}
I want to select all Persons but make sure nobody selected shares a nickname with another. Molly and Steve both share the nickname 'Lefty' so I want to filter one of them out. Only the one with highest priority should be included. If there is a highest priority tie between 2 or more then just pick the first one of them. So in this example I would want an IEnumerable of all people except Steve.
EDIT: Here's another example using music album instead of person, might make more sense.
class Album
{
string Name {get; set;}
int Priority {get;set;}
string[] Aliases {get; set;}
{
class Program
{
var NeilYoungAlbums = new List<Album>
{
new Person{ Name = "Harvest (Remastered)", Priority = 4, Aliases = new string[] { "Harvest (1972)", "Harvest (2012)"}},
new Person{ Name = "On The Beach", Priority = 6, Aliases = new string[] { "The Beach Album", "On The Beach (1974)"}},
new Person{ Name = "Harvest", Priority = 3, Aliases = new string[] { "Harvest (1972)"}},
new Person{ Name = "Freedom", Priority = 5, Aliases = new string[] { "Freedom (1989)"}}
};
}
The idea here is we want to show his discography but we want to skip quasi-duplicates.
I would solve this using a custom IEqualityComparer<T>:
class Person
{
public string Name { get; set; }
public int Priority { get; set; }
public string[] Nicknames { get; set; }
}
class PersonEqualityComparer : IEqualityComparer<Person>
{
public bool Equals(Person x, Person y)
{
if (x == null || y == null) return false;
return x.Nicknames.Any(i => y.Nicknames.Any(j => i == j));
}
// This is bad for performance, but if performance is not a
// concern, it allows for more readability of the LINQ below
// However you should check the Edit, if you want a truely
// LINQ only solution, without a wonky implementation of GetHashCode
public int GetHashCode(Person obj) => 0;
}
// ...
var people = new List<Person>
{
new Person { Name = "Steve", Priority = 4, Nicknames = new[] { "Stevo", "Lefty", "Slim" } },
new Person { Name = "Karen", Priority = 6, Nicknames = new[] { "Kary", "Birdie", "Snookie" } },
new Person { Name = "Molly", Priority = 3, Nicknames = new[] { "Mol", "Lefty", "Dixie" } },
new Person { Name = "Greg", Priority = 5, Nicknames = new[] { "G-man", "Chubs", "Skippy" } }
};
var distinctPeople = people.OrderBy(i => i.Priority).Distinct(new PersonEqualityComparer());
EDIT:
Just for completeness, this could be a possible LINQ only approach:
var personNicknames = people.SelectMany(person => person.Nicknames
.Select(nickname => new { person, nickname }));
var groupedPersonNicknames = personNicknames.GroupBy(i => i.nickname);
var duplicatePeople = groupedPersonNicknames.SelectMany(i =>
i.OrderBy(j => j.person.Priority)
.Skip(1).Select(j => j.person)
);
var distinctPeople = people.Except(duplicatePeople);
A LINQ-only solution
var dupeQuery = people
.SelectMany( p => p.Nicknames.Select( n => new { Nickname = n, Person = p } ) )
.ToLookup( e => e.Nickname, e => e.Person )
.SelectMany( e => e.OrderBy( p => p.Priority ).Skip( 1 ) );
var result = people.Except( dupeQuery ).ToList();
See .net fiddle sample
This works once, then you have to clear the set. Or store the results in a collection.
var uniqueNicknames = new HashSet<string>();
IEnumerable<Person> uniquePeople = people
.OrderBy(T => T.Priority) // ByDescending?
.Where(T => T.Nicknames.All(N => !uniqueNicknames.Contains(N)))
.Where(T => T.Nicknames.All(N => uniqueNicknames.Add(N)));
From a traditional SQL sentence like this:
SELECT Id, Owner, MIN(CallTime)
FROM traffic
WHERE CallType = "IN"
GROUP BY Owner;
where CallTime is a datetime field, what I want is the oldest record belonging to each Owner.
How can I achieve this with Linq?
This was my attempt (I'm using Entity Framework and context is the entity instance):
var query = context.traffic.Where(t => t.CallType == "IN");
var results = query
.GroupBy(t => t.Owner)
.Select(g => new { CallTime = g.Min(h => h.CallTime) });
But I need also access to Id and Owner fields whereas now I have only access to CallTime.
You cannot access Id in the given code because you are grouping by Owner and the Key to the group will be the Owner not the 'traffic' object.
If you group by traffic objects you need some way to tell the groupBy how to compare them properly (i.e. group by owner) This can be done with an IEqualityComparer
e.g.
private class Traffic {
public int Id { get; set; }
public string Owner { get; set; }
public DateTime CallTime { get; set; }
}
private class TrafficEquaityComparer : IEqualityComparer<Traffic> {
public bool Equals(Traffic x, Traffic y) {
return x.Owner == y.Owner;
}
public int GetHashCode(Traffic obj) {
return obj.Owner.GetHashCode();
}
}
private static TrafficEquaityComparer TrafficEqCmp = new TrafficEquaityComparer();
private Traffic[] src = new Traffic[]{
new Traffic{Id = 1, Owner = "A", CallTime = new DateTime(2012,1,1)}, // oldest
new Traffic{Id = 2, Owner = "A", CallTime = new DateTime(2012,2,1)},
new Traffic{Id = 3, Owner = "A", CallTime = new DateTime(2012,3,1)},
new Traffic{Id = 4, Owner = "B", CallTime = new DateTime(2011,3,1)},
new Traffic{Id = 5, Owner = "B", CallTime = new DateTime(2011,1,1)}, //oldest
new Traffic{Id = 6, Owner = "B", CallTime = new DateTime(2011,2,1)},
};
[TestMethod]
public void GetMinCalls() {
var results = src.GroupBy(ts => ts, TrafficEqCmp)
.Select(grp => {
var oldest = grp.OrderBy(g => g.CallTime).First();
return new { Id = oldest.Id,
Owner = grp.Key.Owner,
CallTime = oldest.CallTime };
}); }
this gives
ID : Owner : MinCallTime
1 : A : (01/01/2012 00:00:00)
5 : B : (01/01/2011 00:00:00)
as the results.
Your SQL query doesn't look valid to me: you're using Id but not grouping by it. I assume that you wanted to group by Id and Owner?
var results = query
.GroupBy(t => new {Id = t.Id, Owner = t.Owner})
.Select(g => new { Id = g.Key.Id, Owner = g.Key.Owner, CallTime = g.Min(h => h.CallTime) })
.ToList();
If you want to get the oldest (smallest) ID instead of grouping by it:
var results = query
.GroupBy(t => t.Owner)
.Select(g => new { Id = g.Min(x => x.Id), Owner = g.Key, CallTime = g.Min(h => h.CallTime) })
.ToList();
// custQuery is an IEnumerable<IGrouping<string, Customer>>
var custQuery =
from cust in customers
group cust by cust.City into custGroup
where custGroup.Count() > 2
orderby custGroup.Key
select custGroup;
In that example you select the group