This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
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 <gems@behindlogic.com>.
|
||||
#
|
||||
# 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
|
||||
Reference in New Issue
Block a user