Overview
Array#fetch_values retrieves multiple elements from an array using specified indices, returning them as a new array in the order requested. The method provides controlled access to array elements with built-in bounds checking and optional default value handling through block execution.
Ruby implements fetch_values
as an instance method on Array that accepts variable arguments representing indices. When an index exists within the array bounds, the method returns the corresponding element. When an index falls outside the bounds, the method raises an IndexError unless a block is provided to handle the missing index.
The method differs from bracket notation by enforcing explicit error handling for out-of-bounds access. Unlike Array#values_at which returns nil for missing indices, fetch_values
requires intentional handling of boundary conditions through exception catching or block provision.
numbers = [10, 20, 30, 40, 50]
values = numbers.fetch_values(0, 2, 4)
# => [10, 30, 50]
# With block for missing indices
result = numbers.fetch_values(0, 10, 2) { |index| "missing at #{index}" }
# => [10, "missing at 10", 30]
The method accepts negative indices following Ruby's standard array indexing conventions. Negative indices count backward from the array end, with -1 representing the final element. The method validates each index independently and processes them in the order specified.
letters = ['a', 'b', 'c', 'd', 'e']
backward = letters.fetch_values(-1, -3, -5)
# => ['e', 'c', 'a']
Array#fetch_values returns a new array containing the retrieved elements without modifying the original array. The returned array maintains the order of indices as specified in the method call, not the order elements appear in the source array.
Basic Usage
The fundamental usage pattern involves calling fetch_values
with one or more integer indices to retrieve corresponding array elements. The method processes indices sequentially and builds the result array maintaining the specified order.
fruits = ['apple', 'banana', 'cherry', 'date', 'elderberry']
selection = fruits.fetch_values(1, 3, 0)
# => ['banana', 'date', 'apple']
# Single index still returns array
single = fruits.fetch_values(2)
# => ['cherry']
Empty index lists return empty arrays without error. This behavior supports conditional index building where the index list might be dynamically generated.
data = [100, 200, 300, 400]
indices = []
result = data.fetch_values(*indices)
# => []
Mixed positive and negative indices work together in the same call. Ruby resolves negative indices to their positive equivalents during processing, then retrieves elements at the calculated positions.
matrix_row = [1, 2, 3, 4, 5, 6, 7, 8, 9]
corners = matrix_row.fetch_values(0, -1, 4)
# => [1, 9, 5]
# Duplicate indices retrieve the same element multiple times
duplicates = matrix_row.fetch_values(2, 2, 2)
# => [3, 3, 3]
Block syntax provides default value generation for missing indices. The block receives the problematic index as an argument and returns a replacement value. Ruby calls the block once for each missing index, allowing different default values based on the specific index that failed.
sparse_array = ['first', nil, 'third', nil, 'fifth']
filled = sparse_array.fetch_values(0, 1, 2, 10, 15) do |missing_index|
"default_for_#{missing_index}"
end
# => ['first', nil, 'third', 'default_for_10', 'default_for_15']
The block executes only for indices that fall outside array bounds. Indices pointing to nil values within bounds do not trigger block execution, maintaining the distinction between explicit nil storage and missing elements.
mixed_data = [42, nil, 'text', false]
with_defaults = mixed_data.fetch_values(1, 2, 8) { |i| "missing #{i}" }
# => [nil, 'text', 'missing 8']
Chaining fetch_values
with other array methods creates powerful data extraction patterns. The method integrates naturally with enumerable operations since it returns a standard array.
source = ['red', 'green', 'blue', 'yellow', 'purple', 'orange']
selected_indices = [1, 3, 5]
colors = source.fetch_values(*selected_indices).map(&:upcase)
# => ['GREEN', 'YELLOW', 'ORANGE']
Error Handling & Debugging
Array#fetch_values raises IndexError when accessing indices beyond array bounds without providing a default block. The exception message identifies the specific index that caused the failure, supporting targeted debugging in complex index operations.
data = ['a', 'b', 'c']
begin
result = data.fetch_values(0, 5, 2)
rescue IndexError => error
puts error.message
# => "index 5 outside of array bounds: -3...3"
puts "Failed at index: #{error.message[/index (\d+)/, 1]}"
# => "Failed at index: 5"
end
Multiple out-of-bounds indices generate exceptions for the first invalid index encountered during sequential processing. Ruby does not validate all indices before processing, making error location predictable based on parameter order.
sample = [1, 2, 3, 4, 5]
begin
# Will fail at index 10, not 20
sample.fetch_values(1, 10, 20, 3)
rescue IndexError => error
puts "First invalid index: #{error.message}"
# => "First invalid index: index 10 outside of array bounds: -5...5"
end
Defensive programming patterns combine bounds checking with graceful degradation. This approach prevents exceptions while maintaining data integrity through explicit validation.
def safe_fetch_values(array, *indices)
valid_indices = indices.select { |i| i >= -array.length && i < array.length }
invalid_indices = indices - valid_indices
result = array.fetch_values(*valid_indices) if valid_indices.any?
{
values: result || [],
invalid: invalid_indices,
success: invalid_indices.empty?
}
end
test_array = [10, 20, 30]
outcome = safe_fetch_values(test_array, 0, 5, -1, -10, 2)
# => {
# values: [10, 30, 30],
# invalid: [5, -10],
# success: false
# }
Block-based error recovery enables sophisticated default value strategies based on the missing index position or application context. The block parameter provides access to the exact index that failed, supporting contextual default generation.
class ConfigArray
def initialize(values)
@values = values
end
def get_with_defaults(*indices)
@values.fetch_values(*indices) do |missing_index|
case missing_index
when 0...10
"default_primary_#{missing_index}"
when 10...100
"default_secondary_#{missing_index}"
else
"default_overflow"
end
end
end
end
config = ConfigArray.new(['real_0', 'real_1'])
result = config.get_with_defaults(0, 5, 50, 200)
# => ['real_0', 'default_primary_5', 'default_secondary_50', 'default_overflow']
Debugging complex index operations benefits from logging index resolution before value retrieval. This technique identifies whether problems stem from index calculation or array access logic.
def debug_fetch_values(array, *indices)
puts "Array length: #{array.length}"
puts "Bounds: #{-array.length}...#{array.length}"
indices.each_with_index do |idx, position|
resolved = idx < 0 ? array.length + idx : idx
in_bounds = resolved >= 0 && resolved < array.length
puts "Index[#{position}]: #{idx} -> #{resolved} (#{in_bounds ? 'valid' : 'invalid'})"
end
array.fetch_values(*indices) { |i| "MISSING[#{i}]" }
end
test_data = %w[alpha beta gamma]
debug_fetch_values(test_data, 1, -1, 5, -10)
# Array length: 3
# Bounds: -3...3
# Index[0]: 1 -> 1 (valid)
# Index[1]: -1 -> 2 (valid)
# Index[2]: 5 -> 5 (invalid)
# Index[3]: -10 -> -7 (invalid)
# => ['beta', 'gamma', 'MISSING[5]', 'MISSING[-10]']
Common Pitfalls
Index validation misunderstandings create frequent errors when developers assume fetch_values
behaves like bracket notation or values_at
. Unlike these alternatives, fetch_values
enforces explicit handling of boundary violations through exceptions or blocks.
data = [100, 200, 300]
# Common mistake: assuming nil return for missing indices
# data.fetch_values(0, 5, 1) # IndexError!
# Correct approaches:
safe_with_block = data.fetch_values(0, 5, 1) { |i| nil }
# => [100, nil, 200]
safe_with_rescue = begin
data.fetch_values(0, 5, 1)
rescue IndexError
data.values_at(0, 5, 1) # Falls back to nil-returning behavior
end
# => [100, nil, 200]
Negative index boundaries confuse developers who incorrectly calculate valid ranges. Ruby's negative indexing starts at -1 for the last element, not -0, making the valid negative range -array.length
to -1
.
sequence = ['first', 'second', 'third', 'fourth']
# Pitfall: assuming -0 exists or -5 is valid
# sequence.fetch_values(-0) # Same as 0, not an error
# sequence.fetch_values(-5) # IndexError! Valid range is -4..-1
# Correct negative indexing
valid_negatives = sequence.fetch_values(-4, -1, -2)
# => ['first', 'fourth', 'third']
# Boundary calculation helper
def valid_negative_range(array)
return 0...0 if array.empty?
(-array.length)..-1
end
puts valid_negative_range(sequence)
# => -4..-1
Block parameter confusion leads to incorrect default value logic when developers assume the block receives the element value instead of the missing index. The block parameter always represents the index that failed, not the expected element.
inventory = ['item_0', 'item_1', 'item_2']
# Wrong: thinking block gets element value
wrong_defaults = inventory.fetch_values(0, 5, 1) do |value|
"backup_#{value}" # value is 5 (the index), not an element!
end
# => ['item_0', 'backup_5', 'item_1']
# Correct: using index to generate appropriate default
right_defaults = inventory.fetch_values(0, 5, 1) do |missing_index|
"item_#{missing_index}_placeholder"
end
# => ['item_0', 'item_5_placeholder', 'item_1']
Order dependency misunderstandings occur when developers expect fetch_values
to return elements in array order rather than parameter order. The method preserves the sequence of indices as specified, not their natural array positions.
alphabet = %w[a b c d e f g h i j]
# Pitfall: expecting sorted output
mixed_order = alphabet.fetch_values(7, 2, 9, 0, 5)
# => ['h', 'c', 'j', 'a', 'f'] # Parameter order, not array order!
# Intentional sorting if array order needed
array_ordered = alphabet.fetch_values(7, 2, 9, 0, 5).
zip([7, 2, 9, 0, 5]).
sort_by { |_, index| index }.
map { |element, _| element }
# => ['a', 'c', 'f', 'h', 'j'] # Now in array order
Performance assumptions about repeated index access can cause unnecessary overhead. Each duplicate index triggers a full element lookup, making multiple identical indices inefficient compared to single lookup with value replication.
large_array = Array.new(10000) { |i| "element_#{i}" }
# Inefficient: multiple lookups of same index
start_time = Time.now
inefficient = large_array.fetch_values(*([5000] * 1000))
inefficient_time = Time.now - start_time
# Efficient: single lookup with replication
start_time = Time.now
single_lookup = large_array.fetch_values(5000)
efficient = single_lookup * 1000
efficient_time = Time.now - start_time
puts "Inefficient: #{inefficient_time}s"
puts "Efficient: #{efficient_time}s"
puts "Speedup: #{(inefficient_time / efficient_time).round(2)}x"
Block execution timing creates subtle bugs when developers assume blocks execute before any successful index processing. Ruby processes indices sequentially, executing the block only when an invalid index is encountered, potentially after retrieving valid elements.
side_effects = []
test_array = ['a', 'b', 'c']
result = test_array.fetch_values(1, 10, 2) do |missing_index|
side_effects << "Block called for index #{missing_index}"
"default_#{missing_index}"
end
puts result
# => ['b', 'default_10', 'c']
puts side_effects
# => ['Block called for index 10']
# Block called exactly once, after retrieving 'b' but before retrieving 'c'
Reference
Method Signature
fetch_values(*indices) → Array
fetch_values(*indices) { |missing_index| block } → Array
Parameters
Parameter | Type | Description |
---|---|---|
indices |
Integer |
Variable number of integer indices to retrieve |
missing_index |
Integer |
Block parameter containing the invalid index |
Return Value
Type | Description |
---|---|
Array |
New array containing elements at specified indices in parameter order |
Exceptions
Exception | Condition |
---|---|
IndexError |
When index is outside array bounds and no block provided |
Core Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#fetch_values(*indices) |
Variable indices (Integer) | Array |
Retrieve elements at specified indices |
#fetch_values(*indices) { block } |
Indices + block | Array |
Retrieve with default value generation |
Index Validation Rules
Index Type | Valid Range | Example | Result |
---|---|---|---|
Positive | 0...(array.length) |
arr.fetch_values(0, 2) |
Elements at positions 0, 2 |
Negative | (-array.length)..-1 |
arr.fetch_values(-1, -3) |
Last element, third from end |
Mixed | Combination of above | arr.fetch_values(0, -1) |
First and last elements |
Out of bounds | Any other value | arr.fetch_values(100) |
IndexError or block execution |
Error Messages
Scenario | Error Message Format |
---|---|
Positive index too large | index N outside of array bounds: -LENGTH...LENGTH |
Negative index too small | index -N outside of array bounds: -LENGTH...LENGTH |
Comparison with Related Methods
Method | Out-of-bounds Behavior | Multiple Indices | Block Support |
---|---|---|---|
#fetch_values |
IndexError or block | Yes | Yes (for missing indices) |
#fetch |
IndexError or block | No | Yes (for missing index) |
#values_at |
Returns nil |
Yes | No |
#[] |
Returns nil |
No | No |
Performance Characteristics
Operation | Time Complexity | Space Complexity | Notes |
---|---|---|---|
Single index | O(1) | O(1) | Direct array access |
Multiple indices | O(n) | O(n) | n = number of indices |
Block execution | O(1) per call | O(1) per call | Only for missing indices |
Duplicate indices | O(n) | O(n) | No caching, full lookup each time |
Usage Patterns
Pattern | Use Case | Example |
---|---|---|
Sequential access | Ordered element retrieval | arr.fetch_values(0, 1, 2) |
Sparse access | Non-contiguous elements | arr.fetch_values(1, 5, 8, 12) |
Boundary access | First/last elements | arr.fetch_values(0, -1) |
Safe access | With error handling | arr.fetch_values(idx) { "default" } |
Conditional access | Dynamic index lists | arr.fetch_values(*calculated_indices) |
Block Return Handling
Block Return Type | Behavior | Example |
---|---|---|
String | Included as string element | { "default" } |
Nil | Included as nil element | { nil } |
Array | Included as nested array | { [] } |
Any object | Included as-is | { Object.new } |
Exception | Propagates upward | { raise "error" } |