What are :+ and &:+ in Ruby? - ruby

I've seen these several times but I can't figure out how to use them. The pickaxe says that these are special shortcuts but I wasn't able to find the syntactical description.
I've seen them in such contexts:
[1,2,3].inject(:+)
to calculate sum for example.

Let's start with an easier example.
Say we have an array of strings we want to have in caps:
['foo', 'bar', 'blah'].map { |e| e.upcase }
# => ['FOO', 'BAR', 'BLAH']
Also, you can create so called Proc objects (closures):
block = proc { |e| e.upcase }
block.call("foo") # => "FOO"
You can pass such a proc to a method with the & syntax:
block = proc { |e| e.upcase }
['foo', 'bar', 'blah'].map(&block)
# => ['FOO', 'BAR', 'BLAH']
What this does, is call to_proc on block and then calls that for every block:
some_object = Object.new
def some_object.to_proc
proc { |e| e.upcase }
end
['foo', 'bar', 'blah'].map(&some_object)
# => ['FOO', 'BAR', 'BLAH']
Now, Rails first added the to_proc method to Symbol, which later has been added to the ruby core library:
:whatever.to_proc # => proc { |e| e.whatever }
Therefore you can do this:
['foo', 'bar', 'blah'].map(&:upcase)
# => ['FOO', 'BAR', 'BLAH']
Also, Symbol#to_proc is even smarter, as it actually does the following:
:whatever.to_proc # => proc { |obj, *args| obj.send(:whatever, *args) }
This means that
[1, 2, 3].inject(&:+)
equals
[1, 2, 3].inject { |a, b| a + b }

inject accepts a symbol as a parameter, this symbol must be the name of a method or operator, which is this case is :+
so [1,2,3].inject(:+) is passing each value to the method specified by the symbol, hence summing all elements in the array.
source: https://ruby-doc.org/core-2.5.1/Enumerable.html

Related

Sorting/accessing through an array of nested hashes

I have an array of hashes with names and ages:
array = [ {"bill" => 12}, {"tom" => 13}, {"pat" => 14} ]
I realize that, by calling the first method, this happens:
array.first # => {"bill" => 12}
Without defining a class, I'd like to do:
array.first.name # => "bill"
How can I do that?
Doing:
def name
array[0].keys
end
will define a private method, which can't be called on the receiver.
You have:
h = array.first #=> {"bill" => 12}
h.class #=> Hash
so if you want to create a method first, such that:
h.name #=> "bill"
you must define name on the class of its receiver (h), which is Hash. #shivam has shown you how to that, but (as #AndrewMarshall points out in a comment) that it pollutes the class Hash. A better way is to use Refinements.
Refinements was an experimental addition to v2.0, then modified and made permanent in v2.1. It provides a way to avoid "monkey-patching" by providing "a way to extend a class locally".
We can do that as follows. First, refine Hash within a module M:
module M
refine Hash do
def name
keys.first
end
end
end
The refinement has no effect until the keyword using is invoked on the module:
h = {"bill" => 12}
puts h.name
#-> undefined method `name' for {"bill"=>12}:Hash (NoMethodError)
After activating the refinement we can invoke name on h:
using M
h.name
#-> bill
The refinement applies to the remainder of the file, but does not apply to to code in other files. Suppose we were to add the following to the present file:
class A
def greeting(h)
puts "Hello, #{h.name}"
end
end
Then:
A.new.greeting({"bill" => 12})
#-> Hello, bill
Since your Array array consists of Hashes, when you do array.first a Hash is returned {"bill" => 12}.
You can now define a method Hash#name that can be applied to array.first
(a Hash)
Here:
class Hash
def name
self.keys.first
end
end
array.first.name
# => "bill"
#to print all names
array.each{|a| puts a.name}
# bill
# tom
# pat
# or collect them in an Array
array.map(&:name)
# => ["bill", "tom", "pat"]

Recursively dump Ruby object to json

I have a hash that I need to dump to json, but one of its values is an object:
hash = { a: my_object }
If I try to dump this with something like MultiJson.dump(hash), it will only serialize the top-level hash, and not any deeper, so what I end up with is
'{"a": #<...>}'
This happens even if the object has to_json, to_hash, etc. methods already, and MultiJson.dump(my_object) works just fine.
This seems like something JSON libraries should do, but I guess they don't. Why not? Is there a programmatic reason they shouldn't/cant? Or am I just missing something?
Edit:
A bit more searching around -- I must have messed up something in my initial tests. Oj can dump json recursively with the :compat option:
Oj.dump(my_object, mode: :compat)
MultiJson using Oj passes this option in by default, so that will work too. Although, according to Oj documentation, I'm not sure why this works. It works even without any as_hash or to_json methods.
Generally I do one of two things:
Load JSON's core extensions:
require 'json/add/core'
I don't remember why "add/core" helped over the normal require 'json' but it did.
Or I add a to_hash or to_array method to custom classes and break them down into a Hash or Array that can then be passed off to the JSON class to serialize. Or I add a to_json method and create a Hash or Array and then to_json it, returning the serialized version:
class Foo
def initialize
#bar = 1
#baz = [1, 2, 3]
#hub = {'a' => 0, 'b' => 2}
end
def to_h
{
'bar' => #bar,
'baz' => #baz,
'hub' => #hub
}
end
def to_json
to_h.to_json
end
end
require 'json'
Foo.new.to_json # => "{\"bar\":1,\"baz\":[1,2,3],\"hub\":{\"a\":0,\"b\":2}}"
Add code to do a from_json to a String and you can send and receive your object as JSON. Or add that capability to your initialize method so if it sees a String for a parameter it tries to convert that string back to a Ruby object and then populate the values:
class Foo
def initialize(str = nil)
if str && String === str
obj = JSON[str]
#bar, #baz, #hub = obj.values_at('bar', 'baz', 'hub')
else
#bar = 1
#baz = [1, 2, 3]
#hub = {'a' => 0, 'b' => 2}
end
end
def to_h
{
'bar' => #bar,
'baz' => #baz,
'hub' => #hub
}
end
def to_json
to_h.to_json
end
end
require 'json'
json_stream = Foo.new.to_json # !> assigned but unused variable - json_stream
new_foo = Foo.new('{"bar":3,"baz":4,"hub":{"x":8,"y":9}}')
# => #<Foo:0x007f9ed4054d60 #bar=3, #baz=4, #hub={"x"=>8, "y"=>9}>
As long as JSON knows how to unravel a particular object, it'll do a to_json on it without trouble. For those types that it doesn't know, it's easy to give it a little help.

using a string or key-val pair as a method argument

Is there a better way to write this? basically I want to add an argument to a hash. if the argument is a key-val pair, then id like to add it as is. if the argument is a string i'd like to add it as a key with a nil value. the below code works, but is there a more appropriate (simple) way?
2nd question, does calling an each method on an array with two arguments |key, val| automatically convert an array to a hash as it appears to?
#some_hash = {}
def some_method(input)
if input.is_a? Hash
input.each {|key, val| #some_hash[key] = val}
else
input.split(" ").each {|key, val| #some_hash[key] = val}
end
end
some_method("key" => "val")
This gives the result as instructed in the question, but it works differently from the code OP gave (which means that the OP's code does not work as it says):
#some_hash = {}
def some_method(input)
case input
when Hash then #some_hash.merge!(input)
when String then #some_hash[input] = nil
end
end
some_method("foo" => "bar")
some_method("baz")
#some_hash # => {"foo" => "bar", "baz" => nil}
Second question
An array is never automatically converted to a hash. What you are probably mentioning is the fact that the elements of an array within an array [[:foo, :bar]] can be referred to separately in:
[[:foo, :bar]].each{|f, b| puts f; puts b}
# => foo
# => bar
That is due to destructive assignment. When necessary, Ruby takes out the elements of an array as separate things and tries to adjust the number of variables. It is the same as:
f, b = [:foo, :bar]
f # => :foo
b # => :bar
Here, you don't get f # => [:foo, :bar] and b # => nil.

Accessing Ruby Array as my_array.new_field = "abc"

I need to create a new empty object and access its attributes like my_object.title = 'abc' as opposed to my_object[:title] = 'abc'. How could I do this?
my_object = Array.new
my_object.title = "abc"
# => undefined method `title=' for []:Array
An Array in Ruby is not like an Array in JavaScript - you can't reference elements by name, but only by index. What you probably want is a Hash.
my_object = {}
my_object['title'] = "abc"
Or you can set it at initialization:
my_object = {'title' => 'abc'}
Alternatively, you can use OpenStruct to assign using dynamic attribute setters as you are doing:
my_object = OpenStruct.new
my_object.title = "abc"
It depends on what properties you want the object to have. You gave the example
my_object = Array.new
my_object.title = "abc"
If you want your objects to effectively be arrays, having access to all the methods available to arrays, but in addition you want to add additional properties, the way to do that is to create a subclass of Array:
class MyArray < Array
attr_accessor :title
def initialize(*args)
super
end
end
MyArray.ancestors # => [MyArray, Array, Enumerable, Object, Kernel, BasicObject]
a = MyArray.new(3,2) # => [2,2,2]
b = MyArray.new # => []
b << 4 << 5 << 6 # => [4,5,6]
e = a+b # => [2, 2, 2, 4, 5, 6]
e.class # => Array
a.title = "This is array a"
puts a.title # => "This is array a"
e.title = "me, e" # => NoMethodError: undefined method `title='
b.class # => MyArray
b.is_a? Array # => true
c = [7,8,9] # => [7, 8, 9]
c.is_a? MyArray # => false
d = a+c # => [2, 2, 2, 7, 8, 9]
super, in initialize, is what gives your class instances the properties of an array. When super is invoked within any method, it invokes the parent class method of the same name. So here it calls Array#initialize. Moreover, super passes along all the parameters its method received; that is, you don't need to write
super args
You can do this with most most Ruby objects (e.g., hashes and strings), but there are some exceptions. In particular, you cannot subclass Fixnum or Symbol.

Recursively convert all numeric strings to integers in a Ruby hash

I have a hash of a random size, which may have values like "100", which I would like to convert to integers. I know I can do this using value.to_i if value.to_i.to_s == value, but I'm not sure how would I do that recursively in my hash, considering that a value can be either a string, or an array (of hashes or of strings), or another hash.
This is a pretty straightforward recursive implementation (though having to handle both arrays and hashes adds a little trickiness).
def fixnumify obj
if obj.respond_to? :to_i
# If we can cast it to a Fixnum, do it.
obj.to_i
elsif obj.is_a? Array
# If it's an Array, use Enumerable#map to recursively call this method
# on each item.
obj.map {|item| fixnumify item }
elsif obj.is_a? Hash
# If it's a Hash, recursively call this method on each value.
obj.merge( obj ) {|k, val| fixnumify val }
else
# If for some reason we run into something else, just return
# it unmodified; alternatively you could throw an exception.
obj
end
end
And, hey, it even works:
hsh = { :a => '1',
:b => '2',
:c => { :d => '3',
:e => [ 4, '5', { :f => '6' } ]
},
:g => 7,
:h => [],
:i => {}
}
fixnumify hsh
# => {:a=>1, :b=>2, :c=>{:d=>3, :e=>[4, 5, {:f=>6}]}, :g=>7, :h=>[], :i=>{}}
This is my helper class. It only converts Strings which are just numbers (Integer or Float).
module Helpers
class Number
class << self
def convert(object)
case object
when String
begin
numeric(object)
rescue StandardError
object
end
when Array
object.map { |i| convert i }
when Hash
object.merge(object) { |_k, v| convert v }
else
object
end
end # convert
private
def numeric(object)
Integer(object)
rescue
Float(object)
end # numeric
end # << self
end # Number
end # Helpers
Helpers::Number.convert [{a: ["1", "22sd"]}, 2, ['1.3', {b: "c"}]]
#=> [{:a=>[1, "22sd"]}, 2, [1.3, {:b=>"c"}]]

Resources