Or in general how to filter some elements from collection based on different and complex conditions in single pass
Let's say we have collection of elements
var cats = new List<Cat>{ new Cat("Fluffy"), new Cat("Meowista"), new Cat("Scratchy")};
And somewhere we use this collection
public CatFightResult MarchBoxing(List<Cat> cats, string redCatName, string blueCatName)
{
var redCat = cats.First(cat => cat.Name == redCatName);
var blueCat = cats.First(cat => cat.Name == blueCatName);
var redValue = redCat.FightValue();
var blueValue = blueCat.FightValue();
if (Cat.FightValuesEqualWithEpsilon(redValue, blueValue))
return new CatFightResult{IsDraw: true};
return new CatFightResult{Winner: redValue > blueValue ? redCat : blueCat};
}
Question: Is there a nice way to obtain multiple variables from collection based on some condition(s)?
The question probably requires some sort of uniqueness in collection, let's first assume there is some (i.e. HashSet
/Dictionary
)
AND preferably:
- SINGLE pass/cycle on collection (the most important reason of question, as you can see there are 2 filter operations in above method)
- oneliner or like that, with readability, and the shorter the better
- generic way (
IEnumerable<T>
I think, orICollection<T>
) - typos error-prone and changes/additions safe (minimal use of actual conditions in code, preferably checked
- null/exception check, because my intention that null is valid result for obtained variable
Would be also cool to have ability to provide custom conditions, which probably could be done via Func parameters, but I didn't tested yet.
There are my attempts, which I've posted in my repo https://github.com/phomm/TreeBalancer/blob/master/TreeTraverse/Program.cs
Here is the adaptation to example with Cats:
public CatFightResult MarchBoxing(List<Cat> cats, string redCatName, string blueCatName)
{
var redCat = null;
var blueCat = null;
//1 kinda oneliner, but hard to read and not errorprone
foreach (var c in cats) _ = c.Name == redCatName ? redCat = n : n.Name == blueCatName ? blueCat = n : null;
//2 very good try, because errorprone and easy to read (and find mistake in assignment), but not oneliner and not elegant (but fast) redundant fetching and not single pass at all, up to O(N*N) with FirstOrDefault
var filter = new [] { redCatName, blueCatName }.ToDictionary(x => x.Key, x => cats.FirstOrDefault(n => n.Name == x.Key));
redCat = filter[redCatName];
blueCat = filter[blueCatName];
//3 with readability and ckecks for mistakenly written searching keys (dictionary internal dupe key check) , but not oneliner and not actualy single pass
var dic = new Dictionary<int, Func<Cat, Cat>> { { redCatName, n => redCat = n }, { blueCatName, n => blueCat = n } };
cats.All(n => dic.TryGetValue(n.Name, out var func) ? func(n) is null : true);
//4 best approach, BUT not generic (ofc one can write simple generic IEnumerable<T> ForEach extension method, and it would be strong candidate to win)
cats.ForEach(n => _ = n.Name == redCatName ? redCat = n : n.Name == blueCatName ? blueCat = n : null);
//5 nice approach, but not single pass, enumerating collection twice
cats.Zip(cats, (n, s) => n.Name == redCatName ? redCat = n : n.Name == blueCatName ? blueCat = n : null);
//6 the one I prefer best, however it's arguable due to breaking functional approach of Linq, causing side effects
cats.All(n => (n.Name == redCatName ? redCat = n : n.Name == blueCatName ? blueCat = n : null) is null);
}
All the options with ternary op are not extensible easily and relatively error-prone, but are quite short and Linq-ish, they also rely (some trade-off with confusion) on not returning/using actual results of ternary (with discard "_" or "is null" as bool). I think the approach with Dictionary of Funcs is a good candidate to implement custom conditions, just bake-in them with variables.
Thank you, looking forward your solutions ! :)