hashmodel 0.1.1 → 0.2.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/Gemfile +5 -10
- data/Gemfile.lock +13 -14
- data/README.markdown +11 -2
- data/Rakefile +3 -8
- data/_brainstorm/hash_test.rb +169 -0
- data/_brainstorm/instance_vars.rb +24 -0
- data/_brainstorm/ref_val.rb +16 -0
- data/_brainstorm/spliting.rb +46 -0
- data/_brainstorm/test.rb +27 -0
- data/features/README +1 -1
- data/features/hash_model_flatten.feature +5 -5
- data/features/hash_model_search.feature +2 -2
- data/features/step_definitions/hash_model_steps.rb +2 -2
- data/lib/hash_model/exceptions.rb +1 -1
- data/lib/hash_model/hash_model.rb +455 -363
- data/lib/hash_model/version.rb +9 -11
- data/spec/hash_model/hash_model_spec.rb +216 -119
- metadata +14 -18
@@ -3,7 +3,7 @@ Given /^we have a test table$/ do |table|
|
|
3
3
|
end
|
4
4
|
|
5
5
|
Given /^we have a HashModel instance$/ do
|
6
|
-
@hm =
|
6
|
+
@hm = HashModel.new
|
7
7
|
end
|
8
8
|
|
9
9
|
When /^the HashModel is populated with the test table$/ do
|
@@ -35,7 +35,7 @@ end
|
|
35
35
|
|
36
36
|
Then /^all the siblings should have the same group id$/ do
|
37
37
|
group_ids = []
|
38
|
-
@siblings.each {|record| group_ids << record[:
|
38
|
+
@siblings.each {|record| group_ids << record[:_group_id]}
|
39
39
|
group_ids.uniq.length.should == 1
|
40
40
|
group_ids.uniq[0].should_not == nil
|
41
41
|
end
|
@@ -1,411 +1,503 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
def initialize(parameters={})
|
9
|
-
# Initialize variables
|
10
|
-
clear
|
11
|
-
|
12
|
-
# Map Array methods
|
13
|
-
mimic_methods
|
14
|
-
|
15
|
-
# Set values given as hashes
|
16
|
-
parameters.each { |key,value| instance_variable_set("@#{key}", value) }
|
1
|
+
# A simple MVC type model class for storing hashes as flattenable, searchable records
|
2
|
+
class HashModel
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
def initialize(parameters={})
|
6
|
+
# Initialize variables
|
7
|
+
clear
|
17
8
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
9
|
+
# Map Array methods
|
10
|
+
mimic_methods
|
11
|
+
|
12
|
+
# Set values given as hashes
|
13
|
+
parameters.each { |key,value| instance_variable_set("@#{key}", value) }
|
14
|
+
|
15
|
+
# Allow a single hash to be added with :raw_data
|
16
|
+
@raw_data = [@raw_data] if @raw_data.class == Hash
|
17
|
+
|
18
|
+
check_field_names(@raw_data) if !@raw_data.empty?
|
19
|
+
|
20
|
+
# Setup the flat data
|
21
|
+
flatten
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
## Properties
|
26
|
+
|
27
|
+
attr_accessor :flatten_index, :raw_data
|
28
|
+
|
29
|
+
# Sets field name used to flatten the recordset
|
30
|
+
def flatten_index=(value)
|
31
|
+
@flatten_index = value
|
32
|
+
flatten
|
33
|
+
end
|
34
|
+
|
35
|
+
# Are the records being filtered?
|
36
|
+
def filtered?
|
37
|
+
!!@filter
|
38
|
+
end
|
39
|
+
|
40
|
+
# Trap changes to raw data so we can re-flatten the data
|
41
|
+
def raw_data=(value)
|
42
|
+
value = [] if value.nil?
|
43
|
+
raise SyntaxError, "Raw data may only be an array of hashes" if value.class != Array
|
44
|
+
check_field_names(value)
|
45
|
+
@raw_data = value.clone
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
## Public Methods
|
50
|
+
|
51
|
+
# Freeze all the data properties
|
52
|
+
def freeze
|
53
|
+
instance_variables.each do |var|
|
54
|
+
instance_eval("#{var}.freeze")
|
23
55
|
end
|
24
|
-
|
25
|
-
|
56
|
+
super
|
57
|
+
end
|
58
|
+
|
59
|
+
# Remove the in-place where filter
|
60
|
+
def clear_filter
|
61
|
+
@filter = nil
|
62
|
+
flatten
|
63
|
+
end
|
64
|
+
alias :clear_where :clear_filter # in case this makes more sense to people
|
65
|
+
|
66
|
+
# Reset the HashModel
|
67
|
+
def clear
|
68
|
+
@raw_data = []
|
69
|
+
@modified_data = []
|
70
|
+
@unflatten_data = []
|
71
|
+
@flatten_index = nil
|
72
|
+
@filter = nil
|
73
|
+
end
|
26
74
|
|
27
|
-
attr_accessor :flatten_index, :raw_data
|
28
75
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
76
|
+
## Operators
|
77
|
+
|
78
|
+
# Overload Array#<< function so we can create the flatten index as the first record is added
|
79
|
+
# and allows us to send back this instance of the HashModel instead of an array.
|
80
|
+
def <<(value)
|
81
|
+
case value
|
82
|
+
when HashModel
|
83
|
+
@raw_data.concat(value.raw_data)
|
84
|
+
when Hash
|
85
|
+
# unflatten if needed
|
86
|
+
value = unflatten(value) unless value.to_s.match("__").nil?
|
87
|
+
check_field_names(value)
|
88
|
+
@raw_data << value
|
89
|
+
when Array
|
90
|
+
# It goes crazy if you don't clone the array before recursing
|
91
|
+
value.clone.each{ |member| self << member }
|
92
|
+
else
|
93
|
+
raise SyntaxError, "You may only add a hash, another HashModel, or an array of either"
|
33
94
|
end
|
95
|
+
flatten
|
96
|
+
end
|
97
|
+
# I like the method name "add" for adding to recordsets seems more natural
|
98
|
+
alias :add :<<
|
99
|
+
alias :concat :<<
|
100
|
+
alias :push :<<
|
101
|
+
|
102
|
+
# remap... no loops... you know the deal
|
103
|
+
alias :_equals_ :==
|
34
104
|
|
35
|
-
|
36
|
-
|
37
|
-
|
105
|
+
# Compare values with the HashModel based on the type of values given
|
106
|
+
def ==(value)
|
107
|
+
flatten
|
108
|
+
case value
|
109
|
+
when HashModel
|
110
|
+
@raw_data == value.raw_data &&
|
111
|
+
@flatten_index == value.flatten_index &&
|
112
|
+
@modified_data == value
|
113
|
+
when Array
|
114
|
+
# test for something other than hashes, a flattened recordset, or raw data
|
115
|
+
if !value.empty? && value[0].class == Hash && value[0].has_key?(:_group_id)
|
116
|
+
@modified_data == value
|
117
|
+
else
|
118
|
+
@raw_data == value
|
119
|
+
end
|
120
|
+
else
|
121
|
+
false
|
38
122
|
end
|
123
|
+
end
|
124
|
+
alias :eql? :==
|
125
|
+
|
39
126
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
127
|
+
# Remap spaceship to stop infinite loops
|
128
|
+
alias :_spaceship_ :<=>
|
129
|
+
private :_spaceship_
|
130
|
+
|
131
|
+
# Spaceship - Don't probe me bro'!
|
132
|
+
def <=>(value)
|
133
|
+
case value
|
134
|
+
when HashModel
|
135
|
+
_spaceship_(value)
|
136
|
+
when Array
|
137
|
+
# test for a flattened recordset or raw data
|
138
|
+
if !value.empty? && value[0].has_key?(:_group_id)
|
139
|
+
@modified_data <=> value
|
140
|
+
else
|
141
|
+
@raw_data <=> value
|
142
|
+
end
|
143
|
+
else
|
144
|
+
nil
|
46
145
|
end
|
146
|
+
end
|
47
147
|
|
48
148
|
|
49
|
-
|
149
|
+
## Searching
|
50
150
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
151
|
+
# Tests flat or raw data depending of if you use flat or raw data
|
152
|
+
def include?(value)
|
153
|
+
return false if value.class != Hash
|
154
|
+
@modified_data.include?(value) || @raw_data.include?(value)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Search creating a new instance of HashModel based on this one
|
158
|
+
def where(value=nil, &search)
|
159
|
+
self.clone.where!(value, &search)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Search the flattened records using a
|
163
|
+
def where!(value=nil, &search)
|
164
|
+
# Parameter checks
|
165
|
+
raise SyntaxError, "You may only provide a parameter or a block but not both" if value && !search.nil?
|
66
166
|
|
67
|
-
#
|
68
|
-
|
69
|
-
@raw_data = []
|
70
|
-
@modified_data = []
|
71
|
-
@flatten_index = nil
|
167
|
+
# Allow clearing the filter and returning the entire recordset if nothing is given
|
168
|
+
if !value && search.nil?
|
72
169
|
@filter = nil
|
170
|
+
return flatten
|
73
171
|
end
|
74
|
-
|
75
172
|
|
76
|
-
|
173
|
+
# If given a parameter make our own search based on the flatten index
|
174
|
+
if !value.nil?
|
175
|
+
# Make sure the field name is available to the proc
|
176
|
+
flatten_index = @flatten_index
|
177
|
+
search = proc do
|
178
|
+
instance_variable_get("@#{flatten_index}") == value
|
179
|
+
end # search
|
180
|
+
end # !value.nil?
|
77
181
|
|
78
|
-
#
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
value.clone.each{ |member| self << member }
|
90
|
-
else
|
91
|
-
raise SyntaxError, "You may only add a hash, another HashModel, or an array of either"
|
92
|
-
end
|
93
|
-
flatten
|
182
|
+
# Set and process the filter
|
183
|
+
@filter = search
|
184
|
+
flatten
|
185
|
+
end
|
186
|
+
|
187
|
+
# Return the other records created from the same raw data record as the one(s) searched for
|
188
|
+
def group(value=nil, &search)
|
189
|
+
if !value.nil? || !search.nil?
|
190
|
+
sibling = where(value, &search)
|
191
|
+
else
|
192
|
+
sibling = where &@filter
|
94
193
|
end
|
95
|
-
# I like the method name "add" for adding to recordsets seems more natural
|
96
|
-
alias :add :<<
|
97
|
-
alias :concat :<<
|
98
|
-
alias :push :<<
|
99
194
|
|
100
|
-
#
|
101
|
-
|
195
|
+
# Get all the unique group id's
|
196
|
+
group_ids = sibling.collect {|hash| hash[:_group_id]}.uniq
|
102
197
|
|
103
|
-
#
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
when Array
|
112
|
-
# test for something other than hashes, a flattened recordset, or raw data
|
113
|
-
if !value.empty? && value[0].class == Hash && value[0].has_key?(:hm_group_id)
|
114
|
-
@modified_data == value
|
115
|
-
else
|
116
|
-
@raw_data == value
|
117
|
-
end
|
118
|
-
else
|
119
|
-
false
|
120
|
-
end
|
121
|
-
end
|
122
|
-
alias :eql? :==
|
123
|
-
|
124
|
-
|
125
|
-
# Remap spaceship to stop infinite loops
|
126
|
-
alias :_spaceship_ :<=>
|
127
|
-
private :_spaceship_
|
198
|
+
# Find any records with matching group ids
|
199
|
+
where {group_ids.include? @_group_id}
|
200
|
+
end
|
201
|
+
|
202
|
+
# Group the records in place based on the existing filter
|
203
|
+
# This is basically a short hand for filtering based on
|
204
|
+
# group ids of filtered records
|
205
|
+
def group!(value=nil, &search)
|
128
206
|
|
129
|
-
|
130
|
-
|
131
|
-
case value
|
132
|
-
when HashModel
|
133
|
-
_spaceship_(value)
|
134
|
-
when Array
|
135
|
-
# test for a flattened recordset or raw data
|
136
|
-
if !value.empty? && value[0].has_key?(:hm_group_id)
|
137
|
-
@modified_data <=> value
|
138
|
-
else
|
139
|
-
@raw_data <=> value
|
140
|
-
end
|
141
|
-
else
|
142
|
-
nil
|
143
|
-
end
|
207
|
+
if !value.nil? || !search.nil?
|
208
|
+
where!(value, &search)
|
144
209
|
end
|
210
|
+
|
211
|
+
# Get all the unique group id's
|
212
|
+
group_ids = @modified_data.collect {|hash| hash[:_group_id]}.uniq
|
145
213
|
|
214
|
+
# Find any records with matching group ids
|
215
|
+
where! {group_ids.include? @_group_id}
|
216
|
+
end
|
146
217
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
@modified_data.include?(value) || @raw_data.include?(value)
|
153
|
-
end
|
218
|
+
# Find the raw data record for a given flat record
|
219
|
+
def parent(flat_record)
|
220
|
+
flatten
|
221
|
+
@raw_data[flat_record[:_group_id]]
|
222
|
+
end
|
154
223
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
224
|
+
# Set the array value for self to the flattened hashes based on the flatten_index
|
225
|
+
def flatten
|
226
|
+
# Don't flatten the data if we don't need to
|
227
|
+
return self if !dirty?
|
228
|
+
|
229
|
+
id = -1
|
230
|
+
group_id = -1
|
231
|
+
@modified_data.clear
|
232
|
+
# set the flatten index if this is the first time the function is called
|
233
|
+
@flatten_index = @raw_data[0].keys[0] if @raw_data != [] && @flatten_index.nil?
|
234
|
+
flatten_index = @flatten_index.to_s
|
159
235
|
|
160
|
-
#
|
161
|
-
|
162
|
-
|
163
|
-
|
236
|
+
# Flatten and filter the raw data
|
237
|
+
@raw_data.each do |record|
|
238
|
+
new_records, duplicate_data = flatten_hash(record, flatten_index)
|
239
|
+
# catch raw data records that don't have the flatten index
|
240
|
+
new_records << {@flatten_index.to_sym=>nil} if new_records.empty?
|
241
|
+
group_id += 1
|
242
|
+
new_records.collect! do |new_record|
|
243
|
+
# Double bangs aren't needed but are they more efficient?
|
244
|
+
new_record.merge!( duplicate_data.merge!( { :_id=>(id+=1), :_group_id=>group_id } ) )
|
245
|
+
end
|
164
246
|
|
165
|
-
#
|
166
|
-
|
167
|
-
@filter
|
168
|
-
return flatten
|
247
|
+
# Add the records to modified data if they pass the filter
|
248
|
+
new_records.each do |new_record|
|
249
|
+
@modified_data << new_record if @filter.nil? ? true : (create_object_from_flat_hash(new_record).instance_eval &@filter)
|
169
250
|
end
|
251
|
+
end # raw_data.each
|
252
|
+
set_dirty_hash
|
253
|
+
self
|
254
|
+
end # flatten
|
255
|
+
|
256
|
+
# If the hash_model has been changed but not flattened
|
257
|
+
def dirty?
|
258
|
+
get_current_dirty_hash != @dirty_hash
|
259
|
+
end
|
170
260
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
261
|
+
# Return a string consisting of the flattened data
|
262
|
+
def to_s
|
263
|
+
@modified_data.to_s
|
264
|
+
end
|
265
|
+
|
266
|
+
# Return an array of the flattened data
|
267
|
+
def to_ary
|
268
|
+
@modified_data.to_ary
|
269
|
+
end
|
270
|
+
|
271
|
+
# Iterate over the flattened records
|
272
|
+
def each
|
273
|
+
@modified_data.each do |record|
|
274
|
+
# change or manipulate the values in your value array inside this block
|
275
|
+
yield record
|
183
276
|
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# Convert a flat record into an unflattened record
|
280
|
+
def unflatten(input)
|
281
|
+
HashModel.unflatten(input)
|
282
|
+
end
|
184
283
|
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
284
|
+
# Convert a flat record into an unflattened record
|
285
|
+
def self.unflatten(input)
|
286
|
+
# Seriously in need of a refactor, just looking at this hurts my brain
|
287
|
+
# There's a lot of redundancy here.
|
288
|
+
case input
|
289
|
+
when Hash
|
290
|
+
new_record = {}
|
291
|
+
input.each do |key, value|
|
292
|
+
# recursively look for flattened keys
|
293
|
+
keys = key.to_s.split("__", 2)
|
294
|
+
if keys[1]
|
295
|
+
key = keys[0].to_sym
|
296
|
+
value = unflatten({keys[1].to_sym => value})
|
297
|
+
end
|
195
298
|
|
196
|
-
|
197
|
-
|
299
|
+
# Don't overwrite existing value
|
300
|
+
if (existing = new_record[key])
|
301
|
+
# convert to array and search for subkeys if appropriate
|
302
|
+
if existing.class == Hash
|
303
|
+
# Convert to an array if something other than a hash is added
|
304
|
+
unless value.class == Hash
|
305
|
+
new_record[key] = hash_to_array(existing)
|
306
|
+
new_record[key] << value
|
307
|
+
else
|
308
|
+
# Search subkeys for duplicate values if it's a hash
|
309
|
+
unless (found_keys = existing.keys & value.keys).empty?
|
310
|
+
found_keys.each do |found_key|
|
311
|
+
# How can I remove this redundancy?
|
312
|
+
if new_record[key][found_key].class == Hash
|
313
|
+
unless value[found_key].class == Hash
|
314
|
+
new_record[key] = hash_to_array(new_record[key][found_key])
|
315
|
+
new_record[key] << value[found_key]
|
316
|
+
else
|
317
|
+
new_record[key][found_key].merge!(value[found_key])
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
else
|
322
|
+
new_record[key].merge!(value)
|
323
|
+
end
|
324
|
+
end
|
325
|
+
else
|
326
|
+
new_record[key] << value
|
327
|
+
end
|
328
|
+
else
|
329
|
+
new_record.merge!(key => value)
|
330
|
+
end
|
331
|
+
end
|
332
|
+
new_record
|
333
|
+
when Array
|
334
|
+
# recurse into array
|
335
|
+
input.collect! {|item| unflatten(item) }
|
336
|
+
else
|
337
|
+
input
|
198
338
|
end
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
# group ids of filtered records
|
203
|
-
def group!(value=nil, &search)
|
204
|
-
|
205
|
-
if !value.nil? || !search.nil?
|
206
|
-
where!(value, &search)
|
207
|
-
end
|
208
|
-
|
209
|
-
# Get all the unique group id's
|
210
|
-
group_ids = @modified_data.collect {|hash| hash[:hm_group_id]}.uniq
|
339
|
+
end
|
340
|
+
|
341
|
+
private
|
211
342
|
|
212
|
-
# Find any records with matching group ids
|
213
|
-
where! {group_ids.include? @hm_group_id}
|
214
|
-
end
|
215
343
|
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
344
|
+
# Convert a hash of multiple key/value pairs to an array of single hashes.
|
345
|
+
# {:field1 => "value1", :field2 => "value2"}
|
346
|
+
# becomes
|
347
|
+
# [{:field1 => "value1"}, {:field2 => "value2"}]
|
348
|
+
def self.hash_to_array(hash)
|
349
|
+
array = []
|
350
|
+
hash.each do |key, value|
|
351
|
+
array << {key => value}
|
220
352
|
end
|
353
|
+
array
|
354
|
+
end
|
221
355
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
@modified_data.clear
|
230
|
-
# set the flatten index if this is the first time the function is called
|
231
|
-
@flatten_index = @raw_data[0].keys[0] if @raw_data != [] && @flatten_index.nil?
|
232
|
-
flatten_index = @flatten_index.to_s
|
233
|
-
|
234
|
-
# Flatten and filter the raw data
|
235
|
-
@raw_data.each do |record|
|
236
|
-
new_records, duplicate_data = flatten_hash(record, flatten_index)
|
237
|
-
# catch raw data records that don't have the flatten index
|
238
|
-
new_records << {@flatten_index.to_sym=>nil} if new_records.empty?
|
239
|
-
group_id += 1
|
240
|
-
new_records.collect! do |new_record|
|
241
|
-
# Double bangs aren't needed but are they more efficient?
|
242
|
-
new_record.merge!( duplicate_data.merge!( { :hm_id=>(id+=1), :hm_group_id=>group_id } ) )
|
243
|
-
end
|
244
|
-
|
245
|
-
# Add the records to modified data if they pass the filter
|
246
|
-
new_records.each do |new_record|
|
247
|
-
@modified_data << new_record if @filter.nil? ? true : (create_object_from_flat_hash(new_record).instance_eval &@filter)
|
248
|
-
end
|
249
|
-
end # raw_data.each
|
250
|
-
set_dirty_hash
|
251
|
-
self
|
252
|
-
end # flatten
|
253
|
-
|
254
|
-
# If the hash_model has been changed but not flattened
|
255
|
-
def dirty?
|
256
|
-
get_current_dirty_hash != @dirty_hash
|
257
|
-
end
|
356
|
+
# Convert a hash of multiple key/value pairs to an array of single hashes.
|
357
|
+
# {:field1 => "value1", :field2 => "value2"}
|
358
|
+
# becomes
|
359
|
+
# [{:field1 => "value1"}, {:field2 => "value2"}]
|
360
|
+
def hash_to_array(hash)
|
361
|
+
HashModel.hash_to_array(hash)
|
362
|
+
end
|
258
363
|
|
259
|
-
|
260
|
-
|
261
|
-
|
364
|
+
# Checks hash keys for reserved field names
|
365
|
+
def check_field_names(input)
|
366
|
+
case input
|
367
|
+
when Hash
|
368
|
+
input.each do |key, value|
|
369
|
+
raise ReservedNameError, "use of reserved name :#{key} as a field name." if [:_id, :_group_id].include?(key)
|
370
|
+
check_field_names(value)
|
371
|
+
end
|
372
|
+
when Array
|
373
|
+
input.clone.each { |record| check_field_names(record) }
|
262
374
|
end
|
375
|
+
end
|
263
376
|
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
end
|
377
|
+
# Save a hash for later evaluation
|
378
|
+
def set_dirty_hash
|
379
|
+
@dirty_hash = get_current_dirty_hash
|
380
|
+
end
|
381
|
+
|
382
|
+
# Create a hash based on internal values
|
383
|
+
def get_current_dirty_hash
|
384
|
+
# self.hash won't work
|
385
|
+
[@raw_data.hash, @filter.hash, @flatten_index.hash].hash
|
386
|
+
end
|
387
|
+
|
388
|
+
# Recursively convert a single record into an array of new
|
389
|
+
# records that are flattened based on the given flattened hash key
|
390
|
+
# e.g. {:x=>{:x1=>1}, :y=>{:y1=>{:y2=>2,:y3=>4}, y4:=>5}, :z=>6}
|
391
|
+
# if you wanted to flatten to :x1 you would set flatten_index to :x_x1
|
392
|
+
# To flatten to :y2 you would set flatten_index to :y_y1_y2
|
393
|
+
def flatten_hash(input, flatten_index, recordset=[], duplicate_data={}, parent_key=nil)
|
394
|
+
case input
|
395
|
+
when Hash
|
396
|
+
# Check to see if the found key is on this level - We need to add duplicate data differently if so
|
397
|
+
found_key = (input.select { |key, value| flatten_index == "#{parent_key}#{"__" if !parent_key.nil?}#{key}"} != {})
|
398
|
+
|
399
|
+
# Add records for matching flatten fields and save duplicate record data for later addition to each record.
|
400
|
+
input.each do |key, value|
|
401
|
+
#puts "\nkey: #{key}; value: #{value}"
|
402
|
+
flat_key = "#{parent_key}#{"__" if !parent_key.nil?}#{key}"
|
291
403
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
# Create a hash based on internal values
|
298
|
-
def get_current_dirty_hash
|
299
|
-
# self.hash won't work
|
300
|
-
[@raw_data.hash, @filter.hash, @flatten_index.hash].hash
|
301
|
-
end
|
302
|
-
|
303
|
-
# Recursively convert a single record into an array of new
|
304
|
-
# records that are flattened based on the given flattened hash key
|
305
|
-
# e.g. {:x=>{:x1=>1}, :y=>{:y1=>{:y2=>2,:y3=>4}, y4:=>5}, :z=>6}
|
306
|
-
# if you wanted to flatten to :x1 you would set flatten_index to :x_x1
|
307
|
-
# To flatten to :y2 you would set flatten_index to :y_y1_y2
|
308
|
-
def flatten_hash(input, flatten_index, recordset=[], duplicate_data={}, parent_key=nil)
|
309
|
-
case input
|
310
|
-
when Hash
|
311
|
-
# Check to see if the found key is on this level - We need to add duplicate data differently if so
|
312
|
-
found_key = (input.select { |key, value| flatten_index == "#{parent_key}#{"_" if !parent_key.nil?}#{key}"} != {})
|
404
|
+
#puts "flat_key: #{flat_key}"
|
405
|
+
#puts "flat_index: #{flatten_index}"
|
406
|
+
|
407
|
+
flat_key_starts_with_flatten_index = flat_key.start_with?("#{flatten_index}__")
|
408
|
+
flatten_index_starts_with_flat_key = flatten_index.start_with?(flat_key)
|
313
409
|
|
314
|
-
#
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
#
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
410
|
+
#puts "flat_key_starts_with_flatten_index: #{flat_key_starts_with_flatten_index}"
|
411
|
+
#puts "flatten_index_starts_with_flat_key: #{flatten_index_starts_with_flat_key}"
|
412
|
+
|
413
|
+
# figure out what we need to do based on where we're at in the record's value tree and man does it look ugly
|
414
|
+
if flat_key == flatten_index
|
415
|
+
# go deeper
|
416
|
+
recordset, duplicate_data = flatten_hash(value, flatten_index, recordset, duplicate_data, flat_key)
|
417
|
+
elsif flat_key_starts_with_flatten_index && !flatten_index_starts_with_flat_key
|
418
|
+
# new record
|
419
|
+
recordset << {parent_key.to_sym=>{key=>value}}
|
420
|
+
elsif !flat_key_starts_with_flatten_index && flatten_index_starts_with_flat_key
|
421
|
+
# go deeper
|
422
|
+
recordset, duplicate_data = flatten_hash(value, flatten_index, recordset, duplicate_data, flat_key)
|
423
|
+
elsif found_key
|
424
|
+
# add to dup data for same level as flatten index
|
425
|
+
duplicate_data.merge!(flat_key.to_sym=>value)
|
426
|
+
else
|
427
|
+
# add to dupe data
|
428
|
+
duplicate_data.merge!(key=>value)
|
429
|
+
end
|
430
|
+
end # input.each
|
431
|
+
when Array
|
432
|
+
input.each do |value|
|
433
|
+
recordset, duplicate_data = flatten_hash(value, flatten_index, recordset, duplicate_data, parent_key)
|
434
|
+
end
|
435
|
+
else
|
436
|
+
recordset << {parent_key.to_sym=>input}
|
437
|
+
end # case
|
438
|
+
return recordset, duplicate_data
|
439
|
+
end # flatten_hash
|
440
|
+
|
441
|
+
# Creates an object with instance variables for each field at every level
|
442
|
+
# This allows using a block like {:field1==true && :field2_subfield21="potato"}
|
443
|
+
def create_object_from_flat_hash(record, hash_record=Class.new.new, parent_key=nil)
|
444
|
+
|
445
|
+
# Iterate through the record creating the object recursively
|
446
|
+
case record
|
447
|
+
when Hash
|
448
|
+
record.each do |key, value|
|
449
|
+
flat_key = "#{parent_key}#{"__" if !parent_key.nil?}#{key}"
|
450
|
+
hash_record.instance_variable_set("@#{flat_key}", value)
|
451
|
+
hash_record = create_object_from_flat_hash(value, hash_record, flat_key)
|
452
|
+
end
|
453
|
+
when Array
|
454
|
+
record.each do |value|
|
455
|
+
hash_record = create_object_from_flat_hash(value, hash_record, parent_key)
|
456
|
+
end
|
457
|
+
else
|
458
|
+
hash_record.instance_variable_set("@#{parent_key}", record)
|
459
|
+
end # case
|
350
460
|
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
case method
|
379
|
-
when :[], :each_index, :uniq, :last, :collect, :length, :at, :map, :combination, :count, :cycle, :empty?, :fetch, :index, :first, :permutation, :size, :values_at
|
380
|
-
flatten
|
381
|
-
@modified_data.send(method, *args, &block)
|
382
|
-
when :+, :*
|
383
|
-
case args[0]
|
384
|
-
when HashModel
|
385
|
-
args = [args[0].raw_data]
|
386
|
-
when Hash
|
387
|
-
args = [args]
|
388
|
-
end
|
389
|
-
clone = self.clone
|
390
|
-
clone.raw_data = clone.raw_data.send(method, *args, &block)
|
391
|
-
clone.flatten
|
392
|
-
else
|
393
|
-
raise NoMethodError, "undefined method `#{method}' for #{self}"
|
394
|
-
end
|
461
|
+
hash_record
|
462
|
+
end # create_object_from_flat_hash
|
463
|
+
|
464
|
+
# Deal with the array methods allowing multiple functions to use the same code
|
465
|
+
# You couldn't do this with alias because you can't tell what alias is used.
|
466
|
+
#
|
467
|
+
# My rule for using this vs a seperate method is if I can use the
|
468
|
+
# same code for more than one method it goes in here, if the method
|
469
|
+
# only works for one method then it gets its own method.
|
470
|
+
def wrapper_method(method, *args, &block)
|
471
|
+
# grab the raw data if it's a hashmodel
|
472
|
+
case method
|
473
|
+
when :[], :each_index, :uniq, :last, :collect, :length, :at, :map, :combination, :count, :cycle, :empty?, :fetch, :index, :first, :permutation, :size, :values_at
|
474
|
+
flatten
|
475
|
+
@modified_data.send(method, *args, &block)
|
476
|
+
when :+, :*
|
477
|
+
case args[0]
|
478
|
+
when HashModel
|
479
|
+
args = [args[0].raw_data]
|
480
|
+
when Hash
|
481
|
+
args = [args]
|
482
|
+
end
|
483
|
+
clone = self.clone
|
484
|
+
clone.raw_data = clone.raw_data.send(method, *args, &block)
|
485
|
+
clone.flatten
|
486
|
+
else
|
487
|
+
raise NoMethodError, "undefined method `#{method}' for #{self}"
|
395
488
|
end
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
489
|
+
end
|
490
|
+
|
491
|
+
# create methods like the given object so we can trap them
|
492
|
+
def mimic_methods
|
493
|
+
Array.new.public_methods(false).each do |method|
|
494
|
+
# Don't mimic methods we specifically declare or methods that don't make sense for the class
|
495
|
+
if !self.respond_to?(method)
|
496
|
+
self.class.class_eval do
|
497
|
+
define_method(method) { |*args, &block| wrapper_method(method, *args, &block) }
|
405
498
|
end
|
406
499
|
end
|
407
500
|
end
|
501
|
+
end
|
408
502
|
|
409
|
-
|
410
|
-
|
411
|
-
end # MikBe
|
503
|
+
end # HashModel
|