Idiomatic Ruby - Assign variable only if not nil without using `if` - ruby

I have a whole bunch of lines like this
api_data[:foo] = foo if foo
api_data[:bar] = bar if bar
api_data[:batz] = batz if batz
I want a terse, idiomatic way to assign only if !nil.
Thanks

If you only want to assign values that are non-nil:
api_data.merge!(
{
foo: foo,
bar: bar,
bart: barts
}.reject { |_,v|v.nil? }
)
This is a little messy, so you might want to make an extension to the Hash class that ignores nil assignments:
class PickyHash < Hash
def []=(k,v)
return if (v.nil?)
super
end
end
Then:
api_data = PickyHash.new
api_data[:foo] = 'foo'
api_data[:foo] = nil
api_data
#> {:foo=>"foo"}

api_data[:foo] = foo
api_data[:bar] = bar
api_data[:batz] = batz
api_data.reject! { |_, value| value.nil? }
would do the trick.

Can't say that I like this solution, but anyway:
Hash[[:foo, :bar, :baz].find_all { |var| eval var.to_s }.map { |var| [var, eval(var.to_s)] }]

You can do something like below:
["foo", "bar", "batz"].each do |v|
eval(v).tap {|val| api_data[v.to_sym] = val if val }
end

Here's one using Array#compact and Enumerable#each_with_object:
[foo, bar, batz].compact.each_with_object(api_data) {|elem, acc| acc.merge!({elem => elem})}
Where [foo, bar, batz].compact returns the input array eliminating nil, and you're merging each non-nil element from the input array to api_data hash passed in as the initial value to each_with_object.

Related

How to set a Ruby hash given a "key path" of arbitrary length?

Given:
h = {foo: {bar: 1}}
How to set bar if you don't know how many keys you have?
For example: keys = [:foo, :bar]
h[keys[0]][keys[1]] = :ok
But what if keys can be of arbitrary length and h is of arbitrary depth?
If you're using Ruby 2.3+ then you can use dig thusly:
h.dig(*keys[0..-2])[keys.last] = :ok
dig follows a path through the hash and returns what it finds. But dig won't copy what it finds so you get the same reference that is in h. keys[0..-2] grabs all but the last element of keys so h.dig(*keys[0..-2]) gives you the {bar: 1} Hash from inside h, then you can modify it in-place with a simple assignment.
You could also say:
*head, tail = keys
h.dig(*head)[tail] = :ok
if that's clearer to you than [0..-2].
If you don't have dig then you can do things like:
*head, tail = keys
head.inject(h) { |m, k| m[k] }[tail] = :ok
Of course, if you're not certain that the path specified by keys exists then you'll need to throw in a some nil checks and decide how you should handle keys that don't specify a path into h.
You could do it recursively like this:
h = {foo: {bar: 1}}
keys = [:foo, :bar]
h2 = {
foo: {
bar: {
baz: {
setting: :bad
}
}
}
}
keys2 = [:foo, :bar, :baz, :setting]
def set_nested_value(hash, keys, new_val)
if keys.length == 1
hash[keys.first] = new_val
return
end
keys.each_with_index do |key, i|
if hash[key]
set_nested_value(hash[key], keys[i+1..-1], new_val)
end
end
end
set_nested_value(h, keys, :ok)
p h
=> {:foo=>{:bar=>:ok}}
set_nested_value(h2, keys2, :good)
p h2
=> {:foo=>{:bar=>{:baz=>{:setting=>:good}}}}

Convert List of Objects to Hash

In Ruby, I have a list of objects called Things with an Id property and a value property.
I want to make a Hash that contains Id as the key and Value as the value for the cooresponding key.
I tried:
result = Hash[things.map { |t| t.id, t.value }]
where things is a list of Thing
But this did not work.
class Thing
attr_reader :id, :value
def initialize(id, value)
#id = id
#value = value
end
end
cat = Thing.new("cat", 9)
#=> #<Thing:0x007fb86411ad90 #id="cat", #value=9>
dog = Thing.new("dog",1)
#=> #<Thing:0x007fb8650e49b0 #id="dog", #value=1>
instances =[cat, dog]
#=> [#<Thing:0x007fb86411ad90 #id="cat", #value=9>,
# #<Thing:0x007fb8650e49b0 #id="dog", #value=1>]
instances.map { |i| [i.id, i.value] }.to_h
#=> {"cat"=>9, "dog"=>1}
or, for Ruby versions prior to 2.0:
Hash[instances.map { |i| [i.id, i.value] }]
#=> {"cat"=>9, "dog"=>1}
result = things.map{|t| {t.id => t.value } }
The content of the outer pair of curly brackets is a block, the inner pair forms a hash.
However, if one hash is the desired result (as suggested by Cary Swoveland) this may work:
result = things.each_with_object({}){| t, h | h[t.id] = t.value}

Safely assign value to nested hash using Hash#dig or Lonely operator(&.)

h = {
data: {
user: {
value: "John Doe"
}
}
}
To assign value to the nested hash, we can use
h[:data][:user][:value] = "Bob"
However if any part in the middle is missing, it will cause error.
Something like
h.dig(:data, :user, :value) = "Bob"
won't work, since there's no Hash#dig= available yet.
To safely assign value, we can do
h.dig(:data, :user)&.[]=(:value, "Bob") # or equivalently
h.dig(:data, :user)&.store(:value, "Bob")
But is there better way to do that?
It's not without its caveats (and doesn't work if you're receiving the hash from elsewhere), but a common solution is this:
hash = Hash.new {|h,k| h[k] = h.class.new(&h.default_proc) }
hash[:data][:user][:value] = "Bob"
p hash
# => { :data => { :user => { :value => "Bob" } } }
And building on #rellampec's answer, ones that does not throw errors:
def dig_set(obj, keys, value)
key = keys.first
if keys.length == 1
obj[key] = value
else
obj[key] = {} unless obj[key]
dig_set(obj[key], keys.slice(1..-1), value)
end
end
obj = {d: 'hey'}
dig_set(obj, [:a, :b, :c], 'val')
obj #=> {d: 'hey', a: {b: {c: 'val'}}}
interesting one:
def dig_set(obj, keys, value)
if keys.length == 1
obj[keys.first] = value
else
dig_set(obj[keys.first], keys.slice(1..-1), value)
end
end
will raise an exception anyways if there's no [] or []= methods.
I found a simple solution to set the value of a nested hash, even if a parent key is missing, even if the hash already exists. Given:
x = { gojira: { guitar: { joe: 'charvel' } } }
Suppose you wanted to include mario's drum to result in:
x = { gojira: { guitar: { joe: 'charvel' }, drum: { mario: 'tama' } } }
I ended up monkey-patching Hash:
class Hash
# ensures nested hash from keys, and sets final key to value
# keys: Array of Symbol|String
# value: any
def nested_set(keys, value)
raise "DEBUG: nested_set keys must be an Array" unless keys.is_a?(Array)
final_key = keys.pop
return unless valid_key?(final_key)
position = self
for key in keys
return unless valid_key?(key)
position[key] = {} unless position[key].is_a?(Hash)
position = position[key]
end
position[final_key] = value
end
private
# returns true if key is valid
def valid_key?(key)
return true if key.is_a?(Symbol) || key.is_a?(String)
raise "DEBUG: nested_set invalid key: #{key} (#{key.class})"
end
end
usage:
x.nested_set([:instrument, :drum, :mario], 'tama')
usage for your example:
h.nested_set([:data, :user, :value], 'Bob')
any caveats i missed? any better way to write the code without sacrificing readability?
Searching for an answer to a similar question I developmentally stumbled upon an interface similar to #niels-kristian's answer, but wanted to also support a namespace definition parameter, like an xpath.
def deep_merge(memo, source)
# From: http://www.ruby-forum.com/topic/142809
# Author: Stefan Rusterholz
merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
memo.merge!(source, &merger)
end
# Like Hash#dig, but for setting a value at an xpath
def bury(memo, xpath, value, delimiter=%r{\.})
xpath = xpath.split(delimiter) if xpath.respond_to?(:split)
xpath.map!{|x|x.to_s.to_sym}.push(value)
deep_merge(memo, xpath.reverse.inject { |memo, field| {field.to_sym => memo} })
end
Nested hashes are sort of like xpaths, and the opposite of dig is bury.
irb(main):014:0> memo = {:test=>"value"}
=> {:test=>"value"}
irb(main):015:0> bury(memo, 'test.this.long.path', 'value')
=> {:test=>{:this=>{:long=>{:path=>"value"}}}}
irb(main):016:0> bury(memo, [:test, 'this', 2, 4.0], 'value')
=> {:test=>{:this=>{:long=>{:path=>"value"}, :"2"=>{:"4.0"=>"value"}}}}
irb(main):017:0> bury(memo, 'test.this.long.path.even.longer', 'value')
=> {:test=>{:this=>{:long=>{:path=>{:even=>{:longer=>"value"}}}, :"2"=>{:"4.0"=>"value"}}}}
irb(main):018:0> bury(memo, 'test.this.long.other.even.longer', 'other')
=> {:test=>{:this=>{:long=>{:path=>{:even=>{:longer=>"value"}}, :other=>{:even=>{:longer=>"other"}}}, :"2"=>{:"4.0"=>"value"}}}}
A more ruby-helper-like version of #niels-kristian answer
You can use it like:
a = {}
a.bury!([:a, :b], "foo")
a # => {:a => { :b => "foo" }}
class Hash
def bury!(keys, value)
key = keys.first
if keys.length == 1
self[key] = value
else
self[key] = {} unless self[key]
self[key].bury!(keys.slice(1..-1), value)
end
self
end
end

Deep Convert OpenStruct to JSON

I have an OpenStruct that is nested with many other OpenStructs. What's the best way to deeply convert them all to JSON?
Ideally:
x = OpenStruct.new
x.y = OpenStruct.new
x.y.z = OpenStruct.new
z = 'hello'
x.to_json
// {y: z: 'hello'}
Reality
{ <OpenStruct= ....> }
There is no default methods to accomplish such task because the built-in #to_hash returns the Hash representation but it doesn't deep converts the values.
If a value is an OpenStruct, it's returned as such and it's not converted into an Hash.
However, this is not that complicated to solve. You can create a method that traverses each key/value in an OpenStruct instance (e.g. using each_pair), recursively descends into the nested OpenStructs if the value is an OpenStruct and returns an Hash of just Ruby basic types.
Such Hash can then easily be serialized using either .to_json or JSON.dump(hash).
This is a very quick example, with an update from #Yuval Rimar for arrays of OpenStructs:
def openstruct_to_hash(object, hash = {})
case object
when OpenStruct then
object.each_pair do |key, value|
hash[key] = openstruct_to_hash(value)
end
hash
when Array then
object.map { |v| openstruct_to_hash(v) }
else object
end
end
openstruct_to_hash(OpenStruct.new(foo: 1, bar: OpenStruct.new(baz: 2)))
# => {:foo=>1, :bar=>{:baz=>2}}
Fixes to above solution to handle arrays
def open_struct_to_hash(object, hash = {})
object.each_pair do |key, value|
hash[key] = case value
when OpenStruct then open_struct_to_hash(value)
when Array then value.map { |v| open_struct_to_hash(v) }
else value
end
end
hash
end
Here's yet another approach, modified from lancegatlin's answer. Also adding the method to the OpenStruct class itself.
class OpenStruct
def deep_to_h
each_pair.map do |key, value|
[
key,
case value
when OpenStruct then value.deep_to_h
when Array then value.map {|el| el === OpenStruct ? el.deep_to_h : el}
else value
end
]
end.to_h
end
in initializers/open_struct.rb:
require 'ostruct'
# Because #table is a instance variable of OpenStruct and Object#as_json returns Hash of instance variables.
class OpenStruct
def as_json(options = nil)
#table.as_json(options)
end
end
Usage:
OpenStruct.new({ a: { b: 123 } }).as_json
# Result
{
"a" => {
"b" => 123
}
}
Edit:
This seems to do almost the same thing (notice the keys are symbols instead of strings)
OpenStruct.new({ a: { b: 123 } }).marshal_dump
# Result
{
:a => {
:b => 123
}
}
Same function that can accept arrays as an input too
def openstruct_to_hash(object, hash = {})
case object
when OpenStruct then
object.each_pair do |key, value|
hash[key] = openstruct_to_hash(value)
end
hash
when Array then
object.map { |v| openstruct_to_hash(v) }
else object
end
end
None of the above worked for me with a copy and paste. Adding this solution:
require 'json'
class OpenStruct
def deep_to_h
each_pair.map do |key, value|
[
key,
case value
when OpenStruct then value.deep_to_h
when Array then value.map {|el| el.class == OpenStruct ? el.deep_to_h : el}
else value
end
]
end.to_h
end
end
json=<<HERE
{
"string": "fooval",
"string_array": [
"arrayval"
],
"int": 2,
"hash_array": [
{
"string": "barval",
"string2": "bazval"
},
{
"string": "barval2",
"string2": "bazval2"
}
]
}
HERE
os = JSON.parse(json, object_class: OpenStruct)
puts JSON.pretty_generate os.to_h
puts JSON.pretty_generate os.deep_to_h

Capitalize Second Word in a String (ruby)

I'm trying to create a method to capitalize the second word in a string. The code below works, but I was wondering if there are other ways to do this:
def camelcase(string)
tmp = string.split
tmp[1].capitalize!
tmp.join('')
end
def camelcase(string)
string.gsub(/\s(\w)/) { |match| $1.capitalize }
end
camelcase("foo bar baz") #=> "fooBarBaz"
Or you might wanna take a look at the camelcasemethod that comes with ActiveSupport::Inflector (see: http://apidock.com/rails/String/camelize)
def camelcase(string)
string.sub(/\s.*/) { |s| s.delete(' ').capitalize}
end
puts camelcase("foo bar bas")
=> "fooBarbaz"
You could use tap which "Yields x to the block, and then returns x" according to the docs. In this case capitalize! modifies x in place before being returned to the method chain for further processing by join.
def camelcase(string)
string.split.tap { |words| words[1].capitalize! }.join
end
camelcase('foo bar baz')
=> "fooBarbaz"
Try this:
s = "foo bar"
s.sub(/\s(\w)/) { $1.capitalize } # => "fooBar"

Resources