hash_math 1.1.0 → 1.2.0.pre.alpha

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7df04a330a0de007fb2dba40e7deb6b835ad5aebdf0be39dd596c4af6ee52715
4
- data.tar.gz: b0277b0818de7dcb5f59a82e46e761bc6afb16c7f74ed3fc5390b9cd6addd6bc
3
+ metadata.gz: ac40548aa245ecbca0e7e2cc3dd8d617db1937108aa64f60513a7819b84bf534
4
+ data.tar.gz: 15b253850e012966eeb4aadbcd5873bd9571abe0fa62bd83a242ed39de8e2395
5
5
  SHA512:
6
- metadata.gz: 73f27b9a0746d605e7471aa14f4b1203894c72064555ed19303acc2e2c4e18169e8365daf20aeb299afc5b3faef9ec0552af04bab8b73fdc59bf9beb8baa2bd8
7
- data.tar.gz: bc0eb13e08c690f7506ad413ad4fe1a3d22698cbe8f313efc1af8d62546ca1c0ab2cea4b0ba55f44758661688d9da6e3ff11931aca88a775226bcfb26758315b
6
+ metadata.gz: d2289559ffbb080f16eff2655ab83860ca2779120d17ce5795d163dd762f425da9ab27ec16f7f480ffb84b824eaab60068de18548215a770abb4e24847f6e66f
7
+ data.tar.gz: 5256981292c2f27c706753524f3816b04207a322b179a92028d7e5a902c2d4836fb8b353684beab691c28f0e5c3920715fbff79412a7a8a0e8fa223716e90226
@@ -1,10 +1,16 @@
1
- # 1.1.0 (August 13t, 2020)
1
+ # 1.2.0 (TBD)
2
2
 
3
- Adding:
3
+ Additions:
4
4
 
5
- * HashMath#Unpivot class and implementation.
5
+ * HashMath::Mapper
6
6
 
7
- Removing:
7
+ # 1.1.0 (August 13th, 2020)
8
+
9
+ Additions:
10
+
11
+ * HashMath::Unpivot
12
+
13
+ Removals:
8
14
 
9
15
  * Support for Ruby < 2.5
10
16
 
data/README.md CHANGED
@@ -230,6 +230,72 @@ The `rows` variable should now be equivalent to:
230
230
 
231
231
  Note: `HashMath::Unpivot#add` also exists to update an already instantiated object with new pivot_sets.
232
232
 
233
+ ### Mapper: Constant-Time Cross-Mapper
234
+
235
+ The general use-case for `HashMath::Mapper` is to store pre-materialized lookups and then use those lookups efficiently to fill in missing data for a hash.
236
+
237
+ For example: say we have incoming data representing patients, but this incoming data is value-oriented, not entity/identity-oriented, which may be what we need to ingest it properly:
238
+
239
+ ````ruby
240
+ patient = { patient_id: 2, patient_status: 'active', marital_status: 'married' }
241
+ ````
242
+
243
+ It is not sufficient for us to use patient_status and marital_status, what we need is the entity identifiers for those values. What we can do is load up a HashMath::Mapper instance with these values and let it do the work for us:
244
+
245
+ ````ruby
246
+ patient_statuses = [
247
+ { id: 1, name: 'active' },
248
+ { id: 2, name: 'inactive' },
249
+ { id: 3, name: 'archived' }
250
+ ]
251
+
252
+ marital_statuses = [
253
+ { id: 1, code: 'single' },
254
+ { id: 2, code: 'married' },
255
+ { id: 3, code: 'divorced' }
256
+ ]
257
+
258
+ mappings = [
259
+ {
260
+ lookup: {
261
+ name: :patient_statuses,
262
+ by: :name
263
+ },
264
+ set: :patient_status_id,
265
+ value: :patient_status,
266
+ with: :id
267
+ },
268
+ {
269
+ lookup: {
270
+ name: :marital_statuses,
271
+ by: :code
272
+ },
273
+ set: :marital_status_id,
274
+ value: :marital_status,
275
+ with: :id
276
+ }
277
+ ]
278
+
279
+ mapper = HashMath::Mapper
280
+ .new(mappings)
281
+ .add_each(:patient_statuses, patient_statuses)
282
+ .add_each(:marital_statuses, marital_statuses)
283
+
284
+ mapped_patient = mapper.map(patient)
285
+ ````
286
+
287
+ The variable `mapped_patient` should now be equal to:
288
+
289
+ ````ruby
290
+ {
291
+ patient_id: 2,
292
+ patient_status: 'active',
293
+ marital_status: 'single',
294
+ patient_status_id: 1,
295
+ marital_status_id: 2
296
+ }
297
+ ````
298
+
233
299
  ## Contributing
234
300
 
235
301
  ### Development Environment Configuration
@@ -9,6 +9,7 @@
9
9
 
10
10
  require 'acts_as_hashable'
11
11
 
12
+ require_relative 'hash_math/mapper'
12
13
  require_relative 'hash_math/matrix'
13
14
  require_relative 'hash_math/record'
14
15
  require_relative 'hash_math/table'
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require_relative 'mapper/mapping'
11
+
12
+ module HashMath
13
+ # A Mapper instance can hold multiple constant-time object lookups and then is able to map
14
+ # a hash to its corresponding lookup values. It's main use-case is to fill in missing or
15
+ # update existing key-value pairs with its corresponding relationships.
16
+ class Mapper
17
+ attr_reader :mappings_by_name
18
+
19
+ # Accepts an array of Mapping instances of hashes containing the Mapping instance attributes
20
+ # to initialize.
21
+ def initialize(mappings = [])
22
+ mappings = Mapping.array(mappings)
23
+ @mappings_by_name = pivot_by_name(mappings)
24
+
25
+ freeze
26
+ end
27
+
28
+ # Add an enumerable list of lookup records to this instance's lookup dataset.
29
+ def add_each(name, objects)
30
+ tap { objects.each { |o| add(name, o) } }
31
+ end
32
+
33
+ # Add a lookup record to this instance's lookup dataset.
34
+ def add(name, object)
35
+ tap { mappings_by_name.fetch(name.to_s).add(object) }
36
+ end
37
+
38
+ # Returns a new hash with the added/updated key-value pairs. Note that this only does a
39
+ # shallow copy using Hash#merge.
40
+ def map(hash)
41
+ map!({}.merge(hash || {}))
42
+ end
43
+
44
+ # Mutates the inpuuted hash with the added/updated key-value pairs.
45
+ def map!(hash)
46
+ mappings_by_name.values.each_with_object(hash) do |mapping, _memo|
47
+ mapping.map!(hash)
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def pivot_by_name(array)
54
+ array.each_with_object({}) do |object, memo|
55
+ memo[object.name.to_s] = object
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module HashMath
11
+ class Mapper
12
+ # A Lookup instance maintains its own list of objects using its own key extraction method,
13
+ # called 'by' which will be used to extract the key's value for the lookup.
14
+ # If 'by' is a Proc then it will be called when extracting a new lookup record's lookup value.
15
+ # If it is anything other than a Proc and it will call #[] on the object.
16
+ class Lookup
17
+ acts_as_hashable
18
+
19
+ attr_reader :name, :by
20
+
21
+ def initialize(name:, by:)
22
+ @name = name
23
+ @by = by
24
+ @objects = {}
25
+
26
+ freeze
27
+ end
28
+
29
+ def add_each(array) # :nodoc:
30
+ tap { array.each { |o| add(o) } }
31
+ end
32
+
33
+ def add(object) # :nodoc:
34
+ id = proc_or_brackets(object, by)
35
+
36
+ objects[id] = object
37
+
38
+ self
39
+ end
40
+
41
+ def get(value) # :nodoc:
42
+ objects[value]
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :objects
48
+
49
+ def proc_or_brackets(object, thing)
50
+ return nil unless object
51
+
52
+ thing.is_a?(Proc) ? thing.call(object) : object[thing]
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require_relative 'lookup'
11
+
12
+ module HashMath
13
+ class Mapper
14
+ # Represents one complete configuration for mapping one key-value pair to one lookup.
15
+ #
16
+ # Example:
17
+ # ----------------------------------------------------------------------------------------------
18
+ # mapping = Mapper.make(
19
+ # lookup: { name: :patient_statuses, by: :name },
20
+ # value: :status,
21
+ # set: :patient_status_id,
22
+ # with: :id
23
+ # ).add(id: 1, name: 'active').add(id: 2, name: 'inactive')
24
+ #
25
+ # patient = { id: 1, code: 'active' }
26
+ # mapped_patient = mapping.map!(patient)
27
+ # ----------------------------------------------------------------------------------------------
28
+ #
29
+ # mapped_patient now equals: { id: 1, code: 'active', patient_status_id: 1 }
30
+ class Mapping
31
+ extend Forwardable
32
+ acts_as_hashable
33
+
34
+ attr_reader :value, :set, :with, :lookup
35
+
36
+ def_delegators :lookup, :name, :by, :add, :add_each
37
+
38
+ # lookup: can either be a Mapper#Lookup instance or a hash with the attributes to initialize
39
+ # for a Mapper#Lookup instance.
40
+ # value: the key to use to get the 'value' from the object to lookup.
41
+ # set: the key to set once the lookup record is identified.
42
+ # with: the key use, on the lookup, to get the new value.
43
+ def initialize(lookup:, value:, set:, with:)
44
+ @lookup = Lookup.make(lookup)
45
+ @value = value
46
+ @set = set
47
+ @with = with
48
+
49
+ freeze
50
+ end
51
+
52
+ def add_each(array) # :nodoc:
53
+ tap { lookup.add_each(array) }
54
+ end
55
+
56
+ def add(object) # :nodoc:
57
+ tap { lookup.add(object) }
58
+ end
59
+
60
+ def map!(hash) # :nodoc:
61
+ lookup_value = proc_or_brackets(hash, value)
62
+ lookup_object = lookup.get(lookup_value)
63
+ hash[set] = proc_or_brackets(lookup_object, with)
64
+
65
+ self
66
+ end
67
+
68
+ private
69
+
70
+ def proc_or_brackets(object, thing)
71
+ return nil unless object
72
+
73
+ thing.is_a?(Proc) ? thing.call(object) : object[thing]
74
+ end
75
+ end
76
+ end
77
+ end
@@ -13,7 +13,6 @@ module HashMath
13
13
  # This class has the ability to extrapolate one hash (row) into multiple hashes (rows) while
14
14
  # unpivoting specific keys into key-value pairs.
15
15
  class Unpivot
16
- acts_as_hashable
17
16
  extend Forwardable
18
17
 
19
18
  attr_reader :pivot_set
@@ -34,7 +34,7 @@ module HashMath
34
34
  # the first will serve as the prototype for each returned hash
35
35
  # the second will be one to use for value extraction.
36
36
  # Returns an array of hashes.
37
- def expand(base_hash, value_hash)
37
+ def expand(base_hash, value_hash) # :nodoc:
38
38
  keys.map do |key|
39
39
  base_hash.merge(
40
40
  coalesce_key => key,
@@ -32,7 +32,7 @@ module HashMath
32
32
 
33
33
  # An aggregation of Pivot#expand. This method will iterate over all pivots
34
34
  # and expand them all out.
35
- def expand(hash)
35
+ def expand(hash) # :nodoc:
36
36
  base_hash = make_base_hash(hash)
37
37
 
38
38
  pivots.map { |pivot| pivot.expand(base_hash, hash) }
@@ -8,5 +8,5 @@
8
8
  #
9
9
 
10
10
  module HashMath
11
- VERSION = '1.1.0'
11
+ VERSION = '1.2.0-alpha'
12
12
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require 'spec_helper'
11
+
12
+ describe HashMath::Mapper::Mapping do
13
+ let(:active) { { id: 1, name: 'active' } }
14
+ let(:inactive) { { id: 2, name: 'inactive' } }
15
+ let(:patient_statuses) { [active, inactive] }
16
+
17
+ let(:mapping) do
18
+ {
19
+ lookup: {
20
+ name: :patient_statuses,
21
+ by: :name
22
+ },
23
+ set: :patient_status_id,
24
+ value: :patient_status,
25
+ with: :id
26
+ }
27
+ end
28
+
29
+ subject do
30
+ described_class.make(mapping).add_each(patient_statuses)
31
+ end
32
+
33
+ let(:omitted) do
34
+ { patient_id: 1 }
35
+ end
36
+
37
+ let(:filled) do
38
+ { patient_id: 2, patient_status: 'active' }
39
+ end
40
+
41
+ let(:mismatched) do
42
+ { patient_id: 2, patient_status: 'doesnt_exist' }
43
+ end
44
+
45
+ context 'when keys are missing' do
46
+ it 'returns hash with nil values' do
47
+ expected = omitted.merge(patient_status_id: nil)
48
+
49
+ subject.map!(omitted)
50
+
51
+ expect(omitted).to eq(expected)
52
+ end
53
+ end
54
+
55
+ context 'when keys are present' do
56
+ it 'returns hash with values' do
57
+ expected = filled.merge(patient_status_id: 1)
58
+
59
+ subject.map!(filled)
60
+
61
+ expect(filled).to eq(expected)
62
+ end
63
+ end
64
+
65
+ context 'when keys are present but values are missing' do
66
+ it 'returns hash with nil values' do
67
+ expected = mismatched.merge(patient_status_id: nil)
68
+
69
+ subject.map!(mismatched)
70
+
71
+ expect(mismatched).to eq(expected)
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2019-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require 'spec_helper'
11
+
12
+ describe HashMath::Mapper do
13
+ let(:active) { { id: 1, name: 'active' } }
14
+ let(:inactive) { { id: 2, name: 'inactive' } }
15
+ let(:archived) { { id: 3, name: 'archived' } }
16
+
17
+ let(:single) { { id: 1, code: 'single' } }
18
+ let(:married) { { id: 2, code: 'married' } }
19
+ let(:divorced) { { id: 3, code: 'divorced' } }
20
+
21
+ let(:patient_statuses) { [active, inactive, archived] }
22
+ let(:marital_statuses) { [single, married, divorced] }
23
+
24
+ subject do
25
+ described_class
26
+ .new(mappings)
27
+ .add_each(:patient_statuses, patient_statuses)
28
+ .add_each(:marital_statuses, marital_statuses)
29
+ end
30
+
31
+ let(:mappings) do
32
+ [
33
+ {
34
+ lookup: {
35
+ name: :patient_statuses,
36
+ by: :name
37
+ },
38
+ set: :patient_status_id,
39
+ value: :patient_status,
40
+ with: :id
41
+ },
42
+ {
43
+ lookup: {
44
+ name: :marital_statuses,
45
+ by: :code
46
+ },
47
+ set: :marital_status_id,
48
+ value: :marital_status,
49
+ with: :id
50
+ }
51
+ ]
52
+ end
53
+
54
+ let(:omitted) do
55
+ { patient_id: 1 }
56
+ end
57
+
58
+ let(:filled) do
59
+ { patient_id: 2, patient_status: 'active', marital_status: 'single' }
60
+ end
61
+
62
+ let(:mismatched) do
63
+ { patient_id: 2, patient_status: 'doesnt_exist', marital_status: 'no_exist' }
64
+ end
65
+
66
+ context 'when nil' do
67
+ it 'returns base mapped hash' do
68
+ expected = {
69
+ patient_status_id: nil,
70
+ marital_status_id: nil
71
+ }
72
+
73
+ actual = subject.map(nil)
74
+
75
+ expect(actual).to eq(expected)
76
+ end
77
+ end
78
+
79
+ context 'when keys are missing' do
80
+ it 'returns hash with nil values' do
81
+ expected = omitted.merge(
82
+ patient_status_id: nil,
83
+ marital_status_id: nil
84
+ )
85
+
86
+ actual = subject.map(omitted)
87
+
88
+ expect(actual).to eq(expected)
89
+ end
90
+ end
91
+
92
+ context 'when keys are present' do
93
+ it 'returns hash with values' do
94
+ expected = filled.merge(
95
+ patient_status_id: 1,
96
+ marital_status_id: 1
97
+ )
98
+
99
+ actual = subject.map(filled)
100
+
101
+ expect(actual).to eq(expected)
102
+ end
103
+ end
104
+
105
+ context 'when keys are present but values are missing' do
106
+ it 'returns hash with nil values' do
107
+ expected = mismatched.merge(
108
+ patient_status_id: nil,
109
+ marital_status_id: nil
110
+ )
111
+
112
+ actual = subject.map(mismatched)
113
+
114
+ expect(actual).to eq(expected)
115
+ end
116
+ end
117
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hash_math
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0.pre.alpha
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Ruggio
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-08-13 00:00:00.000000000 Z
11
+ date: 2020-08-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: acts_as_hashable
@@ -160,6 +160,9 @@ files:
160
160
  - bin/console
161
161
  - hash_math.gemspec
162
162
  - lib/hash_math.rb
163
+ - lib/hash_math/mapper.rb
164
+ - lib/hash_math/mapper/lookup.rb
165
+ - lib/hash_math/mapper/mapping.rb
163
166
  - lib/hash_math/matrix.rb
164
167
  - lib/hash_math/matrix/key_value_pair.rb
165
168
  - lib/hash_math/record.rb
@@ -168,6 +171,8 @@ files:
168
171
  - lib/hash_math/unpivot/pivot.rb
169
172
  - lib/hash_math/unpivot/pivot_set.rb
170
173
  - lib/hash_math/version.rb
174
+ - spec/hash_math/mapper/mapping_spec.rb
175
+ - spec/hash_math/mapper_spec.rb
171
176
  - spec/hash_math/matrix_spec.rb
172
177
  - spec/hash_math/record_spec.rb
173
178
  - spec/hash_math/table_spec.rb
@@ -189,15 +194,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
189
194
  version: '2.5'
190
195
  required_rubygems_version: !ruby/object:Gem::Requirement
191
196
  requirements:
192
- - - ">="
197
+ - - ">"
193
198
  - !ruby/object:Gem::Version
194
- version: '0'
199
+ version: 1.3.1
195
200
  requirements: []
196
201
  rubygems_version: 3.0.3
197
202
  signing_key:
198
203
  specification_version: 4
199
204
  summary: Hash-based data structures and algorithms
200
205
  test_files:
206
+ - spec/hash_math/mapper/mapping_spec.rb
207
+ - spec/hash_math/mapper_spec.rb
201
208
  - spec/hash_math/matrix_spec.rb
202
209
  - spec/hash_math/record_spec.rb
203
210
  - spec/hash_math/table_spec.rb