Ruby, Array.select with number of elements - arrays

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)

Related

Ruby array: getting all the elements that have the max value while chaining methods (namely, without having a handle on the sorted array)

Suppose that you need to get all the elements that have the max value in an array.
A possible method would be to sort the array then use Enumerable#take_while:
array = [ 1, 3, 2, 3, 2, 3 ].sort {|a,b| b - a}
array.take_while { |e| e == array[0] }
#=> [3, 3, 3]
Now, when you are beautifully chaining methods and don't want to stop the chain just for storing the sorted array (which you'll need for referencing its first element in the take_while block), how would you do it?
I posted the question and an answer below for reference, but I probably missed better ways, so feel free to post your own method
Another way:
arr = [ 1, 3, 2, 3, 2, 3 ]
arr.sort {|a,b| b - a}.tap { |a| a.select! { |e| e == a.first } }
#=> [3, 3, 3]
Note that arr is not mutated.
ruby < 2.5
My original response to the question: sort.slice_when.first
[ 1, 3, 2, 3, 2, 3 ].sort {|a,b| b - a}.slice_when {|a,b| b != a}.first
#=> [3, 3, 3]
note: As slice_when returns an Enumerator, this solution won't walk through all the sorted array when chaining it with first. There is a more performant solution below tough.
ruby >= 2.5
Combining #engineersmnky and #Cary methods: then and max+select
[ 1, 3, 2, 3, 2, 3 ].then { |arr| mx = arr.max; arr.select { |elm| elm == mx } }
#=> [3, 3, 3]
You can try this
pry(main)> [ 1, 2, 2, 3, 3, 3 ].sort.slice_when {|a,b| b > a}.to_a.last
=> [3, 3, 3]
A bit similar of the last solution but also different.
Source https://ruby-doc.org/core-3.0.2/Enumerable.html#method-i-slice_when

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]

Move x element to the end of an array

I'm trying to find a method to take a particular relation and move it to the end of the array. Basically, I have a current_account and I want to take this account and move it to the end of the account relationship array so that it will display last when I iteration over the relationships. I want to make a scope and use SQL if possible, here is my attempt and I haven't really gotten anywhere.
HTML
<% current_user.accounts.current_sort(current_account).each do |account| %>
<li><%= link_to account.name, switch_account_accounts_path(account_id: account.id) %></li>
<% end %>
This current return a list of sorted by created_at accounts. I don't want it to be sorted by created at but the current_account to be at the bottom so I make a scope called current_sort but I'm not sure what to do here.
CURRENT_SORT SCOPE ON ACCOUNT
scope :current_sort, lambda { |account|
}
I want this scope to return the passed in account last in the association array. How can I do this with SQL or Ruby?
A quick trick to sort a particular element to the end of an array is:
array.sort_by { |v| v == current_account ? 1 : 0 }
If you want to move multiple elements it's easier to do:
to_end = [ a, b ]
array - to_end + to_end
Edit: As Stefan points out, this could potentially re-order items. To fix that:
array.sort_by.with_index do |v, i|
v == current_account ? (array.length + i) : i
end
You can also approach it a different way using partition:
array.partition { |v| v != current_account }.reduce(:+)
Which is a variation on the method used by Stefan in their answer.
You can use partition to split the array by a condition.
array = [1, 2, 3, 4, 5, 6, 7, 8]
current_account = 3
other_accounts, current_accounts = array.partition { |v| v != current_account }
#=> [[1, 2, 4, 5, 6, 7, 8], [3]]
other_accounts
#=> [1, 2, 4, 5, 6, 7, 8]
current_accounts
#=> [3]
The results can be concatenated:
other_accounts + current_accounts
#=> [1, 2, 4, 5, 6, 7, 8, 3]
or in a single line:
array.partition { |v| v != current_account }.flatten(1)
#=> [1, 2, 4, 5, 6, 7, 8, 3]
# or
array.partition { |v| v != current_account }.inject(:+)
#=> [1, 2, 4, 5, 6, 7, 8, 3]

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

Resources