I have a large struct, and after creating an object and setting some of the members to values, I want to be able to set all the members of the object to nil. I tried the following, which doesn't work for some reason:
My_struct = Struct.new(
:member1,
:member2,
:member3
) do
def reset
self.each {|x| x = nil }
end
end
myblock = My_struct.new
puts myblock.member1
myblock.member1 = "value"
puts myblock.member1
myblock.reset
puts myblock.member1
I expected that last puts to return "" but it returns "value". What have I done wrong? Is there a native method, other than destroying and recreating the object each time?
self.each is iterating over the values, not the members/keys of the struct. This should do what you want.
My_struct = Struct.new(
:member1,
:member2,
:member3
) do
def reset
self.members.each {|k| send("#{k}=", nil)}
end
end
myblock = My_struct.new
myblock.member1
#=> nil
myblock.member1 = "value"
#=> "value"
myblock.member1
#=> "value"
myblock.reset
myblock.member1
#=> nil
Problem
I have a large struct, and after creating an object and setting some of the members to values, I want to be able to set all the members of the object to nil.
Solution: Just Replace the Whole Struct Object
Unless you have a special need to keep the object ID the same, the easiest thing to do is simply to create a new Struct, because the documentation for Struct#new says:
Unset parameters default to nil.
For example:
My_struct = Struct.new :member1, :member2, :member3
m = My_struct.new 'foo', 'bar', 'baz'
#=> #<struct My_struct member1="foo", member2="bar", member3="baz">
m = My_struct.new
#=> #<struct My_struct member1=nil, member2=nil, member3=nil>
Obviously, m.object_id will differ every time you assign a new My_struct instance to m, but there are very few use cases where this would (or should) matter. It's certainly a lot easier and less prone to errors than trying to reassign individual members, but your mileage may vary.
My_struct = Struct.new(:member1, :member2, :member3) do
def reset
initialize
end
end
myblock = My_struct.new
myblock.member1 = "value"
p myblock.object_id # => 15379000
myblock.reset
p myblock.member1 # => nil
p myblock.object_id # => 15379000; same object
My_struct = Struct.new(
:member1,
:member2,
:member3
) do
def reset
members.each { |m| self[m] = nil }
end
end
m = My_struct.new #=> #<struct My_struct member1=nil, member2=nil, member3=nil>
m.member1 = "Billy-Bob"
m.member2 = "Girtie"
m.member3 = "Hector"
m.member1 #=> "Billy-Bob"
m.member2 #=> "Girtie"
m.member3 #=> "Hector"
m.reset
m.member1 #=> nil
m.member2 #=> nil
m.member3 #=> nil
I've tried with ruby 2.2.4. All fields a nil by default?
But I've found a way you could change fields you like
require 'pp'
MyStruct = Struct.new( :member, :user, :parent ) do
def initialize
self.members.each { | elem | self.send( "#{elem}=", 1 ) }
end
end
pp MyStruct.new
UPDATE: I had to changed the code. Comment below was right. Now it's working
Related
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}
I'm on Ruby 2.2.1 and have the following situation:
a = ... # some object
h = ... # some hash
p h.size #=> 1
p h.keys.first.hash == a.hash #=> true
p h.keys.first.eql?(a) #=> true
p h.has_key?(a) #=> false
How is this possible? I thought that the hashes matching and eql? returning true were the only conditions for keys to be considered equal.
Edit: Here's the full program. But please note that I'm not asking how to fix it—I know how. I'm asking why Ruby behaves that way! Because I'm confused as to why the API contracts of Hash count for nothing in this situation.
class A
attr_reader :x
def initialize(x)
#x = x
end
MY_HASH = { A.new(5) => 'foo' }
def ==(other)
#x == other.x
end
alias_method :eql?, :==
def hash
#x
end
end
a = A.new(5)
h = A::MY_HASH
p h.size #=> 1
p h.keys.first.hash == a.hash #=> true
p h.keys.first.eql?(a) #=> true
p h.has_key?(a) #=> false
At the time when you create MY_HASH the new hash function of A is not yet defined so MY_HASH will use the default one when creating an index of it's values. When you later define a new hash function it will change how the objects are hashed BUT NOT AUTOMATICALLY update the index in the already existing Hash MY_HASH.
You solve this my initializing MY_HASH after you have defined the new hash method for class A or by running MY_HASH.rehash
p h.has_key?(a) #=> false
A::MY_HASH.rehash
p h.has_key?(a) #=> true
I have a list of immutable value objects. The lookup class provides ways to iterate and query that data:
class Banker
Bank = Struct.new(:name, :bic, :codes)
attr_reader :banks
def initialize
#banks = [
Bank.new('Citibank', '1234567', ['1', '2']),
Bank.new('Wells Fargo', '7654321', ['4']), # etc.
]
end
def find_by_bic(bic)
banks.each do |bank|
return bank if bank.bic == bic
end
end
end
#banks is initialized every time Banker is used. What options are there to cache #banks so that it's reused across different instances of the Banker?
I don't think Struct buys you anything here. How about doing it like this?
Code
class Banker
#all_banks = {}
class << self
attr_reader :all_banks
end
attr_reader :banks
def initialize(banks)
#banks = banks.keys
banks.each { |k,v| self.class.all_banks[k] = v }
end
def find_by_bic(bic)
return nil unless #banks.include?(bic)
self.class.all_banks[bic]
end
end
Note self in self.class is needed to distinguish the class of self from the keyword class.
Example
b1 = Banker.new({ '1234567' => { name: 'Citibank', codes: ["1", "2"] },
'7654321' => { name: 'Wells Fargo', codes: ['4'] } })
b1.banks
#=> ["1234567", "7654321"]
Banker.all_banks
#=> {"1234567"=>{:name=>"Citibank", :codes=>["1", "2"]},
# "7654321"=>{:name=>"Wells Fargo", :codes=>["4"]}}
b1.find_by_bic '7654321'
#=> {:name=>"Wells Fargo", :codes=>["4"]}
b1.find_by_bic '1234567'
#=> {:name=>"Citibank", :codes=>["1", "2"]}
b1.find_by_bic '0000000'
#=> nil
b2 = Banker.new({ '6523155' => { name: 'Bank of America', codes: ["3"] },
'1234567' => { name: 'Citibank', codes: ["1", "2"] } })
b2.banks
#=> ["6523155", "1234567"]
Banker.all_banks
#=> {"1234567"=>{:name=>"Citibank", :codes=>["1", "2"]},
# "7654321"=>{:name=>"Wells Fargo", :codes=>["4"]},
# "6523155"=>{:name=>"Bank of America", :codes=>["3"]}}
b2.find_by_bic '6523155'
#=> {:name=>"Bank of America", :codes=>["3"]}
b2.find_by_bic '1234567'
#=> {:name=>"Citibank", :codes=>["1", "2"]}
b2.find_by_bic '7654321'
#=> nil
Alternatives
If you prefer you could instead add the class method:
def self.new(banks)
banks.each { |k,v| all_banks[k] = v }
super
end
and remove the first line in initialize.
Or, if you have a complete list of all banks, you could instead just make all_banks a constant:
ALL_BANKS = {"1234567"=>{:name=>"Citibank", :codes=>["1", "2"]},
"7654321"=>{:name=>"Wells Fargo", :codes=>["4"]},
"6523155"=>{:name=>"Bank of America", :codes=>["3"]}}
def find_by_bic(bic)
return nil unless #banks.include?(bic)
ALL_BANKS[bic]
end
and change initialize to:
def initialize(bics)
#banks = bics
end
where bics is an array of bic values.
To share immutable data between instances you can use frozen class variables: ##banks ||= [...].freeze
This question already has answers here:
Is it possible to have class.property = x return something other than x?
(3 answers)
Closed 8 years ago.
I want to iterate an array of strings, and assign each of them to a fresh instance of class User, and I expect that I will got an array of User objects:
class User
def name=(name)
#name = name
self
end
end
original_array = ["aaa", "bbb", "bbb"]
result = original_array.collect { |str| User.new.name = str }
but the result is an array of strings!
puts result.inspect # => ["aaa", "bbb", "bbb"]
puts result === original_array # => true
I have no idea of where I went wrong?
What's wrong here is that User.new.name = str returns str, so the value of str gets collected.
Why does it return str? Because, opposed to any other Ruby method, every Ruby setter method returns the passed value, regardless the returned value in the method. For more infos about this behaviour you can check this other SO answer.
Below a IRB-ready Proof of Concept:
def name=(name)
#name = 'another value'
end
returned_value = (self.name = 'a value')
returned_value #=> 'a value'
#name #=> 'another value'
What you want can be done in this ways:
This syntax is valid for any Ruby object, as it uses Object#tap:
User.new.tap { |v| v.name = str }
If User is an ActiveRecord model, as I guess, you can use one of these slightly shorter syntaxes:
User.new name: str
User.new { |v| v.name = str }
I have a few arrays of Ruby objects of class UserInfo:
class UserInfo
attr_accessor :name, :title, :age
end
How can I merge these arrays into one array? A user is identified by its name, so I want no duplicate names. If name, title, age, etc. are equal I'd like to have 1 entry in the new array. If names are the same, but any of the other details differ I probably want those 2 users in a different array to manually fix the errors.
Thanks in advance
Redefine equality comparison on your object, and you can get rid of actual duplicates quickly with Array#uniq
class UserInfo
attr_accessor :name, :title, :age
def == other
name==other.name and title==other.title and age==other.age
end
end
# assuming a and b are arrays of UserInfo objects
c = a | b
# c will only contain one of each UserInfo
Then you can sort by name and look for name-only duplicates
d = c.sort{ |p,q| p.name <=> q.name } #sort by name
name = ""
e = []
d.each do |item|
if item.name == name
e[-1] = [e[-1],item].flatten
else
e << item
end
end
A year ago I monkey patched a kind of cryptic instance_variables_compare on Object. I guess you could use that.
class Object
def instance_variables_compare(o)
Hash[*self.instance_variables.map {|v|
self.instance_variable_get(v)!=o.instance_variable_get(v) ?
[v,o.instance_variable_get(v)] : []}.flatten]
end
end
A cheesy example
require 'Date'
class Cheese
attr_accessor :name, :weight, :expire_date
def initialize(name, weight, expire_date)
#name, #weight, #expire_date = name, weight, expire_date
end
end
stilton=Cheese.new('Stilton', 250, Date.parse("2010-12-02"))
gorgonzola=Cheese.new('Gorgonzola', 250, Date.parse("2010-12-17"))
irb is my weapon of choice
>> stilton.instance_variables_compare(gorgonzola)
=> {"#name"=>"Gorgonzola", "#expire_date"=>#<Date: 4910305/2,0,2299161>}
>> gorgonzola.instance_variables_compare(stilton)
=> {"#name"=>"Stilton", "#expire_date"=>#<Date: 4910275/2,0,2299161>}
>> stilton.expire_date=gorgonzola.expire_date
=> #<Date: 4910305/2,0,2299161>
>> stilton.instance_variables_compare(gorgonzola)
=> {"#name"=>"Gorgonzola"}
>> stilton.instance_variables_compare(stilton)
=> {}
As you can see the instance_variables_compare returns an empty Hash if the two objects has the same content.
An array of cheese
stilton2=Cheese.new('Stilton', 210, Date.parse("2010-12-02"))
gorgonzola2=Cheese.new('Gorgonzola', 250, Date.parse("2010-12-17"))
arr=[]<<stilton<<stilton2<<gorgonzola<<gorgonzola2
One hash without problems and one with
h={}
problems=Hash.new([])
arr.each {|c|
if h.has_key?(c.name)
if problems.has_key?(c.name)
problems[c.name]=problems[c.name]<<c
elsif h[c.name].instance_variables_compare(c) != {}
problems[c.name]=problems[c.name]<<c<<h[c.name]
h.delete(c.name)
end
else
h[c.name]=c
end
}
Now the Hash h contains the objects without merging problems and the problems hash contains those that has instance variables that differs.
>> h
=> {"Gorgonzola"=>#<Cheese:0xb375e8 #name="Gorgonzola", #weight=250, #expire_date=#<Date: 2010-12-17 (4911095/2,0,2299161)>>}
>> problems
=> {"Stilton"=>[#<Cheese:0xf54c30 #name="Stilton", #weight=210, #expire_date=#<Date: 2010-12-02 (4911065/2,0,2299161)>>, #<Cheese:0xfdeca8 #name="Stilton", #weight=250,#expire_date=#<Date: 2010-12-02 (4911065/2,0,2299161)>>]}
As far as I can see you will not have to modify this code at all to support an array of UserInfo objects.
It would most probably be much faster to compare the properties directly or with a override of ==. This is how you override ==
def ==(other)
return self.weight == other.weight && self.expire_date == other.expire_date
end
and the loop changes into this
arr.each {|c|
if h.has_key?(c.name)
if problems.has_key?(c.name)
problems[c.name]=problems[c.name]<<c
elsif h[c.name] != c
problems[c.name]=problems[c.name]<<c<<h[c.name]
h.delete(c.name)
end
else
h[c.name]=c
end
}
Finally you might want to convert the Hash back to an Array
result = h.values
Here's another potential way. If you have a way of identifying each UserInfo, say a to_str method that prints out the values:
def to_str()
return "#{#name}:#{#title}:#{#age}"
end
You can use inject and a hash
all_users = a + b # collection of users to "merge"
res = all_users.inject({})do |h,v|
h[v.to_str] = v #save the value indexed on the string output
h # return h for the next iteration
end
merged = res.values #the unique users