I am looking to extract ranges from an list of integers using linq:
for example I am looking to split the following list:
List<int> numberList = new List<int>() { 30, 60, 90, 120, 150, 180, 270, 300, 330 };
into a list of integer ranges that will look like:
{ 30, 180 }
{ 270, 330 }
ie: where the next seq is greater than 30
another example :
List<int> numberList = new List<int>() { 30, 60, 120, 150, 270, 300, 330 };
into a list of integer ranges that will look like:
{ 30, 60 }
{ 120, 150 }
{ 270, 330 }
I have tried with for loops to find the best way possible however I don't
know where to start trying to use a linq query to do this.
You could write a method to handle the split:
IEnumerable<IList<int>> SplitValues(IList<int> input, int difference = 30)
{
List<int> results = new List<int>();
int last = input.First();
foreach(var value in input)
{
if (value - last > difference)
{
yield return new[] {results.First(), results.Last()};
results = new List<int>();
}
results.Add(value);
last = value;
}
yield return new[] {results.First(), results.Last()};
}
This matches your specifications as described, returning:
{ 30, 60 }
{ 120, 150 }
{ 270, 330 }
Note that a single value within the collection without a range will be duplicated. For example, { 30, 120, 150 } will return:
{ 30, 30 }
{ 120, 150 }
You can do this in one linq statement:
var numberList = new List<int>() { 30, 60, 120, 150, 270, 300, 330 };
var section = 0;
var result = numberList
.Select( (x, i) => new {value = x, section = (i == 0 ? 0 : ((x - numberList[i - 1]) > 30 ? ++section : section))})
.GroupBy(x => x.section)
.Select(x => x.Select(v => v.value).ToList()).ToList();
Well. There are many ways to do so and all have their pros and cons.
So here's yet another solution, hope it will be helpful to someone.
public static IEnumerable<TSource[]> ToRanges<TSource>(
this IEnumerable<TSource> source, Func<TSource, TSource, TSource, bool> isNear)
{
List<TSource[]> result = source./*OrderBy(value => value).*/Aggregate(
new List<TSource[]> { new[] { source.First(), source.First() } },
(ranges, currentValue) => {
TSource[] currentRange = ranges.Last();
TSource previousValue = currentRange[1];
if (isNear(currentRange[0], previousValue, currentValue))
currentRange[1] = currentValue;
else
ranges.Add(new[] { currentValue, currentValue});
return ranges;
}
);
return result;
}
Example usage:
List<int> numbers = new List<int>() { 30, 60, 90, 120, 150, 180, 270, 300, 330 };
// split by max difference
numberList.ToRanges(
(first, previous, current) => current - previous <= 30).ToArray();
// { 30, 180 }
// { 270, 330 }
// split by max range
numberList.ToRanges(
(first, previous, current) => current - first <= 90).ToArray();
// { 30, 120 }
// { 150, 180 }
// { 270, 330 }
In addition, you can split not only integers but also, for example, words by their first letter. Or DateTime/TimeSpan. Or whatever you want.
You must use LINQ? If not, what about:
List<int> numberList = new List<int>() { 30, 60, 120, 150, 270, 300, 330 };
Dictionary<int, int> result = new Dictionary<int, int>();
int lastStart = numberList.First();
for(int i=1; i < numberList.Count; i++)
{
if(numberList[i] >= lastStart + 30)
{
result.Add(lastStart, numberList[i]);
if (i == numberList.Count - 1) break;
lastStart = numberList[i + 1];
i++;
}
}
foreach (var item in result)
{
Console.WriteLine("{{{0}, {1}}}", item.Key, item.Value);
}
You can use TakeWhile and add the result to another list
void SplitByRange()
{
List<int> numberList = new List<int>() { 30, 60, 120, 150, 270, 300, 330 };
IEnumerable<int> aux = new List<int>();
int n = numberList.First();
int skip = 0;
List<List<int>> output = new List<List<int>>();
while ((aux = numberList.Skip(skip).TakeWhile(o => { bool r = (o - n) <= 30; n = o; return r; })).Count() > 0)
{
output.Add(aux.ToList());
skip += aux.Count();
}
}
At the end numberList will be empty and output will be a list of lists.
output[0] // { 30, 60 }
...
The current code will required at least 1 element on the list, and if you have
{ 30, 100 }
It will return as two lists with 1 element in each
{ 30 }
{ 100 }
Try this:
private static List<int[]> GetGroups(List<int> numberList)
{
List<List<int>> groups = new List<List<int>>();
numberList.Zip(numberList.Skip(1), (a, b) =>
{
if ((b - a) == 30)
{
if (groups.Count == 0)
groups.Add(new List<int>());
groups[groups.Count - 1].Add(a);
}
else if (a == b)
{
groups[groups.Count - 1].Add(a);
}
else
{
groups[groups.Count - 1].Add(a);
groups.Add(new List<int>());
}
return a;
}).ToList();
groups[groups.Count - 1].Add(numberList.Last());
return groups.Select(g => new[] { g.First(), g.Last() }).ToList();
}
Sample usage:
//List<int> numberList = new List<int>() { 30, 60, 90, 120, 150, 180, 270, 300, 330 };
List<int> numberList = new List<int>() { 30, 60, 120, 150, 270, 300, 330 };
var result = GetGroups(numberList);
Related
I have the following method which returns an IList<IGrouping<bool, int>>, I was wondering if it was possible to do this using LINQ.(Test case beneath I wrote to see what should happen, small numbers (<=100) should have a 'yes' boolean and big numbers (>100) should have a 'no' boolean.
public IList<IGrouping<bool, int>> GroupSmallAndBigNumbers(int[] numbers)
{
}
[Test]
public void NumbersSmallerThanOrEqualTo100AndBiggerNumbersCanBeGroupedUsingGroupBy()
{
//Arrange
int[] numbers = { 5, 700, 15, 108, 25, 28, 100 };
int[] expectedSmallNumbers = { 5, 15, 25, 28, 100};
int[] expectedBigNumbers = { 700, 108 };
//Act
var results = _examples.GroupSmallAndBigNumbers(numbers);
//Assert
Assert.That(results.Count, Is.EqualTo(2));
var smallNumbers = results[0];
Assert.That(smallNumbers, Is.EquivalentTo(expectedSmallNumbers));
var bigNumbers = results[1];
Assert.That(bigNumbers, Is.EquivalentTo(expectedBigNumbers));
}
You could group by checking if each number is greater than 100.
public IList<IGrouping<bool, int>> GroupSmallAndBigNumbers(int[] numbers)
=> numbers.GroupBy(x => x >= 100).ToList();
I think a Dictionary would work better (and easier) for you. Using that, you can easily do something like this:
int[] numbers = { 5, 700, 15, 108, 25, 28, 100 };
Dictionary<int, bool> dict = new Dictionary<int, bool>();
foreach (int number in numbers)
{
if (number > 100)
dict.Add(number, true);
else
dict.Add(number, false);
}
I have the following code
TimeSpan[] hours = new[] {
new TimeSpan(10,35,50),
new TimeSpan(10,36,48),
new TimeSpan(10,41,48),
new TimeSpan(10,47,58),
new TimeSpan(10,49,14),
new TimeSpan(11,22,15),
new TimeSpan(11,24,18),
new TimeSpan(11,25,25),
};
I want to group the hours and minutes by 5 minutes.
I want to get the result below
1st Group
**10:35:50
10:36:48
10:41:48**
2nd Group
**10:47:58
10:49:14**
3rd Group
**11:22:15
11:24:18
11:25:25**
I have tried the following code but I cannot get the exact result that I need
var query = (from x in hours select x)
.GroupBy(r=> r.Ticks / TimeSpan.FromMinutes(5).Ticks));
You can try to implement a simple loop instead of Linq:
Code:
private static IEnumerable<TimeSpan[]> MyGrouping(IEnumerable<TimeSpan> source,
double minutes) {
List<TimeSpan> list = new List<TimeSpan>();
foreach (TimeSpan item in source.OrderBy(x => x)) {
// shall we start a new group?
if (list.Any() && (item - list.Last()).TotalMinutes > minutes) {
// if yes, return the previous
yield return list.ToArray();
list.Clear();
}
list.Add(item);
}
if (list.Any())
yield return list.ToArray();
}
Demo:
var result = MyGrouping(hours, 5)
.Select((list, index) => $"Group {index + 1}: [{string.Join(", ", list)}]");
Console.Write(string.Join(Environment.NewLine, result));
Outcome:
Group 1: [10:35:50, 10:36:48, 10:41:48]
Group 2: [10:47:58, 10:49:14]
Group 3: [11:22:15, 11:24:18, 11:25:25]
You could do this by using Aggregate to determine which "group" each timespan would belong to and then GroupBy that group
TimeSpan[] hours = new[] {
new TimeSpan(10,35,50),
new TimeSpan(10,36,48),
new TimeSpan(10,41,48),
new TimeSpan(10,47,58),
new TimeSpan(10,49,14),
new TimeSpan(11,22,15),
new TimeSpan(11,24,18),
new TimeSpan(11,25,25),
};
var query = hours.Aggregate(new List<(int group, TimeSpan ts)>(), (acc,curr) => {
if(acc.Count == 0)
acc.Add((0,curr));
else
{
var (lastGroup,lastTs) = acc.Last();
if(curr.Subtract(lastTs).TotalMinutes <= 5)
acc.Add((lastGroup,curr));
else
acc.Add((lastGroup+1,curr));
}
return acc;
}).GroupBy(x => x.group, y => y.ts);
foreach(var item in query)
Console.WriteLine(String.Join(", ", item));
Output is
10:35:50, 10:36:48, 10:41:48
10:47:58, 10:49:14
11:22:15, 11:24:18, 11:25:25
Live example: https://dotnetfiddle.net/zXhw0g
Good question here on Aggregate explained to help you understand it.
You can write a method to group the items like so:
public static IEnumerable<List<TimeSpan>> GroupItemsWithin(IEnumerable<TimeSpan> times, TimeSpan maxDelta)
{
var previous = TimeSpan.MinValue;
var spans = new List<TimeSpan>();
foreach (var span in times)
{
if (previous == TimeSpan.MinValue || (span - previous) <= maxDelta)
{
spans.Add(span);
}
else if (spans.Count > 0)
{
yield return spans;
spans = new List<TimeSpan>{ span };
}
previous = span;
}
if (spans.Count > 0)
yield return spans;
}
Then you can use it like this:
public static void Main()
{
TimeSpan[] hours = new[] {
new TimeSpan(10, 35, 50),
new TimeSpan(10, 36, 48),
new TimeSpan(10, 41, 48),
new TimeSpan(10, 47, 58),
new TimeSpan(10, 49, 14),
new TimeSpan(11, 22, 15),
new TimeSpan(11, 24, 18),
new TimeSpan(11, 25, 25),
};
foreach (var group in GroupItemsWithin(hours, TimeSpan.FromMinutes(5)))
{
Console.WriteLine(string.Join(", ", group));
}
}
Output:
10:35:50, 10:36:48, 10:41:48
10:47:58, 10:49:14
11:22:15, 11:24:18, 11:25:25
If you want to use Linq. Then this is the simplest solution Demo
TimeSpan[] hours = new[]
{ new TimeSpan(10, 35, 50),
new TimeSpan(10, 36, 48),
new TimeSpan(10, 41, 48),
new TimeSpan(10, 47, 58),
new TimeSpan(10, 49, 14),
new TimeSpan(11, 22, 15),
new TimeSpan(11, 24, 18),
new TimeSpan(11, 25, 25), };
int groupID = -1;
var result = hours.OrderBy(h => h).Select((item, index) =>
{
if (index == 0 || (!(Math.Abs(hours[index - 1].Subtract(item).TotalMinutes) <= 5)))
++groupID;
return new { group = groupID, item = item };
}).GroupBy(item => item.group);
I want to create same sized smaller arrays from a big array. But items should distribte randomly. I can distribute by order like following:
int[] source = new[] { 10, 20, 30, 40, 50, 60, 70, 80, 90 };
int i = 0;
int chunkSize = 3;
var result = source.GroupBy(s => i++ / chunkSize).Select(g => g.ToArray()).ToArray();
// [10,20,30][40,50,60][70,80,90]
But result should be random like: // [90,20,50][70,30,60][40,80,10]
Can I do it using linq?
The following implements the suggestions in the comments to add an OrderBy clause. There are, of course, other ways to achieve the result, but this is likely the simplest way using LINQ, as requested.
int[] source = new[] { 10, 20, 30, 40, 50, 60, 70, 80, 90 };
int i = 0;
int chunkSize = 3;
Random r = new Random();
var result = source.OrderBy(x => r.Next()).GroupBy(s => i++ / chunkSize).Select(g => g.ToArray()).ToArray();
You can shuffle any 1D array with this method.
If you modify it to return IList<T> instead of void, you can append it into your query:
var result = source.Shuffle().GroupBy(...)
You could do the following.
var arr = new int[] { 1, 1, 2, 6, 6, 7, 1, 1, 0 };
var sizeOfResultArray = 3;
var result = arr.ChunkBy(sizeOfResultArray);
The extension methods are defined as
public static class Extensions
{
private static Random rng = new Random();
public static IEnumerable<IEnumerable<T>> ChunkBy<T>(this IEnumerable<T> source, int chunkSize)
{
return source.Shuffle()
.Select((x, i) => new { Index = i, Value = x })
.GroupBy(x => x.Index / chunkSize)
.Select(x => x.Select(v => v.Value).ToList())
.ToList();
}
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> list)
{
var collection = list.ToList();
int n = collection.Count();
while (n > 1)
{
n--;
int k = rng.Next(n + 1);
T value = collection[k];
collection[k] = collection[n];
collection[n] = value;
}
return collection;
}
}
The Shuffle is a Fisher-Yates shuffle implementation by Grenade here
You need to randomize and then group. Code below should work
int[] source = new[] { 10, 20, 30, 40, 50, 60, 70, 80, 90 };
Random rand = new Random();
int[] random = source.Select(x => new { num = x, rand = rand.Next() }).OrderBy(x => x.rand).Select(x => x.num).ToArray();
int groupsize = 3;
int[][] groups = random.Select((x, i) => new { num = x, index = i })
.GroupBy(x => x.index / groupsize)
.Select(x => x.Select(y => y.num).ToArray())
.ToArray();
I am attempting to speed up the following LINQ Object query:
var setOfCodes = codeList1
.SelectMany(q => q.Codes)
.Union(codeList2.SelectMany(q => q.Codes)
.Union(codeList3.SelectMany(q => q.Codes)
.ToList();
Where
codeListX is a List<Item>
and
public Item {
public List<int> Codes = new List<int>();
}
Example:
var codeList1 = new List<Item> {
new Item {
Codes = new List<int> {
100,
105,
110
}
},
new Item {
Codes = new List<int> {
100,
110,
115
}
},
};
var codeList2 = new List<Item> {
new Item {
Codes = new List<int> {
150,
155,
160
}
},
new Item {
Codes = new List<int> {
150,
155,
170
}
},
};
And the output should be (Not fussed about order, I can sort later):
100, 105, 110, 115, 150, 155, 160, 170
IE: Outputs a list containing all the codes that appear within the codeListX's.
Is there a faster way to do this?
You can write it like this :
var setOfCodes = new[] { codeList1, codeList2, codeList3 }
.SelectMany(x => x)
.SelectMany(x => x.Codes)
.Distinct()
.ToList();
I have a
list<int> = {14, 24, 56,189,909,1000};
I want to collapse (group?) them by a range such that the ints that fall within the range of each other are collapse into one value.
So the results should be for range = 100
{14,24,56} //since they 24 falls within 100 of 14 and 56 falls within 100 of 24
{189}
{909, 1000} //since they fall within 100 of each other
I know this is possible using a linq group by but I am stumped by the syntax.
I have looked at this answer but cannot figure out what to use for the ranges, since I have only one range i.e. 100.
int[] values = {100, 110, 120, 130, 140, 150, 160, 170};
int[] ranges = {115, 145, 180};
var query = from value in values
group value by ranges.Where(x => value >= x)
.DefaultIfEmpty()
.Last();
foreach (var group in query)
{
Console.WriteLine("{0}: {{{1}}}", group.Key,
string.Join(", ", group));
}
Your best option is using a plain old for loop instead of linq:
var l = new[] { 14, 24, 56, 189, 909, 1000 };
var groups = new List<List<int>>();
groups.Add(new List<int>());
groups[0].Add(l[0]);
for (int i = 1; i < l.Length; i++)
{
if (l[i] - l[i - 1] > 100)
{
groups.Add(new List<int>());
}
groups[groups.Count - 1].Add(l[i]);
}
Edit: This may not apply, especially with the additional requirement in the comment as it 1) starts each group only at one point and 2) would place 150 only in the first group.
I would probably write it as so because of the "dynamic" range (and I'm not sure how Group By could be used without an equally involved bucket process). This function requires that the input is already sorted.
IEnumerable<IEnumerable<int>> GroupByStartingRange (IEnumerable<int> src) {
int? maybeStart;
while ((maybeStart = src.FirstOrDefault() != null) {
if (maybeStart.HasValue) {
var start = maybeStart.Value;
yield return src.TakeWhile(x => x <= start + 100)
src = src.SkipWhile(x => x <= start + 100);
}
}
}
Assuming your ranges are ordered:
int[] values = { 100, 110, 120, 130, 140, 150, 160, 170 };
int[] ranges = { 115, 145, 180 };
var groups = values.GroupBy(x => ranges.First(r => x <= r));