So in Ruby one can mass assign variables like this:
a, b, c = [1, 2, 3]
But what if I wanted to do the same for object methods, but without having to write out the whole thing like so:
foo.a, foo.b, foo.c = [1, 2, 3]
Is there a DRY way to accomplish this?
I am not sure you’ll like it, but the DRYest way I can think of is:
[:a, :b, :c].zip([1, 2, 3]).each { |k, v| foo.public_send "#{k}=", v }
# or vice versa
[1, 2, 3].zip([:a, :b, :c]).each { |v, k| foo.public_send "#{k}=", v }
Or, in more OO way:
class Foo
attr_accessor :a, :b, :c
def massive_assign attrs, values
attrs.zip(values).each { |k, v| public_send "#{k}=", v }
end
end
foo = Foo.new
foo.massive_assign([:a, :b, :c], [1, 2, 3])
Mass assignment to instance variables can be done like below as well:
foo.instance_eval { #a, #b, #c = [1, 2, 3] }
There's no specific syntax, but you could implement a setter for multiple attributes that are passed as a hash. Rails uses a similar approach:
class Foo
attr_accessor :a, :b, :c
def attributes=(attrs)
attrs.each do |name, value|
public_send("#{name}=", value)
end
end
end
foo = Foo.new
#=> #<Foo:0x007fc6d8a1e950>
foo.attributes = { a: 1, b: 2, c: 3 }
#=> #<Foo:0x007fc6d8a1e950 #a=1, #b=2, #c=3>
From the comments up there I assume that the poster is meaning a case where it's not actually foo but a more complex expression; and not only 3 assignments (method calls) but many.
There are 2 techniques that come to mind: tap and send:
some.complex[expression].which.evaluates.to.the.receiving.object.tap do |obj|
value_hash.each_pair do |key,value|
obj.send("#{key}=", value)
end
end
Hope that helps.
Of course, the usual security caveats apply (better make sure value_hash only contains valid/sane/allowed names).
Related
I have this code:
require 'json'
class A
attr_accessor :a, :b, :c
def initialize(a, b, c)
#a = a, #b = b, #c = c
end
end
a = A.new(1, "a", [1, 2, 3])
p a
puts "\n\nJSON: "
puts a.to_json
puts "\n\nJSON.pretty_generate: "
puts JSON.pretty_generate(a)
Output is really dissapointing:
#<A:0x000000019f4678 #b="a", #c=[1, 2, 3], #a=[1, "a", [1, 2, 3]]>
JSON:
"#<A:0x000000019f4678>"
JSON pretty generate:
blahblah.rb:285:in `generate': only generation of JSON objects
or arrays allowed (JSON::GeneratorError)
What's the difference between instance of class and JSON object?
How to convert instance of class to JSON object?
How to convert instance of class to JSON object?
Ruby's JSON implementation can serialize/deserialize custom objects, but you have to provide the details, i.e.
a to_json instance method that returns a JSON representation from your object and
a json_create class method that creates an object from its JSON representation
Here's an example using your class:
require 'json'
class A
attr_accessor :a, :b, :c
def initialize(a, b, c)
#a, #b, #c = a, b, c
end
def to_json(*args)
{
JSON.create_id => self.class.name,
'a' => a,
'b' => b,
'c' => c
}.to_json(*args)
end
def self.json_create(h)
new(h['a'], h['b'], h['c'])
end
end
a = A.new(1, 'a', [1, 2, 3])
#=> #<A:0x007f92cc8f37f0 #a=1, #b="a", #c=[1, 2, 3]>
a.to_json
#=> "{\"json_class\":\"A\",\"a\":1,\"b\":\"a\",\"c\":[1,2,3]}"
JSON.create_id defaults to the string "json_class". It's a special identifier that is recognizes by parse if you pass create_additions: true:
JSON.parse(a.to_json, create_additions: true)
#<A:0x007ff59c0f2578 #a=1, #b="a", #c=[1, 2, 3]>
Without this option, the parser doesn't invoke A.json_create and returns a plain hash instead:
JSON.parse(a.to_json)
#=> {"json_class"=>"A", "a"=>1, "b"=>"a", "c"=>[1, 2, 3]}
Calling JSON.pretty_generate(a) generates the following output:
{
"json_class": "A",
"a": 1,
"b": "a",
"c": [
1,
2,
3
]
}
One thing is a Ruby class:
From Wikipedia:
In object-oriented programming, a class is an extensible
program-code-template for creating objects, providing initial values
for state (member variables) and implementations of behavior (member
functions or methods).
and the other is a JSON object:
JSON: JavaScript Object Notation.
JSON is a syntax for storing and exchanging data.
JSON is an easier-to-use alternative to XML.
This is a well-known problem when serving data from the server to a web browser. You send a JSON representation of the data, normally for working in the server you parse this data to a Ruby object.
The error:
http://apidock.com/ruby/JSON/pretty_generate
You can only parse simple objects to JSON, you cannot parse a String. In order to this you can add the to_json method inside class A:
irb(main):046:0> JSON.pretty_generate("calimero")
JSON::GeneratorError: only generation of JSON objects or arrays allowed
from /Users/toni/.rvm/gems/ruby-2.2.3#stackoverflow/gems/json-1.8.3/lib/json/common.rb:285:in `generate'
from /Users/toni/.rvm/gems/ruby-2.2.3#stackoverflow/gems/json-1.8.3/lib/json/common.rb:285:in `pretty_generate'
from (irb):59
from /Users/toni/.rvm/rubies/ruby-2.2.3/bin/irb:11:in `<main>'
irb(main):060:0> JSON.pretty_generate([1,2,3])
=> "[\n 1,\n 2,\n 3\n]"
I recommend using the Virtus gem. It clarifies the type of the object are you going to parse to JSON, which for me is a good practice knowing what are you converting to JSON and what data are you expecting:
require 'virtus'
require 'json'
class A
include Virtus.model
attr_accessor :a, :b, :c
attribute :a, Integer
attribute :b, String
attribute :c, Array[Integer]
def initialize(a, b, c)
#a = a, #b = b, #c = c
end
end
irb(main):039:0> a = A.new(1, "a", [1, 2, 3])
=> #<A:0x007fc391882ac8 #b="a", #c=[1, 2, 3], #a=[1, "a", [1, 2, 3]]>
irb(main):040:0> a.attributes
=> {:a=>[1, "a", [1, 2, 3]], :b=>"a", :c=>[1, 2, 3]}
irb(main):053:0> require 'json'
=> true
irb(main):085:0> a.attributes.to_json
=> "{\"a\":[1,\"a\",[1,2,3]],\"b\":\"a\",\"c\":[1,2,3]}"
irb(main):096:0>
I would look at serializing the data instead, and make your class works so it can respond with the attributes rather easily.
require 'json'
class A
ATTRS = [:a, :b, :c]
attr_accessor *ATTRS
def initialize(params)
params.each do |k, v|
self.send("#{k}=".to_sym, v)
end
end
def attributes
values = {}
ATTRS.each do |key|
values[key] = self.send(key)
end
values
end
end
a = A.new(a: 1, b: "a", c: [1, 2, 3])
p a
data = a.attributes.to_json
puts data
b = A.new(JSON.parse(data))
p b
To explain this a bit, I'm using send to call the writable method for each attribute in the constructor, this goes through the params and sets the instance variable values.
The attributes method then uses the ATTRS constant array to build out an object map of just the properties of this instance. So we serialize that, and de-serialize to make a new instance.
This is pretty much how rails does it, though ActiveRecord uses an attributes hash for storing the properties over traditional accessors.
I need to implement the callback on #each. The receiver of each might be both Array and Hash. So, I have a code like:
receiver.each do |k, v|
case receiver
when Hash then puts "#{k} = #{v}"
when Array then puts "[#{k}, #{v}]"
end
end
The check for receiver is lame, though. Is there a way to receive/interprete a codeblock argument[s] to clearly distinguish the following cases:
{ a: 1, b: 2 }
versus
[[:a, 1], [:b, 2]]
I tried parenthesis, single argument, splatted argument. Everything just gets an Array of size 2. Am I doomed to stick with explicit type check?
The best you can do is to get the type check out of the loop:
def foo(receiver)
format = receiver.is_a?(Array) ? "[%s, %s]" : "%s = %s"
receiver.each do |k_v|
puts format % k_v
end
end
See String#%
If you want to tell, within the each block, whether the key/value pair came from an array or a hash, you have to resort to monkey patching. It can be done, but it's pretty awful:
class Hash
orig_each = instance_method(:each)
define_method(:each) do |&block|
orig_each.bind(self).call do |kv|
def kv.from_hash?
true
end
def kv.from_array?
false
end
block.call(kv)
end
end
end
class Array
orig_each = instance_method(:each)
define_method(:each) do |&block|
orig_each.bind(self).call do |kv|
def kv.from_hash?
false
end
def kv.from_array?
true
end
block.call(kv)
end
end
end
in use:
e = {a: 1, b: 2}
e.each do |kv|
p [kv.from_hash?, kv.from_array?, kv]
end
# => [true, false, [:a, 1]]
# => [true, false, [:b, 2]]
e = [[:a, 1], [:b, 2]]
e.each do |kv|
p [kv.from_hash?, kv.from_array?, kv]
end
# => [false, true, [:a, 1]]
# => [false, true, [:b, 2]]
Given a nested array or hash as the receiver and some object as the argument, what is the best way to return the path to an occurrence of the object if the receiver includes the object, or nil otherwise? I define path as an array of array indices or hash keys that leads to the object. The argument object will never be any of the hash keys, and will never appear more than once. For example, I expect:
[
:a,
[:b, :c, {:d => :foo}],
:e,
]
.path_to(:foo) # => [1, 2, :d]
{
:a => [3, "foo"],
:b => 5,
:c => 2,
}
.path_to(3) # => [:a, 0]
When there is no occurrence, return nil:
[:foo, "hello", 3]
.path_to(:bar) => nil
If no one comes up with a reasonable answer, then I will post my own answer shortly.
Here you are my own recursive solution. I am sure that it could be improved but it is a good start and works exactly as requested.
# path.rb
module Patheable
def path_to item_to_find
path = []
find_path(self, item_to_find, path)
result = path.empty? ? nil : path
result.tap { |r| puts r.inspect } # just for testing
end
private
def find_path(current_item, item_to_find, result)
if current_item.is_a?(Array)
current_item.each_with_index do |value, index|
find_path(value, item_to_find, result.push(index))
end
elsif current_item.is_a?(Hash)
current_item.each do |key, value|
find_path(value, item_to_find, result.push(key))
end
else
result.pop unless current_item == item_to_find
end
end
end
class Array
include Patheable
end
class Hash
include Patheable
end
[
:a,
[:b, :c, {:d => :foo}],
:e,
].path_to(:foo) # => [1, 2, :d]
{
:a => [3, "foo"],
:b => 5,
:c => 2,
}.path_to(3) # => [:a, 0]
[:foo, "hello", 3].path_to(:bar) # => nil
#end path.rb
# example of use
$ ruby path.rb
[1, 2, :d]
[:a, 0]
nil
Nothing like a bit of recursion.
require 'minitest/autorun'
class Array
def path_to(obj)
# optimize this
Hash[self.each.with_index.to_a.map {|k,v| [v,k]}].path_to(obj)
end
end
class Hash
def path_to(obj)
inverted = self.invert
if inverted[obj]
[inverted[obj]]
else
self.map {|k, v|
if v.respond_to?(:path_to)
if res = v.path_to(obj)
[k] + res
end
end
}.find {|path|
path and path[-1] != nil
}
end
end
end
describe "path_to" do
it "should work with really simple arrays" do
[:a, :e,].path_to(:a).must_equal [0]
end
it "should work with simple arrays" do
[:a, [:b, :c], :e,].path_to(:c).must_equal [1, 1]
end
it "should work with arrays" do
[:a, [:b, :c, {:d => :foo}], :e,].path_to(:foo).must_equal [1, 2, :d]
end
it "should work with simple hashes" do
{:d => :foo}.path_to(:foo).must_equal [:d]
end
it "should work with hashes" do
({:a => [3, "foo"], :b => 5, :c => 2,}.path_to(3).must_equal [:a, 0])
end
end
This is the answer that I came up with.
class Object
def path_to obj; end
end
class Array
def path_to obj
if i = index(obj) then return [i] end
a = nil
_, i = to_enum.with_index.find{|e, _| a = e.path_to(obj)}
a.unshift(i) if i
end
end
class Hash
def path_to obj
if value?(obj) then return [key(obj)] end
a = nil
kv = find{|_, e| a = e.path_to(obj)}
a.unshift(kv.first) if kv
end
end
For example, I have array of single hashes
a = [{a: :b}, {c: :d}]
What is best way to convert it into this?
{a: :b, c: :d}
You may use
a.reduce Hash.new, :merge
which directly yields
{:a=>:b, :c=>:d}
Note that in case of collisions the order is important. Latter hashes override previous mappings, see e.g.:
[{a: :b}, {c: :d}, {e: :f, a: :g}].reduce Hash.new, :merge # {:a=>:g, :c=>:d, :e=>:f}
You can use .inject:
a.inject(:merge)
#=> {:a=>:b, :c=>:d}
Demonstration
Which initiates a new hash on each iteration from the two merged. To avoid this, you can use destructive :merge!( or :update, which is the same):
a.inject(:merge!)
#=> {:a=>:b, :c=>:d}
Demonstration
These two are equivalent (reduce/inject are the same method):
total_hash = hs.reduce({}) { |acc_hash, hash| acc_hash.merge(hash) }
total_hash = hs.reduce({}, :merge)
Note that Hash#merge creates a new hash on each iteration, which may be a problem if you are building a big one. In that case, use update instead:
total_hash = hs.reduce({}, :update)
Alternatively, you can convert the hashes to pairs and then build the final hash:
total_hash = hs.flat_map(&:to_a).to_h
I came across this answer and I wanted to compare the two options in terms of performance to see which one is better:
a.reduce Hash.new, :merge
a.inject(:merge)
using the ruby benchmark module, it turns out that option (2) a.inject(:merge) is faster.
code used for comparison:
require 'benchmark'
input = [{b: "c"}, {e: "f"}, {h: "i"}, {k: "l"}]
n = 50_000
Benchmark.bm do |benchmark|
benchmark.report("reduce") do
n.times do
input.reduce Hash.new, :merge
end
end
benchmark.report("inject") do
n.times do
input.inject(:merge)
end
end
end
the results were
user system total real
reduce 0.125098 0.003690 0.128788 ( 0.129617)
inject 0.078262 0.001439 0.079701 ( 0.080383)
Just use
a.reduce(:merge)
#=> {:a=>:b, :c=>:d}
Try this
a.inject({}){|acc, hash| acc.merge(hash)} #=> {:a=>:b, :c=>:d}
You can transform it to array [[:a, :b]] and after that translate everything to hash {:a=>:b}
# it works like [[:a, :b]].to_h => {:a=>:b}
[{a: :b}, {c: :d}].map { |hash| hash.to_a.flatten }.to_h
# => {:a=>:b, :c=>:d}
... or alternatively an Array which prevents duplicate entries.
Is there some kind of object in Ruby which:
responds to [], []= and <<
silently drops duplicate entries
is Enumerable (or at least supports find_all)
preserves the order in which entries were inserted
?
As far as I can tell, an Array supports points 1, 3 and 4; while a Set supports 1, 2 and 3 (but not 4). And a SortedSet won't do, because my entries don't implement <=>.
As of Ruby 1.9, the built-in Hash object preserves insertion order. For example:
h = {}
h[:z] = 1
h[:b] = 2
h[:a] = 3
h[:x] = 0
p h.keys #=> [:z, :b, :a, :x]
h.delete :b
p h.keys #=> [:z, :a, :x]
h[:b] = 1
p h.keys #=> [:z, :a, :x, :b]
So, you can set any value (like a simple true) for any key and you now have an ordered set. You can test for a key using either h.key?(obj) or, if you always set each key to have a truthy value, just h[obj]. To remove a key, use h.delete(obj). To convert the ordered set to an array, use h.keys.
Because the Ruby 1.9 Set library happens to be built upon a Hash currently, you can currently use Set as an ordered set. (For example, the to_a method's implementation is just #hash.keys.) Note, however, that this behavior is not guaranteed by that library, and might change in the future.
require 'set'
s = Set[ :f, :o, :o, :b, :a, :r ] #=> #<Set: {:f, :o, :b, :a, :r}>
s << :z #=> #<Set: {:f, :o, :b, :a, :r, :z}>
s.delete :o #=> #<Set: {:f, :b, :a, :r, :z}>
s << :o #=> #<Set: {:f, :b, :a, :r, :z, :o}>
s << :o #=> #<Set: {:f, :b, :a, :r, :z, :o}>
s << :f #=> #<Set: {:f, :b, :a, :r, :z, :o}>
s.to_a #=> [:f, :b, :a, :r, :z, :o]
There isn't one as far as I know, and Set by its mathematical nature is meant to be unordered (or at least, implementationally, meant not to guarantee order - in fact its usually implemented as a hash table so it does mess up order).
However, it's not hard to either extend array directly or subclass it to do this. I just tried it out and this works:
class UniqueArray < Array
def initialize(*args)
if args.size == 1 and args[0].is_a? Array then
super(args[0].uniq)
else
super(*args)
end
end
def insert(i, v)
super(i, v) unless include?(v)
end
def <<(v)
super(v) unless include?(v)
end
def []=(*args)
# note: could just call super(*args) then uniq!, but this is faster
# there are three different versions of this call:
# 1. start, length, value
# 2. index, value
# 3. range, value
# We just need to get the value
v = case args.size
when 3 then args[2]
when 2 then args[1]
else nil
end
super(*args) if v.nil? or not include?(v)
end
end
Seems to cover all the bases. I used OReilly's handy Ruby Cookbook as a reference - they have a recipe for "Making sure a sorted array stays sorted" which is similar.
I like this solution although it requires active_support's OrderedHash
require 'active_support/ordered_hash'
class OrderedSet < Set
def initialize enum = nil, &block
#hash = ActiveSupport::OrderedHash.new
super
end
end
=)
You could use a Hash to store the values, and have an incrementing value stored in the value of each Hash pair. Then you can access the set in a sorted manner, albeit slowly, by accessing the objects via their values.
I'll try to add some code in here later to explain further.
I am aware accessing via values is much slower than by keys.
Update 1: In Ruby 1.9, Hash elements are iterated in their insertion order.
Not that I know of, but it wouldn't be hard to roll your own. Just subclass Array and use a Set to maintain your uniqueness constraint.
One question about silent dropping. How would this affect #[]=? If I was trying to overwrite an existing entry with something which was already stored elsewhere, should it remove the would-be-removed element anyway? I think either way could provide nasty surprises down the road.