How to intersect results after GroupBy - c#

To illustrate my problem I have created this simple snippet. I have a class Item
public class Item
{
public int GroupID { get; set; }
public int StrategyID { get; set; }
public List<Item> SeedData()
{
return new List<Item>
{
new Item {GroupID = 1, StrategyID = 1 },
new Item {GroupID = 2, StrategyID = 1 },
new Item {GroupID = 3, StrategyID = 2 },
new Item {GroupID = 4, StrategyID = 2 },
new Item {GroupID = 5, StrategyID = 3 },
new Item {GroupID = 1, StrategyID = 3 },
};
}
}
And what I want to check is that this SeedData method is not returning any duplicated GroupID/StrategyID pairs.
So in my Main method I have this:
Item item = new Item();
var data = item.SeedData();
var groupByStrategyIdData = data.GroupBy(g => g.StrategyID).Select(v => v.Select(gr => gr.GroupID)).ToList();
for (var i = 0; i < groupByStrategyIdData.Count; i++)
{
for (var j = i + 1; j < groupByStrategyIdData.Count; j++)
{
Console.WriteLine(groupByStrategyIdData[i].Intersect(groupByStrategyIdData[j]).Any());
}
}
which is working fine but one of the problems is that I have lost the StrategyID so in my real-case scenario I won't be able to say for which StrategyID/GroupID pair I have duplication so I was wondering is it possible to cut-off the LINQ to here:
var groupByStrategyIdData = data.GroupBy(g => g.StrategyID)
and somehow perform the check on this result?

One of the very easy ways would be to do grouping using some identity for your Item. You can override Equals/GetHashCode for your Item or instead write something like:
Item item = new Item();
var data = item.SeedData();
var duplicates = data.GroupBy(x => string.Format("{0}-{1}", x.GroupID, x.StrategyID))
.Where(group => group.Count() > 1)
.Select(group => group.Key)
.ToList();
Please note, that using a string for identity inside of group by is probably not the best way to do grouping.
As of your question about "cutting" the query, you should also be able to do the following:
var groupQuery = data.GroupBy(g => g.StrategyID);
var groupList = groupQuery.Select(grp => grp.ToList()).ToList();
var groupByStrategyIdData = groupQuery.Select(v => v.Select(gr => gr.GroupID)).ToList();

You may be able to do it another way, as follows:
// Check for duplicates
if (data != null)
{
var grp =
data.GroupBy(
g =>
new
{
g.GroupID,
g.StrategyID
},
(key, group) => new
{
GroupID = key.GroupID,
StrategyId = key.StrategyID,
Count = group.Count()
});
if (grp.Any(c => c.Count > 1))
{
Console.WriteLine("Duplicate exists");
// inside the grp object, you can find which GroupID/StrategyID combo have a count > 1
}
}

Related

C# sort object list with start position and loop

I have a strange question :)
I have a object list looking like this:
var list = new []
{
new { Id = 1, Name = "Marcus" },
new { Id = 2, Name = "Mattias" },
new { Id = 3, Name = "Patric" },
new { Id = 4, Name = "Theodor" },
};
I would like to sort the list providing a "start id"
For example, if I provide "start id" 3, the result should look like this:
Id
Name
3
Patric
4
Theodor
1
Marcus
2
Mattias
I have no idea where to start, so I really need some help from you coding gods
The list is from a sql table, but it does not matter for me where the sort take place (in sql query or in c# code)
Try this:
var list = new []
{
new { Id = 1, Name = "Marcus" },
new { Id = 2, Name = "Mattias" },
new { Id = 3, Name = "Patric" },
new { Id = 4, Name = "Theodor" },
};
var start_id = 3;
var max_id = list.Max(y => y.Id);
var result =
from x in list
orderby (x.Id + max_id - start_id) % max_id
select x;
I get:
With LINQ to objects you can do something like that:
var list = new []
{
new { Id = 1, Name = "Marcus" },
new { Id = 2, Name = "Mattias" },
new { Id = 3, Name = "Patric" },
new { Id = 4, Name = "Theodor" },
};
var startId = 3;
var result = list
.GroupBy(i => i.Id >= startId ? 1 : 0) // split in two groups
.OrderByDescending(g => g.Key) // sort to have the group with startId first
.Select(g => g.OrderBy(i => i.Id)) // sort each group
.SelectMany(i => i) // combine result
.ToList();
Console.WriteLine(string.Join(", ", result.Select(i => i.Id))); // prints "3, 4, 1, 2"
You require 2 criteria to apply:
Order ascending by Id.
Return the Ids greater than threshold before the Ids less than threshold.
You can try:
var offset = 3;
var sorted1 = list
.OrderBy(item => item.Id < offset)
.ThenBy(item => item.Id);
The OrderBy condition yields true if Id is less than offset and false otherwise.
true is greater than false and therefore is returned later
A dirty way could also be:
var offset = 3;
var sorted2 = list
.OrderBy(item => unchecked((uint)(item.Id - offset)));
Here the offset is subtracted from Id and the result converted to unsigned int to make the negative values become very large positive ones. A little hacky. Might not work with queries against SQL providers.
Here's a toy Non-Linq Version
object[] ShiftList(int id)
{
var list = new dynamic[]
{
new { Id = 1, Name = "Marcus" },
new { Id = 2, Name = "Mattias" },
new { Id = 3, Name = "Patric" },
new { Id = 4, Name = "Theodor" },
};
Span<dynamic> listSpan = list;
int indexFound = -1;
for (int i = 0; i < list.Length; i++)
{
if (listSpan[i].Id == id)
{
indexFound = i;
}
}
if (indexFound is -1)
{
return list;
}
var left = listSpan.Slice(0, indexFound);
var right = listSpan[indexFound..];
object[] objs = new object[list.Length];
Span<object> objSpan = objs;
right.CopyTo(objSpan);
left.CopyTo(objSpan[right.Length..]);
return objs;
}
Try using foreach and iterate over each object in your list:
foreach (var item in list)
{
}
from here you should be able to use some of the collection methods for a list to reorder your list.

Remove list in list

I have a list that stores an instance of the list. I want to remove the object from the sublist with IdName == "149"
List<Product> productList = new List<Product>()
{
new Product()
{
Id = 1,
Model = "Phone",
TypeProd = new CheckTypes() { ChTypes = new List<CHType> { new CHType() { Id = 8, IdName = "261"}, new CHType () {Id = 9 , IdName = "149" } } }
},
new Product()
{
Id = 1,
Model = "Printer",
TypeProd = new CheckTypes() { ChTypes = new List<CHType> { new CHType() { Id = 8, IdName = null}, new CHType () {Id = 8 , IdName = "261" } } }
}
};
var pr = productList.Select(s => s.TypeProd).Where(w => w.ChTypes.Any(a => a.IdName != null && a.IdName.Contains("149"))).ToList();
// I
var pr0 = pr.Select(s => s.ChTypes).Where(w => w.Any(a => a.Id == 9)).ToList();
// II
var pr1 = pr.Select(s => s.ChTypes).Except(pr0);
// III
pr.Select(s=>s.ChTypes).ToList().RemoveAll(a => a.Any(item => item.IdName.Contains("149")));
foreach (var item in pr)
{
foreach (var item2 in item.ChTypes)
{
Console.WriteLine(item2.IdName);
}
Console.WriteLine("End");
}
Console.ReadKey();
I get to delete the whole sequence, but how to delete one element from the sequence?
Use Remove() to remove a given item, or use RemoveAt() to remove the item at a given location.
I think, you are complicating it more than it needs to be. You simply need to loop over all the ChTypes List and remove the unwanted ChType. It can be easily accomplished by the below code
foreach (var chType in productList.Select(prod => prod.TypeProd.ChTypes))
chType.RemoveAll(c => c.IdName != null && c.IdName.Contains("149"));
All your LINQ lines for pr, pr0, pr1 and pr.Select().ToList().RemoveAll() are all unnecessary. Also, in your current code you are picking up only CHType with Id = 9, not sure if that is by mistake or your code is correct but your question missed specifying it.
Since the OP insists on avoiding foreach below is a a bad LINQ way of doing it
productList.Select(prod => prod.TypeProd.ChTypes)
.Select(chType => chType.RemoveAll(c => c.IdName != null && c.IdName.Contains("149")))
.ToList();

Rename List item when there is the same string multiple time

I have List of names like:
var list = new List<string> {"Allan", "Michael", "Jhon", "Smith", "George", "Jhon"};
and a combobox which itemssource is my list. As you can see in the list there is Jhon 2 times, what I want is when I put those name into combobox add "2" to second Jhon. I mean when I open the combobox names in it shoud look like:
Allan
Michael
Jhon
Smith
George
Jhon2
I have tired linq to do that but I'm quite new to c#/linq. Could someone show me simple way to do that?
I would do this:
var result = list.Take(1).ToList();
for (var i = 1; i < list.Count; i++)
{
var name = list[i];
var count = list.Take(i - 1).Where(n => n == name).Count() + 1;
result.Add(count < 2 ? name : name + count.ToString());
}
Here is what I would do:
First off, separate the list into two smaller ones, one that contains all the unique names, and one that contains only duplicates:
var duplicates = myList.GroupBy(s => s)
.SelectMany(grp => grp.Skip(1));
var unique = new HashSet<string>(myList).ToList();
Then process:
var result = new List<string>();
foreach (string uniqueName in unique)
{
int index=2;
foreach (string duplicateName in duplicates.Where(dupe => dupe == uniqueName))
{
result.Add(string.Format("{0}{1}", duplicateName, index.ToString()));
index++;
}
}
What we are doing here is the following:
Iterate through unique names.
Initialize a variable index with value 2. This will be the number we add at the end of each name.
Iterate through matching duplicate names.
Modify the name string by adding the number stored at index to the end.
Add this new value to the results list.
Increment index.
Finally, add the unique names back in:
result.AddRange(unique);
The result list should now contain all the same values as the original myList, only difference being that all names that appear more than once have a number appended to their end. Per your specification, there is no name name1. Instead, counting starts from 2.
Another possibility:
var groups = list.Select((name, index) => new { name, index }).GroupBy(s => s.name).ToList();
foreach (var group in groups.Where(g => g.Count() > 1))
{
foreach (var entry in group.Skip(1).Select((g, i) => new { g, i }))
{
list[entry.g.index] = list[entry.g.index] + entry.i;
}
}
Someone might be able to give a more efficient answer, but this does the job.
The dictionary keeps track of how many times a name has been repeated in the list. Each time a new name in the list is encountered, it is added to the dictionary and is added as is to the new list. If the name already exists in the dictionary (with the key check), instead, the count is increased by one in the dictionary and this name is added to the new list with the count (from the dictionary value corresponding to the name as the key) appended to the end of the name.
var list = new List<string> {"Allan", "Michael", "Jhon", "Smith", "George", "Jhon", "George", "George"};
Dictionary<string, int> dictionary = new Dictionary<string,int>();
var newList = new List<string>();
for(int i=0; i<list.Count();i++){
if(!dictionary.ContainsKey(list[i])){
dictionary.Add(list[i], 1);
newList.Add(list[i]);
}
else{
dictionary[list[i]] += 1;
newList.Add(list[i] + dictionary[list[i]]);
}
}
for(int i=0; i<newList.Count(); i++){
Console.WriteLine(newList[i]);
}
Output:
Allan
Michael
Jhon
Smith
George
Jhon2
George2
George3
Check this solution:
public List<string> AddName(IEnumerable<string> list, string name)
{
var suffixSelector = new Regex("^(?<name>[A-Za-z]+)(?<suffix>\\d?)$",
RegexOptions.Singleline);
var namesMap = list.Select(n => suffixSelector.Match(n))
.Select(x => new {name = x.Groups["name"].Value, suffix = x.Groups["suffix"].Value})
.GroupBy(x => x.name)
.ToDictionary(x => x.Key, x => x.Count());
if (namesMap.ContainsKey(name))
namesMap[name] = namesMap[name] + 1;
return namesMap.Select(x => x.Key).Concat(
namesMap.Where(x => x.Value > 1)
.SelectMany(x => Enumerable.Range(2, x.Value - 1)
.Select(i => $"{x.Key}{i}"))).ToList();
}
It handle case when you already has 'Jhon2' in the list
I would do
class Program
{
private static void Main(string[] args)
{
var list = new List<string> { "Allan", "Michael", "Jhon", "Smith", "George", "Jhon" };
var duplicates = list.GroupBy(x => x).Select(r => GetTuple(r.Key, r.Count()))
.Where(x => x.Count > 1)
.Select(c => { c.Count = 1; return c; }).ToList();
var result = list.Select(v =>
{
var val = duplicates.FirstOrDefault(x => x.Name == v);
if (val != null)
{
if (val.Count != 1)
{
v = v + " " + val.Count;
}
val.Count += 1;
}
return v;
}).ToList();
Console.ReadLine();
}
private static FooBar GetTuple(string key, int count)
{
return new FooBar(key, count);
}
}
public class FooBar
{
public int Count { get; set; }
public string Name { get; set; }
public FooBar(string name, int count)
{
Count = count;
Name = name;
}
}

Delete the repeated Item inside a List of object C#

I want to compare the element of a list of object ,delete the repeated Item and increment the number of the quantity of that Item (C# code ), I don't know if I should use LinQ,For or foreach statement : I have a list of OrderItem I want to delete the OrderItem that have the same FK_ArtikelId and increment the Qantity of the OrderItem . Exp:
for (int i=1 ; i < lot.Count ; i ++)
{
for (j = i + 1; j <= lot.Count; j++)
{
if (lot[i].FK_ArticleId.Equals(lot[j].FK_ArticleId))
{
lot[i].Quantity += lot[j].Quantity;
lot.Remove(lot[j]);
}
}
}
You have to use the GroupBy linq method and process the resulting groups: given the class
public class Article
{
public int FK_ArticleId { get; set; }
public int Quantity { get; set; }
}
and the following list:
var list = new List<Article>()
{
new Article() {FK_ArticleId = 1, Quantity = 10}
, new Article() {FK_ArticleId = 1, Quantity = 10}
, new Article() {FK_ArticleId = 1, Quantity = 10}
, new Article() {FK_ArticleId = 2, Quantity = 100}
, new Article() {FK_ArticleId = 2, Quantity = 100}
, new Article() {FK_ArticleId = 3, Quantity = 1000}
};
The following linq query returns what you need:
list.GroupBy(a => a.FK_ArticleId)
.Select(g => new Article() {FK_ArticleId = g.Key, Quantity = g.Sum(a => a.Quantity)});
// article id 1, quantity 30
// article id 2, quantity 200
// article id 3, quantity 1000
If you don't want to create a new article, you can take the first of the resulting group and set its Quantity to the correct value:
var results = list.GroupBy(a => a.FK_ArticleId)
.Select(g =>
{
var firstArticleOfGroup = g.First();
firstArticleOfGroup.Quantity = g.Sum(a => a.Quantity);
return firstArticleOfGroup;
});
I didn't test but this should give you an idea of the power of linq...
var stuff = lot
.GroupBy(p => p.FK_ArticleId)
.Select(g => g)
.ToList();
This should give you groups of articleIDs whereby you can easily get counts, create new consolidated lists etc.
For starters you can't use foreach because you're modifying the list and the Enumerator will throw an exception. You can do the following with Linq:
var grouped = lot.GroupBy(x => x.FK_ArticleId).ToArray();
foreach(var group in grouped)
{
group.First().Quantity = group.Sum(item => item.Quantity);
}
Now, first item in each group will contain the sum of all the quantities of items with the same FK_ArticleId. Now, to get the results use this:
var results = grouped.Select(g => g.First());
At this point it's purely your decision whether to return the results as a separate collection or insert them into the original list. If you opt for the second approach, don't forget to clear the list first:
list.Clear();
list.AddRange(results);
EDIT
A more elegant solution to accumulating the Quantity property into the first item of each group would be the following:
data.GroupBy(x=>x.FK_ArticleId)
.Select(g => g.Aggregate((acc, item) =>
{
acc.Quantity = item.Quantity;
return acc;
}));
This is what I scrapped in LinqPad:

how to get an ordered list with default values using linq

I have an ICollection of records (userID,itemID,rating) and an IEnumerable items
for a specific userID and each itemID from a set of itemIDs, i need to produce a list of the users rating for the items or 0 if no such record exists. the list should be ordered by the items.
example:
records = [(1,1,2),(1,2,3),(2,3,1)]
items = [3,1]
userID = 1
result = [0,2]
my attempt:
dataset.Where((x) => (x.userID == uID) & items.Contains(x.iID)).Select((x) => x.rating);
it does the job but it doesn't return 0 as default value and it isnt ordered...
i'm new to C# and LINQ, a pointer in the correct direction will be very appreciated.
Thank you.
This does the job:
var records = new int[][] { new int[] { 1, 1, 2 }, new int[] { 1, 2, 3 }, new int[] { 2, 3, 1 } };
var items = new int[] { 3, 1 };
var userId = 1;
var result = items.Select(i =>
{
// When there's a match
if (records.Any(r => r[0] == userId && r[1] == i))
{
// Return all numbers
return records.Where(r => r[0] == userId && r[1] == i).Select(r => r[2]);
}
else
{
// Just return 0
return new int[] { 0 };
}
}).SelectMany(r => r); // flatten the int[][] to int[]
// output
result.ToList().ForEach(i => Console.Write("{0} ", i));
Console.ReadKey(true);
How about:
dataset.Where((x) => (x.userID == uID)).Select((x) => items.Contains(x.iID) ? x.rating : 0)
This does the job. But whether it's maintainable/readable solution is topic for another discussion:
// using your example as pseudo-code input
var records = [(1,1,2),(1,2,3),(2,3,1)];
var items = [3,1];
var userID = 1;
var output = items
.OrderByDescending(i => i)
.GroupJoin(records,
i => i,
r => r.ItemId,
(i, r) => new { ItemId = i, Records = r})
.Select(g => g.Records.FirstOrDefault(r => r.UserId == userId))
.Select(r => r == null ? 0 : r.Rating);
How this query works...
ordering is obvious
the ugly GroupJoin - it joins every element from items with all records that share same ItemId into annonymous type {ItemId, Records}
now we select first record for each entry that matches userId - if none is found, null will be returned (thanks to FirstOrDefault)
last thing we do is check whether we have value (we select Rating) or not - 0
How about this. your question sounds bit like an outer join from SQL, and you can do this with a GroupJoin, SelectMany:
var record1 = new Record() { userID = 1, itemID = 1, rating = 2 };
var record2 = new Record() { userID = 1, itemID = 2, rating = 3 };
var record3 = new Record() { userID = 2, itemID = 3, rating = 1 };
var records = new List<Record> { record1, record2, record3 };
int userID = 1;
var items = new List<int> { 3, 1 };
var results = items
.GroupJoin( records.Where(r => r.userID == userID), item => item, record => record.itemID, (item, record) => new { item, ratings = record.Select(r => r.rating) } )
.OrderBy( itemRating => itemRating.item)
.SelectMany( itemRating => itemRating.ratings.DefaultIfEmpty(), (itemRating, rating) => rating);
To explain what is going on
For each item GroupJoin gets the list of rating (or empty list if no rating) for the specified user
OrderBy is obvious
SelectMany flattens the ratings lists, providing a zero if the ratings list is empty (by DefaultIfEmpty)
Hope this makes sense.
Be aware, if there is more than one rating for an item by a user, they will all appear in the final list.

Categories

Resources