[ruby-core:126166] [Ruby Feature#17056] Array#index: Allow specifying the position to start search as in String#index
Issue #17056 has been updated by nobu (Nobuyoshi Nakada). Status changed from Closed to Open I think we should reopen this issue and clarify the API before fixing the implementation. There are several API details that I think need to be decided explicitly. ## Issues to clarify ### 1. Object, block, and Enumerator forms The discussion in #note-7 and #note-8 explicitly considered using an offset with the block form: ```ruby array.index(offset: 10) { |element| ... } ``` However, the reverted implementation only handled `offset:` when an object was given. The following calls raised `ArgumentError`, despite the documented Enumerator signatures: ```ruby array.index(offset: 2) array.index(offset: 2) { |element| ... } array.rindex(offset: -2) array.rindex(offset: -2) { |element| ... } ``` Should all three forms accept `offset:`? ```ruby array.index(object, offset: 2) array.index(offset: 2) { |element| ... } array.index(offset: 2) # => Enumerator ``` The same question applies to `Array#rindex`. Since `Array#find_index` is an alias of `Array#index`, it should have exactly the same behavior. If the block form accepts `offset:`, the Enumerator form should retain the keyword argument so that calling `each` performs the same search. ### 2. Compatibility with searching for a Hash Before this feature, the following searches for the Hash `{ offset: 2 }`: ```ruby [{ offset: 2 }].index(offset: 2) # => 0 ``` If a keyword-only call now means the block/Enumerator form, this behavior changes. Searching for the Hash would still be possible with explicit braces: ```ruby [{ offset: 2 }].index({ offset: 2 }) # => 0 ``` Is this compatibility change acceptable? The same ambiguity exists when a block is given. Previously the keyword-like Hash was the object and the block was ignored with a warning. ### 3. Semantics of `Array#rindex` The reverted implementation did not behave like `String#rindex`. It also documented the signature as `rindex(object, offset: nil)`, but an explicit `nil` was not accepted: ```ruby array.rindex(object, offset: nil) # => TypeError: no implicit conversion of nil into Integer ``` In the implementation, `nil` was only used internally to represent an omitted keyword. Documenting `offset: nil` makes it look like `nil` is an accepted default value. For an Array, `offset: -1` would express the actual default starting position while keeping an explicit `nil` invalid. `String#rindex(pattern, offset)` starts searching backward at `offset`. A non-negative offset is an absolute position. A negative offset is relative to the end. An offset greater than the string length is clamped to the end. For example: ```ruby "ababa".rindex("a", 2) # => 2 "ababa".rindex("a", -2) # => 2 "ababa".rindex("a", 100) # => 4 ``` I think the corresponding Array behavior should be: ```ruby [:a, :b, :a, :b, :a].rindex(:a, offset: 2) # => 2 [:a, :b, :a, :b, :a].rindex(:a, offset: -2) # => 2 [:a, :b, :a, :b, :a].rindex(:a, offset: 100) # => 4 ``` The reverted implementation instead treated a non-negative offset as the lower bound of a search starting at the end: ```ruby [:a, :b, :a, :b, :a].rindex(:a, offset: 2) # => 4 ``` This makes positive and negative offsets have different meanings and is not consistent with `String#rindex`. Should `Array#rindex` follow `String#rindex` exactly? ### 4. Boundary and conversion behavior Should `Array#index` and `Array#rindex` follow the corresponding String methods for all of these cases? - Negative offsets relative to the end - An offset smaller than `-array.size` - An offset equal to `array.size` - An offset greater than `array.size` - Objects accepted through numeric conversion - Explicit `nil` - Integers too large for the internal index type These should be specified and tested for both forward and backward searches. ## Proposed signatures If the above behavior is intended, the signatures would be: ```ruby index(object, offset: 0) -> integer or nil index(offset: 0) { |element| ... } -> integer or nil index(offset: 0) -> Enumerator find_index(object, offset: 0) -> integer or nil find_index(offset: 0) { |element| ... } -> integer or nil find_index(offset: 0) -> Enumerator rindex(object, offset: -1) -> integer or nil rindex(offset: -1) { |element| ... } -> integer or nil rindex(offset: -1) -> Enumerator ``` The implementation has been reverted. Once these points are agreed, I think we should add specs for the complete behavior before reimplementing it. ---------------------------------------- Feature #17056: Array#index: Allow specifying the position to start search as in String#index https://bugs.ruby-lang.org/issues/17056#change-118232 * Author: TylerRick (Tyler Rick) * Status: Open ---------------------------------------- I have a use case of finding the first matching line within a given section in a file. After finding the line number of the start of the section, I want to find the first match after that line. My workaround for now is to use `with_index`: ```ruby lines = pathname.read.lines section_start_line = lines.index {|line| line.start_with?(/#* #{section_name}/) } lines.index.with_index {|line, i| i > section_start_line && line.include?(sought) } ``` I'd like to do it in a more concise way using a feature of `Array#index` that I propose here, which is analogous to `String#index`. If the second parameter of `String#index` is present, it specifies the position in the string to begin the search: ```ruby 'abcabc'.index('a') # => 0 'abcabc'.index('a',2) # => 3 ``` I would expect to also be able to do: ```ruby 'abcabc'.chars.index('a') # => 0 'abcabc'.chars.index('a', 2) ``` Using such feature, I would be able to do: ```ruby lines.index(sought, section_start_line) ``` This would give Ruby better parity with other programming languages like Python: ```python
list('abcabc') ['a', 'b', 'c', 'a', 'b', 'c'] list('abcabc').index('a') 0 list('abcabc').index('a', 2) 3
## End index
We can further think of an optional parameter to specify the position to end the search. The following languages allow specifying both start and end indexes:
- [Python](https://docs.python.org/3/tutorial/datastructures.html)
- [C#](https://docs.microsoft.com/en-us/dotnet/api/system.array.indexof?view=netcore-3.1)
Ruby's `String#index` does not have one, so we could make a separate proposal to add `end` to both methods at the same time.
--
https://bugs.ruby-lang.org/
participants (1)
-
nobu (Nobuyoshi Nakada)