darthjee-core_ext 1.2.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +5 -0
  5. data/Gemfile.lock +62 -0
  6. data/LICENSE +22 -0
  7. data/README.md +188 -0
  8. data/Rakefile +7 -0
  9. data/core_ext.gemspec +27 -0
  10. data/lib/array.rb +23 -0
  11. data/lib/array/hash_builder.rb +20 -0
  12. data/lib/darthjee.rb +4 -0
  13. data/lib/darthjee/core_ext.rb +12 -0
  14. data/lib/darthjee/core_ext/version.rb +5 -0
  15. data/lib/enumerable.rb +45 -0
  16. data/lib/hash.rb +195 -0
  17. data/lib/hash/deep_hash_constructor.rb +83 -0
  18. data/lib/hash/key_changer.rb +76 -0
  19. data/lib/hash/value_changer.rb +61 -0
  20. data/lib/numeric.rb +6 -0
  21. data/lib/symbol.rb +9 -0
  22. data/spec/lib/array_spec.rb +229 -0
  23. data/spec/lib/enumerable_spec.rb +31 -0
  24. data/spec/lib/hash/deep_hash_constructor_spec.rb +167 -0
  25. data/spec/lib/hash/key_changer_spec.rb +55 -0
  26. data/spec/lib/hash_spec.rb +347 -0
  27. data/spec/lib/numeric_spec.rb +61 -0
  28. data/spec/lib/symbol_spec.rb +35 -0
  29. data/spec/spec_helper.rb +32 -0
  30. data/spec/support/models/hash/value_changer/dummy.rb +15 -0
  31. data/spec/support/models/hash/value_changer/dummy_iteractor.rb +12 -0
  32. data/spec/support/shared_examples/array_random.rb +16 -0
  33. data/spec/support/shared_examples/chain_fetch.rb +88 -0
  34. data/spec/support/shared_examples/chain_hash_keys_changer.rb +88 -0
  35. data/spec/support/shared_examples/clean.rb +143 -0
  36. data/spec/support/shared_examples/expected.rb +6 -0
  37. data/spec/support/shared_examples/hash_keys_changer.rb +85 -0
  38. data/spec/support/shared_examples/keys_appender.rb +43 -0
  39. data/spec/support/shared_examples/keys_camelizer.rb +285 -0
  40. data/spec/support/shared_examples/keys_underscorer.rb +82 -0
  41. data/spec/support/shared_examples/remap.rb +89 -0
  42. data/spec/support/shared_examples/value_changer.rb +92 -0
  43. metadata +217 -0
@@ -0,0 +1,195 @@
1
+ require 'hash/value_changer'
2
+ require 'hash/deep_hash_constructor'
3
+ require 'hash/key_changer'
4
+
5
+ class Hash
6
+ def chain_fetch(*keys)
7
+ value = self
8
+
9
+ if block_given?
10
+ value = value.fetch(keys.shift) do |*args|
11
+ missed_keys = keys
12
+ keys = []
13
+ yield(*(args + [missed_keys]))
14
+ end until keys.empty?
15
+ else
16
+ value = value.fetch(keys.shift) until keys.empty?
17
+ end
18
+
19
+ value
20
+ end
21
+
22
+ def squash
23
+ {}.tap do |hash|
24
+ each do |key, value|
25
+ if value.is_a? Hash
26
+ value.squash.each do |k, v|
27
+ new_key = [key, k].join('.')
28
+ hash[new_key] = v
29
+ end
30
+ else
31
+ hash[key] = value
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ def map_to_hash
38
+ {}.tap do |hash|
39
+ each do |k, v|
40
+ hash[k] = yield(k, v)
41
+ end
42
+ end
43
+ end
44
+
45
+ def remap_keys(remap)
46
+ dup.remap_keys!(remap)
47
+ end
48
+
49
+ def remap_keys!(remap)
50
+ new_hash = {}
51
+ remap.each do |o, n|
52
+ new_hash[n] = delete o
53
+ end
54
+ merge! new_hash
55
+ end
56
+
57
+ def lower_camelize_keys(options = {})
58
+ dup.lower_camelize_keys!(options)
59
+ end
60
+
61
+ def lower_camelize_keys!(options = {})
62
+ options = options.merge({ uppercase_first_letter: false })
63
+
64
+ camelize_keys!(options)
65
+ end
66
+
67
+ def camelize_keys(options = {})
68
+ dup.camelize_keys!(options)
69
+ end
70
+
71
+ def camelize_keys!(options = {})
72
+ Hash::KeyChanger.new(self).camelize_keys(options)
73
+ end
74
+
75
+ def underscore_keys(options = {})
76
+ dup.underscore_keys!(options)
77
+ end
78
+
79
+ def underscore_keys!(options = {})
80
+ Hash::KeyChanger.new(self).underscore_keys(options)
81
+ end
82
+
83
+ def exclusive_merge(hash)
84
+ dup.exclusive_merge!(hash)
85
+ end
86
+
87
+ def exclusive_merge!(hash)
88
+ merge!(hash.slice(*keys))
89
+ end
90
+
91
+ # change all keys returning the new map
92
+ # options: { recursive: true }
93
+ # ex: { "a" =>1 }.change_keys{ |key| key.upcase } == { "A" => 1 }
94
+ def change_keys(options = {}, &block)
95
+ deep_dup.change_keys!(options, &block)
96
+ end
97
+
98
+ # change all keys returning the new map
99
+ # options: { recursive: true }
100
+ # ex: { "a":1 }.change_keys{ |key| key.upcase } == { "A":1 }
101
+ def change_keys!(options = {}, &block)
102
+ Hash::KeyChanger.new(self).change_keys(options, &block)
103
+ end
104
+
105
+ # change all publicaly sending method calls
106
+ # options: { recursive: true }
107
+ # ex: { a: 1 }.chain_change_keys(:to_s, :upcase) == { "A" =>1 }
108
+ def chain_change_keys(*calls)
109
+ deep_dup.chain_change_keys!(*calls)
110
+ end
111
+
112
+ # change all publicaly sending method calls
113
+ # options: { recursive: true }
114
+ # ex: { a: 1 }.chain_change_keys(:to_s, :upcase) == { "A" =>1 }
115
+ def chain_change_keys!(*calls)
116
+ options = calls.extract_options!
117
+
118
+ calls.inject(self) do |h, m|
119
+ h.change_keys!(options, &m)
120
+ end
121
+ end
122
+
123
+ # prepend a string to all keys
124
+ # options {
125
+ # recursive: true,
126
+ # type: :keep [keep, string, symbol] (key type to be returned)
127
+ # }
128
+ # ex: { :a => 1, "b"=> 2 }.prepend_to_keys("foo_") == { :foo_a => 1, "foo_b"=> 2 }
129
+ def prepend_to_keys(str, options = {})
130
+ change_key_text(options) do |key|
131
+ "#{str}#{key}"
132
+ end
133
+ end
134
+
135
+ # append a string to all keys
136
+ # options {
137
+ # recursive: true,
138
+ # type: :keep [keep, string, symbol] (key type to be returned)
139
+ # }
140
+ # ex: { :a => 1, "b"=> 2 }.append_to_keys("_bar") == { :a_bar => 1, "b_bar"=> 2 }
141
+ def append_to_keys(str, options = {})
142
+ change_key_text(options) do |key|
143
+ "#{key}#{str}"
144
+ end
145
+ end
146
+
147
+ # sorts keys for hash
148
+ # options: { recursive: true }
149
+ # ex: { b:1, a:2 }.sort_keys == { a:2, b:1 }
150
+ def sort_keys(options = {})
151
+ options = {
152
+ recursive: true
153
+ }.merge(options)
154
+
155
+ {}.tap do |hash|
156
+ keys.sort.each do |key|
157
+ value = self[key]
158
+ hash[key] = value unless value.is_a?(Hash) && options[:recursive]
159
+ hash[key] = value.sort_keys(options) if value.is_a?(Hash) && options[:recursive]
160
+ end
161
+ end
162
+ end
163
+
164
+ # creates a new hash with changes in its values
165
+ # options: {
166
+ # recursive: true,
167
+ # skip_hash:true
168
+ # }
169
+ # ex: { a:1, b:2 }.change_values{ |v| v+1 } == { a:2, b:3 }
170
+ # ex: { a:1, b:{ c:1 } }.change_values(skip_hash:false) { |v| v.to_s } == { a:"1", b:"{ c=>1 }
171
+ # ex: { a:1, b:{ c:1 } }.change_values(skip_hash:true) { |v| v.to_s } == { a:"1", b:{ c=>"1" } }
172
+ def change_values(options = {}, &block)
173
+ deep_dup.change_values!(options, &block)
174
+ end
175
+
176
+ def change_values!(options = {}, &block)
177
+ Hash::ValueChanger.new(options, &block).change(self)
178
+ end
179
+
180
+ def to_deep_hash(separator = '.')
181
+ Hash::DeepHashConstructor.new(separator).deep_hash(self)
182
+ end
183
+
184
+ private
185
+
186
+ # changes the text of the keys
187
+ # options {
188
+ # recursive: true,
189
+ # type: :keep [keep, string, symbol] (key type to be returned)
190
+ # }
191
+ # ex: { :a => 1, "b"=> 2 }.change_key_text{ |key| key.upcase } == { :A => 1, "B"=> 2 }
192
+ def change_key_text(options = {}, &block)
193
+ Hash::KeyChanger.new(self).change_text(options, &block)
194
+ end
195
+ end
@@ -0,0 +1,83 @@
1
+ class Hash::DeepHashConstructor
2
+ attr_accessor :separator
3
+
4
+ def initialize(separator)
5
+ @separator = separator
6
+ end
7
+
8
+ def deep_hash(object)
9
+ if object.is_a? Array
10
+ array_deep_hash(object)
11
+ elsif object.is_a? Hash
12
+ hash_deep_hash(object)
13
+ else
14
+ object
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def array_deep_hash(array)
21
+ array.map { |v| v.is_a?(Hash) ? deep_hash(v) : v }
22
+ end
23
+
24
+ def hash_deep_hash(hash)
25
+ {}.tap do |new_hash|
26
+ hash.each do |k, v|
27
+ base_key, child_key = split_key(k, separator)
28
+ set_deep_hash_positioned_value(new_hash, base_key, v, child_key)
29
+ end
30
+
31
+ new_hash.each do |k, v|
32
+ new_hash[k] = deep_hash(v)
33
+ end
34
+ end
35
+ end
36
+
37
+ def split_key(key, separator)
38
+ separator_rxp = (separator == '.') ? "\\#{separator}" : separator
39
+ skipper = "[^#{separator}]"
40
+ regexp = Regexp.new("^(#{skipper}*)#{separator_rxp}(.*)")
41
+ match = key.match(regexp)
42
+
43
+ match ? match[1..2] : key
44
+ end
45
+
46
+ def set_deep_hash_array_value(hash, base_key, index, value, key = nil)
47
+ key_without_index = base_key.gsub("[#{index}]", '')
48
+ hash[key_without_index] ||= []
49
+
50
+ if key.nil?
51
+ hash[key_without_index][index] = value
52
+ else
53
+ hash[key_without_index][index] ||= {}
54
+ hash[key_without_index][index][key] = value
55
+ end
56
+ end
57
+
58
+ def set_deep_hash_positioned_value(new_hash, base_key, v, child_key)
59
+ index = array_index(base_key)
60
+
61
+ if index
62
+ set_deep_hash_array_value(new_hash, base_key, index, v, child_key)
63
+ else
64
+ set_deep_hash_value(new_hash, base_key, v, child_key)
65
+ end
66
+ end
67
+
68
+ def array_index(key)
69
+ match = key.match(/\[([^)]+)\]/)
70
+ if match
71
+ match[1].to_i
72
+ end
73
+ end
74
+
75
+ def set_deep_hash_value(hash, base_key, value, key = nil)
76
+ if key.nil?
77
+ hash[base_key] = value
78
+ else
79
+ hash[base_key] ||= {}
80
+ hash[base_key][key] = value
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,76 @@
1
+ class Hash::KeyChanger
2
+ attr_reader :hash, :block
3
+
4
+ def initialize(hash)
5
+ @hash = hash
6
+ end
7
+
8
+ def change_keys(settings = {}, &block)
9
+ merge_options({
10
+ recursive: true
11
+ }, settings)
12
+
13
+ if options[:recursive]
14
+ hash.deep_transform_keys!(&block)
15
+ else
16
+ hash.transform_keys!(&block)
17
+ end
18
+ end
19
+
20
+ def camelize_keys(settings = {})
21
+ merge_options({
22
+ uppercase_first_letter: true
23
+ }, settings)
24
+
25
+ type = options[:uppercase_first_letter] ? :upper : :lower
26
+
27
+ change_keys do |k|
28
+ k.camelize(type)
29
+ end
30
+ end
31
+
32
+ def underscore_keys(settings = {})
33
+ merge_options({}, settings)
34
+
35
+ change_keys do |k|
36
+ k.underscore
37
+ end
38
+ end
39
+
40
+ def change_text(options = {}, &block)
41
+ merge_options({
42
+ type: :keep
43
+ }, options)
44
+
45
+ change_keys do |key|
46
+ cast_new_key block.call(key), key.class
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def merge_options(default, custom)
53
+ @options = {}.merge!(default).merge!(custom).merge!(options)
54
+ end
55
+
56
+ def options
57
+ @options ||= {}
58
+ end
59
+
60
+ def cast_new_key(key, old_clazz)
61
+ case class_cast(old_clazz)
62
+ when :symbol then
63
+ key.to_sym
64
+ when :string then
65
+ key.to_s
66
+ end
67
+ end
68
+
69
+ def keep_class?
70
+ options[:type] == :keep
71
+ end
72
+
73
+ def class_cast(old_clazz)
74
+ keep_class? && old_clazz.to_s.downcase.to_sym || options[:type]
75
+ end
76
+ end
@@ -0,0 +1,61 @@
1
+ class Hash::ValueChanger
2
+ attr_accessor :options, :block
3
+
4
+ def initialize(options, &block)
5
+ @options = {
6
+ recursive: true,
7
+ skip_inner: true
8
+ }.merge(options)
9
+
10
+ @block = block
11
+ end
12
+
13
+ def change(object)
14
+ if object.respond_to?(:change_values)
15
+ change_hash(object)
16
+ elsif is_iterable?(object)
17
+ change_array(object)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def change_hash(original_hash)
24
+ original_hash.tap do |hash|
25
+ original_hash.each do |key, value|
26
+ value = new_value(value)
27
+ hash[key] = value
28
+ end
29
+ end
30
+ end
31
+
32
+ def change_array(array)
33
+ array = array.to_a
34
+
35
+ array.each.with_index do |value, index|
36
+ if value.respond_to?(:change_values)
37
+ value = value.change_values(options, &block)
38
+ elsif is_iterable?(value)
39
+ value = change_array(value)
40
+ end
41
+ array[index] = value
42
+ end
43
+ end
44
+
45
+ def change_value?(value)
46
+ !is_iterable?(value) || !options[:skip_inner]
47
+ end
48
+
49
+ def is_iterable?(value)
50
+ value.respond_to?(:each)
51
+ end
52
+
53
+ def new_value(value)
54
+ value = block.call(value) if change_value?(value)
55
+ apply_recursion?(value) ? change(value) : value
56
+ end
57
+
58
+ def apply_recursion?(value)
59
+ is_iterable?(value) && options[:recursive]
60
+ end
61
+ end
@@ -0,0 +1,6 @@
1
+ class Numeric
2
+ def percent_of(n)
3
+ return Float::INFINITY if n == 0
4
+ (to_f / n.to_f) * 100.0
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ class Symbol
2
+ def camelize(type = :upper)
3
+ to_s.camelize(type).to_sym
4
+ end
5
+
6
+ def underscore
7
+ to_s.underscore.to_sym
8
+ end
9
+ end
@@ -0,0 +1,229 @@
1
+ require 'spec_helper'
2
+
3
+ describe Array do
4
+ describe '#chain_map' do
5
+ let(:array) { [ :a, :long_name, :sym ] }
6
+ let(:mapped) { array.chain_map(:to_s, :size, :to_s) }
7
+
8
+ it 'calls each argument as method of the mapped result' do
9
+ expect(mapped).to eq([ '1', '9', '3' ])
10
+ end
11
+
12
+ context 'when an extra block is given' do
13
+ let(:mapped) do
14
+ array.chain_map(:to_s, :size) do |v|
15
+ "final: #{v}"
16
+ end
17
+ end
18
+
19
+ it 'calls each argument as method of the mapped result' do
20
+ expect(mapped).to eq([ 'final: 1', 'final: 9', 'final: 3' ])
21
+ end
22
+ end
23
+ end
24
+
25
+ describe '#as_hash' do
26
+ let(:array) { [1, 2, 3] }
27
+ let(:keys) { %w(a b c) }
28
+ let(:expected) { { 'a' => 1, 'b' => 2, 'c' => 3 } }
29
+
30
+ it 'creates a hash using the array as value and the argument as keys' do
31
+ expect(array.as_hash(keys)).to eq(expected)
32
+ end
33
+
34
+ context 'when there are more keys than values' do
35
+ let(:keys) { %w(a b c d e f) }
36
+ let(:expected) { { 'a' => 1, 'b' => 2, 'c' => 3, 'd' => nil, 'e' => nil, 'f' => nil } }
37
+
38
+ it 'creates a hash with nil values for the extra keys' do
39
+ expect(array.as_hash(keys)).to eq(expected)
40
+ end
41
+
42
+ it { expect { array.as_hash(keys) }.not_to change { keys } }
43
+ it { expect { array.as_hash(keys) }.not_to change { array } }
44
+ end
45
+
46
+ context 'when there are more values than keys' do
47
+ let(:array) { [1, 2, 3, 4, 5, 6, 7] }
48
+
49
+ it { expect { array.as_hash(keys) }.to raise_error(IndexError) }
50
+
51
+ it { expect { array.as_hash(keys) rescue nil }.not_to change { keys } }
52
+ it { expect { array.as_hash(keys) rescue nil }.not_to change { array } }
53
+ end
54
+ end
55
+
56
+ describe '#map_and_find' do
57
+ let(:array) { [1, 2, 3, 4] }
58
+ let(:value) { array.map_and_find(&block) }
59
+
60
+ context 'when block returns nil' do
61
+ let(:block) { proc {} }
62
+ it { expect(value).to be_nil }
63
+ end
64
+
65
+ context 'when block returns false' do
66
+ let(:block) { proc { false } }
67
+ it { expect(value).to be_nil }
68
+ end
69
+
70
+ context 'when block returns a true evaluated value' do
71
+ let(:block) { proc(&:to_s) }
72
+
73
+ it { expect(value).to eq('1') }
74
+
75
+ context 'but not for the first value' do
76
+ let(:transformer) { double(:transformer) }
77
+ let(:block) { proc { |v| transformer.transform(v) } }
78
+
79
+ before do
80
+ allow(transformer).to receive(:transform) do |v|
81
+ v.to_s if v > 1
82
+ end
83
+ value
84
+ end
85
+
86
+ it { expect(value).to eq('2') }
87
+ it 'calls the mapping only until it returns a valid value' do
88
+ expect(transformer).to have_received(:transform).exactly(2)
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ describe '#random' do
95
+ it_behaves_like 'a method that returns a random element', :random
96
+ end
97
+
98
+ describe '#random!' do
99
+ it_behaves_like 'a method that returns a random element', :random!
100
+
101
+ let(:array) { [ 8,4,2 ] }
102
+
103
+ it 'removes an the returned element' do
104
+ expect do
105
+ array.random!
106
+ end.to change { array.size }.by(-1)
107
+ end
108
+ end
109
+
110
+ describe '#map_and_select' do
111
+ let(:array) { [1, 2, 3, 4].map { |i| { value: i} } }
112
+ let(:filtered) { array.map_and_select(&block) }
113
+
114
+ context 'when block returns nil' do
115
+ let(:block) { proc {} }
116
+ it { expect(filtered).to be_empty }
117
+ end
118
+
119
+ context 'when block returns false' do
120
+ let(:block) { proc { false } }
121
+ it { expect(filtered).to be_empty }
122
+ end
123
+
124
+ context 'when block returns a true evaluated value' do
125
+ let(:block) { proc(&:to_s) }
126
+
127
+ it { expect(filtered).to eq(array.map(&:to_s)) }
128
+
129
+ context 'but not for the first value' do
130
+ let(:transformer) { double(:transformer) }
131
+ let(:block) { proc { |v| transformer.transform(v) } }
132
+
133
+ before do
134
+ allow(transformer).to receive(:transform) do |v|
135
+ v.to_s if v[:value] > 1
136
+ end
137
+ filtered
138
+ end
139
+
140
+ it { expect(filtered).to eq(array[1..-1].map(&:to_s)) }
141
+
142
+ it 'calls the mapping only once per element' do
143
+ expect(transformer).to have_received(:transform).exactly(4)
144
+ end
145
+ end
146
+ end
147
+ end
148
+
149
+ describe '#map_to_hash' do
150
+ context 'whe subject is an array' do
151
+ let(:subject) { %w(word1 wooord2) }
152
+ let(:mapping_block) { proc{ |word| word.length } }
153
+ let(:mapped) { subject.map_to_hash(&mapping_block) }
154
+ let(:expected) { { 'word1' => 5, 'wooord2' => 7 } }
155
+
156
+ it { expect(mapped).to be_a(Hash) }
157
+
158
+ it 'has the original array as keys' do
159
+ expect(mapped.keys).to eq(subject)
160
+ end
161
+
162
+ it 'has the mapped values as values' do
163
+ expect(mapped.values).to eq(subject.map(&mapping_block))
164
+ end
165
+
166
+ it 'correctly map keys to value' do
167
+ expect(mapped).to eq(expected)
168
+ end
169
+
170
+ context 'whe subject is an array' do
171
+ let(:subject) { [%w(w1), %w(w2 w3)] }
172
+ let(:mapped) { subject.map_to_hash(&mapping_block) }
173
+ let(:expected) { { %w(w1) => 1, %w(w2 w3) => 2 } }
174
+
175
+
176
+ it 'has the original array as keys' do
177
+ expect(mapped.keys).to eq(subject)
178
+ end
179
+
180
+ it 'has the mapped values as values' do
181
+ expect(mapped.values).to eq(subject.map(&mapping_block))
182
+ end
183
+
184
+ it 'correctly map keys to value' do
185
+ expect(mapped).to eq(expected)
186
+ end
187
+ end
188
+ end
189
+
190
+ context 'whe subject is a hash' do
191
+ let(:subject) { { a: 1, b: 2 } }
192
+ let(:mapping_block) { proc{ |k, v| "#{k}_#{v}" } }
193
+ let(:mapped) { subject.map_to_hash(&mapping_block) }
194
+ let(:expected) { { a: 'a_1', b: 'b_2' } }
195
+
196
+ it { expect(mapped).to be_a(Hash) }
197
+
198
+ it 'has the original keys as keys' do
199
+ expect(mapped.keys).to eq(subject.keys)
200
+ end
201
+
202
+ it 'has the mapped values as values' do
203
+ expect(mapped.values).to eq(subject.map(&mapping_block))
204
+ end
205
+
206
+ it 'correctly map keys to value' do
207
+ expect(mapped).to eq(expected)
208
+ end
209
+
210
+ context 'when hash uses arrays for keys' do
211
+ let(:subject) { { [:a, :b] => 1, [:c, :d] => 2 } }
212
+ let(:mapping_block) { proc{ |k, v| "#{k.join('_')}_#{v}" } }
213
+ let(:expected) { { [:a, :b]=> 'a_b_1', [:c, :d] => 'c_d_2' } }
214
+
215
+ it 'has the original keys as keys' do
216
+ expect(mapped.keys).to eq(subject.keys)
217
+ end
218
+
219
+ it 'has the mapped values as values' do
220
+ expect(mapped.values).to eq(subject.map(&mapping_block))
221
+ end
222
+
223
+ it 'correctly map keys to value' do
224
+ expect(mapped).to eq(expected)
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end