Add Positions (1-based index seq where a value is found)
Positions looks for a value (and more generally any condition) in a sequence and returns positions of those elements that match the value (or a given condition).
Examples
var sentence = "The quick brown fox jumps over the lazy dog";
Console.WriteLine(sentence.Positions('o')
.ToDelimitedString(", "));
// Output: 13, 18, 27, 42
var vowels = "aeiou";
Console.WriteLine(sentence.Positions(ch => vowels.IndexOf(ch) >= 0)
.ToDelimitedString(", "));
// Output: 3, 6, 7, 13, 18, 22, 27, 29, 34, 37, 42
Positions can be built entirely on top of existing operators:
public static IEnumerable<int> Positions<T>(this IEnumerable<T> source, Func<T, bool> predicate) =>
from e in Enumerable.Range(1, int.MaxValue)
.Zip(source, (i, e) => new KeyValuePair<int, T>(i, e))
where predicate(e.Value)
select e.Key;
In general, I'm against adding operators that can be implemented as a trivial combination of others but for something as simple as getting positions, you need quite a few (Range, Zip, Where and Select) and so it's rather non-trivial.
The actual implementation in this PR doesn't use Range and Zip as we have Index.
Positions returns 1-based indexes because positions are not generally zero-based offsets. One tends to think of positions as 1st, 2nd, 3rd, 4th and so on. Given the method name of Positions, I felt compelled to return 1-based indexes but it would be a shame if the 80% use case would require the user to have to combine with Select just to get back 0-based indexes/offsets, as in:
var sentence = "The quick brown fox jumps over the lazy dog";
Console.WriteLine(sentence.Positions('o')
.Select(p => p - 1)
.ToDelimitedString(", "));
// Output: 12, 17, 26, 41
To return 0-based indexes, one would have to come up with another name than Positions (unless I'm overthinking this?). Because we have Index, Indexes is a no-go. An ideal choice would have been IndexOf since Positions is basically IndexOf done right for sequential types but it conflicts with semantics of existing IndexOf implementations like String.IndexOf and List<T>.IndexOf that return the index of the first match only. Is Offsets a better name then?
would be cool call it Indices like the 0-based index functions of haskell and prelude.js
would be cool call it
Indiceslike the 0-based index functions of haskell and prelude.js
True except as I said earlier, we already have Index so Indicies or Indexes is a no-go to avoid confusion.
This was proposed once in #81.
Why not a 0-based FindIndices, a plural version of the existing List<T>.FindIndex ?
Or IndicesWhere with a predicate ?
Since the addition of Choose, this can be implemented simply as Index(1) + Choose:
public static IEnumerable<int> Positions<T>(this IEnumerable<T> source, Func<T, bool> predicate) =>
source.Index(1).Choose(e => predicate(e.Value) ? (true, e.Key) : default);
Therefore I'm abandoning this as too trivial and not something worth adding and maintaining for the few occasions it might be needed.