Get elements of an IEnumerator to a list - c#

I'm new to c# so go easy on me. Anyways, I made a list of numbers
List<int> numbers = new List<int>();
and I want to make a list of each number and its count/frequency.
var grouped = numbers
.GroupBy(i => i)
.Select(i => new { Number = i.Key, Count = i.Count() });
In locals, I can see the group, which has an IEnumerator interface with all of the numbers and their count values image of what I'm talking about. So is there a way to make a list with the numbers and their frequency/count?
Thank you.

IEnumerable<T> is a sequence so it doesn't own a count. But Enumerable.Count is an extension method of IEnumerable<T>
That is, you don't necessarily need to convert an IEnumerable<T> into a List<T>:
var grouped = numbers
.GroupBy(i => i)
.Select(i => new { Number = i.Key, Count = i.Count() });
var groupedCount = grouped.Count();
// You may iterate grouped
foreach(var value in grouped)
{
Console.WriteLine($"{value.Number} {value.Count}");
}
If you really need List<T> semantics, you just need to call Enumerable.ToList:
var grouped = numbers
.GroupBy(i => i)
.Select(i => new { Number = i.Key, Count = i.Count() })
.ToList();
In the other hand, you may directly convert everything into a string as follows:
var groupText = string.Join("\n", numbers
.GroupBy(i => i)
.Select(i => $"Number: {i.Key} Count: {i.Count()}"))

To get a list, you just need to call ToList(), for example:
var grouped = numbers
.GroupBy(i => i)
.Select(i => new { Number = i.Key, Count = i.Count() })
.ToList();
However, you really don't need to do that, you can simply loop over the enumerable as it stands:
foreach(var item in grouped)
{
Console.WriteLine($"{item.Number} occurs {item.Count} times");
}

Sounds like you want ToDictionary with the number as key and the frequency as value:
var grouped = numbers
.GroupBy(i => i)
.Select(i => new { Number = i.Key, Count = i.Count() })
.ToDictionary(x => x.Number, x => x.Count);
Now you can easily print every number and its frequency by looping the dictionary.
In fact you don´t even need neither ToDictionary nor your Select, as the IGrouping returned from GroupBy also derives from IEnumerable which is why you can iterate over it.
foreach(var g in grouped = numbers.GroupBy(i => i))
{
var number = g.Key;
var freq = g.Count();
}

Related

How to rank elements in c# especially when it has duplicates

I have a requirement to rank the array elements and the array has duplicate values. I tried following this Ranking items in a list with LINQ but this doesn't work when the array has duplicate values in it. Any easy way to do it in c#?
For Example :
input = [650,150,150,200]
output = [1,3,3,2]
For Example :
input = [650,200,200,150]
output = [1,2,2,3]
Update: The requirement is as below, what if I add one more element to the array
Ex: [650,150,150,200,100] output needs to be [1,3,3,2,5] instead of [1,3,3,2,4]
You can create a dictionary as rank-lookup source:
int[] array = new[] {650,150,150,200};
Dictionary<int, int> numRanks = array
.GroupBy(i => i)
.OrderByDescending(g => g.Key)
.Select((g, index) => (num:g.Key, rank:index+1))
.ToDictionary(x => x.num, x => x.rank);
int[] result = array.Select(i => numRanks[i]).ToArray();
For your updated requirement you could use a similar approach using a Lookup<TKey, TValue>:
var rankLookup = array
.OrderByDescending(i => i)
.Select((num, index) => (num, index))
.ToLookup(x => x.num, x => x.index + 1);
int[] result = array.Select(i => rankLookup[i].First()).ToArray();
The lookup is like a dictionary that allows duplicate keys. You need to use First here because you are just interested in the rank. If you'd use Count() you'd know how many duplicates it had.
You could create an array of items, distinct and in order, then use the indices to determine the rank of each item.
var ranks = input.Distinct().OrderByDescending(x => x).ToArray();
var ranked = input.Select(x => Array.IndexOf(ranks, x) + 1);
Working example
Update after comment
If rankings need to be skipped, just remove the Distinct:
var ranks = input.OrderByDescending(x => x).ToArray();
var ranked = input.Select(x => Array.IndexOf(ranks, x) + 1);
Array.IndexOf will take the first element when there are duplicates.
Working example

How to OrderBy() as per the order requested in distinct string list

How to use OrderBy for shaping output in the same order as per the requested distinct list
public DataCollectionList GetLatestDataCollection(List<string> requestedDataPointList)
{
var dataPoints = _context.DataPoints.Where(c => requestedDataPointList.Contains(c.dataPointName))
.OrderBy(----------) //TODO: RE-ORDER IN THE SAME ORDER AS REQUESTED requestedDataPointList
.ToList();
dataPoints.ForEach(dp =>
{
....
});
}
Do the sorting on the client side:
public DataCollectionList GetLatestDataCollection(List<string> requestedDataPointList)
{
var dataPoints = _context.DataPoints.Where(c => requestedDataPointList.Contains(c.dataPointName))
.AsEnumerable()
.OrderBy(requestedDataPointList.IndexOf(c.dataPointName));
foreach (var dp in dataPoints)
{
....
});
}
NOTE: Also, I don't think ToList().ForEach() is ever better than foreach ().
It think the fastest method is to join the result back with the request list. This makes use of the fact that LINQ's join preserves the sort order of the first list:
var dataPoints = _context.DataPoints
.Where(c => requestedDataPointList.Contains(c.dataPointName))
.ToList();
var ordered = from n in requestedDataPointList
join dp in dataPoints on n equals dp.dataPointName
select dp;
foreach (var dataPoint in ordered)
{
...
}
This doesn't involve any ordering, joining does it all, which will be close to O(n).
Another fast method consists of creating a dictionary of sequence numbers:
var indexes = requestedDataPointList
.Select((n, i) => new { n, i }).ToDictionary(x => x.n, x => x.i);
var ordered = dataPoints.OrderBy(dp => indexes[dp.dataPointName]);

Is it possible to implement a "Smaller Numbers than Current" using a single LINQ query?

Was doing this problem https://leetcode.com/problems/how-many-numbers-are-smaller-than-the-current-number/submissions/
Input: nums = [8,1,2,2,3]
Output: [4,0,1,1,3]
Explanation: For
nums[0]=8 there exist four smaller numbers than it (1, 2, 2 and 3).
For nums[1]=1 does not exist any smaller number than it. For nums[2]=2
there exist one smaller number than it (1). For nums[3]=2 there exist
one smaller number than it (1). For nums[4]=3 there exist three
smaller numbers than it (1, 2 and 2).
as LINQ-y as possible and came up with a solution which is only half LINQ :(
public class Solution {
public int[] SmallerNumbersThanCurrent(int[] nums) {
var groups = nums
.Select((val, index) => new { index, val })
.GroupBy(x => x.val)
.OrderBy(g => g.Key)
.Select(g => g.Select(x => x.index).ToArray());
var arr = new int[nums.Length];
int numSmaller = 0;
foreach (var indices in groups)
{
foreach (var index in indices)
{
arr[index] = numSmaller;
}
numSmaller += indices.Length;
}
return arr;
}
}
Is anyone here clever enough to help me figure out a way to LINQ-ify the second half of the solution? Preferably O(n log n) as code I have.
I hope I understood your question. You could do the following.
public int[] SmallerNumbersThanCurrent(int[] nums)
{
return nums.Select(x=> nums.Count(c=> c<x)).ToArray();
}
Though I don't think using one LINQ is a good idea here, it is possible to get rid of the foreach you have like this, assuming approximate nlog(n) is required:
nums.Select((num, index) => new { num, index })
// order number
.OrderBy(x => x.num)
// select number with their original index in nums and
// their order in the ordered collection
.Select((x, order) => new { x.num, x.index, order })
// Group the result by number
.GroupBy(x => x.num)
// Consolidate order in the ordered collection by selecting the minimum
// possible order
.Select(g => new
{
numWithOrder = g.Select(_ => new
{
num = _,
minOrder = g.First().order
})
})
// Flatten the collection
.SelectMany(g => g.numWithOrder)
// There should be minOrder number of results in the original collection
// are smaller than the number
.Select(x => new { x.num.index, result = x.minOrder })
// Restore as per original index
.OrderBy(x => x.index)
// Select final result
.Select(x => x.result)
As you might have seen, LINQ kills the readability of the code.
Here is another solution. It uses the Scan extension method from the System.Interactive package, for counting by accumulation the numbers that are smaller than the numbers of the current group.
public int[] SmallerNumbersThanCurrent(int[] nums)
{
return nums
.Select((x, i) => (Item: x, Index: i))
.GroupBy(x => x.Item, x => x.Index)
.OrderBy(g => g.Key)
.Scan(seed: (Indices: Enumerable.Empty<int>(), Counter: 0),
accumulator: (acc, x) => (x, acc.Counter + acc.Indices.Count()))
.SelectMany(acc => acc.Indices,
(acc, element) => (Index: element, CountOfSmallerNumbers: acc.Counter))
.OrderBy(x => x.Index)
.Select(x => x.CountOfSmallerNumbers)
.ToArray();
}
This solution is arguably even more obscure and unreadable than weichch's solution. 😃
The signature of the Scan extension method:
public static IEnumerable<TAccumulate> Scan<TSource, TAccumulate>(
this IEnumerable<TSource> source, TAccumulate seed,
Func<TAccumulate, TSource, TAccumulate> accumulator);
Generates a sequence of accumulated values by scanning the source sequence and applying an accumulator function.
public int[] SmallerNumbersThanCurrentShorter(int[] nums)
{
return (from x in nums select (from y in nums where y < x select y).Count()).ToArray();
}
just do this!!

how do i group a List<int> and return a List<int>

I have a list of ints. I want to group the list and create a new list that contains only the grouped by number that meets a certain condition. this is what i have so far. the declarating for membersList is List
int rows = 5;
List<int> memberKeys = memberKeysList
.GroupBy(x => x)
.Where(x => x.Count() == rows)
.ToList();
Its complaining about converting from groupedby list to a list.
You need to Select the Key to get the number like:
List<int> memberKeys = memberKeysList.GroupBy(x => x)
.Where(x => x.Count() == rows)
.Select(grp => grp.Key)
.ToList();
If you are not going to explicitly select they Key (or the number), then the result of GroupBy clause would be IEnumerable<IGrouping<TKey, TElement>>

Split query into multiple queries and then join the results

I have this function below that takes a list of id's and searches the DB for the matching persons.
public IQueryable<Person> GetPersons(List<int> list)
{
return db.Persons.Where(a => list.Contains(a.person_id));
}
The reason I need to split this into four queries is because the query can't take more than 2100 comma-separated values:
The incoming tabular data stream (TDS) remote procedure call (RPC) protocol stream is incorrect. Too many parameters were provided in this RPC request. The maximum is 2100.
How can I split the list into 4 pieces and make a query for each list. Then join the results into one list of persons?
Solved
I don't want to post it as an own answer and take cred away from #George Duckett's answer, just show the solution:
public IQueryable<Person> GetPersons(List<int> list)
{
var persons = Enumerable.Empty<Person>().AsQueryable<Person>();
var limit = 2000;
var result = list.Select((value, index) => new { Index = index, Value = value })
.GroupBy(x => x.Index / limit)
.Select(g => g.Select(x => x.Value).ToList())
.ToList();
foreach (var r in result)
{
var row = r;
persons = persons.Union(db.Persons.Where(a => row.Contains(a.person_id)));
}
return persons;
}
See this answer for splitting up your list: Divide a large IEnumerable into smaller IEnumerable of a fix amount of item
var result = list.Select((value, index) => new { Index = index, Value = value})
.GroupBy(x => x.Index / 5)
.Select(g => g.Select(x => x.Value).ToList())
.ToList();
Then do a foreach over the result (a list of lists), using the below to combine them.
See this answer for combining the results: How to combine Linq query results
I am not sure why you have a method like this. What exactly are you trying to do. Anyway you can do it with Skip and Take methods that are used for paging.
List<Person> peopleToReturn = new List<Person>();
int pageSize = 100;
var idPage = list.Skip(0).Take(pageSize).ToList();
int index = 1;
while (idPage.Count > 0)
{
peopleToReturn.AddRange(db.Persons.Where(a => idPage.Contains(a.person_id)).ToList());
idPage = list.Skip(index++ * pageSize).Take(pageSize).ToList();
}

Categories

Resources