Search code examples
linqaggregationrangenumeric-ranges

Managing ranges with LINQ challenge


Given the following numbers (representing days of week): 1,2,3,4,5,6,7.

Here are some combination examples and their desired output:

  • 1,2,3,5,6,7 -> 1-3,5-7
  • 1,3,5,7 -> 1,3,5,7
  • 1,2,5,6 -> 1,2,5,6
  • 1,2,3,6,7 -> 1-3,6,7

The idea is that 3 or more consecutive days become a range while single or non-following days are rendered separately (or is it nicer to make range starting from 2).

I don't know where to start should I write a complicated ifed function or this can be done with one of the LINQ functions?
Any juicy suggestions?

I used numbers to simplify the idea of ranges, but in my code I have an flagged enum declared as follows:

[Flags]
public enum DaysOfWeek
{
  Sunday = 0x1,
  Monday = 0x2,
  Tuesday = 0x4,
  Wednesday = 0x8,
  Thursday = 0x10,
  Friday = 0x20,
  Saturday = 0x40
}

I have an entity OpeningTimes with a field DaysOfWeek, that tells what days in week the hour-ranges (defined in another property) of this entity apply to.

So the get the above I use (to really get numbers I'd add Select using index + 1):

var days = Enum.GetValues(typeof(DaysOfWeek))
             .Cast<DaysOfWeek>()
             .Where(dow => Model.DaysOfWeek.HasFlag(dow));

I think the idea is to first remove the numbers within a range.

I believe I'm looking for an aggregation function that receives the previous value as well, and can return another value-type, so I can make a function that if current value -1 equals prev. value, I wait for the next value, until range is not consecutive (or if element stands for itself) which is when I yield return the last bulk as an anonymous object and start working on the new one.

Then I'll make a formatting function that says if (item.First != item.Last) string.Join("-", item.First, Item.Last);


Solution

  • Interesting problem. I decided for readability to have a class representing a range:

    class NumberRange
    {
        public int Start { get; set;}
        public int End { get; set;}
        public override string ToString() 
        {
            return Start == End ? Start.ToString() : String.Format("{0}-{1}",Start,End);
        }
    } 
    

    and an extension method to turn an IEnumerable of ordered integers into an IEnumerable of ranges:

    public static IEnumerable<NumberRange> ToRanges(this IEnumerable<int> numbers)
    {
        NumberRange currentRange = null;
        foreach(var number in numbers)
        {
            if (currentRange == null)
                currentRange = new NumberRange() { Start = number, End = number };
            else if (number == currentRange.End + 1)
                currentRange.End = number;
            else
            {
                yield return currentRange;
                currentRange = new NumberRange { Start = number, End = number };
            }
        }
        if (currentRange != null)
        {
            yield return currentRange;
        }
    }
    

    And with that in place you can get the ranges and format them however you want:

    String.Join(",",
        new int[] { 1,2,3,5,7,8,9,11 }
            .ToRanges()
            .Select(r => r.ToString()))