hash_math 1.1.0 → 1.2.0.pre.alpha

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.
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