1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 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
|