CrackedRuby logo

CrackedRuby

Array fetch_values

Complete guide to Ruby's Array#fetch_values method for retrieving multiple elements by index with optional default value handling.

Core Built-in Classes Array Class
2.4.9

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" }