Compare two hashes no matter symbols or strings, rails - ruby-on-rails

I would like to compare two hashes and forces them to be equal:
one with Symbols on keys and values
the second with only strings.
e.g:
sym_hash = {:id=>58, :locale=>:"en-US"}
string_hash = {"id"=>58, "locale"=>"en-US"}
Try like this does not work:
> sym_hash == string_hash
=> false
I first tried to symbolized the string_hash:
> string_hash.deep_symbolize_keys
=> {:id=>58, :locale=>"en-US"}
But it is still false because sym_hash still has : in front of locale var.
Then I tried to stringified the sym_hash:
> sym_hash.with_indifferent_access
=> {"id"=>58, "locale"=>:"en-US"}
But when I test for equality it is still false for the same reasons.
EDIT
To answer many comments abouy why I wanted those hashes to be equal here, I'll explain what I'm trying to do.
I'm using Reque to manage my jobs. Now I wanted to do a class to avoid having the same* job running, or being enqueued twice in the same time.
(same: for me the same job is a job having the same parameters, I would like to be able to enqueu twice the same jobs having differents ids for instance.)
For that I'm a using the plugin resque-status, so far I'm able to know when a job is running or not. Beside, when I save the params using set I notice that the message written to Redis(because resque-status is using Redis to keep track of the job's status) is not properly saved with symbols.
Here is my class:
# This class is used to run thread-lock jobs with Resque.
#
# It will check if the job with the exact same params is already running or in the queue.
# If the job is not finished, it will returns false,
# otherwise, it will run and returns a the uuid of the job.
#
class JobLock
def self.run(obj, params = {})
# Get rid of completed jobs.
Resque::Plugins::Status::Hash.clear_completed
# Check if your job is currently running or is in the queue.
if !detect_processing_job(obj, params)
job_uuid = obj.create(params)
Resque::Plugins::Status::Hash.set(job_uuid,
job_name: obj.to_s,
params: params)
job_uuid
else
false
end
end
def self.detect_processing_job(obj, params = {})
Resque::Plugins::Status::Hash.statuses.detect do |job|
job['job_name'] == obj.to_s && compare_hashes(job['params'], params)
end
end
def self.compare_hashes(string_hash, sym_hash)
[sym_hash, string_hash].map do |h|
h.map { |kv| kv.map(&:to_s) }.sort
end.reduce :==
end
end
And here how I can use it:
JobLock.run(MyAwesomeJob, id: 58, locale: :"en-US")
As you can see I used #mudasobwa's answer but I hope there is a easier way to achieve what I am trying to do!

How about this?
require 'set'
def sorta_equal?(sym_hash, str_hash)
return false unless sym_hash.size == str_hash.size
sym_hash.to_a.to_set == str_hash.map { |pair|
pair.map { |o| o.is_a?(String) ? o.to_sym : o } }.to_set
end
sym_hash= {:id=>58, :locale=>:"en-US"}
sorta_equal?(sym_hash, {"id"=>58, "locale"=>"en-US"}) #=> true
sorta_equal?(sym_hash, {"locale"=>"en-US", "id"=>58 }) #=> true
sorta_equal?(sym_hash, {"id"=>58, "local"=>"en-US", "a"=>"b" }) #=> false
sorta_equal?(sym_hash, {"id"=>58, "lacole"=>"en-US"}) #=> false
sorta_equal?(sym_hash, {"id"=>58, [1,2,3]=>"en-US"}) #=> false
sorta_equal?({}, {}) #=> true
class A; end
a = A.new
sorta_equal?({:id=>a, :local=>:b}, {"id"=>a, "local"=>"b"}) #=> true

You could try to convert both hashes to JSON, and then compare them:
require 'json'
# => true
sym_hash = {:id=>58, :locale=>:"en-US"}
# => {:id=>58, :locale=>:"en-US"}
string_hash = {"id"=>58, "locale"=>"en-US"}
# => {"id"=>58, "locale"=>"en-US"}
sym_hash.to_json == string_hash.to_json
# => true

The version below works as PHP force-coercion equality:
[sym_hash, string_hash].map do |h|
h.map { |kv| kv.map(&:to_s) }.sort
end.reduce :==
BTW, it’s not a one-liner only because I respect people with smartphones. On terminals of width 80 it’s a perfect oneliner.
To coerce only symbols to strings, preserving numerics to be distinguished from their string representations:
[sym_hash, string_hash].map do |h|
h.map { |kv| kv.map { |e| e.is_a?(Symbol) ? e.to_s : e } }.sort
end.reduce :==

The value of locale in sym_hash is a Symbol :"en-US",
while the value of locale in string_hash is a String.
So they are not equal.
Now if you do:
sym_hash = {:id=>58, :locale=>"en-US"}
string_hash = {"id"=>58, "locale"=>"en-US"}
string_hash.symbolize_keys!
sym_hash == string_hash
=> true

Finaly, to answer my problem I didn't need to force comparaison between hashes. I use Marshal to avoid the problem
class JobLock
def self.run(obj, params = {})
# Get rid of completed jobs.
Resque::Plugins::Status::Hash.clear_completed
# Check if your job is currently running or is in the queue.
if !detect_processing_job(obj, params)
job_uuid = obj.create(params)
Resque::Plugins::Status::Hash.set(job_uuid,
job_name: obj.to_s,
params: Marshal.dump(params))
job_uuid
else
false
end
end
def self.detect_processing_job(obj, params = {})
Resque::Plugins::Status::Hash.statuses.detect do |job|
job['job_name'] == obj.to_s && Marshal.load(job['params']) == params
end
end
end
Anyway, I let this question here because maybe it will help some people in the future...

This is a slight deviation from the original question and an adaptation of some of the suggestions above. If the values can also be String / Symbol agnostic, then may I suggest:
def flat_hash_to_sorted_string_hash(hash)
hash.map { |key_value| key_value.map(&:to_s) }.sort.to_h.to_json
end
this helper function can then be used to assert two hashes have effectively the same values without being type sensitive
assert_equal flat_hash_to_sorted_string_hash({ 'b' => 2, a: '1' }), flat_hash_to_sorted_string_hash({ b: '2', 'a' => 1 }) #=> true
Breakdown:
by mapping a hash, the result is an array
by making the keys / values a consistent type, we can leverage the Array#sort method without raising an error: ArgumentError: comparison of Array with Array failed
sorting gets the keys in a common order
to_h return the object back to a hash
NOTE: this will not work for complex hashes with nested objects or for Float / Int, but as you can see the Int / String comparison works as well. This was inspired by the JSON approach already discussed, but without needing to use JSON, and felt like more than a comment was warranted here as this was the post I found the inspiration for the solution I was seeking.

Related

Is there a lightweight way to lock down a set of keys on a Hash?

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>'

Check string is a valid number or not in ruby

I want to check weather variable contains a valid number or not.
I can validate correctly for null and blank but can not validate text as a "Integer"...
I tried:
if(params[:paramA].blank? || (params[:paramA].is_a?(Integer)) )
I have also tried is_numeric, is_numeric(string), is_number? and other ways...
but did not get success...
I saw such patch:
class String
def is_number?
true if Float(self) rescue false
end
end
if (params[:paramA].blank? || !params[:paramA].is_number?)
Or without the patch:
if (params[:paramA].blank? || (false if Float(params[:paramA]) rescue true))
It supports 12, -12, 12.12, 1e-3 and so on.
If your parameter is for an ActiveRecord model, then you should probably use validates_numericality_of. Otherwise...
You only want integers, right? How about:
if (params[:paramA].blank? || params[:paramA] !~ /^[+-]?\d+$/)
That is, check whether the parameter consists of an optional + or -, followed by 1 or more digits, and nothing else.
If the thing you want to do is this:
I want to check weather variable contains a valid number or not.
You can get it with regex. See it here
s = 'abc123'
if s =~ /[-.0-9]+/ # Calling String's =~ method.
puts "The String #{s} has a number in it."
else
puts "The String #{s} does not have a number in it."
end
In rails you can use the numeric? method on a String or Integer or Float which does exactly what you need.
123.numeric?
# => true
123.45.numeric?
# => true
"123".numeric?
# => true
"123.45".numeric?
# => true
"a1213".numeric?
# => false
UPDATE
My bad, I had a dirty environment, the above works if mongoid version 3 and above is loaded.

Not assigning nil values to a hash

Is there a short hand or best practice for assigning things to a hash when they are nil in ruby? For example, my problem is that I am using another hash to build this and if something in it is nil, it assigns nil to that key, rather than just leaving it alone. I understand why this happens so my solution was:
hash1[:key] = hash2[:key] unless hash2[:key].nil?
Because I cannot have a value in the has where the key actually points to nil. (I would rather have an empty hash than one that has {:key => nil}, that can't happen)
My question would be is there a better way to do this? I don't want to do a delete_if at the end of the assignments.
a little bit shorter if you negate the "unless" statement
hash1[:key] = hash2[:key] if hash2[:key] # same as if ! hash2[:key].nil?
you could also do the comparison in a && statement as suggested in other answers by Michael or Marc-Andre
It's really up to you, what you feel is most readable for you. By design, there are always multiple ways in Ruby to solve a problem.
You could also modify the hash2 :
hash1 = hash2.reject{|k,v| v.nil?}
hash2.reject!{|k,v| v.nil?} # even shorter, if in-place editing of hash2
this would remove key/value pairs :key => nil from hash2 (in place, if you use reject! )
I like this the best, loop and conditional overriding all in one line!
h1 = {:foo => 'foo', :bar => 'bar'}
h2 = {:foo => 'oof', :bar => nil}
h1.merge!(h2) { |key, old_val, new_val| new_val.nil? ? old_val : new_val }
#=> {:foo => 'oof', :bar => 'bar'}
This will replace every value in h1 with the value of h2 where the keys are the same and the h2 value is not nil.
I'm not sure if that's really any better, but
hash2[:key] && hash[:key] = hash2[:key]
could work. Note that this would behave the same way for false and nil, if that's not what you want
!hash2[:key].nil? && hash[:key] = hash2[:key]
would be better. All of this assuming that :key would be an arbitrary value that you may not have control over.
How about something like this?
hash2.each_pair do |key, value|
next if value.nil?
hash1[key] = value
end
If you are doing just a single assignment, this could shave a few characters:
hash2[:key] && hash1[:key] = hash2[:key]
My first example could also be shaved a bit further:
hash2.each_pair{ |k,v| v && hash1[k] = v }
I think the first is the easiest to read/understand. Also, examples 2 and 3 will skip anything that evaluates false (nil or false). This final example is one line and won't skip false values:
hash2.each_pair{ |k,v| v.nil? || hash1[k] = v }
I believe the best practice is to copy the nil value over to the hash. If one passes an option :foo => nil, it can mean something and should override a default :foo of 42, for example. This also makes it easier to have options which should default to true, although one should use fetch in those cases:
opt = hash.fetch(:do_cool_treatment, true) # => will be true if key is not present
There are many ways to copy over values, including nil or false.
For a single key, you can use has_key? instead of the lookup:
hash1[:key] = hash2[:key] if hash2.has_key? :key
For all (or many) keys, use merge!:
hash1.merge!(hash2)
If you only want to do this for a couple of keys of hash2, you can slice it:
hash1.merge!(hash2.slice(:key, ...))
OK, so if the merge doesn't work because you want more control:
hash1[:key] = hash2.fetch(:key, hash1[:key])
This will set hash1's :key to be hash2, unless it doesn't exist. In that case, it will use the default value (2nd argument to fetch), which is hash1's key
Add this to your initializers hash.rb
class Hash
def set_safe(key,val)
if val && key
self[key] = val
end
end
end
use
hash = {}
hash.set_safe 'key', value_or_nil

Equivalent of .try() for a hash to avoid "undefined method" errors on nil? [duplicate]

This question already has answers here:
How to avoid NoMethodError for nil elements when accessing nested hashes? [duplicate]
(4 answers)
Closed 7 years ago.
In Rails we can do the following in case a value doesn't exist to avoid an error:
#myvar = #comment.try(:body)
What is the equivalent when I'm digging deep into a hash and don't want to get an error?
#myvar = session[:comments][#comment.id]["temp_value"]
# [:comments] may or may not exist here
In the above case, session[:comments]try[#comment.id] doesn't work. What would?
You forgot to put a . before the try:
#myvar = session[:comments].try(:[], #comment.id)
since [] is the name of the method when you do [#comment.id].
The announcement of Ruby 2.3.0-preview1 includes an introduction of Safe navigation operator.
A safe navigation operator, which already exists in C#, Groovy, and
Swift, is introduced to ease nil handling as obj&.foo. Array#dig and
Hash#dig are also added.
This means as of 2.3 below code
account.try(:owner).try(:address)
can be rewritten to
account&.owner&.address
However, one should be careful that & is not a drop in replacement of #try. Take a look at this example:
> params = nil
nil
> params&.country
nil
> params = OpenStruct.new(country: "Australia")
#<OpenStruct country="Australia">
> params&.country
"Australia"
> params&.country&.name
NoMethodError: undefined method `name' for "Australia":String
from (pry):38:in `<main>'
> params.try(:country).try(:name)
nil
It is also including a similar sort of way: Array#dig and Hash#dig. So now this
city = params.fetch(:[], :country).try(:[], :state).try(:[], :city)
can be rewritten to
city = params.dig(:country, :state, :city)
Again, #dig is not replicating #try's behaviour. So be careful with returning values. If params[:country] returns, for example, an Integer, TypeError: Integer does not have #dig method will be raised.
The most beautiful solution is an old answer by Mladen Jablanović, as it lets you to dig in the hash deeper than you could with using direct .try() calls, if you want the code still look nice:
class Hash
def get_deep(*fields)
fields.inject(self) {|acc,e| acc[e] if acc}
end
end
You should be careful with various objects (especially params), because Strings and Arrays also respond to :[], but the returned value may not be what you want, and Array raises exception for Strings or Symbols used as indexes.
That is the reason why in the suggested form of this method (below) the (usually ugly) test for .is_a?(Hash) is used instead of (usually better) .respond_to?(:[]):
class Hash
def get_deep(*fields)
fields.inject(self) {|acc,e| acc[e] if acc.is_a?(Hash)}
end
end
a_hash = {:one => {:two => {:three => "asd"}, :arr => [1,2,3]}}
puts a_hash.get_deep(:one, :two ).inspect # => {:three=>"asd"}
puts a_hash.get_deep(:one, :two, :three ).inspect # => "asd"
puts a_hash.get_deep(:one, :two, :three, :four).inspect # => nil
puts a_hash.get_deep(:one, :arr ).inspect # => [1,2,3]
puts a_hash.get_deep(:one, :arr, :too_deep ).inspect # => nil
The last example would raise an exception: "Symbol as array index (TypeError)" if it was not guarded by this ugly "is_a?(Hash)".
The proper use of try with a hash is #sesion.try(:[], :comments).
#session.try(:[], :comments).try(:[], commend.id).try(:[], 'temp_value')
Update: As of Ruby 2.3 use #dig
Most objects that respond to [] expect an Integer argument, with Hash being an exception that will accept any object (such as strings or symbols).
The following is a slightly more robust version of Arsen7's answer that supports nested Array, Hash, as well as any other objects that expect an Integer passed to [].
It's not fool proof, as someone may have created an object that implements [] and does not accept an Integer argument. However, this solution works great in the common case e.g. pulling nested values from JSON (which has both Hash and Array):
class Hash
def get_deep(*fields)
fields.inject(self) { |acc, e| acc[e] if acc.is_a?(Hash) || (e.is_a?(Integer) && acc.respond_to?(:[])) }
end
end
It can be used the same as Arsen7's solution but also supports arrays e.g.
json = { 'users' => [ { 'name' => { 'first_name' => 'Frank'} }, { 'name' => { 'first_name' => 'Bob' } } ] }
json.get_deep 'users', 1, 'name', 'first_name' # Pulls out 'Bob'
say you want to find params[:user][:email] but it's not sure whether user is there in params or not. Then-
you can try:
params[:user].try(:[], :email)
It will return either nil(if user is not there or email is not there in user) or otherwise the value of email in user.
As of Ruby 2.3 this gets a little easier. Instead of having to nest try statements or define your own method you can now use Hash#dig (documentation).
h = { foo: {bar: {baz: 1}}}
h.dig(:foo, :bar, :baz) #=> 1
h.dig(:foo, :zot) #=> nil
Or in the example above:
session.dig(:comments, #comment.id, "temp_value")
This has the added benefit of being more like try than some of the examples above. If any of the arguments lead to the hash returning nil then it will respond nil.
#myvar = session.fetch(:comments, {}).fetch(#comment.id, {})["temp_value"]
From Ruby 2.0, you can do:
#myvar = session[:comments].to_h[#comment.id].to_h["temp_value"]
From Ruby 2.3, you can do:
#myvar = session.dig(:comments, #comment.id, "temp_value")
Another approach:
#myvar = session[:comments][#comment.id]["temp_value"] rescue nil
This might also be consider a bit dangerous because it can hide too much, personally I like it.
If you want more control, you may consider something like:
def handle # just an example name, use what speaks to you
raise $! unless $!.kind_of? NoMethodError # Do whatever checks or
# reporting you want
end
# then you may use
#myvar = session[:comments][#comment.id]["temp_value"] rescue handle
When you do this:
myhash[:one][:two][:three]
You're just chaining a bunch of calls to a "[]" method, an the error occurs if myhash[:one] returns nil, because nil doesn't have a [] method. So, one simple and rather hacky way is to add a [] method to Niclass, which returns nil: i would set this up in a rails app as follows:
Add the method:
#in lib/ruby_extensions.rb
class NilClass
def [](*args)
nil
end
end
Require the file:
#in config/initializers/app_environment.rb
require 'ruby_extensions'
Now you can call nested hashes without fear: i'm demonstrating in the console here:
>> hash = {:foo => "bar"}
=> {:foo=>"bar"}
>> hash[:foo]
=> "bar"
>> hash[:doo]
=> nil
>> hash[:doo][:too]
=> nil
Andrew's answer didn't work for me when I tried this again recently. Maybe something has changed?
#myvar = session[:comments].try('[]', #comment.id)
The '[]' is in quotes instead of a symbol :[]
Try to use
#myvar = session[:comments][#comment.id]["temp_value"] if session[:comments]

Handling a hash as a function argument

I am using Ruby on Rails 3 and I am trying to handle a hash as a function argument.
For example, if I state a function this way:
def function_name(options = {})
...
end
I would like to pass to the function_name a hash like
{"key1"=>"value_1", "key2"=>"value2", "..." => "..."}
and then use that inside the function.
What is the best\common (Rails) way to do that?
P.S.: I have seen the extract_option! method somewhere, but I don't know where I can find some documentation and whether I need that in order to accomplish what I aim.
Simply use the definition you provided:
def function_name(options = {})
puts options["key1"]
end
Call it with:
function_name "key1" => "value1", "key2" => "value2"
or
function_name({"key1" => "value1", "key2" => "value2"})
Array#extract_options! is simply used with methods that have variable method arguments like this:
def function_name(*args)
puts args.inspect
options = args.extract_options!
puts options["key1"]
puts args.inspect
end
function_name "example", "second argument", "key1" => "value"
# prints
["example", "second argument", { "key1" => "value" }]
value
["example", "second argument"]
Another useful method is Hash#symbolize_keys! which lets you not care about whether you pass in strings or symbols to your function so that you can always access things like this options[:key1].
The way you have it declared in your example will work fine.
def function(options = {})
item = options[:item]
need_milk = options[:milk] || false
cow = options[:bovine]
end
function(:item => "Something")
In the case above, item == "Something", need_milk == false and cow == nil.
extract_options is simply an addition to the Array and Hash class via Rails.
def function(something, else, *args)
options = args.extract_options! # returns Hash
end
It is useful if you plan on having many different types of parameters in args but if you only want Hash options, your original way is fine.
Here's a Gist of the code in Rails for extract_options! I personally use it in my code at work by just writing it to an external file and requiring it into my project.
Ruby makes this easy, and you were already doing it right.
Here is a poetry-mode (minimal, DSL-style) example:
def f x = {}
p x
end
f :a => :b
{:a=>:b}
f
{}
f :a => :b, :c => :d
{:a=>:b, :c=>:d}

Resources