Convert list to number range string

Convert list to number range string

This question is pretty much the opposite of this question:
Does C# have built-in support for parsing page-number strings?
So given 
1,3,5,6,7,8,9,10,12:

I will ouput:
1,3,5-10,12

Here is my first attempt.  It seems kind of hacky and is probably the worst code I ever wrote.  Can you suggest an imporovement\better way to do it?
static string numListToRangeStr(List numList)
{
    StringBuilder retString = new StringBuilder();
    numList.Sort();

    bool inRangeFind = false;
    int firstInRange = numList[0];
    int lastNumber = firstInRange;
    bool first = true;

    for (int i = 1; i < numList.Count; i++)
    {
        if (numList[i] == (lastNumber + 1))
        {
            inRangeFind = true;
        }
        else
        {             
            if (inRangeFind)
            {
                if (!first)
                {
                    retString.Append(",");
                }
                retString.Append(firstInRange);
                retString.Append("-");
            }
            else
            {
               if (!first)
                {
                    retString.Append(",");
                }
            }

            retString.Append(lastNumber);

            firstInRange = numList[i];
            inRangeFind = false;
            first = false;
        }

        lastNumber = numList[i];
    }


    if (inRangeFind)
    {
        if (!first)
        {
            retString.Append(",");
        }
        retString.Append(firstInRange);
        retString.Append("-");
    }
    else
    {
        if (!first)
        {
            retString.Append(",");
        }
    }
    retString.Append(lastNumber);

    return retString.ToString();
}

Solutions/Answers:

Answer 1:

When something has several moving parts like this, I think it helps to decompose it into little logical units and then combine them together. The little logical units might even be usable separately. The code below breaks the problem down into:

  • turning the heterogeneous set of sequential and nonsequential numbers into a homogenous set of ranges (possibly including "degenerate" ranges which start and end at the same number)
  • a way to "pretty-print" such ranges: (x,y) prints as "x-y"; (x,x) prints as "x"
  • a way to interperse a separator between elements of an enumerable, and convert the result into a string.

The program is:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication37 {
  public static class Program {
    static void Main(string[] args) {
      var numList=new[] {1, 3, 5, 6, 7, 8, 9, 10, 12};
      Console.WriteLine(numListToPossiblyDegenerateRanges(numList).Select(r => PrettyRange(r)).Intersperse(","));
    }

    /// <summary>
    /// e.g. 1,3,5,6,7,8,9,10,12
    /// becomes
    /// (1,1),(3,3),(5,10),(12,12)
    /// </summary>
    public static IEnumerable<Tuple<int,int>> numListToPossiblyDegenerateRanges(IEnumerable<int> numList) {
      Tuple<int, int> currentRange=null;
      foreach(var num in numList) {
        if(currentRange==null) {
          currentRange=Tuple.Create(num, num);
        } else if(currentRange.Item2==num-1) {
          currentRange=Tuple.Create(currentRange.Item1, num);
        } else {
          yield return currentRange;
          currentRange=Tuple.Create(num, num);
        }
      }
      if(currentRange!=null) {
        yield return currentRange;
      }
    }

    /// <summary>
    /// e.g. (1,1) becomes "1"
    /// (1,3) becomes "1-3"
    /// </summary>
    /// <param name="range"></param>
    /// <returns></returns>
    public static string PrettyRange(Tuple<int,int> range) {
      if(range.Item1==range.Item2) {
        return range.Item1.ToString();
      }
      return string.Format("{0}-{1}", range.Item1, range.Item2);
    }

    public static string Intersperse(this IEnumerable<string> items, string interspersand) {
      var currentInterspersand="";
      var result=new StringBuilder();
      foreach(var item in items) {
        result.Append(currentInterspersand);
        result.Append(item);
        currentInterspersand=interspersand;
      }
      return result.ToString();
    }
  }
}

Answer 2:

This is an old thread, but here's a new answer. I've constructed it as an extension method. This returns the array of ranges, where each 'range' is either a single number ('13') or a pair of numbers ('5-12'):

public static class EnumExt {
    public static string[] ToRanges(this List<int> ints) {
        if (ints.Count < 1) return new string[] { };
        ints.Sort();
        var lng = ints.Count;
        var fromnums = new List<int>();
        var tonums = new List<int>();
        for (var i = 0; i < lng - 1; i++) {
            if (i == 0)
                fromnums.Add(ints[0]);
            if (ints[i + 1] > ints[i] + 1) {
                tonums.Add(ints[i]);
                fromnums.Add(ints[i + 1]);
            }
        }
        tonums.Add(ints[lng - 1]);
        return Enumerable.Range(0, tonums.Count).Select(
            i => fromnums[i].ToString() +
                (tonums[i] == fromnums[i] ? "" : "-" + tonums[i].ToString())
        ).ToArray();
    }
}

If you wish to join them, just use built-in string.Join:

var intlist = new List<int>() { 1, 2, 3, 6, 7, 8, 9, 10, 14 };
Console.WriteLine(string.Join(", ", intlist.ToRanges()));
// Prints: 1-3, 6-10, 14

Answer 3:

Had to solve same problem. Was finding alternatives to my solution which I think looks more logical. Therefore sharing it. Set second parameter to true if you want to sort an unsorted list.

public string ToRangeString(List<int> list, bool withSort) {
  list = list.Distinct().ToList();
  if(withSort) list.Sort();

  StringBuilder result = new StringBuilder();
  int temp;

  for (int i=0; i<list.Count(); i++) {
    temp = list[i];

    //add a number
    result.Append(list[i]);

    //skip number(s) between a range
    while(i<list.Count()-1 && list[i+1] == list[i]+1)
      i++;

    //add the range
    if(temp != list[i])
      result.Append("-").Append(list[i]);

    //add comma
    if(i != list.Count()-1)
      result.Append(", ");

  }
  return result.ToString();
}

Answer 4:

This should work pretty well, not tested for all cases though.

        string s = "1,2,3,4,5,7,8,9,10,11,12,13";
        string[] ints = s.Split(',');
        StringBuilder result = new StringBuilder();

        int num, last = -1;
        bool dash = false;

        for (int ii = 0; ii < ints.Length; ii++)
        {
            num = Int32.Parse(ints[ii]);

            if (num - last > 1)
            {
                if (dash)
                {
                    result.Append(last);
                    dash = false;
                }
                if (result.Length > 0)
                {
                    result.Append(",");
                }
                result.Append(num);                    
            }
            else
            {
                if (dash == false)
                {
                    result.Append("-");
                    dash = true;
                }
            }

            last = num;

            if (dash && ii == ints.Length - 1)
            {
                result.Append(num);
            }
        }

        Console.WriteLine(result);

Answer 5:

I know this is an old thread but thought I'd share my approach. This generates a list of ranges which can be easily converted to a single string.

var numbers = new List<int>() { 1, 3, 5, 6, 7, 8, 9, 10, 12 };
var ranges = new List<string>();

if (numbers.Count == 0)
    return ranges;

numbers = numbers.Distinct().ToList();
numbers.Sort();

int start = numbers[0];
string range = start.ToString();

for (int i = 1; i <= numbers.Count; i++)
{
    if (i < numbers.Count && numbers[i] == numbers[i - 1] + 1)
    {
        range = $"{start} - {numbers[i]}";
        continue;
    }

    ranges.Add(range);

    if (i < numbers.Count)
    {
        start = numbers[i];
        range = start.ToString();
    }
}

var rangeString = string.Join(", ", ranges);  // Outputs: "1, 3, 5 - 10, 12"

Answer 6:

Here a slightly modifed Version of RedFilter's version.

It returns a String instead of an Array of Strings, it Removes 0 ,if in the list, it avoids the Exception if only one Value is in the List.

 public static string ToRanges(this List<int> ints)
    {
        ints.Remove(0); // Note: Remove this if you like to include the Value 0
        if (ints.Count < 1) return "";
        ints.Sort();
        var lng = ints.Count;
        if (lng == 1)
            return ints[0].ToString();

        var fromnums = new List<int>();
        var tonums = new List<int>();
        for (var i = 0 ; i < lng - 1 ; i++)
        {
            if (i == 0)
                fromnums.Add(ints[0]);
            if (ints[i + 1] > ints[i] + 1)
            {
                tonums.Add(ints[i]);
                fromnums.Add(ints[i + 1]);
            }
        }
        tonums.Add(ints[lng - 1]);


        string[] ranges = Enumerable.Range(0, tonums.Count).Select(
            i => fromnums[i].ToString() +
                (tonums[i] == fromnums[i] ? "" : "-" + tonums[i].ToString())
        ).ToArray();

        if (ranges.Length == 1)
            return ranges[0];
        else
            return String.Join(",", ranges);
    }

References