0

I have a simple function that I'm just using as an example here. It's an infinite enumeration!

public static IEnumerable<int> Roll()
{
    while (true)
    {
        yield return new Random().Next(1, 6);
    }
}

And I want this function to keep rolling for a specific amount of clock ticks so I have this method:

    public static List<T> Ticks<T>(this IEnumerable<T> data, int time)
    {
        var list = new List<T>();
        Stopwatch timer = null;
        foreach (var item in data)
        {
            if (timer == null)
            {
                timer = Stopwatch.StartNew();
            }

            if (timer.ElapsedTicks < time)
            {
                list.Add(item);
            }
            else
            {
                break;
            }
        }
        timer?.Stop();
        return list;
    }

But I don't really like how I had to write that as I rather have something more like this:

    public static List<T> Ticks2<T>(this IEnumerable<T> data, int time)
    {
        var list = new List<T>();
        Stopwatch timer = Stopwatch.StartNew();
        using (var en = data.GetEnumerator())
        {
            while (timer.ElapsedTicks < time && en.MoveNext())
            {
                list.Add(en.Current);
            }
        }
        timer.Stop();
        return list;
    }

But the latter function will fail as the GetEnumerator() method will aggregate all data and in an infinitive enumeration, that takes forever...
Wait... I'm wrong... It turned out to be so fast that my list ran out of memory... It's not aggregating, fortunately...
The Ticks() method works fine, btw! I can use this:

Console.WriteLine( Watch.Roll().Ticks(10000).AllToString().AllJoin(", ") );

And it will roll until 10,000 ticks have passed, convert everything to strings and join everything while keeping the chain of methods. This is important, btw. I cannot accept any answer that will break the method chain. Keep in mind that this is just an example.
And no, I can't change anything to that Roll() method. That method is read-only...

So, how can I rewrite this Ticks() method more similar to the Ticks2() method without ending up aggregating all data?


For those curious to the AllToString() and AllJoin methods:

    public static IEnumerable<string> AllToString<T>(this IEnumerable<T> data) => data.Select(item => item.ToString());
    public static string AllJoin<T>(this IEnumerable<T> data, string separator) => string.Join(separator, data.AllToString());

These two methods can be practical for other solutions where you'd be using enumerations and want string output.

Wim ten Brink
  • 25,901
  • 20
  • 83
  • 149
  • In case anyone wonders: the enumeration that I want to walk through is basically a sieve of several methods being chained together. So more like A().B().C().D().Ticks(1000) as each method will yield values to the next one, if conditions are met. It's a bit ugly, but it has to fit in this... – Wim ten Brink Oct 24 '19 at 22:00
  • 1
    "But the latter function will fail as the GetEnumerator() method will aggregate all data and in an infinitive enumeration, that takes forever..." this isn't true - `GetEnumerator()` doesn't iterate through the enumerator. – Dai Oct 24 '19 at 22:03
  • https://stackoverflow.com/users/159145/dai Turns out you're right. It turns out it's so fast compared to the first one that the list that got stored was huge... – Wim ten Brink Oct 24 '19 at 22:08
  • BTW I hope your `AllToString` and `AllJoin` methods are performing string concatenation using a single `StringBuilder` instance instead of `s3 = s1 + s2` because repetitive string concatenation is expensive. – Dai Oct 24 '19 at 22:14
  • public static IEnumerable AllToString(this IEnumerable data) => data.Select(item => item.ToString()); public static string AllJoin(this IEnumerable data, string separator) => string.Join(separator, data.AllToString()); I think they do... :) – Wim ten Brink Oct 25 '19 at 14:20
  • And to be honest, I was working on this enumeration after a long day, very tired and wasn't thinking clearly any more. I'm actually practicing my skills with enumerations to ome up with interesting solutions within a method chain. Basically, to form data pipelines. – Wim ten Brink Oct 25 '19 at 14:31
  • And only now do I notice that AllJoin() calls AllToString() internally, while my code itself also calls AllToString() before calling AllJoin(). I really need more sleep... – Wim ten Brink Oct 25 '19 at 18:11

1 Answers1

1
public static IEnumerable<T> Ticks<T>( this IEnumerable<T> source, Int64 totalTicks )
{
    Stopwatch sw = Stopwatch.StartNew();

    foreach( T item in source )
    {
        if( sw.ElapsedTicks < totalTicks )
        {
            yield return item;
        }
        else
        {
            yield break;
        }
    }
}

You actually don't need any custom extension methods at all: you can also just use TakeWhile instead of Ticks, Select instad of AllToString(), and Aggregate instead of AllJoin:

Stopwatch sw = Stopwatch.StartNew();

String all = Watch.Roll()
    .TakeWhile( _ => sw.ElapsedTicks < 10000 )
    .Select( i => i.ToString() )
    .Aggregate(
        new StringBuilder(),
        ( s, sb ) => sb.Append( s ).Append(", "),
        sb => sb.ToString()
    );

Console.WriteLine( all ):
Dai
  • 141,631
  • 28
  • 261
  • 374
  • AllToString() and AllJoin() aren't very complex methods. The TakeWhile is a good option but requires the stopwatch to be defined outside the method chain. My Ticks() method keeps it within the method chain and actually starts measuring once the real enumeration starts. Calling Roll() will also take a very short time which my Ticks() won't count. – Wim ten Brink Oct 25 '19 at 14:27