Iterate through array forwards then backwards - arrays

[1,2,3,4,5]
=>1,2,3,4,5,4,3,2,1
=>1,2,3,2,3,4,5,4,3 #I need to be able to reverse the iteration at certain points
I first tried something like:
a = [1,2,3,4,5]
a.each {|i|
if i % 9 == 0
a.reverse!
}
but that just reverses the entire array and starts counting from the index it left off on. I need to to shift the direction of each, so to speak.

i, counter = 0, 1 # initialize index to 0, counter to 1
while(i < a.length && i >= 0) do
puts a[i]
i+= counter # increment counter
counter*= -1 if(condition) # Multiply counter with -1 to reverse it
end

Well, here's a moving "cursor" for your array:
module Cursor
def current_index
#current_index ||= 0
end
def step
#current_index = current_index + direction
handle_boundary
end
def step_back
#current_index = current_index + (direction * -1)
handle_boundary
end
def handle_boundary
if current_index == length || current_index == 0
turn_around
end
end
def direction
#direction ||= 1
end
def turn_around
#direction = direction * -1
end
def current
self[current_index]
end
end
And here's how you use it:
array = [1,2,3,4,5]
arary.extend Cursor
array.current # returns the item in current position
array.step # moves a step forward, turns around when it reaches either end of the array
array.step_back # moves a step backward without switching the direction
array.turn_around # switch the direction
Now you can travel around as you want :D

You can make use of Enumerator class to create custom enumerable that can providing custom iteration through the array. In below code, I am monkey-patching Array class for convenience (also due to resemblance of the method to Array#cycle), though solution can be done without monkey-patching as well.
class Array
def reversible_cycle
Enumerator.new do |y|
index = 0
direction = :forward
loop do
direction = :backward if index + 1 >= size
direction = :forward if index <= 0
y << self[index]
index += (direction == :forward ? +1 : -1)
end
end
end
end
p [1,2,3,4,5].reversible_cycle.take(9)
#=> [1, 2, 3, 4, 5, 4, 3, 2, 1]
p [1,2,3,4,5].reversible_cycle.take(13)
#=> [1, 2, 3, 4, 5, 4, 3, 2, 1, 2, 3, 4, 5]
p [1,2,3,4,5].reversible_cycle.take(17)
#> [1, 2, 3, 4, 5, 4, 3, 2, 1, 2, 3, 4, 5, 4, 3, 2, 1]
p [1,2,3,4,5].reversible_cycle.take(21)
#=> [1, 2, 3, 4, 5, 4, 3, 2, 1, 2, 3, 4, 5, 4, 3, 2, 1, 2, 3, 4, 5]
For scenarios where you are changing direction without iterating the array fully in one direction, you will have to give some examples so that one can see how to modify the above code to accommodate that

You could use Ruby's under-appreciated flip-flop operator.
arr = [1,2,3,4,5]
sz = arr.size
(2*sz-1).times { |i| puts i==0..i==arr.size-1 ? arr[i] : arr[sz-i-2] }
1
2
3
4
5
4
3
2
1

Related

Ruby, Array.select with number of elements

I have this array
[1, 2, 3, 4, 5, 6]
I would like to get the first 2 elements that are bigger than 3.
I can do:
elements = []
[1, 2, 3, 4, 5, 6].each do |element|
elements << element if element > 3
break if elements.size == 2
end
puts elements
Is there a more elegant way to do this?
Is there something in the Ruby core like Array.select(num_elements, &block)?
You were nearly there. Just use break with a parameter:
[1, 2, 3, 4, 5, 6].each_with_object([]) do |element, acc|
acc << element if element > 3
break acc if acc.size >= 2
end
Another way to accomplish it, would be to use Enumerator::Lazy with array.lazy.select, or an explicit Enumerator instance with Enumerable#take (here it’s a definite overkill, posting mostly for educational purposes.)
enum =
Enumerator.new do |y|
i = [1, 2, 3, 4, 5, 6].each
loop { i.next.tap { |e| y << e if e > 3 } }
end
enum.take(2)
#⇒ [4, 5]
Sidenote: both examples above would stop traversing the input as soon as two elements are found.
a = [1, 2, 3, 4, 5, 6]
p a.filter {|x| x > 3}.first(2)
Or
p a.select{|x| x > 3}.first(2)
output
[4, 5]
As Cary suggest, the given below code wouldn't be a performance hit if array is bigger, it would stop executing further if 2 elements are found
a.lazy.select{|x| x > 3}.first(2)
Just for having a couple of options more..
ary.each_with_object([]) { |e, res| res << e if e > 3 && res.size < 2 }
or
ary.partition { |e| e > 3 }.first.first(2)

Remove next elements in array with ruby

Given an array containing numbers the following rules apply:
a 0 removes all previous numbers and all subsequent adjacent even numbers.
a 1 removes all previous numbers and all subsequent adjacent odd numbers.
if the first element of the array is 1 it can be removed
I am trying to write an algorithm to reduce the array but I could come up only with a bad looking solution:
def compress(array)
zero_or_one_index = array.rindex { |element| [0,1].include? element }
array.slice!(0, zero_or_one_index) if zero_or_one_index
deleting = true
while deleting
deleting = false
array.each_with_index do |element, index|
next if index.zero?
previous_element = array[index - 1]
if (previous_element == 0 && element.even?) ||
(previous_element == 1 && element.odd?)
array.delete_at(index)
deleting = true
break
end
end
end
array.shift if array[0] == 1
end
The problem is that delete_if and similar, start messing up the result, if I delete elements while iterating on the array, therefore I am forced to use a while loop.
Examples:
compress([3, 2, 0]) #=> [0]
compress([2, 0, 4, 6, 7]) #=> [0,7]
compress([2, 0, 4, 1, 3, 6]) #=> [6]
compress([3, 2, 0, 4, 1, 3, 6, 8, 5]) #=> [6,8,5]
This problem arises in the context of some refactorings I am performing on cancancan to optimize the rules definition.
Here is how I would solve the problem:
def compress(arr)
return arr unless idx = arr.rindex {|e| e == 0 || e == 1}
value = arr[idx]
method_options = [:even?,:odd?]
arr[idx..-1].drop_while do |n|
n.public_send(method_options[value])
end.tap {|a| a.unshift(value) if value.zero? }
end
First we find index of the last occurrence of 0 or 1 using Array#rindex. If none then return the Array.
Then we get the value at that index.
Then we use Array#[] to slice off the tail end of the Array starting at the index.
Then drop all the consecutive adjacent :even? or :odd? numbers respective to the value (0 or 1) using Array#drop_while.
Finally if the value is 0 we place it back into the front of the Array before returning.
Examples
compress([3, 2, 0])
#=> [0]
compress([2, 0, 4, 6, 7])
#=> [0,7]
compress([2, 0, 4, 1, 3, 6])
#=> [6]
compress([3, 2, 0, 4, 1, 3, 6, 8, 5])
#=> [6,8,5]
compress([4, 5, 6])
#=> [4,5,6]
compress([0])
#=> [0]
compress([1])
#=> []
If your goal was to be mutative, as your question and gist seem to suggest, I honestly would not change what I have but rather go with:
def compress!(arr)
arr.replace(compress(arr))
end
For example
a = [3, 2, 0, 4, 1, 3, 6, 8, 5]
a == compress!(a)
#=> true
a
#=> [6,8,5]

How do I remove the beginning elements of my array only if the first element satisfies a condition?

In Ruby, let's say I have an array of ordreed, unique numbers
[0, 1, 2, 4, 6, 8, 10]
If the first element of the array is zero, how do I remove all the elements from teh beginning of the array that are consecutive, starting wiht zero? That is, in the above example, I would want to remove "0", "1", and "2" leaving me with
[4, 6, 8, 10]
But if my array is
[1, 2, 3, 10, 15]
I would expect the array to be unchanged because the first element is not zero.
You could use a mix of drop_while and with_index to only remove the first matching elements:
[0, 1, 2, 4, 6, 8, 10].drop_while.with_index{|x, i| x == i}
# [4, 6, 8, 10]
[1, 1, 2, 4, 6, 8, 10].drop_while.with_index{|x, i| x == i}
# [1, 1, 2, 4, 6, 8, 10]
Note that the second and third elements don't get deleted in the second example, even though they're equal to their indices.
Drop elements, as long as they are equal to their index:
a=a.drop_while.with_index{|e,i| e==i}
Sounds like you're trying to delete entities if they match their idx (provided the first idx is 0). Try this:
if array.first == 0
new_array = array.reject.each_with_index{ |item, idx| item == idx }
end
Although this will only work with ordered arrays of unique numbers, if you're not sure that they are then include: array = array.sort.uniq
You could do:
x = -1
while my_array.first == x + 1 do
x = my_array.shift
end
Note that array.shift is the same as array.pop except that it works from the start of the array.
If I understand you right, then it can be one of possible solutions:
def foo(array)
if array.first.zero?
array.keep_if.with_index { |e, ind| e != ind }
else
array
end
end
> foo([0, 1, 2, 5, 6, 7])
#=> => [5, 6, 7]
> foo([1, 2, 3])
#=> [1, 2, 3]
In short form:
a[0] == 0 ? a[3..-1] : a
In longer form:
if a.first == 0
a[3..(a.size)]
else
a
end

How do I count the number of elements in my array that are unique and are bigger than the element before them?

I'm using Ruby 2.4. I have an array of strings taht are all numbers. I want to count the number of elements in the array that are unique and that are also greater than the element before them (I consider the first array element already greater than its non-existent predecessor). So I tried
data_col = ["3", "6", "10"]
#=> ["3", "6", "10"]
data_col.map { |string| string.to_i.to_s == string ? string.to_i : -2 }.each_cons(2).select { |a, b| a > b && data_col.count(a) == 1 }.count
#=> 0
but the results is zero, despite the fact that all the elements in my array satisfy my criteria. How can I improve the way I count this?
require 'set'
def nbr_uniq_and_bigger(arr)
processed = Set.new
arr.each_cons(2).with_object(Set.new) do |(n,m),keepers|
if processed.add?(m)
keepers << m if m > n
else
keepers.delete(m)
end
end.size + (processed.include?(arr.first) ? 0 : 1)
end
nbr_uniq_and_bigger [1, 2, 6, 3, 2]
#=> 2
nbr_uniq_and_bigger [1, 2, 1, 2, 1, 2]
#=> 0
See Set.add?.
Note the line keepers.delete(m) could be written
keepers.delete(m) if keepers.key(m)
but attempting to delete an element not in the set does not harm.
There are a few things wrong here:
a > b seems like the opposite of what you want to test. That should probably be b > a.
If I followed properly, I think data_col.count(a) is always going to be zero, since a is an integer and data_col contains only strings. Also, I'm not sure you want to be looking for a... b is probably the right element to look for.
I'm not sure you're ever counting the first element here. (You said you consider the first element to be greater than its non-existent predecessor, but where in your code does that happen?)
Here's some code that works:
def foo(x)
([nil] + x).each_cons(2).select { |a, b| (a == nil or b > a) and x.count(b) == 1 }.count()
end
p foo([3, 6, 10]) # 3
p foo([3, 6, 10, 1, 6]) # 2
(If you have an array of strings, feel free to do .map { |s| s.to_i } first.)
One more solution:
def uniq_and_bigger(data)
counts = data.each_with_object(Hash.new(0)) { |e, h| h[e] += 1 } #precalculate
data.each_cons(2).with_object([]) do |(n,m), a|
a << m if m > n && counts[m] == 1
end.size + (counts[data[0]] == 1 ? 1 : 0)
end
uniq_and_bigger([3, 6, 10, 1, 6])
=> 2
uniq_and_bigger([1, 2, 1, 2, 1, 2])
=> 0
Yet another solution. It's O(n) and it returns the desired result for [3, 6, 10].
It uses slice_when :
def unique_and_increasing(array)
duplicates = array.group_by(&:itself).select{ |_, v| v.size > 1 }.keys
(array.slice_when{ |a, b| a < b }.map(&:first) - duplicates).size
end
p unique_and_increasing [3, 6, 10]
# 3
p unique_and_increasing [3, 6, 10, 1, 6]
# 2
p unique_and_increasing [1, 2, 1, 2, 1, 2]
# 0

Redis Lua script not working as expected

As a practice exercise, I'm writing a lua script for Redis which basically executes the JavaScript method Array#copyWithin().
Quoting from MDN,
The copyWithin() method copies the sequence of array elements within
the array to the position starting at target. The copy is taken from
the index positions of the second and third arguments start and end.
Here is the script I've written so far :
local list = redis.call('lrange', KEYS[1], 0, -1)
local target = tonumber(ARGV[1])
local startIndex = tonumber(ARGV[2])
local len = #list
local endIndex = len
--Handle negative startIndex
if startIndex < 0 then
startIndex = len+startIndex
end
--If the third argument is provided, get the endIndex from it
if #ARGV > 2 then
local arg = tonumber(ARGV[3])
if arg >= 0 then
if arg < endIndex then
endIndex = arg
end
else
if len+arg >= 0 then
endIndex = len+arg
else
endIndex = 0
end
end
end
--An array containing the elements which will be copied
local targeted_elements = {}
--Fill elements from the list
for i=1, (endIndex-startIndex+1) do
targeted_elements[i] = list[startIndex+i]
end
--Make sure no additional elements are pushed to the end of array in case of range overflow
local target_end = #targeted_elements
if target + target_end > len then
target_end = len-target
end
--replace all the changed elements of the list in redis
for i=1, target_end do
redis.call('lset', KEYS[1], target+(i-1), targeted_elements[i])
end
During testing, the first test case is successfully cleared :
Test case : convert [1, 2, 3, 4, 5] into [4, 5, 3, 4, 5] using copyWithin(0, 3)
LRANGE MyList 0 -1
> [1, 2, 3, 4, 5]
EVALSHA "sha1 of script" 1 MyList 0 3
(basically the same as `[1, 2, 3, 4, 5].copyWithin(0, 3)`)
> nil
LRANGE MyList 0 -1
> [4, 5, 3, 4, 5]
The second test case however, did not go that smooth.
Test case : convert [1, 2, 3, 4, 5] into [4, 2, 3, 4, 5] using copyWithin(0, 3, 4)
LRANGE MyList 0 -1
> [1, 2, 3, 4, 5]
EVALSHA "sha1 of script" 1 MyList 0 3 4
(basically the same as `[1, 2, 3, 4, 5].copyWithin(0, 3, 4)`)
> nil
LRANGE MyList 0 -1
> [4, 5, 3, 4, 5]
After some debugging, I found that the value of targeted_elements is {4, 5} in both the cases, whereas it should be {4} in case 2.
Is there anything suspicious in the loops? Any help would be great.
I fixed this by modifying the following part of the script :
--Fill elements from the list
for i=1, (endIndex-startIndex+1) do
targeted_elements[i] = list[startIndex+i]
end
Changed it into this:
--Fill elements from the list
for i=1, (endIndex-startIndex) do
targeted_elements[i] = list[startIndex+i]
end
The +1 in the for expression added an additional element to the array. It worked in the first case because the selected part was from 3, 5, so 5-3+1 = 3 which means 3 elements should be selected. But since only 2 elements are left, the case still works.
Whereas for the second case, 4-3+1 = 2, which means 2 elements were being selected, instead of 1.

Resources