Convert list to number range string
Asked Answered
P

7

14

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<int> 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();
}
Procephalic answered 7/10, 2011 at 14:45 Comment(6)
A great question for codereview site.Individualism
A state machine will make this much easier.Individualism
I posted here as exact converse question is on SO: #40661Procephalic
what do you need this to go both ways for?Individualism
Just for display purposes - I probably would have forgotten about it had I not gotten so frustrated trying to implement it as it seemed like an easy task!Procephalic
I realize this is not a C++ question, but if you want a decent reference implementation, you should have a look at the Boost Interval Container library, which has support for doing exactly this. boost.org/doc/libs/1_47_0/libs/icl/doc/html/index.htmlLotta
G
11

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();
    }
  }
}
Goldia answered 7/10, 2011 at 15:2 Comment(2)
This is nice - taking advantages of .net constructs. As opposed to a 'C' style answerProcephalic
Intersperse is just a reimplementation of string.Join.Moon
L
6

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
Leopold answered 5/9, 2012 at 20:22 Comment(2)
Close, but there's an off by one error. Fixed in my edit. Old code failed on {1,3,4,5}.Rensselaerite
I seem to get an exception when I just pass one number in the list?Reheat
P
5

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();
}
Pappose answered 16/12, 2013 at 14:7 Comment(1)
+1 Always a matter of how your brain works I guess, but this is a clear and logical variant to me as well.Phrasing
T
3

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"
Taveda answered 19/4, 2018 at 14:13 Comment(0)
C
1

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);
Cobber answered 7/10, 2011 at 15:5 Comment(3)
Yep - this seems to work correctly for me. A bit cleaner than mine as you don't have any gank after your loopProcephalic
That program fails for, e.g., string s="0,1";Goldia
@Corey, yep i assumed that the numbers start from 1Cobber
S
0

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);
    }
Stlaurent answered 14/6, 2015 at 17:8 Comment(0)
L
0
static string ToRangeString(IEnumerable<int> nums){
    var sb = new StringBuilder();
    int distance = 0;
    int? currentNum = null;
    foreach (var num in nums.OrderBy(num => num)){
        if (currentNum == null){
            sb.Append(num);
            currentNum = num;
            continue;
        }

        if (currentNum == num)
            continue;

        if (currentNum == num - ++distance)
            continue;

        if (currentNum != currentNum + distance - 1){
            sb.Append(distance == 2 ? ',' : '-');
            sb.Append(currentNum + distance - 1);
        }

        sb.Append(',');
        sb.Append(num);
        currentNum = num;
        distance = 0;
    }

    if (distance > 0){
        sb.Append(distance == 1 ? ',' : '-');
        sb.Append(currentNum + distance);
    }
    return sb.ToString();
}

static void Test(){
    Console.WriteLine(ToRangeString([]) == "");
    Console.WriteLine(ToRangeString([int.MinValue, 1]) == "-2147483648,1");
    Console.WriteLine(ToRangeString([1]) == "1");
    Console.WriteLine(ToRangeString([1, 2]) == "1,2");
    Console.WriteLine(ToRangeString([1, 20]) == "1,20");
    Console.WriteLine(ToRangeString([20, 1]) == "1,20");
    Console.WriteLine(ToRangeString([2, 4, 6, 8]) == "2,4,6,8");
    Console.WriteLine(ToRangeString([1, 2, 3, 4, 5]) == "1-5");
    Console.WriteLine(ToRangeString([1, 2, 4, 5, 6]) == "1,2,4-6");
    Console.WriteLine(ToRangeString([1, 2, 3, 5, 6]) == "1-3,5,6");
    Console.WriteLine(ToRangeString([1, 2, 4, 5, 6, 7]) == "1,2,4-7");
    Console.WriteLine(ToRangeString([1, 2, 4, 5, 6, 7, 12]) == "1,2,4-7,12");
    Console.WriteLine(ToRangeString([1, 2, 3, 4, 6, 7]) == "1-4,6,7");
    Console.WriteLine(ToRangeString([1, 2, 3, 5, 6, 7, 8]) == "1-3,5-8");
    Console.WriteLine(ToRangeString([1, 2, 3, 4, 6, 7, 8]) == "1-4,6-8");
    Console.WriteLine(ToRangeString([1, 2, 3, 6, 7, 8, 9]) == "1-3,6-9");
    Console.WriteLine(ToRangeString([1, 2, 3, 4, 7, 8, 9]) == "1-4,7-9");
    Console.WriteLine(ToRangeString([1, 2, 3, 5, 6, 7, 8, 15, 16, 17]) == "1-3,5-8,15-17");
    Console.WriteLine(ToRangeString([1, 2, 3, 5, 6, 7]) == "1-3,5-7");
    Console.WriteLine(ToRangeString([1, 2, 3, 5, 10, 11, 12, 13]) == "1-3,5,10-13");
    Console.WriteLine(ToRangeString([1, 2, 3, 5, 6, 10, 11, 12, 13]) == "1-3,5,6,10-13");
    Console.WriteLine(ToRangeString([1, 2, 4, 5, 6, 9, 10]) == "1,2,4-6,9,10");
    Console.WriteLine(ToRangeString([1, 2, 4, 5, 6, 7, 9, 10]) == "1,2,4-7,9,10");
    Console.WriteLine(ToRangeString([1, 2, 4, 5, 6, 7, 10, 11, 12, 13, 18, 25]) == "1,2,4-7,10-13,18,25");
    Console.WriteLine(ToRangeString([1, 2, 4, 5, 6, 7, 10, 11, 12, 13, 18, 25, 26]) == "1,2,4-7,10-13,18,25,26");
    Console.WriteLine(ToRangeString([1, 2, 4, 5, 6, 7, 10, 12, 13, 14, 15, 18, 25, 26]) == "1,2,4-7,10,12-15,18,25,26");
    Console.WriteLine(ToRangeString([10, 20, 40, 41, 42, 43, 100, 120, 121, 122, 123, 180, 250, 260, 261]) == "10,20,40-43,100,120-123,180,250,260,261");
    Console.WriteLine(ToRangeString([2, 3, 4, 5, 6, 10, 20, 40, 41, 42, 43, 100, 120, 121, 122, 123, 180, 250, 260, 261]) == "2-6,10,20,40-43,100,120-123,180,250,260,261");
    Console.WriteLine(ToRangeString([-1, -2, -4, -5, -6, -7, 9, 10, 11]) == "-7--4,-2,-1,9-11");
    Console.WriteLine(ToRangeString([1, 3, 5, 6, 7, 8, 9, 10, 12]) == "1,3,5-10,12");}
Lepidosiren answered 31/5 at 5:28 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Unknit

© 2022 - 2024 — McMap. All rights reserved.