How do I create a hash in Ruby that compares strings, ignoring case?

10,615

Solution 1

To prevent this change from completely breaking independent parts of your program (such as other ruby gems you are using), make a separate class for your insensitive hash.

class HashClod < Hash
  def [](key)
    super _insensitive(key)
  end

  def []=(key, value)
    super _insensitive(key), value
  end

  # Keeping it DRY.
  protected

  def _insensitive(key)
    key.respond_to?(:upcase) ? key.upcase : key
  end
end

you_insensitive = HashClod.new

you_insensitive['clod'] = 1
puts you_insensitive['cLoD']  # => 1

you_insensitive['CLod'] = 5
puts you_insensitive['clod']  # => 5

After overriding the assignment and retrieval functions, it's pretty much cake. Creating a full replacement for Hash would require being more meticulous about handling the aliases and other functions (for example, #has_key? and #store) needed for a complete implementation. The pattern above can easily be extended to all these related methods.

Solution 2

If you really want to ignore case in both directions and handle all Hash methods like #has_key?, #fetch, #values_at, #delete, etc. , you'll need to do a little work if you want to build this from scratch, but if you create a new class that extends from class ActiveSupport::HashWithIndifferentAccess, you should be able to do it pretty easily like so:

require "active_support/hash_with_indifferent_access"

class CaseInsensitiveHash < HashWithIndifferentAccess
  # This method shouldn't need an override, but my tests say otherwise.
  def [](key)
    super convert_key(key)
  end

  protected

  def convert_key(key)
    key.respond_to?(:downcase) ? key.downcase : key
  end  
end

Here's some example behavior:

h = CaseInsensitiveHash.new
h["HELLO"] = 7
h.fetch("HELLO")                # => 7
h.fetch("hello")                # => 7
h["HELLO"]                      # => 7
h["hello"]                      # => 7
h.has_key?("hello")             # => true
h.values_at("hello", "HELLO")   # => [7, 7]
h.delete("hello")               # => 7
h["HELLO"]                      # => nil

Solution 3

require 'test/unit'
class TestCaseIndifferentHash < Test::Unit::TestCase
  def test_that_the_hash_matches_keys_case_indifferent
    def (hsh = {}).[](key) super(key.upcase) end

    hsh['HELLO'] = 7
    assert_equal 7, hsh['hello']
  end
end

Solution 4

Any reason for not just using string#upcase?

h = Hash.new

h["HELLO"] = 7

puts h["hello".upcase]

If you insist on modifying hash, you can do something like the following

class Hash
alias :oldIndexer :[]

def [](val)
   if val.respond_to? :upcase then oldIndexer(val.upcase) else oldIndexer(val) end
end
end

Since it was brought up, you can also do this to make setting case insensitive:

class Hash
alias :oldSetter :[]=
def []=(key, value)
    if key.respond_to? :upcase then oldSetter(key.upcase, value) else oldSetter(key, value) end
end
end

I also recommend doing this using module_eval.

Solution 5

In general, I would say that this is a bad plan; however, if I were you, I'd create a subclass of hash that overrides the [] method:

class SpecialHash < Hash
  def [](search)
    # Do special code here
  end
end
Share:
10,615
Colen
Author by

Colen

Win32 software developer.

Updated on June 06, 2022

Comments

  • Colen
    Colen about 2 years

    In Ruby, I want to store some stuff in a Hash, but I don't want it to be case-sensitive. So for example:

    h = Hash.new
    h["HELLO"] = 7
    puts h["hello"]
    

    This should output 7, even though the case is different. Can I just override the equality method of the hash or something similar?

    Thanks.

  • Federico Builes
    Federico Builes over 14 years
    Just to add a bit more to this answer: Override #[]= to call #downcase on the keys you receive and then in #[] you can call self.get(search.downcase).
  • Topher Fangio
    Topher Fangio over 14 years
    @Federico Builes - +1 - Thanks for the extra bit :-)
  • Topher Fangio
    Topher Fangio over 14 years
    Neat, I didn't know about that!
  • mletterle
    mletterle over 14 years
    Assumption was made that setting the hash was being done with all caps. You can easily extend the above to []=
  • Carson Reinke
    Carson Reinke about 11 years
    Warning, this approach does not work with nested hashes, as your subclass will not be used
  • Kevin McCarpenter
    Kevin McCarpenter over 9 years
    I realize this was nearly five years ago, but 'you insensitive clod' made me laugh out loud. Bravo.
  • jrg
    jrg over 8 years
    Warning: This doesn't work with nested hashes either.
  • Myrddin Emrys
    Myrddin Emrys over 8 years
    @James As far as I am aware it does if you create each nesting level as a HashClod. If you create default hashes of course it will fail.