3

I have a Column class which has an Index property of type int.

If I have a collection of Column objects, I am looking for a way to test if their indices are contiguous. By contiguous I mean that the indices are next to each other, so if ordered by value they are 1 apart from the next and previous Index.

There can be any number of column objects.

So, for example:

  • 10,11,12,13 => true

  • 3,5,7 => false

  • 1,2,4 => false

Edit

While these examples are of ordered indices, I would like a solution that takes an unordered set of indices.

I feel sure there is probably a neat Linq way of solving this, but I cannot see it.

Expressed in code:

public class Column 
{
    public int Index { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        // Example set of columns 1
        List<Column> columns1 = new List<Column>()
        {
            new Column(){Index = 10},
            new Column(){Index = 11},
            new Column(){Index = 12},
            new Column(){Index = 13},
        };

        // Example set of columns 2
        List<Column> columns2 = new List<Column>()
        {
            new Column(){Index = 3},
            new Column(){Index = 5},
            new Column(){Index = 7},
        };

        // Example set of columns 3
        List<Column> columns3 = new List<Column>()
        {
            new Column(){Index = 1},
            new Column(){Index = 2},
            new Column(){Index = 4},
        };

        var result1 = IndicesAreContiguos(columns1); // => true
        var result2 = IndicesAreContiguos(columns2); // => false
        var result3 = IndicesAreContiguos(columns3); // => false
    }

    public bool IndicesAreContiguos(IEnumerable<Column> columns) 
    {
        // ....???
    }

}
halfer
  • 19,824
  • 17
  • 99
  • 186
Cleve
  • 1,273
  • 1
  • 13
  • 26
  • "ordered by value they are 1 apart from the next and previous Index." - that sounds like you know how to do it already. – Enigmativity Mar 26 '20 at 05:42
  • @Enigmativity Sorry, perhaps I should have been a bit more specific. I appreciate that I could write some long, pedestrian code which might do the job, but I was looking for a more succinct solution. – Cleve Mar 26 '20 at 05:51
  • Why did you tag `[morelinq]`? – Enigmativity Mar 26 '20 at 05:54
  • @Enigmativity, I tagged MoreLinq as I thought people interested in that category might be able to help. – Cleve Mar 26 '20 at 05:55
  • 1
    All your examples are already ordered. You should probably be more specific about the expected output for e.g. 1,3,2 – Jonas Høgh Mar 26 '20 at 05:59
  • @JonasHøgh , yes you are right, but I wanted to keep the question as brief as possible. I assumed that because any solution could start with `OrderBy` this was unnecessary. Please see EDIT to question – Cleve Mar 26 '20 at 06:03
  • yea. I think that is a reasonable assumption myself. when I see a method named `IndicesAreContiguos` I don't expect a reordering to occur in that scope. – Brett Caswell Mar 26 '20 at 06:07
  • _I would like a solution that takes an unordered set of indices._ - is this a task for us to do, or you will explain what kind of problem you get while you tried by your self? – Fabio Mar 26 '20 at 06:09
  • @Fabio this text was added in response to comment made by JonasHogh above. – Cleve Mar 26 '20 at 06:12
  • @Cleve, still, what is the specific issue which preventing you from doing it by yourself? – Fabio Mar 26 '20 at 06:14
  • 1
    @Cleve, just updated another solution, let me know what you think ;) – Clint Mar 26 '20 at 06:30
  • @Clint looks good too! An approach I would not have thought of in a million years! – Cleve Mar 26 '20 at 06:37
  • Does this answer your question? [How to check if a list is ordered?](https://stackoverflow.com/questions/1940214/how-to-check-if-a-list-is-ordered) – Pavel Anikhouski Mar 26 '20 at 07:41

4 Answers4

3

You don't need LINQ for this

public bool IsContig(int[] arr) {
  for(int i = 1; i<arr.Length;i++)
    if(arr[i] - arr[i-1] != 1)
      return false;
  return true;
}

LINQ is a hammer, but not every problem is a nail

(Edit: to accept an unordered set of indices, then consider a modification that sorts the array first. Again, LINQ isn't necessary; Array.Sort would work)

If a sequence of 1,2,3,2,3,2,3,4,3,2,3,4,5,4,5 is contiguous, improve the IF to allow for a result of -1 too

Caius Jard
  • 72,509
  • 5
  • 49
  • 80
  • "it does make it harder to understand the code" - I almost always find it the other way around. The non-LINQ solution you posted is more obtuse to me. – Enigmativity Mar 26 '20 at 06:53
  • Also, the LINQ solution that you provided is different from the implementation of your `IsContig` method. This LINQ solution, `ordered.Skip(1).Zip(ordered, (x, y) => x - y).All(z => z == 1)`, is pretty much the equivalent of your `IsContig` - it also short-circuits on the first failing indice pair. – Enigmativity Mar 26 '20 at 06:55
  • Your claim that this loops appraoch is "Pretty much equivalent" to yours is nowhere near; one uses simple loops that a freshman who's halfway through coding 101 could understand; the other will have that same freshman reaching for google/so wondering what they heck it's doing. I appreciate that you're talking about "under the hood" but on the surface, my advice remains: go for the simple solution, not the perfect one; one day it won't be you maintaining this code – Caius Jard Mar 26 '20 at 07:07
  • I still say that the LINQ approach is the simple solution. To roll your own is like suggesting that baking your own bread is the simple solution compared to buying a loaf at the supermarket. – Enigmativity Mar 26 '20 at 08:22
  • @CaiusJard that's only true because some barbarian decided to teach coding 101 in an imperative language ;) – Jonas Høgh Mar 26 '20 at 09:35
  • @Enigmativity but you would say that, because you know LINQ; Cleve is here *because he doesn't*. It's easy to forget just how hard something is for a third party to understand when you personally can look at it and your brain can instantly assimilate it. Re LINQ being simpler, I'll always disagree - even the method names aren't helpful but grokking that there are two enumerators, one a step behind the other, traversing the same list is probably always going to be harder to understand than a single iterative process where you can see exactly what it's doing in the code that is in front of you – Caius Jard Mar 26 '20 at 09:57
  • "probably always going to be harder to understand" - I disagree. I'm not trying to convince you of anything other than other people can view these things differently. – Enigmativity Mar 26 '20 at 10:36
3

With math you can create a function which will handle unordered collections with only one iteration over collection.

public static bool IsConsecutive(this IEnumerable<int> values)
{
    return values
        .Aggregate((Sum: 0, Min: int.MaxValue, Max: int.MinValue, Count: 0), 
            (total, value) =>
            {
                total.Count += 1;
                total.Sum += value;
                total.Min = total.Min > value ? value : total.Min;
                total.Max = total.Max < value ? value : total.Max;

                return total;
            },
            (total) =>
            {
                var difference = total.Max - total.Min + 1;
                var expectedSum = (total.Count * (total.Min + total.Max)) / 2;

                return difference == total.Count && expectedSum == total.Sum;
            });
}

Solution is based on the formula of sum of consecutive integers (Gauss's Formula)

\sum=\frac{n(a_{1} + a_{n})}{2}

But because formula can be applied for consecutive integers with step other than 1 (for example 2, 4, 6, 8), we added check that step is only one by calculating difference between min and max values and comparing it with the quantity of values.

Usage

var values = new[] { 10, 12, 13, 15, 14, 11 };

if (values.IsConsecutive())
{
    // Do something
}
Fabio
  • 31,528
  • 4
  • 33
  • 72
  • With loops you can create a function *that is 4 lines long* and iterate the collection only once! This is neat, but could maybe do with some comments so the next guy can understand it ;) – Caius Jard Mar 26 '20 at 07:12
  • @CaiusJard, feel free to rewrite it with the loop. It will not change an end result. – Fabio Mar 26 '20 at 07:14
  • it's a good answer to include.. remember the upvote is used by the community reflect on support of these answers - it's reasonable to include distinctive alternatives that may not appear desirable. – Brett Caswell Mar 26 '20 at 07:18
  • I'll point out that one can pass in the delegates to the aggregate function.. there is an appeal to it over using loops as a control flow. – Brett Caswell Mar 26 '20 at 07:19
  • 2
    Very nice answer, @Fabio. – Enigmativity Mar 26 '20 at 08:27
  • @CaiusJard, you right about loops, but by using functions such `Select`, `Where` or `Aggregate` next reader/developer will understand my intentions much quicker than the loops. `Select` - map/convert values into something else, `Where` - filter some value out, `Aggregate` - combine multiple values into one, `SelectMany` - split one into many. Where with `for` loops you always need to dig into the loop to understand what is there, especially when you notice some `if` statements there. – Fabio Mar 26 '20 at 19:49
  • @Fabio, not sure if it will work e.g. with sequence { 1, 1, 4, 4 }, where the sum is the same like for { 1, 2, 3, 4 }. – Tomas Paul Apr 14 '20 at 21:29
2

Give this a go:

public static bool IndicesAreContiguos(IEnumerable<Column> columns)
{
    var ordered = columns.Select(x => x.Index).OrderBy(x => x).ToArray();
    return ordered.Skip(1).Zip(ordered, (x, y) => x - y).All(z => z == 1);
}

This is literally "ordered by value they are 1 apart from the next and previous Index."

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • Really like this! Thanks for your help. – Cleve Mar 26 '20 at 06:20
  • it's a good solution, but there are caveats. Index will have to be unique for this to work; and it does a sort, so it disregards the order of the passed `columns`. – Brett Caswell Mar 26 '20 at 06:39
  • Also be aware that `ToArray` will mean a much higher memory consumption than a for loop for large inputs. – Jonas Høgh Mar 26 '20 at 07:01
  • @JonasHøgh - Yes, but your comparing two different parts of the computation here. The `.ToArray()` is only there to ensure the computation of the `OrderBy` only occurs once. Without it we're forced to sort the list twice. And possibly worse, two different iterations of the source might return different numbers. This algorithm relies on the input being stable. The `.ToArray()` doesn't replace the `foreach` in the non-LINQ approach. That's the job of the `.Zip` operator. – Enigmativity Mar 26 '20 at 08:25
  • Even without the `OrderBy`, you would need to call `ToArray`, or accept that the second line of the code enumerates the input twice in order to zip it with itself. This is another potential performance problem, e.g. if the `IEnumerable` causes a database query to be executed when enumerated. – Jonas Høgh Mar 26 '20 at 09:27
  • 2
    @JonasHøgh - All good points. I'd rather take the `.ToArray` hit on the `OrderBy`. But overall I like Fabio's answer the best. – Enigmativity Mar 26 '20 at 10:37
0

One way would be to create a range from min to max and compare that to the existing indices:

public static bool IndicesAreContiguos(IEnumerable<Column> columns)
{
    var orderedIndices = columns.Select(c => c.Index).OrderBy(i => i);
    if (orderedIndices.Distinct().Count() != columns.Count()) return false;  // Optional.

    int min = columns.Min(c => c.Index);
    int max = columns.Max(c => c.Index);

    return Enumerable.Range(min, max - min + 1).SequenceEqual(orderedIndices);
}
  • @Enigmativity I'm using `DistinctBy` because if the list contains duplicates, there's no need to create the range and compare the sequence. _"what happens if the input wasn't already ordered?"_ I could've sworn that I added an `OrderBy`. Fixed! – 41686d6564 stands w. Palestine Mar 26 '20 at 05:57
  • 1
    It's nicer, but now you're sorting twice and iterating the original sequence a minimum of twice and a maximum of 5 times. – Enigmativity Mar 26 '20 at 06:59