I would like to compare two hashes and forces them to be equal:
eg:
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
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
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
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:
Array#sort
method without raising an error: ArgumentError: comparison of Array with Array failed
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.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.