i'm trying (and actually succeded, but i don't understand how it works) to write a custom method for a hash in my model (I'm working on Ruby on Rails 6).
My hash looks like this
my_hash = {
[['name_1', 'slug_1']=>value_1],
[['name_2', 'slug_2']=>value_2],
[['name_1', 'slug_1']=>value_3],
[['name_2', 'slug_2']=>value_4]
}
So basically a hash of arrays. You notice that the 'keys' are arrays that repeat themselves many times, but with different values. What i want to achieve is to write a custom method that "joins" all the keys in only one key, which will have an array of values assigned, so basically i should be able to get:
my_hash = {
['name_1', 'slug_1']=>"values": [value_1, value_3],
['name_2', 'slug_2']=>"values": [value_2, value_4]
}
For that, I have this piece of code, which i use many times:
my_hash.inject({}) do |hash, record|
# each record has the following format => [["unit_name", "axis.slug"]=>average_value(float)]
keys, value = record
# now keys has ["unit_name", "axis.slug"] and values equals average_value
hash[keys.first] ||= {}
hash[keys.first][keys.last] = value.to_f
hash
end
Since I use this many times, i wanted to write a custom method, so i did:
def format_hash_data my_hash
my_hash.inject({}) do |hash, record|
# each record has the following format => [["unit_name", "axis.slug"]=>average_value(float)]
keys, value = record
# now keys has ["unit_name", "axis.slug"] and values equals average_value
hash[keys.first] ||= {}
hash[keys.first][keys.last] = value.to_f
hash
end
end
And used it like: my_hash = format_hash_data(my_hash) with no success(it threw an error saying that 'format_hash_data' was not a valid method for the class).
So I fiddled around and added 'self' to the name of the method, leaving:
def self.format_hash_data my_hash
my_hash.inject({}) do |hash, record|
# each record has the following format => [["unit_name", "axis.slug"]=>average_value(float)]
keys, value = record
# now keys has ["unit_name", "axis.slug"] and values equals average_value
hash[keys.first] ||= {}
hash[keys.first][keys.last] = value.to_f
hash
end
end
Which, to my surprise, worked flawlessly when using my_hash = format_hash_data(my_hash)
I don't really understand why adding 'self' makes my code works, maybe anyone can shed some light? I tried using things like send() or instance_eval first, to just send the piece of code to the actual hash as a method (something like my_hash.instance_eval(my_method)) but I couldn't get it working.
I'm sorry about the long explanation, I hope i was clear enough so any of you who had this same dilemma can understand. Thanks in advance.
Prepending self. to the method name makes it a class method instead of an instance method. If you are not sure of the difference, you should look it up as it is fundamental to properly defining and using classes and methods.
As a class method, you would use it as:
my_hash = MyHash.format_hash_data(my_hash)
Or if you're in scope of the class, simply my_hash = format_hash_data(my_hash), which is why it worked in your case with the self. prepended (class method definition).
If you want to define it as an instance method (a method that is defined for the instance), you would use it like so:
my_hash = my_hash.format_hash_data
And the definition would use the implicit self of the instance:
def format_hash_data
self.inject({}) do |hash, record|
# each record has the following format => [["unit_name", "axis.slug"]=>average_value(float)]
keys, value = record
# now keys has ["unit_name", "axis.slug"] and values equals average_value
hash[keys.first] ||= {}
hash[keys.first][keys.last] = value.to_f
hash
end
end
Related
To be clear, I'm perfectly happy implementing this functionality as a custom class myself, but I want to make sure I'm not overlooking some bit of ruby or rails magic. I have googled every meaningful permutation of the keywords "ruby rails hash keys values immutable lock freeze". But no luck so far!
Problem: I need to give a Hash a set of keys, possibly at run time, and then lock the set of keys without locking their values. Something like the following:
to_lock = {}
to_lock[:name] = "Bill"
to_lock[:age] = 42
to_lock.freeze_keys # <-- this is what I'm after, so that:
to_lock[:name] = "Bob" # <-- this works fine,
to_lock[:height] # <-- this returns nil, and
to_lock[:height] = 175 # <-- this throws some RuntimeError
Question: Is there a bit of ruby or rails tooling to allow this?
I know of Object#freeze and of Immutable::Hash, but both lock keys and values.
Sticking with out-of-the-box ruby, the use case could be mostly met by manipulating the methods or accessors of classes at runtime, as in this or this, then overriding #method_missing. But that feels quite a bit clunkier. Those techniques also don't really "lock" the set of methods or accessors, it's just awkward to add more. At that point it'd be better to simply write a class that exactly implements the snippet above and maintain it as needed.
You can achieve this by defining a custom []= for your "to-lock" instance of a hash, after you've added the allowed keys:
x = { name: nil, age: nil }
def x.[]=(key, value)
# blow up unless the key already exists in the hash
raise 'no' unless keys.include?(key)
super
end
x[:name] # nil
x[:name] = "Bob" # "Bob"
x[:size] # nil
x[:size] = "large" # raise no
Note that this won't prevent you from inadvertently adding keys using something like merge!.
#meagar has offered an interesting solution, but has pointed out that it only works when attempting to add a key-value pair using Hash#[]. Moreover, it does not prevent keys from being deleted.
Here's another way, but it's rather kludgy, so I think you should probably be looking for a different way to skin your cat.
class Hash
def frozen_keys_create
self.merge(self) { |*_,v| [v] }.freeze
end
def frozen_keys_get_value(k)
self[k].first
end
def frozen_keys_put_value(k, new_value)
self[k].replace [new_value]
self
end
def frozen_keys_to_unfrozen
self.merge(self) { |*_,v| v.first }
end
end
Now let's put them to use.
Create a frozen hash with each value wrapped in an array
sounds = { :cat=>"meow", :dog=>"woof" }.frozen_keys_create
#=> {:cat=>["meow"], :dog=>["woof"]}
sounds.frozen?
#=> true
This prevents keys from being added:
sounds[:pig] = "oink"
#=> RuntimeError: can't modify frozen Hash
sounds.update(:pig=>"oink")
#=> RuntimeError: can't modify frozen Hash
or deleted:
sounds.delete(:cat)
#=> RuntimeError: can't modify frozen Hash
sounds.reject! { |k,_| k==:cat }
#=> RuntimeError: can't modify frozen Hash
Get a value
sounds.frozen_keys_get_value(:cat)
#=> "meow"
Change a value
sounds.frozen_keys_put_value(:dog, "oooooowwwww")
#=> {:cat=>["meow"], :dog=>["oooooowwwww"]}
Convert to a hash whose keys are not frozen
new_sounds = sounds.frozen_keys_to_unfrozen
#=> {:cat=>"meow", :dog=>"oooooowwwww"}
new_sounds.frozen?
#=> false
Add and delete keys
Maybe even add (private, perhaps) methods to add or delete key(s) to override the desired behaviour.
class Hash
def frozen_keys_add_key_value(k, value)
frozen_keys_to_unfrozen.tap { |h| h[k] = value }.frozen_keys_create
end
def frozen_keys_delete_key(k)
frozen_keys_to_unfrozen.reject! { |key| key == k }.frozen_keys_create
end
end
sounds = { :cat=>"meow", :dog=>"woof" }.frozen_keys_create
#=> {:cat=>["meow"], :dog=>["oooowwww"]}
new_sounds = sounds.frozen_keys_add_key_value(:pig, "oink")
#=> {:cat=>["meow"], :dog=>["woof"], :pig=>["oink"]}
new_sounds.frozen?
#=> true
newer_yet = new_sounds.frozen_keys_delete_key(:cat)
#=> {:dog=>["woof"], :pig=>["oink"]}
newer_yet.frozen?
#=> true
Sounds like a great use-case for the built-in Struct
irb(main):001:0> s = Struct.new(:name, :age).new('Bill', 175)
=> #<struct name="Bill", age=175>
irb(main):002:0> s.name = 'Bob'
=> "Bob"
irb(main):003:0> s.something_else
NoMethodError: undefined method `something_else' for #<struct name="Bob", age=175>
from (irb):3
from /home/jtzero/.rbenv/versions/2.3.0/bin/irb:11:in `<main>'
Assume we have a rails params hash full of nested hashes and arrays. Is there a way to alter every string value (whether in nested hashes or arrays) which matches a certain criteria (e.g. regex) and still keep the output as a params hash (still containing nested hashes arrays?
I want to do some sort of string manipulation on some attributes before even assigning them to a model. Is there any better way to achieve this?
[UPDATE]
Let's say we want to select the strings that have an h in the beginning and replace it with a 'b'. so we have:
before:
{ a: "h343", b: { c: ["h2", "s21"] } }
after:
{ a: "b343", b: { c: ["b2", "s21"] } }
For some reasons I can't do this with model callbacks and stuff, so it should have be done before assigning to the respective attributes.
still keep the output as a params hash (still containing nested hashes arrays
Sure.
You'll have to manipulate the params hash, which is done in the controller.
Whilst I don't have lots of experience with this I just spent a bunch of time testing -- you can use a blend of the ActionController::Parameters class and then using gsub! -- like this:
#app/controllers/your_controller.rb
class YourController < ApplicationController
before_action :set_params, only: :create
def create
# Params are passed from the browser request
#model = Model.new params_hash
end
private
def params_hash
params.require(:x).permit(:y).each do |k,v|
v.gsub!(/[regex]/, 'string')
end
end
end
I tested this on one of our test apps, and it worked perfectly:
--
There are several important points.
Firstly, when you call a strong_params hash, params.permit creates a new hash out of the passed params. This means you can't just modify the passed params with params[:description] = etc. You have to do it to the permitted params.
Secondly, I could only get the .each block working with a bang-operator (gsub!), as this changes the value directly. I'd have to spend more time to work out how to do more elaborate changes.
--
Update
If you wanted to include nested hashes, you'd have to call another loop:
def params_hash
params.require(:x).permit(:y).each do |k,v|
if /_attributes/ ~= k
k.each do |deep_k, deep_v|
deep_v.gsub!(/[regex]/, 'string'
end
else
v.gsub!(/[regex]/, 'string')
end
end
end
In general you should not alter the original params hash. When you use strong parameters to whitelist the params you are actually creating a copy of the params - which can be modified if you really need to.
def whitelist_params
params.require(:foo).permit(:bar, :baz)
end
But if mapping the input to a model is too complex or you don't want to do it on the model layer you should consider using a service object.
Assuming you have a hash like this:
hash = { "hello" => { "hello" => "hello", "world" => { "hello" => "world", "world" => { "hello" => "world" } } }, "world" => "hello" }
Then add a function that transforms the "ello" part of all keys and values into "i" (meaning that "hello" and "yellow" will become "hi" and "yiw")
def transform_hash(hash, &block)
hash.inject({}){ |result, (key,value)|
value = value.is_a?(Hash) ? transform_hash(value, &block) : value.gsub(/ello/, 'i')
block.call(result, key.gsub(/ello/, 'i'), value)
result
}
end
Use the function like:
new_hash = transform_hash(hash) {|hash, key, value| hash[key] = value }
This will transform your hash and it's values regardless of the nesting level. However, the values should be strings (or another Hash) otherwise you'll get an error. to solve this problem just change the value.is_a?(Hash) conditional a bit.
NOTE that I strongly recommend you NOT to change the keys of the hash!
To solve my problem, I set fictitious Car Model below:
Car Model has
3 attributes(id, car_name, owner_name) and
2 methods which return integers(riders, passengers).
I want to get ONE HASH which has the values of 2 attributes and 2 methods from all of cars. To solve this problem, my temporary solution is below:
json_format = Car.all.to_json(only: [:car_name, :owner_name], methods: [:riders, :passengers])
final_hash = ActiveSupport::JSON.decode(json_format)
This works, but this is bad because I use 'to_json' method only for its optional function.
Is their any other choice to getting the one hash directly from Car Model via its own optional function?
Use as_json. It's what to_json uses under the hood, and accepts all the same options but returns a Hash.
I feel the same as you that using the json commands for a method unrelated to json is poor practice.
If you don't mind being verbose, you could do something like this:
formatted_cars = Array.new
Car.all.each do |c|
formatted_cars.push( car_name: c.car_name,
owner_name: c.owner_name,
riders: c.riders,
passengers: c.passengers )
end
I find when hitting both attributes and methods it's cleanest just to specify the assignments like this. This is also the technique I would use in passing an object with its virtual attributes to javascript.
I agree that turning it into Json and back is pretty stupid. I would do this like so.
Car.all.collect{|car| hash = {}; [:car_name, :owner_name, :riders, :passengers].each{|key| hash[key] = car.send(key)}; hash}
it would be cleaner to put this into a method in the object
#in Car class
def stats_hash
hash = {}
[:car_name, :owner_name, :riders, :passengers].each do |key|
hash[key] = self.send(key)
end
hash
end
then do
Car.all.collect(&:stats_hash)
Is it possible to dynamically create key names of a hash? I'm passing the following hash parameters:
params[:store][:store_mon_open(5i)]
params[:store][:store_mon_closed(5i)]
params[:store][:store_tue_open(5i)]
params[:store][:store_tue_closed(5i)]
.
.
.
params[:store][:store_sun_open(5i)]
params[:store][:store_sun_closed(5i)]
To check if each parameter exists, I'm using two arrays:
days_of_week = [:mon, :tue, ..., :sun]
open_or_closed = [:open, :closed]
But, I can't seem to figure out how to dynamically create the params hash (the second key( with the array. Here's what I have so far:
days_of_week.each do |day_of_week|
open_or_closed.each do |store_status|
if !eval("params[:store][:store_#{day_of_week}_#{store_status}(5i)").nil
[DO SOMETHING]
end
end
end
I've tried a bunch of things including the eval method (as listed above) but rails seems to dislike the parentheses around the "5i". Any help is greatly appreciated!
You should be able to do
if params[:store]["store_#{day_of_week}_#{store_status}(5i)".to_sym]
Note that you were missing the ? on .nil? and that !object.nil? can be shortened to just object
Assuming this is a HashWithIndifferentAccess, you should be able to access it via string just as you could with a symbol. Thus:
days_of_week.each do |day_of_week|
open_or_closed.each do |store_status|
key = "store_#{day_of_week}_#{store_status}(5i)"
unless params[:store][key]
# DO SOMETHING
end
end
end
If it's not a HashWithIndifferentAccess then you should just be able to call key.to_sym to turn it into a symbol.
Here's my Rails class
class SkinnyEmployee
include ActiveModel::Validations
attr_accessor :uid, :name
validates :uid, :presence => true
def initialize(id, name)
#uid = id
#name = name
end
def ==(other)
puts "Calling =="
raise ArgumentError.new("other is nil or bad in "+self.to_s) if other.nil? or !other.instance_of?(SkinnyEmployee)
return (self.class == other.class && self.uid == other.uid)
end
alias :eql? :==
end
I have a hash of SkinnyEmployee objects. E.g.,
skinny_hash = {SkinnyEmployee.new("123", "xyz") => 1, SkinnyEmployee.new("456", "abc") => 2}
I have another SkinnyEmployee object that I want to look up. E.g.,
entry = SkinnyEmployee.new("456", "abc")
When I do
skinny_hash.keys.index(entry)
I get 1, as expected. But when I do
skinny_hash.has_key?(entry)
I get false.
Why is that? Doesn't has_key? also use == or eql? to find whether a key exists in a hash?
Thanks much for the help!
First, this drove me nuts. What you're doing looked absolutely correct to me, and, as you already know, doesn't work.
I can take you part of the way to a solution:
http://ruby-doc.org/core-2.0.0/Object.html#method-i-hash
quoting:
Generates a Fixnum hash value for this object. This function must have the property that a.eql?(b) implies a.hash == b.hash.
The hash value is used along with eql? by the Hash class to determine if two objects reference the same hash key. Any hash value that exceeds the capacity of a Fixnum will be truncated before being used.
I added:
def hash
1
end
to your SkinnyEmployee Class, and has_key? started returning true. Obviously that's not a solution, but I'm thinking it at least puts you on the path to one.
You have overwritten the eql? method used by Array#index but not the hash method used by Hash#has_key?.
From Ruby docs for Object#hash
Generates a Fixnum hash value for this object. This function must have the property that a.eql?(b) implies a.hash == b.hash.
The Object#hash and Object#eql? methods return equal if and only if the objects occupy the same space in memory. Some classes like Array overwrite both methods to return true if the compared array's have same elements.
For your case you can define the hash method like:
def hash
"#{self.class}_#{self.uid}".hash
end
This would satisfy the docs criteria for hash method given above.
That is happening because the object you are using as a key and they one you are using to search the key are different.
Every time you call SkinnyEmployee.new it will create a new, different, object. For example
employee_1 = SkinnyEmployee.new("123", "xyz")
employee_2 = SkinnyEmployee.new("123", "xyz")
employee_1 == employee_1 #=> true
employee_2 == employee_2 #=> true
employee_2 == employee_1 #=> false
If you call object_id on both employee_1 and employee_2 you will notice that it gives you different id's.
Using has_key? will check for the exact same object, and that won't be the case if you use SkinnyEmployee.new("456", "abc").
You would need a way to retrieve the exact same object, store it in a variable or in the DB, you are using as a key and use it as an attribute for has_key? for it to work.
Hope this can help you.