map 1.7.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/README +41 -5
  2. data/lib/map.rb +215 -5
  3. data/test/map_test.rb +78 -0
  4. 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 ordered
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 rad ordered hashes that are both ordered, string/symbol
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
- # tons more goodness, see test/map_test.rb
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
- again, see test/map_test.rb
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 = '1.7.0' unless defined?(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
- return value.to_map if value.respond_to?(:to_map)
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
- # converions
437
+ # conversions
396
438
  #
397
- def to_map
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
@@ -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: 11
4
+ hash: 15
5
5
  prerelease: false
6
6
  segments:
7
- - 1
8
- - 7
7
+ - 2
9
8
  - 0
10
- version: 1.7.0
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-23 00:00:00 -07:00
18
+ date: 2010-12-24 00:00:00 -07:00
19
19
  default_executable:
20
20
  dependencies: []
21
21