3
\$\begingroup\$

I'm refactoring my scheduler and for one of its triggers I need an endless counter that would automatically restart. Since this is a simple and very common task I don't want to ever have to implement it again so I encapsulated it in such a way that it hopefully can be reused everywhere else.


The counter is represented by an enumerable interface with a couple or properties that provide information about the counter and methods allowing to interact with it.

public interface IInfiniteCounter : IEnumerable<(int Value, InfiniteCounterState State)>
{
    int Min { get; }

    int Max { get; }

    int Length { get; }

    int Current { get; }

    (int Value, InfiniteCounterState State) Next();

    void Reset();
}

It uses an enum to inform the caller about the type of the value. It returns First each time the counter restarted, Intermediate for values inbetween and Last for the last value.

public enum InfiniteCounterState
{
    First,
    Intermediate,
    Last,
}

I have one implementation of the interface. The InfiniteCounter that is a forward running counter:

public class InfiniteCounter : IInfiniteCounter
{
    private int _current;

    public InfiniteCounter(int min, int max)
    {
        Min = min;
        Max = max;
    }

    public InfiniteCounter(int max) : this(0, max) { }

    public int Min { get; }

    public int Max { get; }

    public int Length => Max - Min;

    public int Current => _current + Min;

    private bool IsLast => _current == Length;

    public (int Value, InfiniteCounterState State) Next()
    {
        if (IsLast)
        {
            Reset();
        }

        return
        (
            Current,
            _current++ == 0
                ? InfiniteCounterState.First
                : IsLast
                    ? InfiniteCounterState.Last
                    : InfiniteCounterState.Intermediate
        );
    }

    public void Reset()
    {
        _current = 0;
    }

    public IEnumerator<(int Value, InfiniteCounterState State)> GetEnumerator()
    {
        while (true)
        {
            yield return Next();
        }
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

In order to have it running backwards I created two extensions. One that is an extension for the IInfiniteCounter

public static class InfiniteCounterExtensions
{
    public static IEnumerable<(int Value, InfiniteCounterState State)> Countdown(this IInfiniteCounter counter)
    {
        return counter.Select(x => (x.Value.Flip(counter.Min, counter.Max), x.State));
    }
}

and one that is flipping the value:

public static class IndexMath
{
    public static int Flip(this int value, int min, int max)
    {
        return (-(value - max + 1) % (max - min)) + min;
    }
}

Examples:

void Main()
{
    var take = 4;
    new InfiniteCounter(3).Take(take).Dump("0-2");
    new InfiniteCounter(3).Countdown().Take(take).Dump("2-0");
    new InfiniteCounter(2, 5).Take(take).Dump("2-4");
    new InfiniteCounter(2, 5).Countdown().Take(take).Dump("4-2");

    0.Flip(0, 3).Dump();
    1.Flip(0, 3).Dump();
}

What do you think about it? It should be convenient to use and every piece of it should be testable. (Argument checking will be added later)

\$\endgroup\$
3
\$\begingroup\$
public interface IInfiniteCounter : IEnumerable<(int Value, InfiniteCounterState State)>

vs

    int Current { get; }

    (int Value, InfiniteCounterState State) Next();

    void Reset();

The interface itself looks a lot more like IEnumerator<> than IEnumerable<>, so IMO implementing IEnumerable<> is misleading and dangerous. The docs for IEnumerable<> don't say either way whether or not it's possible to have two active enumerators at the same time, but I can't think of any implementations in the standard library for which it isn't possible, and I've probably written a lot of code which assumes that it is possible.


    int Min { get; }

    int Max { get; }

    int Length { get; }

I can safely assume that Min is inclusive, but I absolutely need a comment to tell me whether Max is or not, and it would be helpful to have a comment indicating the standard implementation of Length. Also, should Length be uint both because it's guaranteed to be non-negative and to avoid overflow in edge cases?


public static class IndexMath
{
    public static int Flip(this int value, int min, int max)
    {
        return (-(value - max + 1) % (max - min)) + min;
    }
}

I don't understand why this method uses %. For me the obvious interpretation of the name (and the use case) is max - (value - min). If there's some trickery going on to avoid or handle overflow, it needs explanatory comments.

\$\endgroup\$
  • \$\begingroup\$ This is an interesting point with the enumerator thing... it's hard to decide which one it actually should be... I think it should be cleaner if Next wasn't part of the interface... oops, or not, I need this because it's not running in a loop but will be called by the scheduler on each tick... You're right, the % should fix the overflow of the value. \$\endgroup\$ – t3chb0t Mar 7 at 12:35
1
\$\begingroup\$

@Peter Taylor is right. It was some strange enumerable/enumerator hybrid so I have turned it into a real IEnumerator<T> of itself because I think is makes it easier to use in various scenarios.

If I now want to have an IEnumerable I can use the new AsEnumerable extension and if I want to have a countdown then I can flip the value with new ValueAsCountdown() extension. The State has become Position.

It hasn't changed much but here's the full code (that I still need to document... later):

public interface IInfiniteCounter : IEnumerator<IInfiniteCounter>
{
    /// <summary>
    /// Gets the min value of the counter.
    /// </summary>
    int Min { get; }

    /// <summary>
    /// Gets the max exclusive value of the counter.
    /// </summary>
    int Max { get; }

    /// <summary>
    /// Gets the total length of the counter.
    /// </summary>
    int Length { get; }

    /// <summary>
    /// Gets the current value of the counter.
    /// </summary>
    int Value { get; }

    /// <summary>
    /// Gets the relative position of the counter between min and max.
    /// </summary>
    InfiniteCounterPosition Position { get; }
}

public class InfiniteCounter : IInfiniteCounter
{
    private int _value;

    public InfiniteCounter(int min, int max)
    {
        Min = min;
        Max = max;
        Reset();
    }

    public InfiniteCounter(int max) : this(0, max) { }

    public int Min { get; }

    public int Max { get; }

    public int Length => Max - Min;

    public int Value => _value + Min;

    IInfiniteCounter IEnumerator<IInfiniteCounter>.Current => this;

    object IEnumerator.Current => this;

    public InfiniteCounterPosition Position =>
        _value == 0
            ? InfiniteCounterPosition.First
            : _value == Length - 1
                ? InfiniteCounterPosition.Last
                : InfiniteCounterPosition.Intermediate;

    public bool MoveNext()
    {
        if (Position == InfiniteCounterPosition.Last)
        {
            Reset();
        }

        _value++;

        return true;
    }

    public void Reset()
    {
        _value = -1;
    }

    public void Dispose()
    {
        // There is nothing to dispose.
    }
}

public static class IndexMath
{
    public static int Flip(this int value, int min, int max)
    {
        return (-(value - max + 1) % (max - min)) + min;
    }
}

public static class InfiniteCounterExtensions
{
    public static IEnumerable<IInfiniteCounter> AsEnumerable(this IEnumerator<IInfiniteCounter> counter)
    {
        while (counter.MoveNext())
        {
            yield return counter.Current;
        }
    }

    public static int ValueAsCountdown(this IInfiniteCounter counter)
    {
        return counter.Value.Flip(counter.Min, counter.Max);
    }
}

public enum InfiniteCounterPosition
{
    First,
    Intermediate,
    Last,
}
\$\endgroup\$
  • \$\begingroup\$ I have small issue with InfiniteCounterPosition. I guess you intend to have a proper range, and not have Min=Max, in which case a given position could be both the First and Last. \$\endgroup\$ – Rick Davin Mar 9 at 0:05
  • \$\begingroup\$ @RickDavin good point, I was thinking about restricting the length to at least two elements. Otherwise it doesn't make sense to use it at all ;-) \$\endgroup\$ – t3chb0t Mar 9 at 9:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.