Grouping results "fairly" using LINQ - c#

I have a list of system users that are awaiting to be assigned with an account.
The assignment algorithm is very simple, assigning should be as fair as possible which means that if I have 40 accounts and 20 system users I need to assign 2 accounts per system user.
If I have 41 accounts and 20 system users I need to assign 2 accounts per system user and split the remaining accounts between the system users again (in this case, one system user will be assigned with one extra account).
I am trying to figure out how to do this while using a LINQ query.
So far I figured that grouping should be involved and my query is the following:
from account in accounts
let accountsPerSystemUser = accounts.Count / systemUsers.Count
let leftover = accounts.Count % systemUsers.Count
from systemUser in systemUsers
group account by systemUser into accountsGroup
select accountsGroup
However I am uncertain how to proceed from here.
I am positive that I am missing a where clause here that will prevent grouping if you reached the maximum amount of accounts to be assigned to a system user.
How do I implement the query correctly so that the grouping will know how much to assign?

Here is a simple implementation that works if you can restrict yourself to a IList<T> for the accounts (you can always use ToList though).
public static IEnumerable<IGrouping<TBucket, TSource>> DistributeBy<TSource, TBucket>(
this IEnumerable<TSource> source, IList<TBucket> buckets)
{
var tagged = source.Select((item,i) => new {item, tag = i % buckets.Count});
var grouped = from t in tagged
group t.item by buckets[t.tag];
return grouped;
}
// ...
var accountsGrouped = accounts.DistributeBy(systemUsers);
Basically this grabs each account's index and "tags" each with the remainder of integer division of that index by the number of system users. These tags are the indices of the system users they will belong to. Then it just groups them by the system user at that index.
This ensures your fairness requirement because the remainder will cycle between zero and one minus the number of system users.
0 % 20 = 0
1 % 20 = 1
2 % 20 = 2
...
19 % 20 = 19
20 % 20 = 0
21 % 21 = 1
22 % 22 = 2
...
39 % 20 = 19
40 % 20 = 0

You can't do this using "pure LINQ" (i.e. using query comprehension syntax), and to be honest LINQ probably isn't the best approach here. Nonetheless, here's an example of how you might do it:
var listB = new List<string>() { "a", "b", "c", "d", "e" };
var listA = new List<string>() { "1", "2", "3" };
var groupings = (from b in listB.Select((b, i) => new
{
Index = i,
Element = b
})
group b.Element by b.Index % listA.Count).Zip(listA, (bs, a) => new
{
A = a,
Bs = bs
});
foreach (var item in groupings)
{
Console.WriteLine("{0}: {1}", item.A, string.Join(",", item.Bs));
}
This outputs:
1: a,d
2: b,e
3: c

I don't thin "pure" LINQ is really suited to solve this problem. Nevertheless here is a solution that only requires two IEnumerable:
var users = new[] { "A", "B", "C" };
var accounts = new[] { 1, 2, 3, 4, 5, 6, 7, 8 };
var accountsPerUser = accounts.Count()/users.Count();
var leftover = accounts.Count()%users.Count();
var assignments = users
.Select((u, i) => new {
User = u,
AccountsToAssign = accountsPerUser + (i < leftover ? 1 : 0),
AccountsAlreadyAssigned =
(accountsPerUser + 1)*(i < leftover ? i : leftover)
+ accountsPerUser*(i < leftover ? 0 : i - leftover)
})
.Select(x => new {
x.User,
Accounts = accounts
.Skip(x.AccountsAlreadyAssigned)
.Take(x.AccountsToAssign)
});
To cut down on the text I use the term User instead of SystemUser.
The idea is quite simple. The first leftover users are assigned accountsPerUser + 1 from accounts. The remaining users are only assigned accountsPerUser.
The first Select uses the overload that provides an index to compute these values:
User | Index | AccountsAlreadyAssigned | AccountsToAssign
-----+-------+-------------------------+-----------------
A | 0 | 0 | 3
B | 1 | 3 | 3
C | 1 | 6 | 2
The second Select uses these values to Skip and Take the correct numbers from accounts.
If you want to you can "merge" the two Select statements and replace the AccountsAlreadyAssigned and AccountsToAssign with the expressions used to compute them. However, that will make the query really hard to understand.
Here is a "non-LINQ" alternative. It is based on IList but could easily be converted to IEnumerable. Or instead of returning the assignments as tuples it could perform the assignments inside the loop.
IEnumerable<Tuple<T, IList<U>>> AssignEvenly<T, U>(IList<T> targetItems, IList<U> sourceItems) {
var fraction = sourceItems.Count/targetItems.Count;
var remainder = sourceItems.Count%targetItems.Count;
var sourceIndex = 0;
for (var targetIndex = 0; targetIndex < targetItems.Count; ++targetIndex) {
var itemsToAssign = fraction + (targetIndex < remainder ? 1 : 0);
yield return Tuple.Create(
targetItems[targetIndex],
(IList<U>) sourceItems.Skip(sourceIndex).Take(itemsToAssign).ToList()
);
sourceIndex += itemsToAssign;
}
}

Related

MVC 5 How to bind linq result(Number) to array[i]?

Let's say i have a inventory slot range from 1 to 100 how to find the first empty slot from the range?
First, i'm thinking of using Array.FindIndex but i'm stuck at here.
var InventorySlot = (from i in db.InventoryModel where i.userId == 1 select i.slot).ToArray();
int index = Array.FindIndex(InventorySlot, item => item == null );
But this will create
InventorySlot[0] = 1
InventorySlot[1] = 3
InventorySlot[2] = 4
So how to create like this?
InventorySlot[1] = x
InventorySlot[3] = x
InventorySlot[4] = x
Perhaps i can use Array.FindIndex to locate the empty array? The index variable i need is 2(First empty number in the array)
What you need to do is compare the result from the database with the range without exceptions:
var firstEmptySlot = Enumerable.Range(1, InventorySlot.Max()).Except(InventorySlot).First();
If you want to get the largest slot plus one if no slot is empty, you can do:
var firstEmptySlot = Enumerable.Range(1, InventorySlot.Max()+1).Except(InventorySlot).First();
You would need to test firstEmptySlot in this case to make sure it is <= 100.
You could combine testing and the query by limiting the range to 1 - 100:
var firstEmptySlot = Enumerable.Range(1, Math.Min(InventorySlot.Max()+1, 100)).Except(InventorySlot).FirstOrDefault();
You will get a 0 back if no empty slots from 1 - 100 are available.

Best Algorithm to find intersection between 2 Intervals

I have a table in the database called Control:
Table structure:
Id | Name | MinValue (decimal) | MaxValue(decimal)
I have some restrictions on that table, one of it's restrictions is : no intersections.
Example : if the table has some values as follows:
row1 : 1 | Test1 | 1.3 | 2.5 //valid
row2 : 2 | Test2 | 3.3 | 4.5 // valid
row3 : 3 | Test3 | 5 | 6 // valid
Now if I want to add a new record, it must not intersect with any other row
Example:
row4 : 4 | Test4 | 5.1 | 10 //not valid since slot from 5 to 6 is reserved
row5 : 5 | Test5 | 1.0 | 1.4 // not valid since slot from 1.3 to 2.5 is reserved
I'm using this code, and it worked perfectly, but I wonder if there is a better solution and more efficient :
var allRows = db.Control.ToList();
var minValue = control.MinimumValue;
var maxValue = control.MaximumValue;
bool flag = true;
foreach(var row in allRows)
{
for(var i = minValue; i <= maxValue && flag ; i = decimal.Add( i , (decimal) 0.01))
{
if(i >= row.MinimumValue && i <= row.MaximumValue)
{
flag = false;
min = row.MinimumValue;
max = row.MaximumValue;
break;
}
}
}
if (flag)
{
//add
}
else
{
//intersection
}
Any suggestions ?
I think this is a O(LogN) issue...
Keep segments Ordered by their Start Value.
in a valid list s[i].end < s[i+1].start for any i
when inserting new segment, find it's position (the one that which start is closest (but lesser) than your new segment) call it i
if((seg[i-1].end < new.start) && (seg[i+1].start > new.end))
//OK to insert
else
// intersect
Let's assume this is the object you're trying to add :
var control = new Control()
{
Name = 'name',
MinValue = 5,
MaxValue = 6
};
You can do the following:
var biggerThanMinValue = db.Control.Count(x => x.MinValue >= control.MinValue) != 0;
var biggerThanMaxValue = db.Control.Count(x => x.MaxValue >= control.MaxValue) != 0;
if (!biggerThanMinValue && !biggerThanMinValue)
{
db.Control.Add(control); // or whatever your add operation is
}
By doing so you:
do NOT load the whole table in-memory -> performance gain terms of time, traffic and memory
let the database use it's data structures/algorithms to verify that the item can be added (the db should be able to optimize this request) -> another performance gain (cpu + time)
have clearer backend code
have less code to test
Edit: I suppose you could also ask the database to sort your table by min/max value and then make some validation (1 or 2 ifs), but the first approach is better, imo.

How to filter a List of int list with condition?

I have this List<List<int>>:
{{1,2},{1,3},{1,4},{2,3},{2,4},{3,4}}
In this list there are 6 list, which contain numbers from 1 to 4, and the occurrence of each number is 3;
I want to filter it in order to get:
{{1,2}{1,3}{2,4}{3,4}}
here the occurrence of each number is 2;
the lists are generated dynamically and I want to be able to filter also dynamically, base on the occurrence;
Edit-More Details
I need to count how many times a number is contain in the List<List<int>>, for the above example is 3. Then I want to exclude lists from the List<List<int>> in order to reduce the number of times from 3 to 2,
The main issue for me was to find a way to not block my computer :), and also to get each number appear for 2 times (mandatory);
Well if it's always a combination of 2 numbers, and they have to appear N times on the list, it means that depending on the N You gonna have:
4 (different digits) x 2 (times hey have to appear) = 8 digits = 4 pairs
4 x 3 (times) = 12 = 6 (pairs)
4 x 4 = 16 = 8 pairs
That means - that from 6 pairs we know we must select 4 pairs that best match the criteria
so based on the basic combinatorics (https://www.khanacademy.org/math/probability/probability-and-combinatorics-topic/permutations/v/permutation-formula)
we have a 6!/2! = (6*5*4*3*2*1)/(2*1)= 360 possible permutations
basically You can have 360 different ways how You put the the second list together.
because it doesn't matter how You arrange the items in the list (the order of items in the list) then the number of possible combinations is 6!/(2!*4!) = 15
https://www.khanacademy.org/math/probability/probability-and-combinatorics-topic/combinations-combinatorics/v/combination-formula
so the thing is - you have 15 possible answers to Your question.
Which means - you only need to loop over it for 15 times.
There are only 15 ways to chose 4 items out of the list of 6
seems like this is a solution to Your - "killing the machine" question.
so next question - how do we find all the possible 'combination'
Let's define all the possible items that we can pick from the input array
for example 1-st, 2-nd, 3-rd and 4-th..
1,2,3,4....... 1,2,3,5...... 1,2,3,6 ...
All the combinations would be (from here https://stackoverflow.com/a/10629938/444149)
static IEnumerable<IEnumerable<T>> GetKCombs<T>(IEnumerable<T> list, int length) where T : IComparable
{
if (length == 1) return list.Select(t => new T[] { t });
return GetKCombs(list, length - 1)
.SelectMany(t => list.Where(o => o.CompareTo(t.Last()) > 0),
(t1, t2) => t1.Concat(new T[] { t2 }));
}
and invoke with (because there are 6 items to pick from, who's indexed are 0,1,2,3,4 and 5)
var possiblePicks = GetKCombs(new List<int> { 0, 1, 2, 3, 4, 5 }, 4);
we get 15 possible combinations
so now - we try taking 4 elements out of the first list, and check if they match the criteria.. if not.. then take another combination
var data = new List<List<int>>
{
new List<int> { 1,2 },
new List<int> { 1,3 },
new List<int> { 1,4 },
new List<int> { 2,3 },
new List<int> { 2,4 },
new List<int> { 3,4 }
};
foreach (var picks in possiblePicks)
{
var listToTest = new List<List<int>>(4);
foreach (var i in picks)
listToTest.Add(data[i]);
var ok = Check(listToTest, 2);
if (ok)
break;
}
private bool Check(List<List<int>> listToTest, int limit)
{
Dictionary<int, int> ret = new Dictionary<int, int>();
foreach (var inputElem in listToTest)
{
foreach (var z in inputElem)
{
var returnCount = ret.ContainsKey(z) ? ret[z] : 0;
if (!ret.ContainsKey(z))
ret.Add(z, returnCount + 1);
else
ret[z]++;
}
}
return ret.All(p => p.Value == limit);
}
I'm sure this can be further optimized to minimize the amount of iterations other the 'listToTest'
Also, this is a lazy implementation (Ienumerable) - so if it so happens that the very first (or second) combination is successful, it stop iterating.
I accepted the Marty's answer because fixed my issue, any way trying to use his method for larger lists, I found my self blocking again my computer so I start looking for another method and I end it up with this one:
var main = new List<HashSet<int>> {
new HashSet<int> {1,2},
new HashSet<int> {1,3},
new HashSet<int> {1,4},
new HashSet<int> {2,3},
new HashSet<int> {2,4},
new HashSet<int> {3,4} };
var items = new HashSet<int>(from l in main from p in l select p); //=>{1,2,3,4}
for (int i =main.Count-1;i-->0; )
{
var occurence=items.Select(a=> main.Where(x => x.Contains(a)).Count()).ToList();
var occurenceSum = 0;
foreach(var j in main[i])
{
occurenceSum += occurence[j - 1];
if (occurenceSum==6) //if both items have occurence=3, then the sum=6, then I can remove that list!
{
main.RemoveAt(i);
}
}
}

Sum of one after another sequence

I have this scenario where I have to add the numbers inside a collection. An example would serve a best example.
I have these values in my database:
| Foo |
| 1 |
| 5 |
| 8 |
| 4 |
Result:
| Foo | Result |
| 1 | 1 |
| 5 | 6 |
| 8 | 14 |
| 4 | 18 |
As you can see, it has a somewhat fibonacci effect but the twist here is the numbers are given.
I can achieve this result with the help of for loop but is this possible doing in Linq. Like querying the database then having a result like above?
Any help would be much appreciated. Thanks!
I'm not sure how exactly you're touching the database, but here's a solution that can probably be improved upon:
var numbers = new List<int> { 1, 5, 8, 4 };
var result = numbers.Select((n, i) => numbers.Where((nn, ii) => ii <= i).Sum());
This overload of Select and Where takes the object (each number) and the index of that object. For each index, I used numbers.Where to Sum all the items with a lower and equal index.
For example, when the Select gets to the number 8 (index 2), numbers.Where grabs items with index 0-2 and sums them.
MoreLINQ has a Scan method that allows you to aggregate the values in a sequence while yielding each intermediate value, rather than just the final value, which is exactly what you're trying to do.
With that you can write:
var query = data.Scan((sum, next) => sum + next);
The one overload that you need here will be copied below. See the link above for details and additional overloads:
public static IEnumerable<TSource> Scan<TSource>(this IEnumerable<TSource> source,
Func<TSource, TSource, TSource> transformation)
{
if (source == null) throw new ArgumentNullException("source");
if (transformation == null) throw new ArgumentNullException("transformation");
return ScanImpl(source, transformation);
}
private static IEnumerable<T> ScanImpl<T>(IEnumerable<T> source, Func<T, T, T> f)
{
using (var i = source.GetEnumerator())
{
if (!i.MoveNext())
throw new InvalidOperationException("Sequence contains no elements.");
var aggregator = i.Current;
while (i.MoveNext())
{
yield return aggregator;
aggregator = f(aggregator, i.Current);
}
yield return aggregator;
}
}
I think you can achieve this with :
var acc = 0;
var result = numbers.Select(i =>
{
acc += i;
return acc;
}).ToList();
You need the ToList to be sure it will be run only once (otherwise the acc will keep growing).
Also I'm not sure it can be converted to a query (and performed server side).
Thomas Levesque posted a response to a similar question where he provide a SelectAggregate method who provide the intermediate values of an aggregate computation.
It's look like this feature is not present in Linq by default, so you probably will not be able to perform the computation server side using Linq.
You could do the following
int runningTotal = 0;
var runningTotals = numbers.Select(n => new
{
Number = n,
RunningTotal = (runningTotal += n)
});
This will give you the number and the running total.
It's just an adaption of #Jonesy's answer:
int[] ints = new[] {1, 5, 8, 4};
var result = ints.Select((x, y) => x + ints.Take(y).Sum());

How to Custom Group By multiple records according some criteria

please consider this scenario:
I have data like this :
Id Group Value
-----------------------------------
1 1 100
2 1 120
3 1 100
4 2 90
5 2 105
6 3 300
7 4 123
8 4 110
9 5 100
10 5 110
I want to do GroupBy on this data as 1,2,3 place in one group and 4,5 place in another group. How I can do this with LINQ?
thanks
Edit 1)
I want to place 1,2,3 in one group and 4,5 in another group and perform Count aggregation on them and I get 6 for first group and 4 for secnd group
How about this?
int[] groupA = { 1, 2, 3 };
int[] groupB = { 4, 5 };
var result = data
.GroupBy(a => groupA.Contains(a.Group) ? "A" :
groupB.Contains(a.Group) ? "B" :
"N/a")
.Select(a => new
{
KEY = a.Key,
VALUE = a.Count()
});
In this case you want a function which maps 1,2,3 to 1 lets say and 4,5 to 2 lets say, then func<int,int> grouper = i => (i -1) / 3 should suit your purpose for this data set.
The linq is then:
var result = data.GroupBy( r => (r.Group-1)/3,
g => g.Count());
Note: If this is going to be done database side you need to be careful as the division operator may not map as you want. I.e. in c# int division returns an int so gives you the integer part of the fraction, while the database may return a Double hence breaking the wanted grouping. In this case you will want something like r => ((r.Group -1) - (r.Group -1) %3 )/3 instead.
Thus grouping rule is completely unclear and you need only count of items, I think you don't need grouping at all.
int[] groups = { 1, 2, 3 }; // or { 4, 5 } for other groups
var groupsCount = items.Where(i => groups.Contains(i.Group))
.Count();
You can put this query into method (assume items is a field)
int GetItemsCountInGroups(params int[] groups)
{
return items.Where(i => groups.Contains(i.Group))
.Count();
}
Usage
int count = GetItemsCountInGroups(1, 2, 3); // 6
int anotherCount = GetItemsCountInGroups(4, 5); // 4

Categories

Resources