tags:

views:

365

answers:

2

O' LINQ-fu masters, please help.

I have a requirement where I have to add items into a List(Of T) (let's call it Target) from an IEnumerable(Of T) (let's call it Source) using Target.AddRange() in VB.NET.

Target.AddRange(Source.TakeWhie(Function(X, Index) ?))

The ? part is a tricky condition that is something like: As long as the as yet unenumerated count is not equal to what is needed to fill the list to the minimum required then randomly decide if the current item should be taken, otherwise take the item. Somethig like...

Source.Count() - Index = _minimum_required - _curr_count_of_items_taken _
OrElse GetRandomNumberBetween1And100() <= _probability_this_item_is_taken
' _minimum_required and _probability_this_item_is_taken are constants

The confounding part is that _curr_count_of_items_taken needs to be incremented each time the TakeWhile statement is satisfied. How would I go about doing that?

I'm also open to a solution that uses any other LINQ methods (Aggregate, Where, etc.) instead of TakeWhile.

If all else fails then I will go back to using a good old for-loop =)

But hoping there is a LINQ solution. Thanks in advance for any suggestions.

EDIT: Good old for-loop version as requested:

Dim _source_total As Integer = Source.Count()
For _index As Integer = 0 To _source_total - 1
    If _source_total - _index = MinimumRows - Target.Count _
    OrElse NumberGenerator.GetRandomNumberBetween1And100 <= _possibility_item_is_taken Then
        Target.Add(Source(_index))
    End If
Next

EDITDIT: David's no-side-effects answer comes closes to what I need while staying readable. Maybe he's the only one who could understand my poorly communicated pseudo-code =). The OrderBy(GetRandomNumber) is brilliant in hindsight. I just need to change the Take(3) part to Take(MinimumRequiredPlusAnOptionalRandomAmountExtra) and drop the OrderBy and Select at the end. Thanks to the rest for suggestions.

+4  A: 

You need to introduce a side-effect, basically.

In C# this is relatively easy - you can use a lambda expression which updates a captured variable. In VB this may still be possible, but I wouldn't like to guess at the syntax. I don't quite understand your condition (it sounds a little backwards) but you could do something like:

The C# would be something like:

int count = 0;

var query = source.TakeWhile(x => count < minimumRequired ||
                                  rng.Next(100) < probability)
                  .Select(x => { count++; return x; });

target.AddRange(query);

The count will be incremented each time an item is actually taken.

Note that I suspect you actually want Where instead of TakeWhile - otherwise the first time the rng gives a high number, the sequence will end.

EDIT: If you can't use side-effects directly you may be able to use a horrible hack. I haven't tried this, but...

public static T Increment<T>(ref int counter, T value)
{
    counter++;
    return value;
}

...

int count = 0;
var query = source.TakeWhile(x => count < minimumRequired ||
                                  rng.Next(100) < probability)
                  .Select(x => Increment(ref count, x));

target.AddRange(query);

In other words, you put the side-effect into a separate method, and call the method using pass-by-reference for the counter. No idea if it would work in VB, but possibly worth a try. On the other hand, a loop might be simpler...

As a completely different way of approaching it, is your source an in-memory collection already, which you can iterate through cheaply? If so, just use:

var query = Enumerable.Concat(source.Take(minimumRequired),
                              source.Skip(minimumRequired)
                                    .TakeWhile(condition));

In other words, definitely grab the first n elements, and then start again, skip the first n elements and take the rest based on the condition.

Jon Skeet
+1 because it's basically the same answer I gave! Not sure I understood the requirement, but I think Lambdas in VB are limited to pure expressions, not statement blocks, so it probably isn't going to be pretty.
Daniel Earwicker
Jon, thanks for the suggestion. I had thought of the side-effect way but VB.NET doesn't allow multi-statement lambdas. So can't do the { count++; return x;} part without some hacking that would probably make it more verbose than the for-loop =/. Any other ideas?
fung
Added some other options.
Jon Skeet
+2  A: 

If your task is to extract 3 random images from a collection of 50 random images, this works great.

target.AddRange( source.OrderBy(GetRandomNumber).Take(3) );

If you require order preservation, that's not too hard to add:

target.AddRange( source
  .Select( (x, i) => new {x, i})
  .OrderBy(GetRandomNumber)
  .Take(3)
  .OrderBy( z => z.i)
  .Select( z => z.x)
);


If requirements are to (for whatever reason)

  • favor items at the end of the list
  • allow more items through than requested (5 instead of 3, but only sometimes)

then I'd write the foreach loop.

David B
Ooh, that order preservation version is beautiful.
Daniel Earwicker