map 1.7.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
|