module Hashery # A PathHash is a hash whose values can be accessed in the normal manner, # or with keys that are slash (`/`) separated strings. To get the whole hash # as a single flattened level, call `#flat`. All keys are converted to strings. # All end-of-the-chain values are kept in whatever value they are. # # s = PathHash['a' => 'b', 'c' => {'d' => :e}] # s['a'] #=> 'b' # s['c'] #=> {slashed: 'd'=>:e} # s['c']['d'] #=> :e # s['c/d'] #=> :e # # PathHash is derived from the SlashedHash class in the HashMagic project # by Daniel Parker . # # Copyright (c) 2006 BehindLogic (http://hash_magic.rubyforge.org) # # Authors: Daniel Parker # # TODO: This class is very much a work in progess and will be substantially rewritten # for future versions. # class PathHash < Hash # # Initialize PathHash. # # hsh - Priming Hash. # def initialize(hsh={}) raise ArgumentError, "must be a hash or array of slashed values" unless hsh.is_a?(Hash) || hsh.is_a?(Array) @constructor = hsh.is_a?(Hash) ? hsh.class : Hash @flat = flatten_to_hash(hsh) end # Standard Hash methods, plus the overwritten ones #include StandardHashMethodsInRuby # Behaves like the usual Hash#[] method, but you can access nested hash # values by composing a single key of the traversing keys joined by '/': # # hash['c']['d'] # is the same as: # hash['c/d'] # def [](key) rg = Regexp.new("^#{key}/?") start_obj = if @constructor == OrderedHash @constructor.new((@flat.instance_variable_get(:@keys_in_order) || []).collect {|e| e.gsub(rg,'')}) else @constructor.new end v = @flat.has_key?(key) ? @flat[key] : self.class.new(@flat.reject {|k,v| !(k == key || k =~ rg)}.inject(start_obj) {|h,(k,v)| h[k.gsub(rg,'')] = v; h}) v.is_a?(self.class) && v.empty? ? nil : v end # # Same as above, except sets value rather than retrieving it. # def []=(key,value) @flat.reject! {|k,v| k == key || k =~ Regexp.new("^#{key}/")} if value.is_a?(Hash) flatten_to_hash(value).each do |hk,hv| @flat[key.to_s+'/'+hk.to_s] = hv end else @flat[key.to_s] = value end end def clear # :nodoc: @flat.clear end # # # def fetch(key,default=:ehisehoah0928309q98y30,&block) # :nodoc: value = @flat.has_key?(key) ? @flat[key] : self.class.new(@flat.reject {|k,v| !(k == key || k =~ Regexp.new("^#{key}/"))}.inject({}) {|h,(k,v)| h[k.split('/',2)[1]] = v; h}) if value.is_a?(self.class) && value.empty? if default == :ehisehoah0928309q98y30 if block_given? block.call(key) else raise IndexError end value else default end else value end end # # Delete entry from Hash. Slashed keys can be used here, too. # # key - The key to delete. # block - Produces the return value if key not found. # # Returns delete value. # def delete(key,&block) value = @flat.has_key?(key) ? @flat[key] : self.class.new(@flat.reject {|k,v| !(k == key || k =~ Regexp.new("^#{key}/"))}.inject({}) {|h,(k,v)| h[k.split('/',2)[1]] = v; h}) return block.call(key) if value.is_a?(self.class) && value.empty? && block_given? @flat.keys.reject {|k| !(k == key || k =~ Regexp.new("^#{key}/"))}.each {|k| @flat.delete(k)} return value end # def empty? @flat.empty? end # This gives you the slashed key of the value, no matter where the value is in the tree. def index(value) @flat.index(value) end # def inspect @flat.inspect.insert(1,'slashed: ') end # This gives you only the top-level keys, no slashes. To get the list of slashed keys, do hash.flat.keys def keys @flat.inject([]) {|a,(k,v)| a << [k.split('/',2)].flatten[0]; a}.uniq end # This is rewritten to mean something slightly different than usual: Use this to restructure the hash, for cases when you # end up with an array holding several hashes. def rehash # :nodoc: @flat.rehash end # Gives a list of all keys in all levels in the multi-level hash, joined by slashes. # # {'a'=>{'b'=>'c', 'c'=>'d'}, 'b'=>'c'}.slashed.flat.keys # #=> ['a/b', 'a/c', 'b'] # def flat @flat end # Expands the whole hash to Hash objects ... not useful very often, it seems. def expand inject({}) {|h,(k,v)| h[k] = v.is_a?(SlashedHash) ? v.expand : v; h} end def to_string_array flatten_to_array(flat,[]) end def slashed # :nodoc: self end # Same as ordered! but returns a new SlashedHash object instead of modifying the same. def ordered(*keys_in_order) dup.ordered!(*keys_in_order) end # Sets the SlashedArray as ordered. The *keys_in_order must be a flat array # of slashed keys that specify the order for each level: # # s = {'a'=>{'b'=>'c', 'c'=>'d'}, 'b'=>'c'}.slashed # s.ordered!('b', 'a/c', 'a/b') # s.expand # => {'b'=>'c', 'a'=>{'c'=>'d', 'b'=>'c'}} # # Note that the expanded hashes will *still* be ordered! # def ordered!(*keys_in_order) return self if @constructor == OrderedHash @constructor = OrderedHash @flat = @flat.ordered(*keys_in_order) self end # def ==(other) case other when SlashedHash @slashed == other.instance_variable_get(:@slashed) when Hash self == SlashedHash.new(other) else raise TypeError, "Cannot compare #{other.class.name} with SlashedHash" end end private def flatten_to_hash(hsh) flat = @constructor.new if hsh.is_a?(Array) hsh.each do |e| flat.merge!(flatten_to_hash(e)) end elsif hsh.is_a?(Hash) hsh.each do |k,v| if v.is_a?(Hash) flatten_to_hash(v).each do |hk,hv| flat[k.to_s+'/'+hk.to_s] = hv end else flat[k.to_s] = v end end else ks = hsh.split('/',-1) v = ks.pop ks = ks.join('/') if !flat[ks].nil? if flat[ks].is_a?(Array) flat[ks] << v else flat[ks] = [flat[ks], v] end else flat[ks] = v end end flat end def flatten_to_array(value,a) if value.is_a?(Array) value.each {|e| flatten_to_array(e,a)} elsif value.is_a?(Hash) value.inject([]) {|aa,(k,v)| flatten_to_array(v,[]).each {|vv| aa << k+'/'+vv.to_s}; aa}.each {|e| a << e} else a << value.to_s end a end end end