Group and separate list - c#

I have below entity structure
public class Item
{
public EnumType Type { get; set; }
public int Price { get; set; }
}
public enum EnumType
{
A =1,
B=2,
C =3
}
I have a list of items as follow
var items = new List<Item>
{
new Item{ Price=5, Type= EnumType.B},
new Item{ Price=5, Type= EnumType.B},
new Item{ Price=5, Type= EnumType.B},
new Item{ Price=10, Type= EnumType.B},
new Item{ Price=10, Type= EnumType.B},
new Item{ Price=10, Type= EnumType.B},
new Item{ Price=15, Type= EnumType.C},
new Item{ Price=15, Type= EnumType.C},
new Item{ Price=15, Type= EnumType.C},
new Item{ Price=15, Type= EnumType.C},
new Item{ Price=15, Type= EnumType.C}
};
If the price and type are same, based on type it need to exclude every nth item from the list and then calculate the sum.
i.e type B = 3, Type C = 4
Which means in above sample data, since there are 3 items each in type B once it group by price and type it need to exclude every 3rd item when calculate sum.
So sum for type B will be 5+5+10+10 and sum for type C will be 15+15+15+15
I tried using modular but seems its not the correct direction
I have tried this so far
static int GetFactorByType(EnumType t)
{
switch(t)
{
case EnumType.A:
return 2;
case EnumType.B:
return 3;
case EnumType.C:
return 4;
default:
return 2;
}
}
var grp = items.GroupBy(g => new { g.Type, g.Price }).Select(s => new
{
type= s.Key.Type,
price = s.Key.Price,
count = s.Count()
}).Where(d => d.count % GetFactorByType(d.type) == 0).ToList();

Here's one solve:
//track the type:nth element discard
var dict = new Dictionary<EnumType, int?>();
dict[EnumType.B] = 3;
dict[EnumType.C] = 4;
//groupby turns our list of items into two collections, depending on whether their type is b or c
var x = items.GroupBy(g => new { g.Type })
.Select(g => new //now project a new collection
{
g.Key.Type, //that has the type
SumPriceWithoutNthElement = //and a sum
//the sum is calculated by reducing the list based on index position: in where(v,i), the i is the index of the item.
//We drop every Nth one, N being determined by a dictioary lookup or 2 if the lookup is null
//we only want list items where (index%N != N-1) is true
g.Where((v, i) => (i % (dict[g.Key.Type]??2)) != ((dict[g.Key.Type] ?? 2) - 1))
.Sum(r => r.Price) //sum the price for the remaining
}
).ToList(); //tolist may not be necessary, i just wanted to look at it
It seemed to me like your question words and your example are not aligned. You said (and did in code):
If the price and type are same, based on type it need to exclude every nth item from the list and then calculate the sum. i.e type B = 3, Type C = 4
Which to me means you should group by Type and Price, so B/5 is one list, and B/10 is another list. But you then said:
Which means in above sample data, since there are 3 items each in type B once it group by price and type it need to exclude every 3rd item when calculate sum. So sum for type B will be 5+5+10+10
I couldn't quite understand this. To me there are 3 items in B/5, so B/5 should be a sum of 10 (B/5 + B/5 + excluded). There are 3 items in B/10, again, should be (B/10 + B/10 + excluded) for a total of 20.
The code above does not group by price. It outputs a collection of 2 items, Type=B,SumWithout=30 and Type=C,SumWithout=60. This one groups by price too, it outputs a 3 item collection:
var x = items.GroupBy(g => new { g.Type, g.Price })
.Select(g => new
{
g.Key.Type,
g.Key.Price,
SumPriceWithoutNthElement =
g.Where((v, i) => (i % (dict[g.Key.Type]??2)) != ((dict[g.Key.Type] ?? 2) - 1))
.Sum(r => r.Price) }
).ToList();
The items are Type=B,Price=5,SumWithout=10 and Type=B,Price=10,SumWithout=20 and Type=C,Price=15,SumWithout=60
Maybe you mean group by type&price, remove every 3rd item (from b, 4th item from c etc), then group again by type only and then sum
This means if your type B prices were
1,1,1,1,2,2,2,2,2
^ ^
we would remove one 1 and one 2 (the Ines with arrows under them), then sum for a total of 9. This is different to removing every 3rd for all type b:
1,1,1,1,2,2,2,2,2
^ ^ ^
?
In which case, maybe group by Type/sum again the SumWithout output from my second example
I did consider that there might be a more efficient ways to do this without LINQ.. and it would nearly certainly be easier to understand the code if if were non LINQ - LINQ isn't necessarily a wonderful magic bullet that can kill all ptoblems, and even though it might look like a hammer with which every problem can be beaten, sometimes it's good to avoid
Depending on how you intended the problem to be solved (is price part of the group key or not) building a dictionary and accumulating 0 instead of th price every Nth element might be one way.. The other way, if price is to be part of the key, could be to sum all the prices and then subtract (count/N)*price from the total price

Grouping by a new object, which is always unique, guarantees you that you'll have as many groups as you have items. Try something like this:
var grp = items.GroupBy(g => $"{g.Type}/{g.Price}").Select(s => new
{
type= s.Value.First().Type,
price = s.Value.First().Price,
count = s.Value.Count()
}).Where(d => count % GetFactorByType(d.type) == 0).ToList();
This way, you group by a string composed from the type/price combination, so if the items are equivalent, the strings will be equal.
The $"{g.Type}/{g.Price}"string amounts to "B/5" for your first three item examples, so it's quite readable as well.

Related

Intersect two object lists on a common property and then compare a different property

I have two lists
List<objA> List1
List<objA> List2
I want to compare these two list on ID field, once a match is found I want to compare another field Distace amongst these two lists and grab the object with the lower value.
Using Linq isn't is not giving the result I want, atleast for the first part of the problem.
var test = List1.Select(x => x.ID)
.Intersect(List2.Select(y => y.ID));
Here's one way you could do this with Linq. Firstly, join the two lists together with Union. Then, group them by the Id field. Lastly, order those sub lists by Distance within the grouping, and take the first one of each to get a list of objects by Id with the minimum available distance.
var aList = new[]
{
new SomeObject() { Id = 1, Distance = 3 },
new SomeObject() { Id = 2, Distance = 5 }
};
var bList = new[]
{
new SomeObject() { Id = 1, Distance = 2 },
new SomeObject() { Id = 2, Distance = 6 }
};
var results = aList
.Union(bList)
.GroupBy(a => a.Id, a => a)
.Select(a => a.OrderBy(b => b.Distance).First());

Group Multiple Properties with Logical OR

I have a class with multiple properties, of which I'm interested in two. Say, PropA and PropB in the following example.
public class GroupByOR
{
public string PropA { get; set; }
public string PropB { get; set; }
public int SomeNumber { get; set; }
public GroupByOR(string a, string b, int num) { PropA = a; PropB = b; SomeNumber = num; }
}
And I would like to group a list of this class' objects, criteria being an item should fall into a group if either PropA or PropB matches.
For example, let's say my list looks like this:
List<GroupByOR> list = new List<GroupByOR>
{
new GroupByOR("CA", "NY", 1), // Item 1
new GroupByOR("CA", "OR", 2), // Item 2
new GroupByOR("NY", "OR", 5) // Item 3
};
Then my desired outcome is this:
Group 1: Items 1 and 2 (based on CA)
Group 2: Items 1 and 3 (based on NY)
Group 3: Items 2 and 3 (based on OR)
Looking around, I found this and many other examples, but they all seem to focus on grouping by multiple properties, but with an AND operation.
Is what I'm trying to achieve even possible?
Or else is join the way to go here? If so can you please direct me in the right direction?
You're going to end up with more items than you started with, so GroupBy isn't the whole answer. You need something like this:
List<GroupByOR> list = new List<GroupByOR>
{
new GroupByOR("CA", "NY", 1), // Item 1
new GroupByOR("CA", "OR", 2), // Item 2
new GroupByOR("NY", "OR", 5) // Item 3
};
var lookupA = list.ToLookup(e => e.PropA);
var lookupB = list.ToLookup(e => e.PropB);
var keys = lookupA.Select(e => e.Key)
.Concat(lookupB.Select(e => e.Key)).Distinct();
var result = keys.Select(e =>
new
{
Key = e,
Values = lookupA[e].Concat(lookupB[e]).Distinct()
});
This is projecting the result into a new anonymous type with Key being the value of PropA or PropB and Values being an IEnumerable<GroupByOr> of all the matching elements.
Edit: Code walkthrough
The first two linq lines are making a lookup (effectively a multi-valued dictionary) out of the enumeration, with the given key. These can be enumerated as IEnumerable<IGrouping<TKey, TValue>>, but can also be used for efficient lookups.
var lookupA = list.ToLookup(e => e.PropA);
var lookupB = list.ToLookup(e => e.PropB);
The next line is finding all the distinct values of PropA and PropB (using the lookups, but could have gone back to the list for this too).
var keys = lookupA.Select(e => e.Key).Concat(lookupB.Select(e => e.Key)).Distinct();
The last line is taking the distinct keys, and taking the matching enumerations from both the propA lookup and the propB lookup then concatenating them, and (now I've spotted another bug) de-duplicating them.
The select statement is producing an anonymous type - these types can't be referred to explicitly, but then can be assigned to a var and they can be enumerated. If you want to store the resulting value, you'd have to make a non-anonymous type.
var result = keys.Select(e =>
new
{
Key = e,
Values = lookupA[e].Concat(lookupB[e]).Distinct()
});
Edit: Output
CA
Values: (PropA: CA, PropB: NY, SomeNumber: 1) (PropA: CA, PropB: OR, SomeNumber: 2)
NY
Values: (PropA: NY, PropB: OR, SomeNumber: 5) (PropA: CA, PropB: NY, SomeNumber: 1)
OR
Values: (PropA: CA, PropB: OR, SomeNumber: 2) (PropA: NY, PropB: OR, SomeNumber: 5)

Get Rank from list of object depending upon some condition

I have list of object of class which contain totalScore as one property.I want to get rank of Team depending upon totalscore of team.Here is the list of object I called it as List data= new List();
so data contain object of scoreboard class with total score property.
I need rank of team depending upon totalscore.Here is the code that I try but it give result like Rank=1,2,2,3 but I need Rank=1,2,2,4 like this.
data.OrderByDescending(x => x.totalScore).GroupBy(x => x.totalScore)
.SelectMany((g, i) => g.Select(e => new { data = e.Rank = i + 1 }))
.ToList();
The data list contain unique team but there total score may be same so same totalscore team must be in one rank. please help me!
If you need to update the list in-place:
int i = 0;
decimal? prevValue = null;
foreach(var item in data.OrderByDescending(x => x.totalScore))
{
item.Rank = prevValue == item.totalScore ? i : ++i;
prevValue = item.totalScore;
}
A different notation (which I prefer for readability) but essentially the same answer as provided by user3185569.
var i = 1;
var results = (from d in data orderby d.totalScore descending select new { Obj = d, Rank = i++ } ).ToList();

Best way to join / sort two lists

I am trying to figure out what would be the best / fastest way to accomplish next task.
There is a list of int:
{ 4, 1, 112, 78 }
and there is a list of objects:
object { Id, Date, Value }
Rules:
{int list} contains Ids which are not sorted in any particular order
{int list} contains unknown number of elements
{object list} will always have only one Id occurrence in one particular day. There can be not one date with two same Ids (the list of object is already supplied like this). You could say that Id+Date represents a unique object.
JOIN Part: one day could have 1...n items, where 'n' represents the number of elements in {int list}. Requirement is that in final result all days have 'n' Ids. So if the day 1/1/2014 does not have item with Id=42, then a new item will be added to this list with Value=0.
SORT part: {object list} needs to be sorted by date, and then by Id, but the Id order must be the same as it is in {int list}.
What would be the best algorithm to accomplish this task? This is what I do currently:
// first I insert all the missing Ids
// to achieve this, I sorted lists so I now when to expect which Id
var orderedIntList = intList.OrderBy(x => x).ToList();
var orderedObjectList = objectList.OrderBy(x => x.Date).ThenBy(x => x.Id).ToList();
for (int i = 0; i < totalRecords; i++)
{
currentIndex = i % 4;
currentId = orderedIntList[currentIndex];
if (orderedObjectList.Count <= i || currentId != orderedObjectList[i].Id)
orderedObjectList.Insert(i, new Object { Date = currentDate, Id = currentId });
currentDate = orderedList[i].Date;
}
// then in order to have items sorted in original order, I use LINQ join
int counter = 0;
var aListWithIndex = activityIds.Select(x => new { Index = counter++, Id = x }).ToList();
return (from a in aListWithIndex
join b in orderedObjectList on a.Id equals b.Id
orderby b.Date, a.Index
select b
)
.ToList();

Speed of linq query grouping and intersect in particular

Say 3 lists exist with over 500,000 records and we need to perform a set of operations (subsets shown below):
1) Check for repeating ids in list one and two and retrieve distinct ids while Summing up "ValuesA" for duplicate ids and put results in a list. Lets call this list list12.
2) compare all the values with matching ids between list 3 list12 and print results say to console.
3) ensure optimal performance.
This what i have so far:
var list1 = new List<abc>()
{
new abc() { Id = 0, ValueA = 50},
new abc() { Id = 1, ValueA = 40},
new abc() { Id = 1, ValueA = 70}
};
var list2 = new List<abc>()
{
new abc() { Id = 0, ValueA = 40},
new abc() { Id = 1, ValueA = 60},
new abc() { Id = 3, ValueA = 20},
};
var list3 = new List<abc>()
{
new abc() { Id = 0, ValueA = 50},
new abc() { Id = 1, ValueA = 40},
new abc() { Id = 4, ValueA = 70},
};
1) with the help of the solution from here [link][1] I was able to resolve part 1.
var list12 = list2.GroupBy(i => i.Id)
.Select(g => new
{
Id = g.Key,
NewValueA = g.Sum(j => j.ValueA),
});
2)I cant seem to properly get the complete result set from this part. I can get the matching account numbers, maybe someone knows of a faster way other than hashsets, but I also need the ValueA from each list along with the matching account numbers.
foreach (var values in list3.ToHashSet().Select(i => i.ID).Intersect(list12.ToHashSet().Select(j => j.UniqueAccount)))
{
Console.WriteLine(values) //prints matching account number
//?? how do I get ValueA with from both lists with this in the quickest way possible
}
3) my only attempt at improving performance from reading online is to use hashsets as I seen in the attempt above but I may be doing this incorrectly and someone may have a better solution
I don't think that any conversion to HashSet, however efficient, will increase performance. The reason is that the lists must be enumerated to create the HashSets and then the HashSets must be enumerated to get to the results.
If you put everything in one LINQ statement the number of enumerations will be minimized. And by calculating the sums at the end the number of calculations is reduced to the absolute minimum:
list1.Concat(list2)
.Join(list3, x => x.Id, l3 => l3.Id, (l12,l3) => l12)
.GroupBy (x => x.Id)
.Select(g => new
{
Id = g.Key,
NewValueA = g.Sum(j => j.ValueA),
})
With your data this shows:
Id NewValueA
0 90
1 170
I don't know if I understood all requirements well, but this should give you the general idea.
If you want to get access to both elements you probably want a join. A join is a very general construct that can be used to construct all other set operations.

Categories

Resources