Add duplicates together in List - c#

First question :)
I have a List<Materiau> (where Materiau implements IComparable<Materiau>), and I would like to remove all duplicates and add them together
(if two Materiau is the same (using the comparator), merge it to the first and remove the second from the list)
A Materiau contains an ID and a quantity, when I merge two Materiau using += or +, it keeps the same ID, and the quantity is added
I cannot control the input of the list.
I would like something like this:
List<Materiau> materiaux = getList().mergeDuplicates();
Thank you for your time :)

Check out Linq! Specifically the GroupBy method.
I don't know how familiar you are with sql, but Linq lets you query collections similarly to how sql works.
It's a bit in depth to explain of you are totally unfamiliar, but Code Project has a wonderful example
To sum it up:
Imagine we have this
List<Product> prodList = new List<Product>
{
new Product
{
ID = 1,
Quantity = 1
},
new Product
{
ID = 2,
Quantity = 2
},
new Product
{
ID = 3,
Quantity = 7
},
new Product
{
ID = 4,
Quantity = 3
}
};
and we wanted to group all the duplicate products, and sum their quantities.
We can do this:
var groupedProducts = prodList.GroupBy(item => item.ID)
and then select the values out of the grouping, with the aggregates as needed
var results = groupedProducts.Select( i => new Product
{
ID = i.Key, // this is what we Grouped By above
Quantity = i.Sum(prod => prod.Quantity) // we want to sum up all the quantities in this grouping
});
and boom! we have a list of aggregated products

Lets say you have a class
class Foo
{
public int Id { get; set; }
public int Value { get; set; }
}
and a bunch of them inside a list
var foocollection = new List<Foo> {
new Foo { Id = 1, Value = 1, },
new Foo { Id = 2, Value = 1, },
new Foo { Id = 2, Value = 1, },
};
then you can group them and build the aggregate on each group
var foogrouped = foocollection
.GroupBy( f => f.Id )
.Select( g => new Foo { Id = g.Key, Value = g.Aggregate( 0, ( a, f ) => a + f.Value ) } )
.ToList();

List<Materiau> distinctList = getList().Distinct(EqualityComparer<Materiau>.Default).ToList();

Related

Select all from one List, replace values that exist on another List

Few days ago I asked same question with SQL, but now it arises in C# code
Lets say we have this kind of class for holding different id/text pairs:
public class Text {
public int id { get; set; }
public string text { get; set; }
...
}
Now lets populate some data,
ListA gets a lot of data:
List<Text> ListA = new List<Text>{
new () {id = 1, text = "aaa1"},
new () {id = 2, text = "aaa2"},
new () {id = 3, text = "aaa3"},
new () {id = 4, text = "aaa4"},
new () {id = 5, text = "aaa5"},
new () {id = 6, text = "aaa6"},
};
And ListB gets just a little bit of data:
List<Text> ListB = new List<Text>{
new () {id = 4, text = "bbb4"},
new () {id = 5, text = "bbb5"},
};
And now what we are looking:
var result = ... // Some Linq or Lambda magic goes here
// and if we do:
foreach(var item in result){
Console.WriteLine(item.Id + " " + item.Text);
}
// Result will be:
1 : aaa1
2 : aaa2
3 : aaa3
4 : bbb4
5 : bbb5
6 : aaa6
You can try looking for id within ListB:
var result = ListA
.Select(a => ListB.FirstOrDefault(b => b.id == a.id) ?? a);
Here for each a within ListA we try to find corresponding by id (b.id == a.id) item within ListB. If no such item is found we just return ListA item: ?? item
In case of .Net 6 you can use overloaded .FirstOrDefault version (we can pass a as a default value):
var result = ListA
.Select(a => ListB.FirstOrDefault(b => a.id == b.id, a));
It might be more efficient to convert ListB to a Dictionary first:
var dictB = ListB.ToDictionary(x=> x.id)
Then you can write
var result = ListA.Select(x => dictB.TryGetValue(x.id, out var b) ? b : x)
UPD Rewrote taking comment suggestions into account
One option is to do an Union operation, by specifying an EqualityComparer. If the order is important, you can do an OrderBy operation at the end.
class TextIdComparer : EqualityComparer<Text> {
public override bool Equals(Text x, Text y) => x.id == y.id;
}
var result = ListB.Union(ListA, new TextIdComparer()).OrderBy(x => x.id)

Get items from List A where Id is common in both List A and List B and counter of that Id is more than 1 in List A or List B

I was looking to get items from ListA, where the value of Id is same in both of the lists, and the count of Id must be more than 1 in list A or list B
var items = itemsA.Where(x => itemsB.Select(y => y.Id == x.Id).Count() > 1);
This gives me the result where same Ids in itemsB is more then 1, I want to use a or condition to check for the same counter in itemsA
Eg 1:
ListA=[{"id"=1,"name="abc"},{"id=1, "name"="def"}]
ListB=[{"id=2","name="xyz"}, {"id=1, "name"="mno"}]
Should return [{"id"=1,"name="abc"},{"id=1, "name"="def"}] because id =1 exists in listB and the count of id with value 1 in listA is more then 1.
Eg 2:
ListA=[{"id"=2,"name="abc"},{"id=1, "name"="def"}]
ListB=[{"id=1","name="xyz"}, {"id=1, "name"="mno"}]
should return {"id=1, "name"="def"} because common id in both list is 1 and the count of id with value 1 in ListB is more then 1.
I am not certain this is the best solution, but as far as I've understood the question, it should be a solution.
Assuming you have an Item class as follows:
public class Item
{
public int Id { get; set; }
public string Name { get; set; }
}
and define itemsA and itemsB as List<Item>s, you can first find all Ids that are present in both lists, then select the applicable items from itemsA based on occurrence of each Id in either list:
IEnumerable<int> idsInBothItemLists = itemsA
.Select(a => a.Id)
.Intersect(itemsB.Select(b => b.Id))
.Distinct();
List<Item> items = itemsA
.Where(a => idsInBothItemLists.Contains(a.Id))
.GroupBy(a => a.Id)
.Where(gr =>
gr.Skip(1).Any() ||
itemsB.Where(b => b.Id == gr.Key).Skip(1).Any())
.SelectMany(gr => gr.Select(item => item))
.ToList();
(.Skip(1).Any() serves the same purpose as .Count() > 1 in your original code; it simply checks whether there are any items left after skipping the first item.)
Printing the output from the suggested population of itemsA and itemsB
foreach (var entry in items)
{
Console.WriteLine(entry.Id + " " + entry.Name);
}
e.g. for input
var itemsA = new List<Item>
{
new Item { Id = 1, Name = "abc" },
new Item { Id = 3, Name = "def" },
new Item { Id = 1, Name = "ghi" },
new Item { Id = 2, Name = "jkl" }
};
var itemsB = new List<Item>
{
new Item { Id = 2, Name = "xyz" },
new Item { Id = 2, Name = "jkl" },
new Item { Id = 1, Name = "mno" },
new Item { Id = 3, Name = "pqr" }
};
gives
1 abc
1 ghi
2 jkl

Add two objects in the same list

I have a list of objects. Each object contains an ID and a value. There are several objects which have the same ID but different values. How would I go about adding the values together based on the matching ID? Additionally, how would I go about removing one entry after the addition is complete?
The object in question:
public class MyObject
{
public int ID { get; set; }
public int Value { get; set; }
}
Below is where I am getting the duplicate objects from a list of all objects. I'm simply getting all the duplicate IDs into a list of strings and then grabbing the entire duplicate object in the duplicateObjects list.
List<MyObject> myObjects = GetMyObjectsList();
List<string> duplicateIds = myObjects.GroupBy(x => x.ID)
.Where(group => group.Count() > 1)
.Select(group => group.Key).ToList();
List<MyObject> duplicateObjects = myObjects.Where(x => duplicateIds.Contains(x.ID)).ToList();
I'm stuck on the last steps which are adding the duplicate values and then removing one of the duplicates. How would I accomplish this with Linq?
Assume the list look like this
var list = new MyObject[]
{
new MyObject {ID = 1, Value = 2},
new MyObject {ID = 2, Value = 2},
new MyObject {ID = 1, Value = 3},
new MyObject {ID = 4, Value = 4},
new MyObject {ID = 2, Value = 4},
};
Then just select from list group by ID and sum value like this
var result = (from tm in list
group tm by tm.ID into Test
select new
{
ID = Test.Key,
Value = Test.Sum(x => x.Value)
});
Output
ID = 1, Value = 5
ID = 2, Value = 6
ID = 4, Value = 4
working fiddle here
I hope I understood your question correctly.As clarified in the comment,you want to "sum" all the values grouped by ID and then remove one of the duplicates. Please verify if the following is how you could like it to be behave.
var duplicates = myObjects.GroupBy(x => x.ID)
.Select(group => new { Group = group.Count() > 1 ? group.ToList().Take(group.Count()-1) : group.ToList(),Sum=group.Count()});
For Input
Output
If you need to exclude non-duplicates, then you would need to include an additional Where Condition
var duplicates = myObjects.GroupBy(x => x.ID)
.Where(x=>x.Count()>1)
.Select(group => new { Group = group.Count() > 1 ? group.ToList().Take(group.Count()-1) : group.ToList(),Sum=group.Count()});

Group by in LINQ on a property in an array

I have an array for which I want to group the items based on a property. I tried the below code, but it is not grouping correctyly. MyArray is the array and Id is the property on which I want to do the grouping.
var docGroup = (from x in MyArray
group x by x.Id).Select(grp => new
{
Id = grp.Key,
Results = grp.ToList(),
})
.Results
.ToList());
To keep it simple if I just make it
var docGroup = from x in MyArray group x by x.Id;
where Id is a string "123" in the array and MyArray[2] has both the same Id. When I check the docGroup it has two entries and both have the 123 key instead of just one entry with the 123 key.
Here's a very simple example:
class Program
{
static void Main(string[] args)
{
Test[] tArray = new Test[3];
Test t = new Test() { Id = "123", Val="First" };
Test t1 = new Test() { Id = "123", Val="Second" };
Test t2 = new Test() { Id = "1234", Val="Third" };
tArray[0] = t;
tArray[1] = t1;
tArray[2] = t2;
var g = from x in tArray group x by x.Id;
}
}
class Test
{
public string Id { get; set; }
public string Val { get; set; }
}
Now if I look at g it has count 2 of which one is the Id 123 and the second is the Id 1234. I am not sure what is going wrong with my array. So this seems to work, but I am not sure what is going on with my array. I'll do some research on it.
Sorry guys, I found the issue. The Id was in a value property in MyArray which I was not using and so it was not grouping correctly. Thanks for the help everyone.
Everything works as expected.
GroupBy produces an enumerable of IGrouping. Since you have two distinct keys ("123" and "1234") you will get an enumerable of two elements. These grouping have a uniqe key and they're by themself enumerables.
So
g.Where(x => x.Key == "123").ToList();
will contain two elements (First, Second) and
g.Where(x => x.Key == "1233").ToList();
will contain one element (Third).

Select multiple fields group by and sum

I want to do a query with linq (list of objects) and I really don't know how to do it, I can do the group and the sum but can't select rest of the fields.
Example:
ID Value Name Category
1 5 Name1 Category1
1 7 Name1 Category1
2 1 Name2 Category2
3 6 Name3 Category3
3 2 Name3 Category3
I want to group by ID, SUM by Value and return all fields like this.
ID Value Name Category
1 12 Name1 Category1
2 1 Name2 Category2
3 8 Name3 Category3
Updated :
If you're trying to avoid grouping for all the fields, you can group just by Id:
data.GroupBy(d => d.Id)
.Select(
g => new
{
Key = g.Key,
Value = g.Sum(s => s.Value),
Name = g.First().Name,
Category = g.First().Category
});
But this code assumes that for each Id, the same Name and Category apply. If so, you should consider normalizing as #Aron suggests. It would imply keeping Id and Value in one class and moving Name, Category (and whichever other fields would be the same for the same Id) to another class, while also having the Id for reference. The normalization process reduces data redundancy and dependency.
void Main()
{
//Me being lazy in init
var foos = new []
{
new Foo { Id = 1, Value = 5},
new Foo { Id = 1, Value = 7},
new Foo { Id = 2, Value = 1},
new Foo { Id = 3, Value = 6},
new Foo { Id = 3, Value = 2},
};
foreach(var x in foos)
{
x.Name = "Name" + x.Id;
x.Category = "Category" + x.Id;
}
//end init.
var result = from x in foos
group x.Value by new { x.Id, x.Name, x.Category}
into g
select new { g.Key.Id, g.Key.Name, g.Key.Category, Value = g.Sum()};
Console.WriteLine(result);
}
// Define other methods and classes here
public class Foo
{
public int Id {get;set;}
public int Value {get;set;}
public string Name {get;set;}
public string Category {get;set;}
}
If your class is really long and you don't want to copy all the stuff, you can try something like this:
l.GroupBy(x => x.id).
Select(x => {
var ret = x.First();
ret.value = x.Sum(xt => xt.value);
return ret;
}).ToList();
With great power great responsibility comes. You need to be careful. Line ret.value = x.Sum(xt => xt.value) will change your original collection, as you are passing reference, not new object. If you want to avoid it, you need to add some Clone method into your class like MemberwiseClone (but again, this will create shallow copy, so be careful). Afer that just replace the line with: var ret = x.First().Clone();
try this:
var objList = new List<SampleObject>();
objList.Add(new SampleObject() { ID = 1, Value = 5, Name = "Name1", Category = "Catergory1"});
objList.Add(new SampleObject() { ID = 1, Value = 7, Name = "Name1", Category = "Catergory1"});
objList.Add(new SampleObject() { ID = 2, Value = 1, Name = "Name2", Category = "Catergory2"});
objList.Add(new SampleObject() { ID = 3, Value = 6, Name = "Name3", Category = "Catergory3"});
objList.Add(new SampleObject() { ID = 3, Value = 2, Name = "Name3", Category = "Catergory3"});
var newList = from val in objList
group val by new { val.ID, val.Name, val.Category } into grouped
select new SampleObject() { ID = grouped.ID, Value = grouped.Sum(), Name = grouped.Name, Category = grouped.Category };
to check with LINQPad:
newList.Dump();

Categories