Jumbalaya Wanton
Jumbalaya Wanton

Reputation: 1621

How to find the next element given a certain element in array

I wrote a method to get the next element after a given element inside an array. If I provide the method with c, I want it to return e; if e, I want it to return a, etc.:

array = %w[a f c e]

def find_element_after(element, array)
  index = array.find_index(element) + 1
  array.at(index)
end

array.find_element_after("c", array)

If I pass in the last element I will get nil. But I want to return the first element instead.

I can solve this with if and else. But I want to know if Ruby has better way?

Upvotes: 1

Views: 1916

Answers (5)

Stanislav Kr.
Stanislav Kr.

Reputation: 554

If need a cycle so to get the first element if the last passed

def find_after(element, array)
  idx = array.index(element)
  array[idx - array.length + 1]
end

Upvotes: 0

Cary Swoveland
Cary Swoveland

Reputation: 110665

Here are some other ways to do that.

#1 Use the method Array#rotate

def nxt(arr, e)
  arr.rotate(arr.index(e)+1).first
end

arr = %w[a f c e]

nxt(arr, 'a') #=> "f"
nxt(arr, 'f') #=> "c"
nxt(arr, 'c') #=> "e"
nxt(arr, 'e') #=> "a"

I think this reads well, but it has the disadvantage of creating a temporary array the size of arr.

#2 Use Array#rotate to construct a hash

h = Hash[arr.zip(arr.rotate(1))]
  #=> {"a"=>"f", "f"=>"c", "c"=>"e", "e"=>"a"}

h['a'] #=> "f"
h['f'] #=> "c"
h['c'] #=> "e"
h['e'] #=> "a"

#3 Use Array#cycle to create an enumerator

enum = arr.cycle

def nxt(enum, v)
  until enum.next == v do
  end
  enum.next
end

nxt(enum, 'a') #=> "f"
nxt(enum, 'f') #=> "c"
nxt(enum, 'c') #=> "e"
nxt(enum, 'e') #=> "a"

The latter two approaches should be relatively efficient if you had several values to map.

Upvotes: 1

Marek Lipka
Marek Lipka

Reputation: 51151

You could modify your method, taking array size into account, like this:

array = %w[a f c e]

def find_element_after(element, array)
  index = array.find_index(element) + 1
  array.at(index % array.size) # divide calculated index modulo array size
end

find_element_after('e', array)
# => "a"

If you want to make your method proof to passing argument that isn't member of array, you could do:

def find_element_after(element, array)
  index = array.find_index(element)
  array.at((index + 1) % array.size) if index
end
find_element_after('u', array)
# => nil

or:

def find_element_after(element, array)
  return nil unless array.include?(element)
  index = array.find_index(element)
  array.at(index % array.size)
end

as you see, there's many possible solutions. Feel free to experiment.

Upvotes: 4

Cristian Lupascu
Cristian Lupascu

Reputation: 40516

If you pass in the last element, it actually works. The index gets evaluated to the last index, and retrieving the element at lastindex + 1 from the array returns nil.

The problem is when you provide an element that is not present in the array. It's this scenario that will result in the index being nil and then throwing the NoMethodError when you call + 1 on it.

To fix this case, define your method like this:

def find_element_after(element, array)
  index = array.find_index(element)
  array.at(index + 1) if index
end

Here's a demo showing how it works now (run online):

array = %w[a f c e]

def find_element_after(element, array)
  index = array.find_index(element)
  array.at(index + 1) if index
end

p find_element_after("c", array) # element in the middle - prints "e"
p find_element_after("e", array) # last element - prints "nil"
p find_element_after("z", array) # element not present in the array - prints "nil" (no error)

Upvotes: 2

Uri Agassi
Uri Agassi

Reputation: 37409

You can use each_cons to iterate the array using pairs:

def find_element_after(element, array)
  cons = array.each_cons(2).find { |i1, i2| i1 == element }
  cons.nil? ? array.first : cons.last
end

find_element_after('c', array)
# => "e"
find_element_after('e', array)
# => "a"

Upvotes: 1

Related Questions