0

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!

Kruupös
  • 5,097
  • 3
  • 27
  • 43
  • these hashes ***are*** different – Andrey Deineko Mar 10 '17 at 15:45
  • Yes, in this case I would like to modify them in order to make them similar – Kruupös Mar 10 '17 at 15:45
  • 1
    It's not a one-liner (unless you wrap it in a method), but you can loop over the hashes and compare the values based on their `to_s` method. I can provide a code sample if you want. – MrDanA Mar 10 '17 at 15:53
  • I would like a generic method that also can compare empty hashes. So I would prefer to not loop over each hashes. But please give it a try! – Kruupös Mar 10 '17 at 15:56
  • 1. From your example it would appear that for one hash (`sym_hash`), the keys are all symbols and no value is a string, and that for the other hash (`string_hash`) all keys are strings and no value is a symbol. Correct? (That's different than what you said.) 2. You want to know if the two hashes are equal if all the keys and values in `string_hash` that are strings are converted to symbols. Correct? 3. Please clarify what you mean by "forces them to be equal". – Cary Swoveland Mar 10 '17 at 23:40
  • @CarySwoveland, I uptated my question to be more specific about what I am trying to achieve. For 1- How it is different that what I said? but yes you are right, I'm just failling to see my mistake ^^' To clarify, I would like to see is the content are the same in both hashes no matter symbols or strings. – Kruupös Mar 11 '17 at 12:28

6 Answers6

2

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 Tin Man
  • 158,662
  • 42
  • 215
  • 303
Alejandro Montilla
  • 2,626
  • 3
  • 31
  • 35
  • This doesn't work when `string_hash = {"locale"=>"en-US", "id"=>58, }`, which should returns `false`, but should return `true`. – Cary Swoveland Mar 10 '17 at 23:02
  • Correction: "...which returns false but should return true." – Cary Swoveland Mar 11 '17 at 14:19
  • Better keep `string_hash` as is and convert `sym_hash` to json and then parse json. This way order of keys doesn't matter. Like this: `JSON.parse({:x=>1}.to_json) == {'x'=> 1}`. – Artem P Aug 13 '21 at 03:35
2

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 
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
1

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 :==
Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160
1

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
Zic
  • 31
  • 5
  • Yes I am well aware of this problem, that is the whole point of my question actually :) I cannot rewrite the hashes on the fly because they are inside a function and accept a `params = {}` parameter. – Kruupös Mar 10 '17 at 16:20
  • I think than you need to loop through the hashes `string_hash.values.map(&:to_s) == sym_hash.values.map(&:to_s)` Or may you add the original code, so we could have a bit of context in order to find the best solution? – Zic Mar 10 '17 at 16:27
0

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

Kruupös
  • 5,097
  • 3
  • 27
  • 43
0

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.

SMAG
  • 652
  • 6
  • 12