<p>Does C# have built-in support for parsing strings of page numbers?  By page numbers, I mean the format you might enter into a print dialog that's a mixture of comma and dash-delimited.</p>

<p>Something like this:</p>

<pre><code>1,3,5-10,12
</code></pre>

<p>What would be really nice is a solution that gave me back some kind of list of all page numbers represented by the string.  In the above example, getting a list back like this would be nice:</p>

<pre><code>1,3,5,6,7,8,9,10,12
</code></pre>

<p>I just want to avoid rolling my own if there's an easy way to do it.</p>

[{'answer_id': 40165, 'author': 'Daniel Jennings', 'author_id': 3641, 'author_profile': 'https://Stackoverflow.com/users/3641', 'pm_score': 3, 'selected': False, 'text': '<p>It doesn\'t have a built-in way to do this, but it would be trivial to do using String.Split.</p>\n\n<p>Simply split on \',\' then you have a series of strings that represent either page numbers or ranges. Iterate over that series and do a String.Split of \'-\'. If there isn\'t a result, it\'s a plain page number, so stick it in your list of pages. If there is a result, take the left and right of the \'-\' as the bounds and use a simple for loop to add each page number to your final list over that range.</p>\n\n<p>Can\'t take but 5 minutes to do, then maybe another 10 to add in some sanity checks to throw errors when the user tries to input invalid data (like "1-2-3" or something.)</p>\n'}, {'answer_id': 40180, 'author': 'Keith', 'author_id': 905, 'author_profile': 'https://Stackoverflow.com/users/905', 'pm_score': 6, 'selected': True, 'text': '<p>Should be simple:</p>\n\n<pre><code>foreach( string s in "1,3,5-10,12".Split(\',\') ) \n{\n    // try and get the number\n    int num;\n    if( int.TryParse( s, out num ) )\n    {\n        yield return num;\n        continue; // skip the rest\n    }\n\n    // otherwise we might have a range\n    // split on the range delimiter\n    string[] subs = s.Split(\'-\');\n    int start, end;\n\n    // now see if we can parse a start and end\n    if( subs.Length &gt; 1 &amp;&amp;\n        int.TryParse(subs[0], out start) &amp;&amp;\n        int.TryParse(subs[1], out end) &amp;&amp;\n        end &gt;= start )\n    {\n        // create a range between the two values\n        int rangeLength = end - start + 1;\n        foreach(int i in Enumerable.Range(start, rangeLength))\n        {\n            yield return i;\n        }\n    }\n}\n</code></pre>\n\n<p><strong>Edit:</strong> thanks for the fix ;-) </p>\n'}, {'answer_id': 40207, 'author': 'ljs', 'author_id': 3394, 'author_profile': 'https://Stackoverflow.com/users/3394', 'pm_score': 3, 'selected': False, 'text': '<p>Keith\'s approach seems nice. I put together a more naive approach using lists. This has error checking so hopefully should pick up most problems:-</p>\n\n<pre><code>public List&lt;int&gt; parsePageNumbers(string input) {\n  if (string.IsNullOrEmpty(input))\n    throw new InvalidOperationException("Input string is empty.");\n\n  var pageNos = input.Split(\',\');\n\n  var ret = new List&lt;int&gt;();\n  foreach(string pageString in pageNos) {\n    if (pageString.Contains("-")) {\n      parsePageRange(ret, pageString);\n    } else {\n      ret.Add(parsePageNumber(pageString));\n    }\n  }\n\n  ret.Sort();\n  return ret.Distinct().ToList();\n}\n\nprivate int parsePageNumber(string pageString) {\n  int ret;\n\n  if (!int.TryParse(pageString, out ret)) {\n    throw new InvalidOperationException(\n      string.Format("Page number \'{0}\' is not valid.", pageString));\n  }\n\n  return ret;\n}\n\nprivate void parsePageRange(List&lt;int&gt; pageNumbers, string pageNo) {\n  var pageRange = pageNo.Split(\'-\');\n\n  if (pageRange.Length != 2)\n    throw new InvalidOperationException(\n      string.Format("Page range \'{0}\' is not valid.", pageNo));\n\n  int startPage = parsePageNumber(pageRange[0]),\n    endPage = parsePageNumber(pageRange[1]);\n\n  if (startPage &gt; endPage) {\n    throw new InvalidOperationException(\n      string.Format("Page number {0} is greater than page number {1}" +\n      " in page range \'{2}\'", startPage, endPage, pageNo));\n  }\n\n  pageNumbers.AddRange(Enumerable.Range(startPage, endPage - startPage + 1));\n}\n</code></pre>\n'}, {'answer_id': 40384, 'author': 'Lasse V. Karlsen', 'author_id': 267, 'author_profile': 'https://Stackoverflow.com/users/267', 'pm_score': 2, 'selected': False, 'text': '<p>Here\'s something I cooked up for something similar.</p>\n\n<p>It handles the following types of ranges:</p>\n\n<pre><code>1        single number\n1-5      range\n-5       range from (firstpage) up to 5\n5-       range from 5 up to (lastpage)\n..       can use .. instead of -\n;,       can use both semicolon, comma, and space, as separators\n</code></pre>\n\n<p>It does not check for duplicate values, so the set <em>1,5,-10</em> will produce the sequence <em>1, 5, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10</em>.</p>\n\n<pre><code>public class RangeParser\n{\n    public static IEnumerable&lt;Int32&gt; Parse(String s, Int32 firstPage, Int32 lastPage)\n    {\n        String[] parts = s.Split(\' \', \';\', \',\');\n        Regex reRange = new Regex(@"^\\s*((?&lt;from&gt;\\d+)|(?&lt;from&gt;\\d+)(?&lt;sep&gt;(-|\\.\\.))(?&lt;to&gt;\\d+)|(?&lt;sep&gt;(-|\\.\\.))(?&lt;to&gt;\\d+)|(?&lt;from&gt;\\d+)(?&lt;sep&gt;(-|\\.\\.)))\\s*$");\n        foreach (String part in parts)\n        {\n            Match maRange = reRange.Match(part);\n            if (maRange.Success)\n            {\n                Group gFrom = maRange.Groups["from"];\n                Group gTo = maRange.Groups["to"];\n                Group gSep = maRange.Groups["sep"];\n\n                if (gSep.Success)\n                {\n                    Int32 from = firstPage;\n                    Int32 to = lastPage;\n                    if (gFrom.Success)\n                        from = Int32.Parse(gFrom.Value);\n                    if (gTo.Success)\n                        to = Int32.Parse(gTo.Value);\n                    for (Int32 page = from; page &lt;= to; page++)\n                        yield return page;\n                }\n                else\n                    yield return Int32.Parse(gFrom.Value);\n            }\n        }\n    }\n}\n</code></pre>\n'}, {'answer_id': 205166, 'author': 'Community', 'author_id': -1, 'author_profile': 'https://Stackoverflow.com/users/-1', 'pm_score': 0, 'selected': False, 'text': '<p>Here\'s a slightly modified version of lassevk\'s code that handles the string.Split operation inside of the Regex match. It\'s written as an extension method and you can easily handle the duplicates problem using the Disinct() extension from LINQ.</p>\n\n<pre><code>    /// &lt;summary&gt;\n    /// Parses a string representing a range of values into a sequence of integers.\n    /// &lt;/summary&gt;\n    /// &lt;param name="s"&gt;String to parse&lt;/param&gt;\n    /// &lt;param name="minValue"&gt;Minimum value for open range specifier&lt;/param&gt;\n    /// &lt;param name="maxValue"&gt;Maximum value for open range specifier&lt;/param&gt;\n    /// &lt;returns&gt;An enumerable sequence of integers&lt;/returns&gt;\n    /// &lt;remarks&gt;\n    /// The range is specified as a string in the following forms or combination thereof:\n    /// 5           single value\n    /// 1,2,3,4,5   sequence of values\n    /// 1-5         closed range\n    /// -5          open range (converted to a sequence from minValue to 5)\n    /// 1-          open range (converted to a sequence from 1 to maxValue)\n    /// \n    /// The value delimiter can be either \',\' or \';\' and the range separator can be\n    /// either \'-\' or \':\'. Whitespace is permitted at any point in the input.\n    /// \n    /// Any elements of the sequence that contain non-digit, non-whitespace, or non-separator\n    /// characters or that are empty are ignored and not returned in the output sequence.\n    /// &lt;/remarks&gt;\n    public static IEnumerable&lt;int&gt; ParseRange2(this string s, int minValue, int maxValue) {\n        const string pattern = @"(?:^|(?&lt;=[,;]))                      # match must begin with start of string or delim, where delim is , or ;\n                                 \\s*(                                 # leading whitespace\n                                 (?&lt;from&gt;\\d*)\\s*(?:-|:)\\s*(?&lt;to&gt;\\d+)  # capture \'from &lt;sep&gt; to\' or \'&lt;sep&gt; to\', where &lt;sep&gt; is - or :\n                                 |                                    # or\n                                 (?&lt;from&gt;\\d+)\\s*(?:-|:)\\s*(?&lt;to&gt;\\d*)  # capture \'from &lt;sep&gt; to\' or \'from &lt;sep&gt;\', where &lt;sep&gt; is - or :\n                                 |                                    # or\n                                 (?&lt;num&gt;\\d+)                          # capture lone number\n                                 )\\s*                                 # trailing whitespace\n                                 (?:(?=[,;\\b])|$)                     # match must end with end of string or delim, where delim is , or ;";\n\n        Regex regx = new Regex(pattern, RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);\n\n        foreach (Match m in regx.Matches(s)) {\n            Group gpNum = m.Groups["num"];\n            if (gpNum.Success) {\n                yield return int.Parse(gpNum.Value);\n\n            } else {\n                Group gpFrom = m.Groups["from"];\n                Group gpTo = m.Groups["to"];\n                if (gpFrom.Success || gpTo.Success) {\n                    int from = (gpFrom.Success &amp;&amp; gpFrom.Value.Length &gt; 0 ? int.Parse(gpFrom.Value) : minValue);\n                    int to = (gpTo.Success &amp;&amp; gpTo.Value.Length &gt; 0 ? int.Parse(gpTo.Value) : maxValue);\n\n                    for (int i = from; i &lt;= to; i++) {\n                        yield return i;\n                    }\n                }\n            }\n        }\n    }\n</code></pre>\n'}, {'answer_id': 25334542, 'author': 'Chris Fazzio', 'author_id': 1274209, 'author_profile': 'https://Stackoverflow.com/users/1274209', 'pm_score': 3, 'selected': False, 'text': "<p>Below is the code I just put together to do this.. You can enter in the format like.. 1-2,5abcd,6,7,20-15,,,,,,</p>\n\n<p>easy to add-on for other formats</p>\n\n<pre><code>    private int[] ParseRange(string ranges)\n    { \n        string[] groups = ranges.Split(',');\n        return groups.SelectMany(t =&gt; GetRangeNumbers(t)).ToArray();\n    }\n\n    private int[] GetRangeNumbers(string range)\n    {\n        //string justNumbers = new String(text.Where(Char.IsDigit).ToArray());\n\n        int[] RangeNums = range\n            .Split('-')\n            .Select(t =&gt; new String(t.Where(Char.IsDigit).ToArray())) // Digits Only\n            .Where(t =&gt; !string.IsNullOrWhiteSpace(t)) // Only if has a value\n            .Select(t =&gt; int.Parse(t)) // digit to int\n            .ToArray();\n        return RangeNums.Length.Equals(2) ? Enumerable.Range(RangeNums.Min(), (RangeNums.Max() + 1) - RangeNums.Min()).ToArray() : RangeNums;\n    }\n</code></pre>\n"}, {'answer_id': 33666576, 'author': 'w.b', 'author_id': 2720372, 'author_profile': 'https://Stackoverflow.com/users/2720372', 'pm_score': 0, 'selected': False, 'text': '<p>The answer I came up with:</p>\n\n<pre><code>static IEnumerable&lt;string&gt; ParseRange(string str)\n{\n    var numbers = str.Split(\',\');\n\n    foreach (var n in numbers)\n    {\n       if (!n.Contains("-")) \n           yield return n;\n       else\n       {\n           string startStr = String.Join("", n.TakeWhile(c =&gt; c != \'-\'));\n           int startInt = Int32.Parse(startStr);\n\n           string endStr = String.Join("", n.Reverse().TakeWhile(c =&gt; c != \'-\').Reverse());\n           int endInt = Int32.Parse(endStr);\n\n           var range = Enumerable.Range(startInt, endInt - startInt + 1)\n                                 .Select(num =&gt; num.ToString());\n\n           foreach (var s in range)\n               yield return s;\n        }\n    }\n}\n</code></pre>\n'}, {'answer_id': 33667542, 'author': 'bradgonesurfing', 'author_id': 158285, 'author_profile': 'https://Stackoverflow.com/users/158285', 'pm_score': 2, 'selected': False, 'text': '<p>You can\'t be sure till you have test cases. In my case i would prefer to be white space delimited instead of comma delimited. It make the parsing a little more complex.</p>\n\n<pre><code>    [Fact]\n    public void ShouldBeAbleToParseRanges()\n    {\n        RangeParser.Parse( "1" ).Should().BeEquivalentTo( 1 );\n        RangeParser.Parse( "-1..2" ).Should().BeEquivalentTo( -1,0,1,2 );\n\n        RangeParser.Parse( "-1..2 " ).Should().BeEquivalentTo( -1,0,1,2 );\n        RangeParser.Parse( "-1..2 5" ).Should().BeEquivalentTo( -1,0,1,2,5 );\n        RangeParser.Parse( " -1  ..  2 5" ).Should().BeEquivalentTo( -1,0,1,2,5 );\n    }\n</code></pre>\n\n<p>Note that Keith\'s answer ( or a small variation) will fail the last test where there is whitespace between the range token. This requires a tokenizer and a proper parser with lookahead.</p>\n\n<pre><code>namespace Utils\n{\n    public class RangeParser\n    {\n\n        public class RangeToken\n        {\n            public string Name;\n            public string Value;\n        }\n\n        public static IEnumerable&lt;RangeToken&gt; Tokenize(string v)\n        {\n            var pattern =\n                @"(?&lt;number&gt;-?[1-9]+[0-9]*)|" +\n                @"(?&lt;range&gt;\\.\\.)";\n\n            var regex = new Regex( pattern );\n            var matches = regex.Matches( v );\n            foreach (Match match in matches)\n            {\n                var numberGroup = match.Groups["number"];\n                if (numberGroup.Success)\n                {\n                    yield return new RangeToken {Name = "number", Value = numberGroup.Value};\n                    continue;\n                }\n                var rangeGroup = match.Groups["range"];\n                if (rangeGroup.Success)\n                {\n                    yield return new RangeToken {Name = "range", Value = rangeGroup.Value};\n                }\n\n            }\n        }\n\n        public enum State { Start, Unknown, InRange}\n\n        public static IEnumerable&lt;int&gt; Parse(string v)\n        {\n\n            var tokens = Tokenize( v );\n            var state = State.Start;\n            var number = 0;\n\n            foreach (var token in tokens)\n            {\n                switch (token.Name)\n                {\n                    case "number":\n                        var nextNumber = int.Parse( token.Value );\n                        switch (state)\n                        {\n                            case State.Start:\n                                number = nextNumber;\n                                state = State.Unknown;\n                                break;\n                            case State.Unknown:\n                                yield return number;\n                                number = nextNumber;\n                                break;\n                            case State.InRange:\n                                int rangeLength = nextNumber - number+ 1;\n                                foreach (int i in Enumerable.Range( number, rangeLength ))\n                                {\n                                    yield return i;\n                                }\n                                state = State.Start;\n                                break;\n                            default:\n                                throw new ArgumentOutOfRangeException();\n                        }\n                        break;\n                    case "range":\n                        switch (state)\n                        {\n                            case State.Start:\n                                throw new ArgumentOutOfRangeException();\n                                break;\n                            case State.Unknown:\n                                state = State.InRange;\n                                break;\n                            case State.InRange:\n                                throw new ArgumentOutOfRangeException();\n                                break;\n                            default:\n                                throw new ArgumentOutOfRangeException();\n                        }\n                        break;\n                    default:\n                        throw new ArgumentOutOfRangeException( nameof( token ) );\n                }\n            }\n            switch (state)\n            {\n                case State.Start:\n                    break;\n                case State.Unknown:\n                    yield return number;\n                    break;\n                case State.InRange:\n                    break;\n                default:\n                    throw new ArgumentOutOfRangeException();\n            }\n        }\n    }\n}\n</code></pre>\n'}, {'answer_id': 43974476, 'author': 'jdweng', 'author_id': 5015238, 'author_profile': 'https://Stackoverflow.com/users/5015238', 'pm_score': 0, 'selected': False, 'text': '<p>Regex is not efficient as following code.  String methods are more efficient than Regex and should be used when possible.</p>\n\n<pre><code>using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text;\nusing System.Text.RegularExpressions;\n\nnamespace ConsoleApplication1\n{\n    class Program\n    {\n        static void Main(string[] args)\n        {\n            string[] inputs = {\n                                 "001-005/015",\n                                 "009/015"\n                             };\n\n            foreach (string input in inputs)\n            {\n                List&lt;int&gt; numbers = new List&lt;int&gt;();\n                string[] strNums = input.Split(new char[] { \'/\' }, StringSplitOptions.RemoveEmptyEntries);\n                foreach (string strNum in strNums)\n                {\n                    if (strNum.Contains("-"))\n                    {\n                        int startNum = int.Parse(strNum.Substring(0, strNum.IndexOf("-")));\n                        int endNum = int.Parse(strNum.Substring(strNum.IndexOf("-") + 1));\n                        for (int i = startNum; i &lt;= endNum; i++)\n                        {\n                            numbers.Add(i);\n                        }\n                    }\n                    else\n                        numbers.Add(int.Parse(strNum));\n                }\n                Console.WriteLine(string.Join(",", numbers.Select(x =&gt; x.ToString())));\n            }\n            Console.ReadLine();\n\n        }\n    }\n}\n</code></pre>\n'}, {'answer_id': 43974770, 'author': 'fubo', 'author_id': 1315444, 'author_profile': 'https://Stackoverflow.com/users/1315444', 'pm_score': 1, 'selected': False, 'text': '<p>One line approach with <code>Split</code> and <code>Linq</code></p>\n\n<pre><code>string input = "1,3,5-10,12";\nIEnumerable&lt;int&gt; result = input.Split(\',\').SelectMany(x =&gt; x.Contains(\'-\') ? Enumerable.Range(int.Parse(x.Split(\'-\')[0]), int.Parse(x.Split(\'-\')[1]) - int.Parse(x.Split(\'-\')[0]) + 1) : new int[] { int.Parse(x) });\n</code></pre>\n'}, {'answer_id': 51023394, 'author': 'Community', 'author_id': -1, 'author_profile': 'https://Stackoverflow.com/users/-1', 'pm_score': 0, 'selected': False, 'text': "<p>My solution:</p>\n\n<ul>\n<li>return list of integers</li>\n<li>reversed/typo/duplicate possible: 1,-3,5-,7-10,12-9 => 1,3,5,7,8,9,10,12,11,10,9 (used when you want to extract, repeat pages)</li>\n<li>option to set total of pages: 1,-3,5-,7-10,12-9 (Nmax=9) => 1,3,5,7,8,9,9</li>\n<li><p>autocomplete: 1,-3,5-,8 (Nmax=9) => 1,3,5,6,7,8,9,8</p>\n\n<pre><code>    public static List&lt;int&gt; pageRangeToList(string pageRg, int Nmax = 0)\n{\n    List&lt;int&gt; ls = new List&lt;int&gt;();\n    int lb,ub,i;\n    foreach (string ss in pageRg.Split(','))\n    {\n        if(int.TryParse(ss,out lb)){\n            ls.Add(Math.Abs(lb));\n        } else {\n            var subls = ss.Split('-').ToList();\n            lb = (int.TryParse(subls[0],out i)) ? i : 0;\n            ub = (int.TryParse(subls[1],out i)) ? i : Nmax;\n            ub = ub &gt; 0 ? ub : lb; // if ub=0, take 1 value of lb\n            for(i=0;i&lt;=Math.Abs(ub-lb);i++) \n                ls.Add(lb&lt;ub? i+lb : lb-i);\n        }\n    }\n    Nmax = Nmax &gt; 0 ? Nmax : ls.Max(); // real Nmax\n    return ls.Where(s =&gt; s&gt;0 &amp;&amp; s&lt;=Nmax).ToList();\n}\n</code></pre></li>\n</ul>\n"}]
['https://Stackoverflow.com/questions/40161', 'https://Stackoverflow.com', 'https://Stackoverflow.com/users/305/']