String interpolation with subhashes - ruby-on-rails

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

Related

Get unique properties from array of hashes in ruby

Given an array of hashes, I want to create a method that returns a hash where the keys are the unique values of the hashes in the array.
For example, I'd like to take
[
{foo: 'bar', baz: 'bang'},
{foo: 'rab', baz: 'bang'},
{foo: 'bizz', baz: 'buzz'}
]
and return
{
foo: ['bar', 'rab', 'bizz'],
baz: ['bang', 'buzz']
}
I am currently accomplishing this using:
def my_fantastic_method(data)
response_data = { foo: [], baz: []}
data.each { |data|
data.attributes.each { |key, value|
response_data[key.to_sym] << value
}
}
response_data.each { |key, value| response_data[key] = response_data[key].uniq }
response_data
end
Is there a more elegant way of doing this? Thanks!
Your current approach is already pretty good; I don't see much room for improvement. I would write it like this:
def my_fantastic_method(data_list)
data_list.each_with_object(Hash.new { |h, k| h[k] = Set.new }) do |data, result|
data.attributes.each do |key, value|
result[key.to_sym] << value
end
end
end
By setting a default value on each hash value, I have eliminated the need to explicitly declare foo: [], bar: [].
By using each_with_object, I have eliminated the need to declare a local variable and explicitly return it at the end.
By using Set, there is no need to call uniq on the final result. This requires less code, and is more performant. However, if you really want the final result to be a mapping to Arrays rather than Sets, then you would need to call to_a on each value at the end of the method.
I have used different variable names for data_list and data. Call these whatever you like, but it's typically considered bad practice to shadow outer variables.
Here are a couple of one-liners. (I'm pretty sure #eiko was being facetious, but I'm proving him correct)
This one reads well and is easy to follow (caveat: requires Ruby 2.4+ for transform_values):
array.flat_map(&:entries).group_by(&:first).transform_values{|v| v.map(&:last).uniq}
Here's another, using the block form of merge to specify an alternate merge method, which in this case is combining the values into a uniq array:
array.reduce{|h, el| h.merge(el){|k, old, new| ([old]+[new]).flatten.uniq}}
You already have a pretty good answer, but I felt golfy and so here is a shorter one:
def the_combiner(a)
hash = {}
a.map(&:to_a).flatten(1).each do |k,v|
hash[k] ||= []
hash[k].push(v)
end
hash
end
Try this:
array.flat_map(&:entries)
.group_by(&:first)
.map{|k,v| {k => v.map(&:last)} }
OR
a.inject({}) {|old_h, new_h|
new_h.each_pair {|k, v|
old_h.key?(k) ? old_h[k] << v : old_h[k]=[v]};
old_h}
If, as in the example, all hashes have the same keys, you could do as follows.
arr = [{ foo: 'bar', baz: 'bang' },
{ foo: 'rab', baz: 'bang' },
{ foo: 'bizz', baz: 'buzz' }]
keys = arr.first.keys
keys.zip(arr.map { |h| h.values_at(*keys) }.transpose.map(&:uniq)).to_h
#=> {:foo=>["bar", "rab", "bizz"], :baz=>["bang", "buzz"]}
The steps are as follows.
keys = arr.first.keys
#=> [:foo, :baz]
a = arr.map { |h| h.values_at(*keys) }
#=> [["bar", "bang"], ["rab", "bang"], ["bizz", "buzz"]]
b = a.transpose
#=> [["bar", "rab", "bizz"], ["bang", "bang", "buzz"]]
c = b.map(&:uniq)
#=> [["bar", "rab", "bizz"], ["bang", "buzz"]]
d = c.to_h
#=> <array of hashes shown above>

ruby: add boundaries \b option when using Regexp.union (array)?

I am creating a regex matcher using:
Regexp.new(Regexp.union(some_hash.keys))
is it possible to add a boundaries filter to each element of the array so I have:
/\bkey1\b|\bkey2\b|,....../
For regexp keys:
Regexp.union(some_hash.keys.map { |k| /\b#{k}\b/ })
or for literal keys:
Regexp.union(some_hash.keys.map { |k| /\b#{Regexp.escape(k)}\b/ })
The result of Regexp.union is already a Regexp, no need for Regexp.new. In fact, we can also use plain strings inside Regexp.union, the difference being we don't initialise the flags in each subexpression:
Regexp.union(some_hash.keys.map { |k| "\\b#{k}\\b" })
Regexp.union(some_hash.keys.map { |k| "\\b#{Regexp.escape(k)}\\b" })
You can generate regex in simplest way without using Regexp as
hash = {'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4}
/\b#{hash.keys.join('\b|\b')}\b/
=>/\ba\b|\bb\b|\bc\b|\bd\b/
Not exactly but you can use the scan method ...
for example:-
a = "cruel world"
a.scan(/\w+/) #=> ["cruel", "world"]
a.scan(/.../) #=> ["cru", "el ", "wor"]
a.scan(/(...)/) #=> [["cru"], ["el "], ["wor"]]
a.scan(/(..)(..)/) #=> [["cr", "ue"], ["l ", "wo"]]

Process nested hash to convert all values to strings

I have the following code which takes a hash and turns all the values in to strings.
def stringify_values obj
#values ||= obj.clone
obj.each do |k, v|
if v.is_a?(Hash)
#values[k] = stringify_values(v)
else
#values[k] = v.to_s
end
end
return #values
end
So given the following hash:
{
post: {
id: 123,
text: 'foobar',
}
}
I get following YAML output
--- &1
:post: *1
:id: '123'
:text: 'foobar'
When I want this output
---
:post:
:id: '123'
:text: 'foobar'
It looks like the object has been flattened and then been given a reference to itself, which causes Stack level errors in my specs.
How do I get the desired output?
A simpler implementation of stringify_values can be - assuming that it is always a Hash. This function makes use of Hash#deep_merge method added by Active Support Core Extensions - we merge the hash with itself, so that in the block we get to inspect each value and call to_s on it.
def stringify_values obj
obj.deep_merge(obj) {|_,_,v| v.to_s}
end
Complete working sample:
require "yaml"
require "active_support/core_ext/hash"
def stringify_values obj
obj.deep_merge(obj) {|_,_,v| v.to_s}
end
class Foo
def to_s
"I am Foo"
end
end
h = {
post: {
id: 123,
arr: [1,2,3],
text: 'foobar',
obj: { me: Foo.new}
}
}
puts YAML.dump (stringify_values h)
#=>
---
:post:
:id: '123'
:arr: "[1, 2, 3]"
:text: foobar
:obj:
:me: I am Foo
Not sure what is the expectation when value is an array, as Array#to_s will give you array as a string as well, whether that is desirable or not, you can decide and tweak the solution a bit.
There are two issues. First: the #values after the first call would always contain an object which you cloned in the first call, so in the end you will always receive a cloned #values object, no matter what you do with the obj variable(it's because of ||= operator in your call). Second: if you remove it and will do #values = obj.clone - it would still return incorrect result(deepest hash), because you are overriding existing variable call after call.
require 'yaml'
def stringify_values(obj)
temp = {}
obj.each do |k, v|
if v.is_a?(Hash)
temp[k] = stringify_values(v)
else
temp[k] = v.to_s
end
end
temp
end
hash = {
post: {
id: 123,
text: 'foobar',
}
}
puts stringify_values(hash).to_yaml
#=>
---
:post:
:id: '123'
:text: foobar
If you want a simple solution without need of ActiveSupport, you can do this in one line using each_with_object:
obj.each_with_object({}) { |(k,v),m| m[k] = v.to_s }
If you want to modify obj in place pass obj as the argument to each_with_object; the above version returns a new object.
If you are as aware of converting values to strings, I would go with monkeypatching Hash class:
class Hash
def stringify_values
map { |k, v| [k, Hash === v ? v.stringify_values : v.to_s] }.to_h
end
end
Now you will be able to:
require 'yaml'
{
post: {
id: 123,
text: 'foobar'
},
arr: [1, 2, 3]
}.stringify_values.to_yaml
#⇒ ---
# :post:
# :id: '123'
# :text: foobar
# :arr: "[1, 2, 3]"
In fact, I wonder whether you really want to scramble Arrays?

How do I search a Ruby hash from an array of values, and then return a hash based on the findings?

Here's an example hash and an example array to search in the hash:
nicknames = { "Black Mamba" => "Kobe Bryant",
"Half Man Half Amazing" => "Vince Carter",
"The Big Fundamental" => "Tim Duncan",
"Big Ticket" => "Kevin Garnett",
"Obi-Wan Ginobili" => "Manu Ginobili",
"The Answer" => "Allen Iverson" }
names = [ "Vince Carter", "Manu Ginobili", "Allen Iverson" ]
I want to return:
selected = { "Half Man Half Amazing" => "Vince Carter", "Obi-Wan Ginobili" => "Manu Ginobili", "The Answer" = "Allen Iverson" }
What's a good way to do this? Thanks!
You can simply do the following:
nicknames.select { |key, value| names.include?(value) }
(copy-paste the code you provided and mine in your IRB console and you'll see it working).
If the values in the hash are unique, then you can reverse the keys and the values. MrYoshiji's code works, but here is a more efficient way.
hash = nicknames.invert
names.to_enum.with_object({}){|k, h| h[hash[k]] = k}

what is the best way to convert a json formatted key value pair to ruby hash with symbol as key?

I am wondering what is the best way to convert a json formatted key value pair to ruby hash with symbol as key:
example:
{ 'user': { 'name': 'foo', 'age': 40, 'location': { 'city' : 'bar', 'state': 'ca' } } }
==>
{ :user=>{ :name => 'foo', :age =>'40', :location=>{ :city => 'bar', :state=>'ca' } } }
Is there a helper method can do this?
using the json gem when parsing the json string you can pass in the symbolize_names option. See here: http://flori.github.com/json/doc/index.html (look under parse)
eg:
>> s ="{\"akey\":\"one\",\"bkey\":\"two\"}"
>> JSON.parse(s,:symbolize_names => true)
=> {:akey=>"one", :bkey=>"two"}
Leventix, thank you for your answer.
The Marshal.load(Marshal.dump(h)) method probably has the most integrity of the various methods because it preserves the original key types recursively.
This is important in case you have a nested hash with a mix of string and symbol keys and you want to preserve that mix upon decode (for instance, this could happen if your hash contains your own custom objects in addition to highly complex/nested third-party objects whose keys you cannot manipulate/convert for whatever reason, like a project time constraint).
E.g.:
h = {
:youtube => {
:search => 'daffy', # nested symbol key
'history' => ['goofy', 'mickey'] # nested string key
}
}
Method 1: JSON.parse - symbolizes all keys recursively => Does not preserve original mix
JSON.parse( h.to_json, {:symbolize_names => true} )
=> { :youtube => { :search=> "daffy", :history => ["goofy", "mickey"] } }
Method 2: ActiveSupport::JSON.decode - symbolizes top-level keys only => Does not preserve original mix
ActiveSupport::JSON.decode( ActiveSupport::JSON.encode(h) ).symbolize_keys
=> { :youtube => { "search" => "daffy", "history" => ["goofy", "mickey"] } }
Method 3: Marshal.load - preserves original string/symbol mix in the nested keys. PERFECT!
Marshal.load( Marshal.dump(h) )
=> { :youtube => { :search => "daffy", "history" => ["goofy", "mickey"] } }
Unless there is a drawback that I'm unaware of, I'd think Method 3 is the way to go.
Cheers
There isn't anything built in to do the trick, but it's not too hard to write the code to do it using the JSON gem. There is a symbolize_keys method built into Rails if you're using that, but that doesn't symbolize keys recursively like you need.
require 'json'
def json_to_sym_hash(json)
json.gsub!('\'', '"')
parsed = JSON.parse(json)
symbolize_keys(parsed)
end
def symbolize_keys(hash)
hash.inject({}){|new_hash, key_value|
key, value = key_value
value = symbolize_keys(value) if value.is_a?(Hash)
new_hash[key.to_sym] = value
new_hash
}
end
As Leventix said, the JSON gem only handles double quoted strings (which is technically correct - JSON should be formatted with double quotes). This bit of code will clean that up before trying to parse it.
Recursive method:
require 'json'
def JSON.parse(source, opts = {})
r = JSON.parser.new(source, opts).parse
r = keys_to_symbol(r) if opts[:symbolize_names]
return r
end
def keys_to_symbol(h)
new_hash = {}
h.each do |k,v|
if v.class == String || v.class == Fixnum || v.class == Float
new_hash[k.to_sym] = v
elsif v.class == Hash
new_hash[k.to_sym] = keys_to_symbol(v)
elsif v.class == Array
new_hash[k.to_sym] = keys_to_symbol_array(v)
else
raise ArgumentError, "Type not supported: #{v.class}"
end
end
return new_hash
end
def keys_to_symbol_array(array)
new_array = []
array.each do |i|
if i.class == Hash
new_array << keys_to_symbol(i)
elsif i.class == Array
new_array << keys_to_symbol_array(i)
else
new_array << i
end
end
return new_array
end
Of course, there is a json gem, but that handles only double quotes.
Another way to handle this is to use YAML serialization/deserialization, which also preserves the format of the key:
YAML.load({test: {'test' => { ':test' => 5}}}.to_yaml)
=> {:test=>{"test"=>{":test"=>5}}}
Benefit of this approach it seems like a format that is better suited for REST services...
The most convenient way is by using the nice_hash gem: https://github.com/MarioRuiz/nice_hash
require 'nice_hash'
my_str = "{ 'user': { 'name': 'foo', 'age': 40, 'location': { 'city' : 'bar', 'state': 'ca' } } }"
# on my_hash will have the json as a hash
my_hash = my_str.json
# or you can filter and get what you want
vals = my_str.json(:age, :city)
# even you can access the keys like this:
puts my_hash._user._location._city
puts my_hash.user.location.city
puts my_hash[:user][:location][:city]
If you think you might need both string and symbol keys:
JSON.parse(json_string).with_indifferent_access

Resources