5

Is there a C# equivalent of Python's range with step?


Documentation:

For a positive step, the contents of a range r are determined by the formula r[i] = start + step*i where i >= 0 and r[i] < stop.

For a negative step, the contents of the range are still determined by the formula r[i] = start + step*i, but the constraints are i >= 0 and r[i] > stop.


Example:

>>> list(range(0, 10, 3))
[0, 3, 6, 9]
>>> list(range(0, -10, -1))
[0, -1, -2, -3, -4, -5, -6, -7, -8, -9]
Community
  • 1
  • 1
budi
  • 6,351
  • 10
  • 55
  • 80
  • 3
    `Enumerable.Range(0, ((stop-start)/step) + ((stop-start)%step == 0 ? 0 : 1 )).Select(i => start + step * i)` – juharr May 10 '17 at 15:46
  • C# also has ranges now (with the syntax `start..end`), but afaik it doesn't support a step parameter. – Hutch Moore Mar 06 '20 at 14:10

3 Answers3

6

I would go with two methods implementation. First one for parameters validation and providing defaults:

public static IEnumerable<int> Range(int start, int stop, int step = 1)
{
    if (step == 0)
        throw new ArgumentException(nameof(step));

    return RangeIterator(start, stop, step);
}

That is required for iterators with deferred execution. Otherwise, you will not validate arguments until iterator will be executed. Which might happen a long time after you get an iterator reference. And iterator itself (actually with C# 7 you can use a local function instead of creating separate method):

private static IEnumerable<int> RangeIterator(int start, int stop, int step)
{
    int x = start;

    do
    {
        yield return x;
        x += step;
        if (step < 0 && x <= stop || 0 < step && stop <= x)
            break;
    }
    while (true);
}

To implement Python's range behavior we need one more method which accepts only stop parameter. We can simplify code with C# 6 expression-bodied member:

public static IEnumerable<int> Range(int stop) => RangeIterator(0, stop, 1);

You also can make static methods available in global scope with C# 6. Assume class with Range method description is named PythonUtils:

using static YourNamespace.PythonUtils;

And usage in code will look like

foreach(var i in Range(0, 10, 3))
   Print(i);

You can also use default values

Range(0, 10, 3)     // [0,3,6,9]
Range(4, -3, -1)    // [4,3,2,1,0,-1,-2]
Range(5)            // [0,1,2,3,4]
Range(2, 5)         // [2,3,4]

Looks like Pascal-case Python :)

Sergey Berezovskiy
  • 232,247
  • 41
  • 429
  • 459
  • 1
    I like this solution better than my own, just a few things I'd like to point out: `range()` is not valid in Python, and `range(5)` should return `[0, 1, 2, 3, 4]`. I didn't specify these scenarios in my original question, I just included them in my answer just to make it feel as Python-like as possible. – budi May 10 '17 at 16:34
  • 1
    @budi sorry, I have forgotten details of Python's range. Updated answer with additional Range method. And I have removed some defaults from Range method with three parameters – Sergey Berezovskiy May 10 '17 at 17:11
  • 1
    Nitpicking here, but `list(range(1, -3, 2))` in python returns `[]`, however your solution `Range(1, -3, 2)` returns `[1]`. I've unaccepted for now, since my solution handled this case; I'll gladly re-accept if you want to update your solution! – budi Jul 06 '17 at 21:12
4

We can implement a static utility class to handle this.

For completeness, this solution mimics Python's range behavior for one parameter (stop), two parameters (start, stop), and three parameters (start, stop, step):

using System;
using System.Collections.Generic;

public static class EnumerableUtilities
{
    public static IEnumerable<int> RangePython(int start, int stop, int step = 1)
    {
        if (step == 0)
            throw new ArgumentException("Parameter step cannot equal zero.");

        if (start < stop && step > 0)
        {
            for (var i = start; i < stop; i += step)
            {
                yield return i;
            }
        }
        else if (start > stop && step < 0)
        {
            for (var i = start; i > stop; i += step)
            {
                yield return i;
            }
        }
    }

    public static IEnumerable<int> RangePython(int stop)
    {
        return RangePython(0, stop);
    }
}

Example Usage with Step:

foreach (var i in EnumerableUtilities.RangePython(0, 10, 3))
{
    Console.WriteLine(i);
}

Output:

0
3
6
9
budi
  • 6,351
  • 10
  • 55
  • 80
1

Inspired by Sergey's solution and mimics python's range.

static class Utils
{
    public static IEnumerable<int> Range(int start, int stop, int step = 1)
    {
        if (step == 0)
            throw new ArgumentException(nameof(step));

        while (step > 0 && start < stop || step < 0 && start > stop)
        {
            yield return start;
            start += step;
        }
    }

    public static IEnumerable<int> Range(int stop) => Range(0, stop, 1);
}

void Main()
{
    var ranges = new IEnumerable<int>[] {
        Utils.Range(0, 10, 3),    // [0,3,6,9]
        Utils.Range(4, -3, -1),   // [4,3,2,1,0,-1,-2]
        Utils.Range(5),           // [0,1,2,3,4]
        Utils.Range(2, 5),        // [2,3,4]
        Utils.Range(1, -3, 2),    // []
    };
    
    Array.ForEach(ranges, Console.WriteLine);
}

A small optimization can be made to the while loop for better performance, which I like.

if (step > 0) while (start < stop)
{
    yield return start;
    start += step;
}
else while (start > stop)
{
    yield return start;
    start += step;
}

There might be a clever-er way to do this.

frederick99
  • 1,033
  • 11
  • 18