lib/hashery/fuzzy_hash.rb in hashery-1.4.0 vs lib/hashery/fuzzy_hash.rb in hashery-1.5.0

- old
+ new

@@ -1,153 +1,306 @@ -require 'hashery/fuzzyhash' +require 'set' + +# = Fuzzy Hash +# +# A weird hash with special semantics for regex keys. +# +# This is useful when you want to have a lookup table that can either contain strings or regexes. +# For instance, you might want a catch all for certain regexes that perform a certain logic. +# +# >> hash = FuzzyHash.new +# >> hash[/^\d+$/] = 'number' +# >> hash[/.*/] = 'something' +# >> hash['chunky'] = 'bacon' +# >> hash['foo'] = 'vader' +# +# >> hash['foo'] +# << 'vader' +# >> hash['food'] +# << 'something' +# >> hash['123'] +# << 'number' +# +# Copyright (c) 2009 Joshua Hull + +class FuzzyHash + + def initialize(init_hash = nil) + @fuzzies = [] + @hash_reverse = {} + @fuzzies_reverse = {} + @fuzzy_hash = {} + @hash = {} + init_hash.each{ |key,value| self[key] = value } if init_hash + end + + def clear + hash.clear + fuzzies.clear + hash_reverse.clear + fuzzies_reverse.clear + end + + def size + hash.size + fuzzies.size + end + alias_method :count, :size + + + def ==(o) + o.is_a?(FuzzyHash) + o.send(:hash) == hash && + o.send(:fuzzies) == fuzzies + end + + def empty? + hash.empty? && fuzzies.empty? + end + + def keys + hash.keys + fuzzy_hash.keys + end + + def values + hash.values + fuzzies.collect{|r| r.last} + end + + def each + hash.each{|k,v| yield k,v } + fuzzies.each{|v| yield v.first, v.last } + end + + def delete_value(value) + hash.delete(hash_reverse[value]) || ((rr = fuzzies_reverse[value]) && fuzzies.delete_at(rr[0])) + end + + def []=(key, value) + if Regexp === key + fuzzies.delete_if{|f| f.first.inspect.hash == key.inspect.hash} + fuzzies_reverse.delete_if{|k, v| v[1].inspect.hash == key.inspect.hash} + hash_reverse.delete_if{|k,v| v.inspect.hash == key.inspect.hash} + + fuzzy_hash[key] = value + fuzzies << [key, value] + reset_fuzz_test! + fuzzies_reverse[value] = [fuzzies.size - 1, key, value] + else + hash[key] = value + hash_reverse.delete_if{|k,v| v.hash == key.hash} + hash_reverse[value] = key + end + value + end + + def replace(src, dest) + if hash_reverse.key?(src) + key = hash_reverse[src] + hash[key] = dest + hash_reverse.delete(src) + hash_reverse[dest] = key + elsif fuzzies_reverse.key?(src) + key = fuzzies_reverse[src] + fuzzies[rkey[0]] = [rkey[1], dest] + fuzzies_reverse.delete(src) + fuzzies_reverse[dest] = [rkey[0], rkey[1], dest] + end + end + + def [](key) + (hash.key?(key) && hash[key]) || + ((lookup = fuzzy_lookup(key)) && lookup && lookup.first) || + fuzzy_hash[key] + end + + def match_with_result(key) + if hash.key?(key) + [hash[key], key] + else + fuzzy_lookup(key) + end + end + + private + attr_reader :fuzzies, :hash_reverse, :fuzzies_reverse, :hash, :fuzzy_hash + attr_writer :fuzz_test + + def reset_fuzz_test! + self.fuzz_test = nil + end + + def fuzz_test + unless @fuzz_test + @fuzz_test = Object.new + @fuzz_test.instance_variable_set(:'@fuzzies', fuzzies) + method = " + def match(str) + case str\n + " + fuzzies.each_with_index do |reg, index| + method << "when #{reg.first.inspect}; [@fuzzies[#{index}][1], Regexp.last_match(0)];" + end + method << "end\nend\n" + @fuzz_test.instance_eval method + end + @fuzz_test + end + + def fuzzy_lookup(key) + if !fuzzies.empty? && (value = fuzz_test.match(key)) + value + end + end + +end +