Use Linq to Group a Sequence of Numbers with No Gaps

Use LINQ to group a sequence of numbers with no gaps

var array = new int[] { 1, 2, 3, 4, 7, 8, 11, 15, 16, 17, 18 };

var result = string.Join(",", array
.Distinct()
.OrderBy(x => x)
.GroupAdjacentBy((x, y) => x + 1 == y)
.Select(g => new int[] { g.First(), g.Last() }.Distinct())
.Select(g => string.Join("-", g)));

with

public static class LinqExtensions
{
public static IEnumerable<IEnumerable<T>> GroupAdjacentBy<T>(
this IEnumerable<T> source, Func<T, T, bool> predicate)
{
using (var e = source.GetEnumerator())
{
if (e.MoveNext())
{
var list = new List<T> { e.Current };
var pred = e.Current;
while (e.MoveNext())
{
if (predicate(pred, e.Current))
{
list.Add(e.Current);
}
else
{
yield return list;
list = new List<T> { e.Current };
}
pred = e.Current;
}
yield return list;
}
}
}
}

Use LINQ to group a sequence by date with no gaps

In this case I think that a standard foreach loop is probably more readable than a LINQ query:

var relatedActivities = new List<TActivity>();
bool found = false;

foreach (var item in activities.OrderBy(a => a.ActivityDate))
{
int count = relatedActivities.Count;
if ((count > 0) && (relatedActivities[count - 1].ActivityDate.Date.AddDays(1) != item.ActivityDate.Date))
{
if (found)
break;

relatedActivities.Clear();
}

relatedActivities.Add(item);
if (item.ID == activity.ID)
found = true;
}

if (!found)
relatedActivities.Clear();

For what it's worth, here's a roughly equivalent -- and far less readable -- LINQ query:

var relatedActivities = activities
.OrderBy(x => x.ActivityDate)
.Aggregate
(
new { List = new List<TActivity>(), Found = false, ShortCircuit = false },
(a, x) =>
{
if (a.ShortCircuit)
return a;

int count = a.List.Count;
if ((count > 0) && (a.List[count - 1].ActivityDate.Date.AddDays(1) != x.ActivityDate.Date))
{
if (a.Found)
return new { a.List, a.Found, ShortCircuit = true };

a.List.Clear();
}

a.List.Add(x);
return new { a.List, Found = a.Found || (x.ID == activity.ID), a.ShortCircuit };
},
a => a.Found ? a.List : new List<TActivity>()
);

Does Linq provide a way to easily spot gaps in a sequence?

A very simple approach to find the first number of the first gap would be the following:

int[] existingNumbers = /* extract all numbers from all filenames and order them */
var allNumbers = Enumerable.Range(0, 1000000);
var result = allNumbers.Where(x => !existingNumbers.Contains(x)).First();

This will return 1,000,000 if all numbers have been used and no gaps exist.

This approach has the drawback that it performs rather badly, as it iterates existingNumbers multiple times.

A somewhat better approach would be to use Zip:

allNumbers.Zip(existingNumbers, (a, e) => new { Number = a, ExistingNumber = e })
.Where(x => x.Number != x.ExistingNumber)
.Select(x => x.Number)
.First();

An improved version of DuckMaestro's answer that actually returns the first value of the first gap - and not the first value after the first gap - would look like this:

var tmp = existingNumbers.Select((x, i) => new { Index = i, Value = x })
.Where(x => x.Index != x.Value)
.Select(x => (int?)x.Index)
.FirstOrDefault();

int index;
if(tmp == null)
index = existingNumbers.Length - 1;
else
index = tmp.Value - 1;

var nextNumber = existingNumbers[index] + 1;

Grouping a sequence of numbers with no gaps in javascript using underscore.js

I would just do it with reduce and not worry about another library.

[1, 2, 3, 4, 7, 8, 11, 15, 16, 17, 18].reduce((arr, val, i, a) => {
if (!i || val !== a[i - 1] + 1) arr.push([]);
arr[arr.length - 1].push(val);
return arr;
}, []);

var result = [1, 2, 3, 4, 7, 8, 11, 15, 16, 17, 18].reduce((arr, val, i, a) => {  if (!i || val !== a[i - 1] + 1) arr.push([]);  arr[arr.length - 1].push(val);  return arr;}, []);
console.log(result);

C# apply sequence number to group items

You can GroupBy by Name, then for each group sets the InstanceID property:

var groups = objects.GroupBy(m => m.Name);

foreach (var group in groups)
{
int index = 0;
foreach (var item in group)
item.InstanceID = index++;
}

EDIT:

You can create extension methods ForEach receiving the item and the index:

static class Extensions
{
public static void ForEach<T>(this IEnumerable<T> collection, Action<T> action)
{
foreach (var item in collection)
action(item);
}

public static void ForEach<T>(this IEnumerable<T> collection, Action<T, int> action)
{
int index = 0;
foreach (var item in collection)
action(item, index++);
}
}

And use it like so:

var groups = objects.GroupBy(m => m.Name);
groups.ForEach(g => g.ForEach((o, i) => o.InstanceID = i));

Linq splitting group if there is missing number

See if this works. No for loop, just linq

            List<int> list = new List<int> { 1, 2, 3, 5, 6, 7, 9, 10};
List<int> splitIndex = list.Skip(1).Select((x,i) => new { x = x, i = i}).Where(x => list[x.i] + 1 != x.x).Select(x => x.i).ToList();
//add last index
splitIndex.Add(list.Count - 1);
var results = splitIndex.Select((x,i) => (i == 0) ? list.Take(x + 1).ToList() : list.Skip(splitIndex[i - 1] + 1).Take(splitIndex[i] - splitIndex[i - 1]).ToList()).ToList();

Use Linq to break a list by special values?

Simple loop would be good option.

Alternatives:

  • Enumerable.Aggregate and start new list on 0
  • Write own extension similar to Create batches in linq or Use LINQ to group a sequence of numbers with no gaps

Aggregate sample

var result = list.Aggregate(new List<List<int>>(),
(sum,current) => {
if(current == 0)
sum.Add(new List<int>());
else
sum.Last().Add(current);
return sum;
});

Note: this is only sample of the approach working for given very friendly input like {0,1,2,0,3,4}.

One can even make aggregation into immutable lists but that will look insane with basic .Net types.

Is it possible to group LINQ elements based on a sequence?

If I understand your question correctly, you want to replace the null values in the list with a value based on the first non-null value. I don't see why you'd need a second list of nulls for this. Here's an attempt to just modify the list in-place, although it's not much shorter than what you already have:

var A = new List<float?> { 0f, 1f, 2f, 5f, null, null, null, 7f, null, 8f };

for (int i = A.IndexOf(null); i != -1; i = A.IndexOf(null, i))
{
int j = 0;
do { j++; } while (A[i + j] == null);
float f = A[i + j].Value / (j + 1);
do { A[i++] = f; } while (j --> 0);
}

// A == { 0f, 1f, 2f, 5f, 1.75f, 1.75f, 1.75f, 1.75f, 4f, 4f }

The code repeatedly searches the list for a nulls (continuing where it left off when it found a null previously), counts the number of nulls next to each other, and then distributes the first non-null value across the gap. The code assumes that there is always a non-null value after each gap.

As pointed out in numerous comments, the use of LINQ does not provide any real advantage here.

Finding consecutive rows to group based on arbitrary

Using the GroupAdjacent by function described at https://stackoverflow.com/a/4682163/6137718 :

public static class LinqExtensions
{
public static IEnumerable<IEnumerable<T>> GroupAdjacentBy<T>(
this IEnumerable<T> source, Func<T, T, bool> predicate)
{
using (var e = source.GetEnumerator())
{
if (e.MoveNext())
{
var list = new List<T> { e.Current };
var pred = e.Current;
while (e.MoveNext())
{
if (predicate(pred, e.Current))
{
list.Add(e.Current);
}
else
{
yield return list;
list = new List<T> { e.Current };
}
pred = e.Current;
}
yield return list;
}
}
}
}

The result you want can be gotten like this. Note that I use string for year, but this can be changes to use int, if needed. Using the following class structure:

    class Entry
{
public Entry(string year, double service, double earnings)
{
this.Year = year;
this.Service = service;
this.Earnings = earnings;
}

public string Year;
public double Service;
public double Earnings;
}

You can get the result you desire by doing something like this:

var result = list.GroupAdjacentBy((x, y) => x.Service == 1 && y.Service == 1).
Select(g => new Entry(
g.First().Year.Substring(0,4) + g.Last().Year.Substring(4,4),
g.Sum(e => e.Service),
g.Sum(e => e.Earnings)));

An example of my code can be found at https://dotnetfiddle.net/RqmYa9 .

I'm unsure as to why, in your example result, the second entry has 1 for Service instead of 0.5. If you wanted all Services to be at least one, you can do a ternary in the query when you select the sum of Service.

Linq - Group then compare elements within each group

If you want something a bit more elegant, you can use the GroupAdjacent by function described at https://stackoverflow.com/a/4682163/6137718 :

public static class LinqExtensions
{
public static IEnumerable<IEnumerable<T>> GroupAdjacentBy<T>(
this IEnumerable<T> source, Func<T, T, bool> predicate)
{
using (var e = source.GetEnumerator())
{
if (e.MoveNext())
{
var list = new List<T> { e.Current };
var pred = e.Current;
while (e.MoveNext())
{
if (predicate(pred, e.Current))
{
list.Add(e.Current);
}
else
{
yield return list;
list = new List<T> { e.Current };
}
pred = e.Current;
}
yield return list;
}
}
}
}

We can use this to group all adjacent elements that have the same Val, after sorting by Id and Dt. Then from each group, we select the first one, as that represents the most recent change. The updated code would look something like this:

public IEnumerable<MyClass> GetUpdatedVals(List<MyClass> myVals, string groupName)
{
return myVals
.Where(v => v.GroupName == groupName)
.OrderBy(v => v.Id)
.ThenBy(v => v.Dt)
.GroupAdjacentBy((x, y) => x.Val == y.Val && x.Id == y.Id)
.Select(g => g.First());
}


Related Topics



Leave a reply



Submit