-1

I have a list of ordered numbers in C# and i want to calculate the min and max values that can take according to their secuencial value, with LINQ

The list is always ordered and never is empty.

For example:

My list object:

1060
1061
....
1089
1090
6368
6369
....
6383
6384
30165
30166
....
30214
30215

My expected results:

1060-1090
6368-6384
30165-30215

Thanks.

5 Answers5

1

For problems like these, the Zip method is handy. This is what it does:

Applies a specified function to the corresponding elements of two sequences, producing a sequence of the results.

It can be used to pair the consecutive elements of a sequence, by ziping the sequence with itself.

var source = new List<int> { 1, 2, 3, 4, 5, 11, 12, 13, 21, 22 };
var gaps = source
    .Zip(source.Skip(1), (n1, n2) => (n1, n2, gap: n2 - n1)) // Calculate the gaps
    .Where(e => e.gap != 1) // Select non sequential pairs
    .ToArray();
var gapsEx = gaps
    .Prepend((n1: 0, n2: source.First(), gap: 0)) // Add the first element
    .Append((n1: source.Last(), n2: 0, gap: 0)) // Add the last element
    .ToArray();
var results = gapsEx
    .Zip(gapsEx.Skip(1), (e1, e2) => (from: e1.n2, to: e2.n1)); // Pairwise gaps

Console.WriteLine($"Results: {String.Join(", ", results.Select(r => r.from + "-" + r.to))}");

Output:

Results: 1-5, 11-13, 21-22

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
1

Consider creating an extension method for IEnumerable<TSource>, so you can use it as if it was a LINQ function. See Extension Methods Demystified

Your example didn't handle several problems:

  • What if your input sequence is empty?
  • What if the input is not ordered?
  • What if you've got several time the same value: 1 2 3 3 3 3 4 5?
  • What if you have sub-sequences with only one contiguous number: 1 2 7 18 19?

So let's give a proper requirement:

Given an input sequence of integer numbers, create an output sequence of integer pairs, where the values are the first and the last number of a sequence of contiguous numbers in the input sequence.

Examples:

  • 1060 1061 ... 1089 1090 6368 6369 ... 6384 30165 ... => [1060, 1090] [6369, 6384] [30165
  • 2 3 4 5 17 18 19 4 5 6 7 1 2 3 4 5 => [2, 5] [17, 19] [4, 7] [1 5]
  • 2 3 4 5 6 8 9 => [2, 5] [6, 6] [8, 9]

I'll return the sequence of pairs as a sequence of Tuple<int, int>. If desired you can create a dedicated class for this.

static IEnumerable<Tuple<int, int>> ToMinMaxTuples(this IEnumerable<int> source)
{
    // TODO: source == null
    var enumerator = source.GetEnumerator();
    if (enumerator.MoveNext())
    {
        // there is at least one item in source
        int min = enumerator.Current;
        int max = min;
        while (enumerator.MoveNext())
        {
            // there is another item in the sequence
            if (enumerator.Current == max + 1)
            {
                // current is part of the current sequence, continue with next number
                max = enumerator.Current;
            }
            else
            {
                // current is not part of the current sequence,
                // it is the start of the next one
                // yield return [min, max] as a Tuple:
                yield return new Tuple<int, int>(min, max);

                // start the next sequence:
                min = enumerator.Current;
                max = min;
            }
        }
    }
}

usage:

IEnumerable<Tuple<int, int>> result = myInputList.ToMinMaxTuples();

Or in the middle of some big LINQ statement:

var result = Students
    .Where(student => student.Country == "Republique Française")
    .Select(student => student.Grade)
    .ToMinMaxTuples()
    .OrderBy(tuple => tuple.Item1)
    .ThenBy(tuple => tuple.Item2);
Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116
  • 1
    I think your impementation has a bug. The last range is not yielded. Btw don't forget to [dispose your enumerators](https://stackoverflow.com/a/21337552/11178549)! – Theodor Zoulias Apr 28 '19 at 21:52
  • You are right: the last range is not yielded. If I've got time (and if I remember to do this) I'll correct it. Maybe someone else is willing to add the yield return of the last pair. And of course, we should put GetEnumerator in a using statement – Harald Coppoolse Apr 29 '19 at 08:31
  • Thanks Harald. I'll try your solution. – Gonzalo Bustamante Apr 29 '19 at 14:19
1
//Sample list of ordered integers
List<int> lst = new List<int>{101,102,103,104,106,107,108,111,112,114,120,121};

// find minimum element of each sub-sequence within the above list
var minBoundaries = lst.Where(i => !lst.Contains(i-1)).ToList();

// find maximum element of each sub-sequence within the above list
var maxBoundaries = lst.Where(i => !lst.Contains(i+1)).ToList();

//format minimum and maximum elements of each sub-sequence as per the sample output in the question
var result = new List<string>();
for(int i = 0; i < maxBoundaries.Count; i++) 
    result.Add(minBoundaries[i]+"-"+maxBoundaries[i]);
akg179
  • 1,287
  • 1
  • 6
  • 14
0

If you implement a simple pair class then you can use the .Aggregate() LINQ method. The pair class would be necessary since Tuples are immutable, but it can easily be constructed like so...

public class MinMaxPair<T>
{
    public MinMaxPair(T min, T max)
    {
        Min = min;
        Max = max;
    }

    public T Min;
    public T Max;
}

Then with that in place, the .Aggregate() call would simply be

nums.Aggregate(
    new List<MinMaxPair<int>>(),
    (sets, next) =>
    {
        if (!sets.Any() || next - sets.Last().Max > 1)
        {
            sets.Add(new MinMaxPair<int>(next, next));
        }
        else
        {
            var minMax = sets.Last();
            if (next < minMax.Min)
                minMax.Min = next;
            else
                minMax.Max = next;
        }
        return sets;
    });
Chris
  • 325
  • 1
  • 3
  • 11
0

Using a pair enhanced version of my Scan extension method, which is based on the APL scan operator that is similar to aggregate, but returns the intermediate results, I have created variable generalized grouping methods. Using GroupByPairsWhile I (had previously) created a GroupBySequential method for this sort of problem.

public static class IEnumerableExt {
    // TKey combineFn((TKey Key, T Value) PrevKeyItem, T curItem):
    // PrevKeyItem.Key = Previous Key
    // PrevKeyItem.Value = Previous Item
    // curItem = Current Item
    // returns new Key
    public static IEnumerable<(TKey Key, T Value)> ScanToPairs<T, TKey>(this IEnumerable<T> src, TKey seedKey, Func<(TKey Key, T Value), T, TKey> combineFn) {
        using (var srce = src.GetEnumerator())
            if (srce.MoveNext()) {
                var prevkv = (seedKey, srce.Current);

                while (srce.MoveNext()) {
                    yield return prevkv;
                    prevkv = (combineFn(prevkv, srce.Current), srce.Current);
                }
                yield return prevkv;
            }
    }

    // bool testFn(T prevItem, T curItem)
    // returns groups by runs of matching bool
    public static IEnumerable<IGrouping<int, T>> GroupByPairsWhile<T>(this IEnumerable<T> src, Func<T, T, bool> testFn) =>
        src.ScanToPairs(1, (kvp, cur) => testFn(kvp.Value, cur) ? kvp.Key : kvp.Key + 1)
           .GroupBy(kvp => kvp.Key, kvp => kvp.Value);

    public static IEnumerable<IGrouping<int, int>> GroupBySequential(this IEnumerable<int> src) => src.GroupByPairsWhile((prev, cur) => prev + 1 == cur);

}

With the extension method, your problem is simple:

var ans = src.GroupBySequential().Select(g => new { Min = g.Min(), Max = g.Max() });

This assumes the list is not ordered. If the list is known to be ordered, you could use First() and Last() instead of Min() and Max().

NOTE: The extension methods may seem complicated, but they provide the basis for multiple different types of grouping, including grouping by runs of equal items, grouping by generalized test functions, and with various seed and ending strategies for dealing with the first and last element when working in pairs.

NetMage
  • 26,163
  • 3
  • 34
  • 55