How can I set an object attribute dynamically in Ruby e.g.
def set_property(obj, prop_name, prop_value)
#need to do something like > obj.prop_name = prop_value
#we can use eval but I'll prefer a faster/cleaner alternative:
eval "obj.#{prop_name} = #{prop_value}"
end
Use send:
def set_property(obj, prop_name, prop_value)
obj.send("#{prop_name}=",prop_value)
end
Object#instance_variable_set() is what you are looking for, and is the cleaner version of what you wanted.
Example:
your_object = Object.new
your_object.instance_variable_set(:#attribute, 'value')
your_object
# => <Object:0x007fabda110408 #attribute="value">
Ruby documentation about Object#instance_variable_set
If circumstances allow for an instance method, the following is not overly offensive:
class P00t
attr_reader :w00t
def set_property(name, value)
prop_name = "##{name}".to_sym # you need the property name, prefixed with a '#', as a symbol
self.instance_variable_set(prop_name, value)
end
end
Usage:
p = P00t.new
p.set_property('w00t', 'jeremy')
This answer (https://stackoverflow.com/a/7466385/6094965) worked for me:
Object.send(attribute + '=', value)
attribute has to be a String. So if you are iterating trough an array of Symbols (like me), you can use to_s.
Object.send(attribute.to_s + '=', value)
Related
Using interpolated strings is it possible to call multiple #{} within each other?
For example I want to create a variable and add a number onto the end. I also want to attach a result from a column in site, but also increment the number as well. What the best method of doing the below code?
#site.each do |site|
1.upto(4) do |i|
eval("#var#{i} = #{site.site_#{i}_location}", b)
end
end
So #var1 = site.site_1_location, #var2 = site.site_2_location, etc.
Mocking #sites data:
#sites = [OpenStruct.new(
site_1_location: 'Japan',
site_2_location: 'India',
site_3_location: 'Chile',
site_4_location: 'Singapore'
)]
You can use instance_variable_set to set the instance variable
#sites.each do |site|
1.upto(4) do |i|
instance_variable_set("#var#{i}", site.send("site_#{i}_location"))
end
end
Now you can access the variables:
#var1 # returns "Japan"
#var2 # returns "India"
#var3 # returns "Chile"
#var4 # returns "Singapore"
Using #send might help
code = "#var#{i} = site.send(:site_#{i}_location)"
eval(code)
Yes of cause, nesting of #{} is possible.
Stupid example, but it shows the possibilties of nesting:
x = 1
y = 2
"abc_#{"#{x}_#{y if y > 1}"}"
# => 'abc_1_2'
Never the less, the solution for your code, suggested by Imram Ahmad (https://stackoverflow.com/a/66002619/14485176) is the better aproach to solve your problem!
I'm looking for a way to assign a model attribute from a string. This is within a concern shared across multiple models with different attributes, so I can't just hard code the attribute names.
I'm having no problem retrieving attributes using a string with 'send':
self.send("#{attribute_name}")
But I'm having difficulty assigning the attribute from a string. E.g
self."attribute_name" = "<user input>"
The dreaded eval function does what I need, but it is clearly terrible unsafe:
eval("self.#{attribute_name} = '<user input>'")
Is there a function I am missing that can achieve this without eval? Or is my approach all wrong and I should do back to the drawing board? Thanks.
Given the the attribute has a public setter method, this works:
class X
attr_accessor :foo
def dynamic_set(attr_name, value)
public_send("#{attr_name}=", value)
end
end
x = X.new
x.dynamic_set("foo", "bar")
p x.foo
# => "bar"
You should save them as instance variables.
To get:
instance_variable_get("##{attribute_name}")
To set:
instance_variable_set("##{attribute_name}", user_input)
Everyone knows two of the ways to create an empty array: Array.new and []. The first one is 'standard', you might say, and the second one is simply syntax sugar. Many different objects such as Hash and maybe even String are shorthanded through this method.
My question is: Is there a way to define my own delimimers for objects? An example would be <>. Maybe an alias like '<' => 'MyObject.new(' and '>' => ')'?
[] is an array literal, {} is a hash literal. There are plenty of these shorthand forms in Ruby. Check this wikibook out for more information.
There is no object literal, but you can use (source):
a = Struct.new(:foo,:bar).new(34,89)
a.foo # 34
a.bar # 89
No. (And ew anyway.) Delimiters are part of the parse process.
You can define operators, like <; that's different than a delimiter. For example, you could redefine < to take a block, and use that block to create a class, or a method, etc. But... I don't think I would.
You could do:
class MyObject; end
def [](*args)
MyObject.new *args
end
# but you can't use it directly:
o = [] #=> [] (empty Array)
# you must to refer to self:
o = self[] #=> #<MyObject:0x1234567>
# but since self depends on where are you, you must assign self to a global variable:
$s = self
o = $s[]
# or to a constant:
S = self
o = S[]
# however, in that case it's better to do it in the proper class:
class << MyObject
def [](*args)
new *args
end
end
# and assign it to a single-letter constant to reduce characters:
S = MyObject
# so
o = S[] #=> #<MyObject:0x1234568>
I can't think on something more compact.
There has to be a built in way of doing this, right?
class Object
def send_chain(arr)
o=self
arr.each{|a| o=o.send(a) }
return o
end
end
I just ran across this and it really begs for inject:
def send_chain(arr)
arr.inject(self) {|o, a| o.send(a) }
end
Building upon previous answers, in case you need to pass arguments to each method, you can use this:
def send_chain(arr)
Array(arr).inject(self) { |o, a| o.send(*a) }
end
You can then use the method like this:
arr = [:to_i, [:+, 4], :to_s, [:*, 3]]
'1'.send_chain(arr) # => "555"
This method accepts single arguments as well.
No, there isn't a built in way to do this. What you did is simple and concise enough, not to mention dangerous. Be careful when using it.
On another thought, this can be extended to accept arguments as well:
class Object
def send_chain(*args)
o=self
args.each do |arg|
case arg
when Symbol, String
o = o.send arg # send single symbol or string without arguments
when Array
o = o.send *arg # smash the inner array into method name + arguments
else
raise ArgumentError
end
end
return o
end
end
this would let you pass a method name with its arguments in an array, like;
test = MyObject.new
test.send_chain :a_method, [:a_method_with_args, an_argument, another_argument], :another_method
How about this versatile solution without polluting the Object class:
def chain_try(arr)
[arr].flatten.inject(self_or_instance, :try)
end
or
def chain_send(arr)
[arr].flatten.inject(self_or_instance, :send)
end
This way it can take a Symbol, a String or an Array with a mix of both even.🤔
example usage:
chain_send([:method1, 'method2', :method3])
chain_send(:one_method)
chain_send('one_method')
In Ruby, is it possible to pass by reference a parameter with value-type semantics (e.g. a Fixnum)?
I'm looking for something similar to C#'s 'ref' keyword.
Example:
def func(x)
x += 1
end
a = 5
func(a) #this should be something like func(ref a)
puts a #should read '6'
Btw. I know I could just use:
a = func(a)
You can accomplish this by explicitly passing in the current binding:
def func(x, bdg)
eval "#{x} += 1", bdg
end
a = 5
func(:a, binding)
puts a # => 6
Ruby doesn't support "pass by reference" at all. Everything is an object and the references to those objects are always passed by value. Actually, in your example you are passing a copy of the reference to the Fixnum Object by value.
The problem with the your code is, that x += 1 doesn't modify the passed Fixnum Object but instead creates a completely new and independent object.
I think, Java programmers would call Fixnum objects immutable.
In Ruby you can't pass parameters by reference. For your example, you would have to return the new value and assign it to the variable a or create a new class that contains the value and pass an instance of this class around. Example:
class Container
attr_accessor :value
def initialize value
#value = value
end
end
def func(x)
x.value += 1
end
a = Container.new(5)
func(a)
puts a.value
You can try following trick:
def func(x)
x[0] += 1
end
a = [5]
func(a) #this should be something like func(ref a)
puts a[0] #should read '6'
http://ruby-doc.org/core-2.1.5/Fixnum.html
Fixnum objects have immediate value. This means that when they are assigned or
passed as parameters, the actual object is passed, rather than a reference to
that object.
Also Ruby is pass by value.
However, it seems that composite objects, like hashes, are passed by reference:
fp = {}
def changeit(par)
par[:abc] = 'cde'
end
changeit(fp)
p fp
gives
{:abc=>"cde"}