Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

LINQ to count Continues repeated items(int) in an int Array?

Here is an scenario of my question: I have an array, say:

{ 4, 1, 1, 3, 3, 2, 5, 3, 2, 2 }

The result should be something like this (array element => its count):

4 => 1
1 => 2
3 => 2
2 => 1
5 => 1
3 => 1
2 => 2

I know this can be achieved by for loop.

But google'd a lot to make this possible using lesser lines of code using LINQ without success.

like image 469
sandeep Avatar asked Jul 04 '12 13:07

sandeep


4 Answers

I believe the most optimal way to do this is to create a "LINQ-like" extension methods using an iterator block. This allows you to perform the calculation doing a single pass over your data. Note that performance isn't important at all if you just want to perform the calculation on a small array of numbers. Of course this is really your for loop in disguise.

static class Extensions {

  public static IEnumerable<Tuple<T, Int32>> ToRunLengths<T>(this IEnumerable<T> source) {
    using (var enumerator = source.GetEnumerator()) {
      // Empty input leads to empty output.
      if (!enumerator.MoveNext())
        yield break;

      // Retrieve first item of the sequence.
      var currentValue = enumerator.Current;
      var runLength = 1;

      // Iterate the remaining items in the sequence.
      while (enumerator.MoveNext()) {
        var value = enumerator.Current;
        if (!Equals(value, currentValue)) {
          // A new run is starting. Return the previous run.
          yield return Tuple.Create(currentValue, runLength);
          currentValue = value;
          runLength = 0;
        }
        runLength += 1;
      }

      // Return the last run.
      yield return Tuple.Create(currentValue, runLength);
    }
  }

}

Note that the extension method is generic and you can use it on any type. Values are compared for equality using Object.Equals. However, if you want to you could pass an IEqualityComparer<T> to allow for customization of how values are compared.

You can use the method like this:

var numbers = new[] { 4, 1, 1, 3, 3, 2, 5, 3, 2, 2 };
var runLengths = numbers.ToRunLengths();

For you input data the result will be these tuples:

4 1 
1 2 
3 2 
2 1 
5 1 
3 1 
2 2 
like image 159
Martin Liversage Avatar answered Sep 24 '22 10:09

Martin Liversage


(Adding another answer to avoid the two upvotes for my deleted one counting towards this...)

I've had a little think about this (now I've understood the question) and it's really not clear how you'd do this nicely in LINQ. There are definitely ways that it could be done, potentially using Zip or Aggregate, but they'd be relatively unclear. Using foreach is pretty simple:

// Simplest way of building an empty list of an anonymous type...
var results = new[] { new { Value = 0, Count = 0 } }.Take(0).ToList();

// TODO: Handle empty arrays
int currentValue = array[0];
int currentCount = 1;

foreach (var value in array.Skip(1))
{
    if (currentValue != value)
    {
        results.Add(new { Value = currentValue, Count = currentCount });
        currentCount = 0;
        currentValue = value;
    }
    currentCount++;
}
// Handle tail, which we won't have emitted yet
results.Add(new { Value = currentValue, Count = currentCount });
like image 22
Jon Skeet Avatar answered Sep 22 '22 10:09

Jon Skeet


Here's a LINQ expression that works (edit: tightened up code just a little more):

var data = new int[] { 4, 1, 1, 3, 3, 2, 5, 3, 2, 2 };
var result = data.Select ((item, index) =>
                        new
                        {
                            Key = item,
                            Count = (index == 0 || data.ElementAt(index - 1) != item) 
                                ? data.Skip(index).TakeWhile (d => d == item).Count ()
                                : -1
                        }
                          )
                  .Where (d => d.Count != -1);

And here's a proof that shows it working.

like image 25
Brad Rem Avatar answered Sep 23 '22 10:09

Brad Rem


This not short enough?

public static IEnumerable<KeyValuePair<T, int>> Repeats<T>(
        this IEnumerable<T> source)
{
    int count = 0;
    T lastItem = source.First();

    foreach (var item in source)
    {
        if (Equals(item, lastItem))
        {
            count++;
        }
        else
        {
           yield return new KeyValuePair<T, int>(lastItem, count);
           lastItem = item;
           count = 1;
        }
    }

    yield return new KeyValuePair<T, int>(lastItem, count);
}

I'll be interested to see a linq way.

like image 35
Jodrell Avatar answered Sep 21 '22 10:09

Jodrell