MoreLINQ icon indicating copy to clipboard operation
MoreLINQ copied to clipboard

Add Positions (1-based index seq where a value is found)

Open atifaziz opened this issue 8 years ago • 4 comments

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?

atifaziz avatar Jan 13 '17 10:01 atifaziz

would be cool call it Indices like the 0-based index functions of haskell and prelude.js

leandromoh avatar Mar 11 '17 20:03 leandromoh

would be cool call it Indices like 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.

atifaziz avatar Apr 21 '17 23:04 atifaziz

This was proposed once in #81.

atifaziz avatar Jun 21 '17 07:06 atifaziz

Why not a 0-based FindIndices, a plural version of the existing List<T>.FindIndex ?

Or IndicesWhere with a predicate ?

Orace avatar Nov 03 '19 22:11 Orace

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.

atifaziz avatar Jan 30 '23 17:01 atifaziz