I have piece of code that currently takes an array of elements (in this case blog posts), sorts their numerical ID in descending order, puts them into groupings of 3, and iterates through each and every one of them.
I need to add a condition where if blog.published is false for any blog, do not display it and use the next blog.
Whenever a blog that has the published value set to false, the grouping will set the position in the grouping to nil, rather than filling it with the next blog entry, leaving some groupings with only 2 or 1 blogs on the view. I have attempted increasing the index by replacing each with each_with_index and iterating the index value when the condition is met, but this also yields the same issue. Here is my current code.
<% #blogs.order('created_at DESC').in_groups_of(3, false).each do |group| %>
<div class="row pt-3">
<% group.each do |blog| %>
<% next if blog.published == false && current_page?(root_path) %>
<%# Do View Stuff For Each Blog Here %>
<% end #each %>
</div>
<% end #grouping %>
Each grouping should always have 3 blogs and should skip over any blog that has the publication value set to false.
#blogs = Blog.where.not(published: false).order('created_at desc').in_groups_of(3, false)
PS. If you're doing #blogs = Blog.all in your controller, try not to build AR queries inside your view. Even queries aren't executed until you call it somehow, they should be fully-built in your controller method.
If I get the point, this is a possible solution. I'm using pure Ruby and a collection of hashes as example, but it should be easy to transpose it to Rails.
So, given the collection:
posts = [ {id: 1, pub: true}, {id: 2, pub: false}, {id: 3, pub: true}, {id: 4, pub: true}, {id: 5, pub: true}, {id: 6, pub: false}, {id: 7, pub: true}, {id: 8, pub: true}, {id: 9, pub: true}, {id: 10, pub: true}, {id: 11, pub: true}, {id: 12, pub: true}, {id: 13, pub: false}, {id: 14, pub: true} ]
Using Enumerable#each_slice and Array#reject:
groups_of_three = posts.each_slice(3).map { |group| group.reject { |h| h[:pub] == false } }
Printing out tho show the result:
groups_of_three.each { |group| p group }
# [{:id=>1, :pub=>true}, {:id=>3, :pub=>true}]
# [{:id=>4, :pub=>true}, {:id=>5, :pub=>true}]
# [{:id=>7, :pub=>true}, {:id=>8, :pub=>true}, {:id=>9, :pub=>true}]
# [{:id=>10, :pub=>true}, {:id=>11, :pub=>true}, {:id=>12, :pub=>true}]
# [{:id=>14, :pub=>true}]
Related
This question already has an answer here:
Find and replace specific hash and it's values within array
(1 answer)
Closed 4 years ago.
I have an array of hashes as below
status_arr = [{id: 5, status: false},
{id: 7, status: false},
{id: 3, status: false},
{id: 9, status: false} ]
I would like to update the hash with status: true if it has ids 5, 7
update_ids = [5, 9]
I am trying the following and has no idea to proceed
status_arr.select{ |arr| update_ids.include?(arr[:id]) arr[:status] = true}
Expected output:
status_arr = [{id: 5, status: true},
{id: 7, status: false},
{id: 3, status: false},
{id: 9, status: true} ]
require 'set'
update_ids = Set.new([5,3])
status_arr.map{ |s| s[:status] = update_ids.include?(s[:id]); s }
#=> [{:id=>5, :status=>true}, {:id=>7, :status=>false}, {:id=>3, :status=>true}, {:id=>9, :status=>false}]
instead of Set you can use just a Hash
update_ids = {5 => true, 3=> true}
status_arr.map{ |s| s[:status] = update_ids.include?(s[:id]); s }
#=> [{:id=>5, :status=>true}, {:id=>7, :status=>false}, {:id=>3, :status=>true}, {:id=>9, :status=>false}]
Or an array, but it will have some performance issues for big arrays
update_ids = [5,3]
status_arr.map{ |s| s[:status] = update_ids.include?(s[:id]); s }
#=> [{:id=>5, :status=>true}, {:id=>7, :status=>false}, {:id=>3, :status=>true}, {:id=>9, :status=>false}]
Say I have this collection of objects:
[
{value: 1, contents: "one"},
{value: 2, contents: "two"},
{value: 3, contents: "three"},
{value: 4, contents: "four"},
{value: 5, contents: "five"}
]
And want to invert the relation of values to contents, like so:
[
{value: 5, contents: "one"},
{value: 4, contents: "two"},
{value: 3, contents: "three"},
{value: 2, contents: "four"},
{value: 1, contents: "five"}
]
I was unable to think of an algorithm to accomplish this. I'm using Ruby, but I'm not so concerned about the code as I am about the method of accomplishing this.
a = [
{value: 1, contents: "one"},
{value: 2, contents: "two"},
{value: 3, contents: "three"},
{value: 4, contents: "four"},
{value: 5, contents: "five"}
]
a.map{|h| h[:value]}.reverse.zip(a.map{|h| h[:contents]})
.map{|k, v| {value: k, contents: v}}
# =>
# [
# {:value=>5, :contents=>"one"},
# {:value=>4, :contents=>"two"},
# {:value=>3, :contents=>"three"},
# {:value=>2, :contents=>"four"},
# {:value=>1, :contents=>"five"}
#]
Or,
a.each_index.map{|i| {value: a[-i - 1][:value], contents: a[i][:contents]}}
Letting arr equal your array of hashes, here are a couple of ways you could do it.
Two passes, no indices
value_vals = arr.map {|h| h[:value]}.reverse
#=> [5, 4, 3, 2, 1]
arr.map { |h| {value: value_vals.shift, contents: h[:contents]}}
#=> [{:value=>5, :contents=>"one"},
# {:value=>4, :contents=>"two"},
# {:value=>3, :contents=>"three"},
# {:value=>2, :contents=>"four"},
# {:value=>1, :contents=>"five"}]
One pass, but not pretty
arr.each_index.map {|i,a| {value: arr[-1-i][:value], contents: arr[i][:contents]}}
#=> [{:value=>5, :contents=>"one"},
# {:value=>4, :contents=>"two"},
# {:value=>3, :contents=>"three"},
# {:value=>2, :contents=>"four"},
# {:value=>1, :contents=>"five"}]
TL;DR
arr.zip(arr.reverse).map {|a, b| a.merge(value: b[:value]) }
Since reverse makes a copy of the array, this will take twice as much memory as other methods—which for most data sets probably isn't an issue at all. But if it is, there's an easy way to avoid it. See the "Bonus" section at the end of my answer.
Building an algorithm
The simplest (and probably best) solution is to walk the array, and for each item get the :value from its counterpart at the other end of the array. You can get an item's "counterpart" by subtracting the item's index from the index of the last item (i.e. the size of the array minus 1). So, if you have five items in an array called arr, the steps of the algorithm looks like this:
end_idx = arr.size - 1 # => 4
new_arr = []
new_arr[0] = { value: arr[end_idx - 0][:value], contents: arr[0][:contents] }
new_arr[1] = { value: arr[end_idx - 1][:value], contents: arr[1][:contents] }
new_arr[2] = { value: arr[end_idx - 2][:value], contents: arr[2][:contents] }
new_arr[3] = { value: arr[end_idx - 3][:value], contents: arr[3][:contents] }
new_arr[4] = { value: arr[end_idx - 4][:value], contents: arr[4][:contents] }
As you can see, every step is the same but with one number incremented, so I bet you already know how to turn this into a loop:
end_idx = arr.size - 1 # => 4
new_arr = []
0.upto(end_idx) do |idx|
new_arr[idx] = { value: arr[end_idx - idx][:value],
contents: arr[idx][:contents] }
end
Easy, and to be honest a perfectly good solution. However, it's not very "Rubyish." How do we make it more Rubyish? I'm glad you asked!
Make it more Rubyish
It's a pretty common situation to want, as an output, an array with one item corresponding to each item in an input array. Because it's so common, we have the Enumerable#map method, which does exactly that: It "maps" every item in an input array (or other Enumerable) to an item in an output array.
map walks over the items of the array, which is just what we need, but it's missing one thing we need: The index of the current item. To get that, we can "chain" the with_index method onto the map method, and now, in addition to the array item itself, the block will be passed a second argument, which is its index. Now we have everything we need:
end_idx = vals.size - 1
arr.map.with_index do |hsh, idx|
{ value: arr[end_idx - idx][:value],
contents: hsh[:contents] }
end
Alternatively, if we don't want to explicitly specify the structure of the hash (as we might if the hash comes from, say, user input or a database query and might have keys other than :value and :contents that we want to preserve without having to keep track of changes to the input form or database schema), we could do this:
end_idx = vals.size - 1
arr.map.with_index do |hsh, idx|
hsh.merge(value: arr[end_idx - idx][:value])
end
But I've saved the best for last.
At last...
arr.zip(arr.reverse_each).map do |a, b|
a.merge(value: b[:value])
end
What's going on here? The Array#zip method takes two arrays and "zips" them up, so e.g. [1, 2, 3].zip([:a, :b, :c]) yields [[1, :a], [2, :b], [3, :c]], so we do that with our array and its reverse (or, rather, an Enumerable that yields successive items from the end of the array, which is what reverse_each returns), and then we use map to set the value at :value from the latter on a copy of the former (using merge).
Bonus: Why reverse_each and not just reverse? Because we can make it lazy! Suppose arr has a billion items. If you call arr.zip(arr.reverse), now you have a (two-dimensional) array with two billion items. Maybe that's not a big deal (or maybe you don't have anywhere near a billion items), but if it is, laziness can help us out:
new_enum = arr.lazy.zip(arr.reverse_each).map do |a, b|
a.merge(value: b[:value])
end
# => #<Enumerator::Lazy: ...>
All we've done is added lazy, and now we get an Enumerator back instead of an array. This won't even do any work until we call, say, each or some other Enumerable method on it, and when we do that it will only operate on as many items as we ask it to. For example, say we just want the first three items:
new_enum.take(3).to_a
# => [ { value: 1000000000, contents: "one" },
# { value: 999999999, contents: "two" },
# { value: 999999998, contents: "three" } ]
Thanks to laziness, we never had to make a copy of the whole array and reverse it (and take up the corresponding amount of memory); we only had to deal with three items.
And if you do want all of the items, but still want to avoid making a copy of the whole array, just call new_enum.to_a.
First of all full disclosure. I attempted to solve this issue at this thread but I wanted to repost in hopes of getting some help in Ruby:
best way to sort an array of objects by category and infinite subcategory
I have made a feeble attempt a that solution but can't even get close and it's not even worth showing. Below is my attempt. It would mean the world to me if someone could give some good direction on this.
data = [{id: 1, name: "parent test 1", parent_id: nil, top_level_category_id: nil},
{id: 2, name: "test 2", parent_id: 1, top_level_category_id: 1},
{id: 3, name: "test 3", parent_id: 1, top_level_category_id: 1},
{id: 4, name: "parent test 4", parent_id: nil, top_level_category_id: nil},
{id: 5, name: "test 5", parent_id: 3, top_level_category_id: 4},
{id: 6, name: "test 6", parent_id: 4, top_level_category_id: 4},
{id: 7, name: "test 7", parent_id: 4, top_level_category_id: 4}]
This is what I am hoping to accomplish
parent test 1
test 2
test 3
test 5
parent test 2
test 6
test 7
-
ord_cat = {}
for item in data
if item[:parent_id] == nil
ord_cat[item[:id]] = {:name => item[:name], :children => {}}
end
end
# fill child directories
for item in data
if item[:parent_id] != nil
ord_cat[item[:top_level_category_id]][:children].merge!({item[:id] => item[:name]})
end
end
puts ord_cat
This is the output
{1=>{:name=>"parent test 1", :children=>{2=>"test 2", 3=>"test 3", 5=>"test 5"}}, 4=>{:name=>"parent test 4", :children=>{6=>"test 6", 7=>"test 7"}}}
This is clearly not nesting "test 5" properly. I'm not thrilled with the structure of the object.
We can construct the ancestor list for each node and sorting based on that list, in effect giving a depth-first traversal of the tree.
A bit lengthy solution, but would solve the problem I believe. Have used hash more to avoid scanning the array multiple times.
data = [{id: 1, name: "parent test 1", parent_id: nil, top_level_category_id: nil},
{id: 2, name: "test 2", parent_id: 1, top_level_category_id: 1},
{id: 3, name: "test 3", parent_id: 1, top_level_category_id: 1},
{id: 4, name: "parent test 4", parent_id: nil, top_level_category_id: nil},
{id: 5, name: "test 5", parent_id: 3, top_level_category_id: 4},
{id: 6, name: "test 6", parent_id: 4, top_level_category_id: 4},
{id: 7, name: "test 7", parent_id: 4, top_level_category_id: 4}]
# Amount of indentation for printing each node's nesting
INDENTATION_PER_LEVEL = 1
id_to_data_map = {}
id_to_parent_id_map = {}
data.each { |d|
id_to_data_map[d[:id]] = d
id_to_parent_id_map[d[:id]] = d[:parent_id]
}
data_with_ancestors = {}
data.each do |record|
ancestors = [record[:name]]
# Temporary parent
parent = record
while true do
parent_id = id_to_parent_id_map[parent[:id]]
break if parent_id.nil? # Hit the root - get out.
parent = id_to_data_map[parent_id]
ancestors << parent[:name]
end
# Construct a list of ancestors for the node, with the oldest ancestor first.
data_with_ancestors[record[:name]] = ancestors.reverse
end
# Sort the flattened list based on the ancestor string constructed by joining all the parent names.
sorted_list = data_with_ancestors.sort_by {|name, ancestors| ancestors.join(" ")}
# Add indentation for the record names based on their nesting in the tree.
print_info = sorted_list.collect {|name, ancestors| (" " * (ancestors.size - 1) * INDENTATION_PER_LEVEL) + name}
print_info.each { |record| puts record }
I have an array of ids order say
order = [5,2,8,6]
and another array of hash
[{id: 2,name: name2},{id: 5,name: name5}, {id: 6,name: name6}, {id: 8,name: name8}]
I want it sorted as
[{id: 5,name: name5},{id: 2,name: name2}, {id: 8,name: name8}, {id: 6,name: name6}]
What could be best way to implement this? I can implement this with iterating both and pushing it to new array but looking for better solution.
Try this
arr = [
{:id=>2, :name=>"name2"}, {:id=>5, :name=>"name5"},
{:id=>6, :name=>"name6"}, {:id=>8, :name=>"name8"}
]
order = [5,2,8,6]
arr.sort_by { |a| order.index(a[:id]) }
# => [{:id=>5, :name=>"name5"}, {:id=>2, :name=>"name2"},
#{:id=>8, :name=>"name8"}, {:id=>6, :name=>"name6"}]
Enumerable#in_order_of (Rails 7+)
Starting from Rails 7, there is a new method Enumerable#in_order_of.
A quote right from the official Rails docs:
in_order_of(key, series)
Returns a new Array where the order has been set to that provided in the series, based on the key of the objects in the original enumerable.
[ Person.find(5), Person.find(3), Person.find(1) ].in_order_of(:id, [ 1, 5, 3 ])
=> [ Person.find(1), Person.find(5), Person.find(3) ]
If the series include keys that have no corresponding element in the Enumerable, these are ignored. If the Enumerable has additional elements that aren't named in the series, these are not included in the result.
It is not perfect in a case of hashes, but you can consider something like:
require 'ostruct'
items = [{ id: 2, name: 'name2' }, { id: 5, name: 'name5' }, { id: 6, name: 'name6' }, { id: 8, name: 'name8' }]
items.map(&OpenStruct.method(:new)).in_order_of(:id, [5,2,8,6]).map(&:to_h)
# => [{:id=>5, :name=>"name5"}, {:id=>2, :name=>"name2"}, {:id=>8, :name=>"name8"}, {:id=>6, :name=>"name6"}]
Sources:
Official docs - Enumerable#in_order_of.
PR - Enumerable#in_order_of #41333.
Rails 7 adds Enumerable#in_order_of.
I have an array in my Rails 3.1 apps that has made by several objects:
[#<Hardware id: 10, brand_id: 5, model: "B4200", description: "Stampante OKI B4200", typology_id: 3, sub_typology_id: 10, created_at: nil, updated_at: nil>, #<Hardware id: 19, brand_id: 9, model: "JetLab", description: "JetLab - 600 ", typology_id: 5, sub_typology_id: nil, created_at: nil, updated_at: nil>]
and I want remove one object from this array. Using Rails console, I've tried to do something such as (try to remove first object):
array.pop=#<Hardware id: 10, brand_id: 5, model: "B4200", description: "Stampante OKI B4200", typology_id: 3, sub_typology_id: 10, created_at: nil, updated_at: nil>
but it doesn't work. How can I do this?
UPDATED: My goal isn't to pop last element on array, but a generic object (everywhere inside array) that I should find using mysql search query.
my_array = [ 1, 2, 3 ]
item = my_array.pop
puts item
# => 3
puts my_array
# => [ 1, 2 ]
You probably want to use the Array#delete function
an_array = [1,3,4]
an_array.delete(3)
# => 3
puts an_array
# => [1,4]
Check it out in the Ruby documentation:
http://www.ruby-doc.org/core-1.9.3/Array.html#method-i-delete