Ruby efficient each loop - ruby-on-rails

Is there a better way to write this? markdown is a StringIO
coverage_hash_arr = [
{
"Module": "Mobile",
"name": "Sheila Chapman",
"age": 21
},
{
"Module": "Web",
"name": "Hendricks Walton",
"age": 40
},
{
"Module": "Misc",
"name": "Torres Mcdonald",
"age": 39
}
]
coverage_hash_arr.each do |the_hash|
    markdown << "------- Status on #{the_hash[:Module]} -------\n"
    the_hash.delete(:Module)
    the_hash.each {|key, value| markdown << "- #{key}: #{value} \n"}
    markdown << "----------------------------------------------\n"
end
I tried this and it seems to work but I wonder if there's a better way (recursion)?
coverage_hash_arr.collect do |the_hash|
the_hash.each do |key,value|
key == :Module ? markdown << "--------- Status for #{value} ----------\n" : markdown << " - #{key}: #{value} \n"
end
markdown << "------------------------------------\n\n"
end

You could:
use puts instead of << to avoid explicit newlines
use center to center the caption horizontally
use map to generate the attribute strings and utilize puts' behavior of printing array elements on separate lines
use without to get a hash without the :Module key
use * to repeat a string
Applied to your code:
markdown = StringIO.new
coverage_hash_arr.each do |hash|
markdown.puts " Status on #{hash[:Module]} ".center(46, '-')
markdown.puts hash.without(:Module).map { |k, v| "- #{k}: #{v}" }
markdown.puts '-' * 46
markdown.puts
end
Output via puts markdown.string:
-------------- Status on Mobile --------------
- name: Sheila Chapman
- age: 21
----------------------------------------------
--------------- Status on Web ----------------
- name: Hendricks Walton
- age: 40
----------------------------------------------
--------------- Status on Misc ---------------
- name: Torres Mcdonald
- age: 39
----------------------------------------------
Note that the above isn't proper Markdown syntax. You might want to change your output to something like this:
### Status on Mobile
- name: Sheila Chapman
- age: 21
### Status on Web
- name: Hendricks Walton
- age: 40
### Status on Misc
- name: Torres Mcdonald
- age: 39

Here's a more streamlined version which has been adapted to be more idiomatic Ruby:
# Define your hashes with keys having consistent case, and omit extraneous
# surrounding quotes unless defining keys like "this-name" which are not
# valid without escaping.
coverage = [
{
module: "Mobile",
name: "Sheila Chapman",
age: 21
},
{
module: "Web",
name: "Hendricks Walton",
age: 40
},
{
module: "Misc",
name: "Torres Mcdonald",
age: 39
}
]
# Iterate over each of these elements...
coverage.each do |entry|
markdown << "------- Status on #{entry[:module]} -------\n"
entry.each do |key, value|
# Skip a particular key
next if (key == :module)
markdown << "- #{key}: #{value} \n"
end
markdown << "----------------------------------------------\n"
end
This can be adapted to have a list of keys to exclude, or the inverse, of having a list of keys to actually print.
There's really nothing wrong with your approach specifically. The major faux-pas committed is in using delete on the data, which mangles it, rendering it useless if you needed to print this again.
It's generally best to try and avoid tampering with the data you're iterating over unless the purpose of that code is clear in its intent to alter it.

It looks like your input data has always the same key/value pairs and I would be more explicit to make it easier to read and to understand what the actual output is:
coverage_hash_arr.each do |hash|
markdown << <<~STRING
------- Status on #{hash[:Module]} -------
- name: #{hash[:name]}
- age: #{hash[:age]}
----------------------------------------------
STRING
end

Related

How to sort a hierarchical array of hashes

I'm working with an array like the below:
arr = [{
item: "Subject",
id: "16",
parent_id: ""
},
{
item: "Math",
id: "17",
parent_id: "16"
},
{
item: "Geology",
id: "988",
parent_id: "208"
},
{
item: "Biology",
id: "844",
parent_id: "208"
},
{
item: "Botany",
id: "594",
parent_id: "844"
},
{
item: "Science",
id: "208",
parent_id: "16"
}
]
I'm wanting to sort them so and print them out so that they display like this, grouping them and showing their parentage within the hierarchy as indentations:
Subject
Math
Science
Geology
Biology
Botany
I'm fairly stumped on how to accomplish this. I am ultimately wanting to iterate through the array only once, but I get stuck when I realize that a parent item may come after its child. Any help is greatly appreciated.
Edit: eliminated the duplicate item
To sort an array by an attribute of its elements...
arr = arr.sort_by{ |e| e[:parent_id] }
Then a little recursion to walk the tree...
def display_hierarchy(arr, parent_id="", level = 0)
current_indent = 2 * level
arr.select{ |e| e[:parent_id] == parent_id }.each do |elem|
name = elem[:item]
puts name.rjust(name.length + current_indent)
display_hierarchy(arr, elem[:id], level + 1)
end
end
Putting it all together...
arr = arr.sort_by{ |e| e[:parent_id] }
display_hierarchy(arr)
Note, if you have duplicate parents, you will get duplicate branches (hint: your example array has a duplicate "Science" node).
Also note, sorting the array ahead of time doesn't really matter for the tree, since it is a hierarchy, I just added it to show how to do it. You'd probably just sort each set of children like so...
def display_hierarchy(arr, parent_id="", level = 0)
current_indent = 2 * level
arr.select{ |e| e[:parent_id] == parent_id }.sort_by{ |e| e[:item]}.each do |elem|
name = elem[:item]
puts name.rjust(name.length + current_indent)
display_hierarchy(arr, elem[:id], level + 1)
end
end
And you get this...
Subject
Math
Science
Biology
Botany
Geology
Science
Biology
Botany
Geology
I am ultimately wanting to iterate through the array only once
In a way, you can do that: by transforming the array into a different structure that's more fit for the task. You'd still have to traverse the resulting collection again though.
Say, you could partition the array into groups by their :parent_id:
children = arr.group_by { |subject| subject[:parent_id] }
Then it's your typical recursive walk, where indentation is added on every level:
hierarchical_print = proc do |node, indent = ""|
puts(indent + node[:item]) # base
new_indent = indent + " "
children[node[:id]]&.each do |child| # recursion
hierarchical_print.(child, new_indent)
end
end
Notice the use of &. (the "lonely operator") in there that only calls the method if the callee isn't nil. Because getting a value from a hash with a key that isn't there returns nil.
In your case the root is a node with an empty parent id, so it's readily available from the children hash as well:
hierarchical_print.(children[""].first)
Subject
Math
Science
Geology
Biology
Botany
Science
Geology
Biology
Botany
...well, yes, you do have two Sciences in there.

Parsing a string list with multiple values into JSON

I have about thirty thousand records with a string column that has been stored in the following format, with different keys:
"something: this, this and that, that, other stuff, another: name, another name, last: here"
In rails, I want to change it into a hash like
{
something: [ "this", "this and that", "that" ],
another: [ "name", "another name" ],
last: [ "here" ]
}
Is there a way to do this elegantly? I was thinking of splitting at the colon, then doing a reverse search of the first space.
There are about a hundred ways to solve this. A pretty straightforward one is this:
str = "something: this, this and that, that, other stuff, another: name, another name, last: here"
key = nil
str.scan(/\s*([^,:]+)(:)?\s*/).each_with_object({}) do |(val, colon), hsh|
if colon
key = val.to_sym
hsh[key] = []
else
hsh[key] << val
end
end
# => {
# something: ["this", "this and that", "that", "other stuff"],
# another: ["name", "another name"],
# last: ["here"]
# }
It works by scanning the string with the following regular expression:
/
\s* # any amount of optional whitespace
([^,:]+) # one or more characters that aren't , or : (capture 1)
(:)? # an optional trailing : (capture 2)
\s* # any amount of optional whitespace
/x
Then it iterates over the matches and puts them into a hash. When a match has a trailing colon (capture 2), a new hash key is created with an empty array for a value. Otherwise the value (capture 1) is added to the array for the most recent key.
Or…
A somewhat less straightforward but cleverer approach is to let the RegExp do more work:
MATCH_LIST_ENTRY = /([^:]+):\s*((?:[^,]+(?:,\s*|$))+?)(?=[^:,]+:|$)/
def parse_list2(str)
str.scan(MATCH_LIST_ENTRY).map do |k, vs|
[k.to_sym, vs.split(/,\s*/)]
end.to_h
end
I won't pick apart the RegExp for this one, but it's simpler than it looks. Regexper does a pretty good job of explaining it.
You can see both of these in action on repl.it here: https://repl.it/#jrunning/LongtermMidnightblueAssembler
If str is the string given in the example, the desired hash can be constructed as follows.
str.split(/, *(?=\p{L}+:)/).
each_with_object({}) do |s,h|
k, v = s.split(/: +/)
h[k.to_sym]= v.split(/, */)
end
#=> {:something=>["this", "this and that", "that", "other stuff"],
# :another=>["name", "another name"],
# :last=>["here"]}
Note:
str.split(/, *(?=\p{L}+:)/)
#=> ["something: this, this and that, that, other stuff",
# "another: name, another name",
# "last: here"]
This regular expression reads, "match a comma followed by zero or more spaces, the match to be immediately followed by one or more Unicode letters followed by a colon, (?=\p{L}+:) being a positive lookahead".
elegantly:
result_hash = {}
string.scan(/(?<key>[\w]+(?=:))|(?<value>[\s\w]+(?=(,|\z)))/) do |key,value|
if key.present?
result_hash[key] = []
current_key = key
elsif value.present?
result_hash[current_key] << value.strip
end
end
then jsonize:
json = result_hash.to.json

Ruby: Passing down key/value after transforming objects in array

Given data:
data = [
{"id":14, "sort":1, "content":"9", foo: "2022"},
{"id":14, "sort":4, "content":"5", foo: "2022"},
{"id":14, "sort":2, "content":"1", foo: "2022"},
{"id":14, "sort":3, "content":"0", foo: "2022"},
{"id":15, "sort":4, "content":"4", foo: "2888"},
{"id":15, "sort":2, "content":"1", foo: "2888"},
{"id":15, "sort":1, "content":"3", foo: "2888"},
{"id":15, "sort":3, "content":"3", foo: "2888"},
{"id":16, "sort":1, "content":"8", foo: "3112"},
{"id":16, "sort":3, "content":"4", foo: "3112"},
{"id":16, "sort":2, "content":"4", foo: "3112"},
{"id":16, "sort":4, "content":"9", foo: "3112"}
]
Got the contents concatenated by their sort and ids with:
formatted = data.group_by { |d| d[:id]}.transform_values do |value_array|
value_array.sort_by { |b| b[:sort] }
.map { |c| c[:content] }.join
end
puts formatted
#=> {14=>"9105", 15=>"3134", 16=>"8449"}
I know that foo exists inside value_array but wondering how can I include foo to exist inside the formatted variable so I can map through it to get the desired output or if it's possible?
Desired Output:
[
{"id":14, "concated_value":"9105", foo: "2022"},
{"id":15, "concated_value":"3134", foo: "2888"},
{"id":16, "concated_value":"8449", foo: "3112"}
]
Since :foo is unique to :id. You can do this as follows:
data.group_by {|h| h[:id]}.map do |_,sa|
sa.map(&:dup).sort_by {|h| h.delete(:sort) }.reduce do |m,h|
m.merge(h) {|key,old,new| key == :content ? old + new : old }
end.tap {|h| h[:concated_value] = h.delete(:content) }
end
#=> [
# {"id":14, foo: "2022", "concated_value":"9105"},
# {"id":15, foo: "2888", "concated_value":"3134"},
# {"id":16, foo: "3112", "concated_value":"8449"}
# ]
First we group by id. group_by {|h| h[:id]}
Then we dup the hashes in the groups (so as not to destory the original). map(&:dup)
Then we sort by sort and delete it at the same time. .sort_by {|h| h.delete(:sort) }
Then we merge the groups together and concatenate the content key only.
m.merge(h) {|key,old,new| key == :content ? old + new : old }
Then we just change the key for content to concated_value tap {|h| h[:concated_value] = h.delete(:content) }
We can use first value from value_array to get our :id & :foo values
formatted = data.group_by { |d| d[:id]}.values.map do |value_array|
concated_value = value_array.sort_by { |b| b[:sort] }
.map { |c| c[:content] }.join
value_array.first.slice(:id, :foo)
.merge concated_value: concated_value
end
I think this is a good usecase for reduce, since after grouping you need first to get rid of the ID in the resulting [ID, VALUES] array from group_by and just return a reduced version of the VALUES part - this can all be done without any ActiveSupport etc. dependencies:
data
.group_by{ |d| d[:id] } # Get an array of [ID, [VALUES]]
.reduce([]) do |a, v| # Reduce it into a new empty array
# Append a new hash to the new array
a << {
id: v[1].first[:id], # Just take the ID of the first entry
foo: v[1].first[:foo], # Dito for foo
concatenated: v[1]
.sort_by{ |s| s[:sort] } # now sort all hashes by its sort key
.collect{ |s| s[:content] } # collect the content
.join # and merge it into a string
}
end
Output:
[{:id=>14, :foo=>"2022", :concatenated=>"9105"},
{:id=>15, :foo=>"2888", :concatenated=>"3134"},
{:id=>16, :foo=>"3112", :concatenated=>"8449"}]
EDIT
I had some other approach in mind when i started to write the previous solution, reduce was not really necessary, since the size of the array after group_by does not change, so a map is sufficient.
But while rewriting the code, i was thinking that creating a new hash with all the keys and copying all the values from the first hash within VALUES was a bit too much work, so it would be easier to just reject the overhead keys:
keys_to_ignore = [:sort, :content]
data
.group_by{ |d| d[:id] } # Get an array of [ID, [VALUES]]
.map do |v|
v[1]
.first # Take the first hash from [VALUES]
.merge({'concatenated': v[1] # Insert the concatenated values
.sort_by{ |s| s[:sort] } # now sort all hashes by its sort key
.collect{ |s| s[:content] } # collect the content
.join # and merge it into a string
})
.select { |k, _| !keys_to_ignore.include? k }
end
Output
[{:id=>14, :foo=>"2022", :concatenated=>"9105"},
{:id=>15, :foo=>"2888", :concatenated=>"3134"},
{:id=>16, :foo=>"3112", :concatenated=>"8449"}]
Online demo here
This will work even without Rails:
$irb> formatted = []
$irb> data.sort_by!{|a| a[:sort]}.map {|z| z[:id]}.uniq.each_with_index { |id, index| formatted << {id: id, concated_value: data.map{|c| (c[:id] == id ? c[:content] : nil)}.join, foo: data[index][:foo]}}
$irb> formatted
[{:id=>14, :concated_value=>"9105", :foo=>"2022"},
{:id=>15, :concated_value=>"3134", :foo=>"2888"},
{:id=>16, :concated_value=>"8449", :foo=>"3112"}]
data.sort_by { |h| h[:sort] }.
each_with_object({}) do |g,h| h.update(g[:id]=>{ id: g[:id],
concatenated_value: g[:content].to_s, foo: g[:foo] }) { |_,o,n|
o.merge(concatenated_value: o[:concatenated_value]+n[:concatenated_value]) }
end.values
#=> [{:id=>14, :concatenated_value=>"9105", :foo=>"2022"},
# {:id=>15, :concatenated_value=>"3134", :foo=>"2888"},
# {:id=>16, :concatenated_value=>"8449", :foo=>"3112"}]
This uses the form of Hash#update (aka merge!) that employs a block to determine the values of keys (here the value of :id) that are present in both hashes being merged. See the doc for the description of the three block variables (here _, o and n).
Note the receiver of values (at the end) is the following.
{ 14=>{ :id=>14, :concatenated_value=>"9105", :foo=>"2022" },
15=>{ :id=>15, :concatenated_value=>"3134", :foo=>"2888" },
16=>{ :id=>16, :concatenated_value=>"8449", :foo=>"3112" } }

String interpolation with subhashes

In my code I want to use string interpolation for an email subject I am generating.
output = "this is my %{title}" % {title: "Text here"}
This works as expected, but is there a way to use hashes inside of hashes and still be able to use string interpolation?
It would be awesome if I could do something like:
output = "this is my %{title.text}" % {title: {text: "text here"}}
In Ruby 2.3, sprintf checks the hash's default value, so you could provide a default_proc to dig up the nested value:
hash = {title: {text: "text here"}}
hash.default_proc = proc { |h, k| h.dig(*k.to_s.split('.').map(&:to_sym)) }
"this is my %{title.text}" % hash
#=> "this is my text here"
Kind of hacky, but it seems to work.
I don't think this is possible with % method. You'd have to use regular Ruby interpolation with "#{}".
I'd also point out that you can use OpenStruct.
title = OpenStruct.new(text: 'text here')
output = "this is my #{title.text}"
It's actually not hard to make this work if you write a simple utility method to "squash" a nested Hash's keys, e.g.:
def squash_hash(hsh, stack=[])
hsh.reduce({}) do |res, (key, val)|
next_stack = [ *stack, key ]
if val.is_a?(Hash)
next res.merge(squash_hash(val, next_stack))
end
res.merge(next_stack.join(".").to_sym => val)
end
end
hsh = { foo: { bar: 1, baz: { qux: 2 } }, quux: 3 }
p squash_hash(hsh)
# => { :"foo.bar" => 1, :"foo.baz.qux" => 2, :quux => 3 }
puts <<END % squash_hash(hsh)
foo.bar: %{foo.bar}
foo.baz.qux: %{foo.baz.qux}
quux: %{quux}
END
# => foo.bar: 1
# foo.baz.qux: 2
# quux: 3

What's the optimal way to deal with non-existent variables in Rails?

I'm working on a Rails app that pulls data in from Groupon's API and displays them on our site.
Take the follow data structure, for example:
---
- "id": deal one
"options":
"redemptionLocations":
- "streetAddress1": 123 Any Street"
- "id": deal two
"options": []
If I wanted to loop through each deal, and display the streetAddress1 if it exists, what's the optimal way to do that in Rails?
Just do:
if(defined? streetAddress1) then
print streetAddress1 + " is set"
end
Hope it helps
The best practice should be to use present?:
puts "It is #{object.attribute}" if object.attribute.present?
If you have an array of objects and want to loop only over those that have the attribute set, you can use select:
array.select{|object| object.attribute.present?}.each do |object|
...
end
If you have a deeply nested structure you can create a custom function to check if a key exists and display its value:
def nested_value hash, *args
tmp = hash
args.each do |arg|
return nil if tmp.nil? || !tmp.respond_to?(:[]) || (tmp.is_a?(Array) && !arg.is_a?(Integer))
tmp = tmp[arg]
end
tmp
end
For example, if you have the following YAML loaded from your example:
k = [
{ "id"=>"deal one",
"options"=>{"redemptionLocations"=>[{"streetAddress1"=>"123 Any Street\""}]}},
{ "id"=>"deal two",
"options"=>[]}]
Then you can do this:
nested_value k.first, 'options', 'redemptionLocations', 0, 'streetAddress1'
=> "123 Any Street \""
nested_value k.last, 'options', 'redemptionLocations', 0, 'streetAddress1'
=> nil

Resources