map 1.7.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README +41 -5
- data/lib/map.rb +215 -5
- data/test/map_test.rb +78 -0
- metadata +5 -5
data/README
CHANGED
@@ -2,10 +2,10 @@ NAME
|
|
2
2
|
map.rb
|
3
3
|
|
4
4
|
SYNOPSIS
|
5
|
-
the ruby container you've always wanted: a string/symbol indifferent
|
6
|
-
hash that works in all rubies
|
5
|
+
the awesome ruby container you've always wanted: a string/symbol indifferent
|
6
|
+
ordered hash that works in all rubies
|
7
7
|
|
8
|
-
maps are
|
8
|
+
maps are bitchin ordered hashes that are both ordered, string/symbol
|
9
9
|
indifferent, and have all sorts of sweetness like recursive conversion, more
|
10
10
|
robust implementation than HashWithIndifferentAccess, support for struct
|
11
11
|
like (map.foo) access, and support for option/keyword access which avoids
|
@@ -93,8 +93,44 @@ DESCRIPTION
|
|
93
93
|
options = Map.options(:read_only => true)
|
94
94
|
read_only = options.getopt(:read_only, :default => false) #=> true
|
95
95
|
|
96
|
-
#
|
96
|
+
# maps support some really nice operators that hashes/orderedhashes do not
|
97
97
|
#
|
98
|
+
m = Map.new
|
99
|
+
m.set(:h, :a, 0, 42)
|
100
|
+
m.has?(:h, :a) #=> true
|
101
|
+
p m #=> {'h' => {'a' => [42]}}
|
102
|
+
m.set(:h, :a, 1, 42.0)
|
103
|
+
p m #=> {'h' => {'a' => [42, 42.0]}}
|
104
|
+
|
105
|
+
m.get(:h, :a, 1) #=> 42.0
|
106
|
+
m.get(:x, :y, :z) #=> nil
|
107
|
+
m[:x][:y][:z] #=> raises exception!
|
108
|
+
|
109
|
+
# they also support some different iteration styles
|
110
|
+
#
|
111
|
+
m = Map.new
|
112
|
+
|
113
|
+
m.set(
|
114
|
+
[:a, :b, :c, 0] => 0,
|
115
|
+
[:a, :b, :c, 1] => 10,
|
116
|
+
[:a, :b, :c, 2] => 20,
|
117
|
+
[:a, :b, :c, 3] => 30
|
118
|
+
)
|
119
|
+
|
120
|
+
m.set(:x, :y, 42)
|
121
|
+
m.set(:x, :z, 42.0)
|
122
|
+
|
123
|
+
m.depth_first_each do |key, val|
|
124
|
+
p key => val
|
125
|
+
end
|
126
|
+
|
127
|
+
#=> [:a, :b, :c, 0] => 0
|
128
|
+
#=> [:a, :b, :c, 1] => 10
|
129
|
+
#=> [:a, :b, :c, 2] => 20
|
130
|
+
#=> [:a, :b, :c, 3] => 30
|
131
|
+
#=> [:x, :y] => 42
|
132
|
+
#=> [:x, :z] => 42.0
|
133
|
+
|
98
134
|
|
99
135
|
USAGE
|
100
|
-
|
136
|
+
see lib/map.rb and test/map_test.rb
|
data/lib/map.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
class Map < Hash
|
2
|
-
Version = '
|
2
|
+
Version = '2.0.0' unless defined?(Version)
|
3
3
|
Load = Kernel.method(:load) unless defined?(Load)
|
4
4
|
|
5
5
|
class << Map
|
@@ -51,6 +51,38 @@ class Map < Hash
|
|
51
51
|
allocate.update(other.to_hash)
|
52
52
|
end
|
53
53
|
|
54
|
+
def conversion_methods
|
55
|
+
@conversion_methods ||= (
|
56
|
+
map_like = ancestors.select{|ancestor| ancestor <= Map}
|
57
|
+
type_names = map_like.map do |ancestor|
|
58
|
+
name = ancestor.name.to_s.strip
|
59
|
+
next if name.empty?
|
60
|
+
name.downcase.gsub(/::/, '_')
|
61
|
+
end.compact
|
62
|
+
type_names.map{|type_name| "to_#{ type_name }"}
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
def add_conversion_method!(method)
|
67
|
+
method = method.to_s.strip
|
68
|
+
raise ArguementError if method.empty?
|
69
|
+
module_eval(<<-__, __FILE__, __LINE__)
|
70
|
+
unless public_method_defined?(#{ method.inspect })
|
71
|
+
def #{ method }
|
72
|
+
self
|
73
|
+
end
|
74
|
+
end
|
75
|
+
unless conversion_methods.include?(#{ method.inspect })
|
76
|
+
conversion_methods.unshift(#{ method.inspect })
|
77
|
+
end
|
78
|
+
__
|
79
|
+
end
|
80
|
+
|
81
|
+
def inherited(other)
|
82
|
+
other.module_eval(&Dynamic)
|
83
|
+
super
|
84
|
+
end
|
85
|
+
|
54
86
|
# iterate over arguments in pairs smartly.
|
55
87
|
#
|
56
88
|
def each_pair(*args)
|
@@ -95,6 +127,13 @@ class Map < Hash
|
|
95
127
|
alias_method '[]', 'new'
|
96
128
|
end
|
97
129
|
|
130
|
+
Dynamic = lambda do
|
131
|
+
conversion_methods.reverse_each do |method|
|
132
|
+
add_conversion_method!(method)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
module_eval(&Dynamic)
|
136
|
+
|
98
137
|
|
99
138
|
# instance constructor
|
100
139
|
#
|
@@ -150,7 +189,10 @@ class Map < Hash
|
|
150
189
|
end
|
151
190
|
|
152
191
|
def convert_value(value)
|
153
|
-
|
192
|
+
conversion_methods.each do |method|
|
193
|
+
return value.send(method) if value.respond_to?(method)
|
194
|
+
end
|
195
|
+
|
154
196
|
case value
|
155
197
|
when Hash
|
156
198
|
klass.coerce(value)
|
@@ -392,10 +434,18 @@ class Map < Hash
|
|
392
434
|
string = '{' + array.join(", ") + '}'
|
393
435
|
end
|
394
436
|
|
395
|
-
#
|
437
|
+
# conversions
|
396
438
|
#
|
397
|
-
def
|
398
|
-
self
|
439
|
+
def conversion_methods
|
440
|
+
self.class.conversion_methods
|
441
|
+
end
|
442
|
+
|
443
|
+
conversion_methods.each do |method|
|
444
|
+
module_eval(<<-__, __FILE__, __LINE__)
|
445
|
+
def #{ method }
|
446
|
+
self
|
447
|
+
end
|
448
|
+
__
|
399
449
|
end
|
400
450
|
|
401
451
|
def to_hash
|
@@ -429,10 +479,20 @@ class Map < Hash
|
|
429
479
|
end
|
430
480
|
alias_method 'to_a', 'to_array'
|
431
481
|
|
482
|
+
def to_list
|
483
|
+
list = []
|
484
|
+
each_pair do |key, val|
|
485
|
+
list[key.to_i] = val if(key.is_a?(Numeric) or key.to_s =~ %r/^\d+$/)
|
486
|
+
end
|
487
|
+
list
|
488
|
+
end
|
489
|
+
|
432
490
|
def to_s
|
433
491
|
to_array.to_s
|
434
492
|
end
|
435
493
|
|
494
|
+
# oh rails - would that map.rb existed before all this non-sense...
|
495
|
+
#
|
436
496
|
def stringify_keys!; self end
|
437
497
|
def stringify_keys; dup end
|
438
498
|
def symbolize_keys!; self end
|
@@ -441,6 +501,156 @@ class Map < Hash
|
|
441
501
|
def to_options; dup end
|
442
502
|
def with_indifferent_access!; self end
|
443
503
|
def with_indifferent_access; dup end
|
504
|
+
|
505
|
+
# a sane method missing that only supports reading previously set values
|
506
|
+
#
|
507
|
+
def method_missing(method, *args, &block)
|
508
|
+
method = method.to_s
|
509
|
+
case method
|
510
|
+
when /=$/
|
511
|
+
key = method.chomp('=')
|
512
|
+
value = args.shift
|
513
|
+
self[key] = value
|
514
|
+
else
|
515
|
+
key = method
|
516
|
+
super unless has_key?(key)
|
517
|
+
self[key]
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
# support for compound key indexing and depth first iteration
|
522
|
+
#
|
523
|
+
def get(*keys)
|
524
|
+
keys = keys.flatten
|
525
|
+
return self[keys.first] if keys.size <= 1
|
526
|
+
keys, key = keys[0..-2], keys[-1]
|
527
|
+
collection = self
|
528
|
+
keys.each do |k|
|
529
|
+
k = alphanumeric_key_for(k)
|
530
|
+
collection = collection[k]
|
531
|
+
return collection unless collection.respond_to?('[]')
|
532
|
+
end
|
533
|
+
collection[alphanumeric_key_for(key)]
|
534
|
+
end
|
535
|
+
|
536
|
+
def has?(*keys)
|
537
|
+
keys = keys.flatten
|
538
|
+
collection = self
|
539
|
+
return collection_has_key?(collection, keys.first) if keys.size <= 1
|
540
|
+
keys, key = keys[0..-2], keys[-1]
|
541
|
+
keys.each do |k|
|
542
|
+
k = alphanumeric_key_for(k)
|
543
|
+
collection = collection[k]
|
544
|
+
return collection unless collection.respond_to?('[]')
|
545
|
+
end
|
546
|
+
return false unless(collection.is_a?(Hash) or collection.is_a?(Array))
|
547
|
+
collection_has_key?(collection, alphanumeric_key_for(key))
|
548
|
+
end
|
549
|
+
|
550
|
+
def collection_has_key?(collection, key)
|
551
|
+
case collection
|
552
|
+
when Hash
|
553
|
+
collection.has_key?(key)
|
554
|
+
when Array
|
555
|
+
return false unless key
|
556
|
+
(0...collection.size).include?(Integer(key))
|
557
|
+
end
|
558
|
+
end
|
559
|
+
|
560
|
+
def set(*args)
|
561
|
+
if args.size == 1 and args.first.is_a?(Hash)
|
562
|
+
options = args.shift
|
563
|
+
else
|
564
|
+
options = {}
|
565
|
+
value = args.pop
|
566
|
+
keys = args
|
567
|
+
options[keys] = value
|
568
|
+
end
|
569
|
+
|
570
|
+
options.each do |keys, value|
|
571
|
+
keys = Array(keys).flatten
|
572
|
+
|
573
|
+
collection = self
|
574
|
+
if keys.size <= 1
|
575
|
+
collection[keys.first] = value
|
576
|
+
next
|
577
|
+
end
|
578
|
+
|
579
|
+
key = nil
|
580
|
+
|
581
|
+
keys.each_cons(2) do |a, b|
|
582
|
+
a, b = alphanumeric_key_for(a), alphanumeric_key_for(b)
|
583
|
+
|
584
|
+
case b
|
585
|
+
when Numeric
|
586
|
+
collection[a] ||= []
|
587
|
+
raise(IndexError, "(#{ collection.inspect })[#{ a.inspect }]=#{ value.inspect }") unless collection[a].is_a?(Array)
|
588
|
+
|
589
|
+
when String, Symbol
|
590
|
+
collection[a] ||= {}
|
591
|
+
raise(IndexError, "(#{ collection.inspect })[#{ a.inspect }]=#{ value.inspect }") unless collection[a].is_a?(Hash)
|
592
|
+
end
|
593
|
+
collection = collection[a]
|
594
|
+
key = b
|
595
|
+
end
|
596
|
+
|
597
|
+
collection[key] = value
|
598
|
+
end
|
599
|
+
|
600
|
+
return options.values
|
601
|
+
end
|
602
|
+
|
603
|
+
def Map.alphanumeric_key_for(key)
|
604
|
+
return key if Numeric===key
|
605
|
+
key.to_s =~ %r/^\d+$/ ? Integer(key) : key
|
606
|
+
end
|
607
|
+
|
608
|
+
def alphanumeric_key_for(key)
|
609
|
+
Map.alphanumeric_key_for(key)
|
610
|
+
end
|
611
|
+
|
612
|
+
def Map.depth_first_each(enumerable, path = [], accum = [], &block)
|
613
|
+
Map.pairs_for(enumerable) do |key, val|
|
614
|
+
path.push(key)
|
615
|
+
if((val.is_a?(Hash) or val.is_a?(Array)) and not val.empty?)
|
616
|
+
Map.depth_first_each(val, path, accum)
|
617
|
+
else
|
618
|
+
accum << [path.dup, val]
|
619
|
+
end
|
620
|
+
path.pop()
|
621
|
+
end
|
622
|
+
if block
|
623
|
+
accum.each{|keys, val| block.call(keys, val)}
|
624
|
+
else
|
625
|
+
[path, accum]
|
626
|
+
end
|
627
|
+
end
|
628
|
+
|
629
|
+
def Map.pairs_for(enumerable, *args, &block)
|
630
|
+
if block.nil?
|
631
|
+
pairs, block = [], lambda{|*pair| pairs.push(pair)}
|
632
|
+
else
|
633
|
+
pairs = false
|
634
|
+
end
|
635
|
+
|
636
|
+
result =
|
637
|
+
case enumerable
|
638
|
+
when Hash
|
639
|
+
enumerable.each_pair(*args, &block)
|
640
|
+
when Array
|
641
|
+
enumerable.each_with_index(*args) do |val, key|
|
642
|
+
block.call(key, val)
|
643
|
+
end
|
644
|
+
else
|
645
|
+
enumerable.each_pair(*args, &block)
|
646
|
+
end
|
647
|
+
|
648
|
+
pairs ? pairs : result
|
649
|
+
end
|
650
|
+
|
651
|
+
def depth_first_each(*args, &block)
|
652
|
+
Map.depth_first_each(enumerable=self, *args, &block)
|
653
|
+
end
|
444
654
|
end
|
445
655
|
|
446
656
|
module Kernel
|
data/test/map_test.rb
CHANGED
@@ -204,6 +204,32 @@ Testing Map do
|
|
204
204
|
assert{ o.is_a?(d) }
|
205
205
|
end
|
206
206
|
|
207
|
+
testing 'that subclassing creates custom conversion methods' do
|
208
|
+
c = Class.new(Map) do
|
209
|
+
def self.name()
|
210
|
+
:C
|
211
|
+
end
|
212
|
+
end
|
213
|
+
assert{ c.conversion_methods.map{|x| x.to_s} == %w( to_c to_map ) }
|
214
|
+
o = c.new
|
215
|
+
assert{ o.respond_to?(:to_map) }
|
216
|
+
assert{ o.respond_to?(:to_c) }
|
217
|
+
|
218
|
+
assert{ o.update(:a => {:b => :c}) }
|
219
|
+
assert{ o[:a].class == c }
|
220
|
+
end
|
221
|
+
|
222
|
+
testing 'that custom conversion methods can be added' do
|
223
|
+
c = Class.new(Map)
|
224
|
+
o = c.new
|
225
|
+
foobar = {:k => :v}
|
226
|
+
def foobar.to_foobar() self end
|
227
|
+
c.add_conversion_method!('to_foobar')
|
228
|
+
assert{ c.conversion_methods.map{|x| x.to_s} == %w( to_foobar to_map ) }
|
229
|
+
o[:foobar] = foobar
|
230
|
+
assert{ o[:foobar] == foobar }
|
231
|
+
end
|
232
|
+
|
207
233
|
testing 'that map supports basic option parsing for methods' do
|
208
234
|
%w( options_for options opts ).each do |method|
|
209
235
|
args = [0,1, {:k => :v, :a => false}]
|
@@ -223,6 +249,58 @@ Testing Map do
|
|
223
249
|
end
|
224
250
|
end
|
225
251
|
|
252
|
+
testing 'that maps can be converted to lists with numeric indexes' do
|
253
|
+
m = Map[0, :a, 1, :b, 2, :c]
|
254
|
+
assert{ m.to_list == [:a, :b, :c] }
|
255
|
+
end
|
256
|
+
|
257
|
+
testing 'that method missing hacks allow setting values, but not getting them until they are set' do
|
258
|
+
m = Map.new
|
259
|
+
assert{ (m.key rescue $!).is_a?(Exception) }
|
260
|
+
assert{ m.key = :val }
|
261
|
+
assert{ m[:key] == :val }
|
262
|
+
assert{ m.key == :val }
|
263
|
+
end
|
264
|
+
|
265
|
+
testing 'that maps support compound key/val setting' do
|
266
|
+
m = Map.new
|
267
|
+
assert{ m.set(:a, :b, :c, 42) }
|
268
|
+
assert{ m[:a][:b][:c] == 42 }
|
269
|
+
assert{ m.get(:a, :b, :c) == 42 }
|
270
|
+
assert{ m.set([:x, :y, :z] => 42.0, [:A, 2] => 'forty-two') }
|
271
|
+
assert{ m[:A].is_a?(Array) }
|
272
|
+
assert{ m[:A].size == 3}
|
273
|
+
assert{ m[:A][2] == 'forty-two' }
|
274
|
+
assert{ m[:x][:y].is_a?(Hash) }
|
275
|
+
assert{ m[:x][:y][:z] == 42.0 }
|
276
|
+
end
|
277
|
+
|
278
|
+
testing 'that maps support depth_first_each' do
|
279
|
+
m = Map.new
|
280
|
+
prefix = %w[ a b c ]
|
281
|
+
keys = []
|
282
|
+
n = 0.42
|
283
|
+
|
284
|
+
10.times do |i|
|
285
|
+
key = prefix + [i]
|
286
|
+
val = n
|
287
|
+
keys.push(key)
|
288
|
+
assert{ m.set(key => val) }
|
289
|
+
n *= 10
|
290
|
+
end
|
291
|
+
|
292
|
+
assert{ m.get(:a).is_a?(Hash) }
|
293
|
+
assert{ m.get(:a, :b).is_a?(Hash) }
|
294
|
+
assert{ m.get(:a, :b, :c).is_a?(Array) }
|
295
|
+
|
296
|
+
n = 0.42
|
297
|
+
m.depth_first_each do |key, val|
|
298
|
+
assert{ key == keys.shift }
|
299
|
+
assert{ val == n }
|
300
|
+
n *= 10
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
226
304
|
protected
|
227
305
|
def new_int_map(n = 1024)
|
228
306
|
map = assert{ Map.new }
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: map
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 15
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
|
-
-
|
8
|
-
- 7
|
7
|
+
- 2
|
9
8
|
- 0
|
10
|
-
|
9
|
+
- 0
|
10
|
+
version: 2.0.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Ara T. Howard
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2010-12-
|
18
|
+
date: 2010-12-24 00:00:00 -07:00
|
19
19
|
default_executable:
|
20
20
|
dependencies: []
|
21
21
|
|