239 lines
6.8 KiB
Ruby
239 lines
6.8 KiB
Ruby
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
|