hash_math 0.0.1 → 1.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.
@@ -0,0 +1,47 @@
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 Unpivot
12
+ # A single pivot definition consists of which columns to coalesce and to where.
13
+ class Pivot
14
+ acts_as_hashable
15
+
16
+ attr_reader :coalesce_key,
17
+ :coalesce_key_value,
18
+ :keys
19
+
20
+ # keys is an array of keys to include in the un-pivoting.
21
+ # coalesce_key is the new key to use.
22
+ # coalesce_key_value is the new key to use for its corresponding values.
23
+ def initialize(keys:, coalesce_key:, coalesce_key_value:)
24
+ @keys = Array(keys)
25
+ @coalesce_key = coalesce_key
26
+ @coalesce_key_value = coalesce_key_value
27
+
28
+ freeze
29
+ end
30
+
31
+ # The most rudimentary portion of the Unpivoting algorithm, this method works on
32
+ # just one pivot and returns the extrapolated, un-pivoted rows.
33
+ # Takes two hashes as input:
34
+ # the first will serve as the prototype for each returned hash
35
+ # the second will be one to use for value extraction.
36
+ # Returns an array of hashes.
37
+ def expand(base_hash, value_hash) # :nodoc:
38
+ keys.map do |key|
39
+ base_hash.merge(
40
+ coalesce_key => key,
41
+ coalesce_key_value => value_hash[key]
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,54 @@
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 'pivot'
11
+
12
+ module HashMath
13
+ class Unpivot
14
+ # A set of pivots for an Unpivot class to perform.
15
+ class PivotSet
16
+ acts_as_hashable
17
+ extend Forwardable
18
+
19
+ attr_reader :pivots
20
+
21
+ def_delegators :pivots, :any?
22
+
23
+ def initialize(pivots: [])
24
+ @pivots = Pivot.array(pivots)
25
+ end
26
+
27
+ # Adds another Pivot configuration object to this objects list of pivots.
28
+ # Returns self.
29
+ def add(pivot)
30
+ tap { pivots << Pivot.make(pivot) }
31
+ end
32
+
33
+ # An aggregation of Pivot#expand. This method will iterate over all pivots
34
+ # and expand them all out.
35
+ def expand(hash) # :nodoc:
36
+ base_hash = make_base_hash(hash)
37
+
38
+ pivots.map { |pivot| pivot.expand(base_hash, hash) }
39
+ end
40
+
41
+ private
42
+
43
+ def make_base_hash(hash)
44
+ keys_to_remove = key_set
45
+
46
+ hash.reject { |k, _v| keys_to_remove.include?(k) }
47
+ end
48
+
49
+ def key_set
50
+ pivots.flat_map(&:keys).to_set
51
+ end
52
+ end
53
+ end
54
+ end
@@ -8,5 +8,5 @@
8
8
  #
9
9
 
10
10
  module HashMath
11
- VERSION = '0.0.1'
11
+ VERSION = '1.2.0'
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,139 @@
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
+ describe '#add' do
67
+ it 'raises ArgumentError when name is nil' do
68
+ expect { subject.add(nil, {}) }.to raise_error(ArgumentError)
69
+ end
70
+
71
+ it 'raises ArgumentError when name is empty string' do
72
+ expect { subject.add('', {}) }.to raise_error(ArgumentError)
73
+ end
74
+ end
75
+
76
+ describe '#add_each' do
77
+ it 'raises ArgumentError when name is nil' do
78
+ expect { subject.add_each(nil, []) }.to raise_error(ArgumentError)
79
+ end
80
+
81
+ it 'raises ArgumentError when name is empty string' do
82
+ expect { subject.add_each('', []) }.to raise_error(ArgumentError)
83
+ end
84
+ end
85
+
86
+ describe '#map' do
87
+ context 'with nil input' do
88
+ it 'returns base mapped hash' do
89
+ expected = {
90
+ patient_status_id: nil,
91
+ marital_status_id: nil
92
+ }
93
+
94
+ actual = subject.map(nil)
95
+
96
+ expect(actual).to eq(expected)
97
+ end
98
+ end
99
+
100
+ context 'with missing keys input' do
101
+ it 'returns hash with nil values' do
102
+ expected = omitted.merge(
103
+ patient_status_id: nil,
104
+ marital_status_id: nil
105
+ )
106
+
107
+ actual = subject.map(omitted)
108
+
109
+ expect(actual).to eq(expected)
110
+ end
111
+ end
112
+
113
+ context 'with present keys input' do
114
+ it 'returns hash with values' do
115
+ expected = filled.merge(
116
+ patient_status_id: 1,
117
+ marital_status_id: 1
118
+ )
119
+
120
+ actual = subject.map(filled)
121
+
122
+ expect(actual).to eq(expected)
123
+ end
124
+ end
125
+
126
+ context 'with present keys input but no values match lookups' do
127
+ it 'returns hash with nil values' do
128
+ expected = mismatched.merge(
129
+ patient_status_id: nil,
130
+ marital_status_id: nil
131
+ )
132
+
133
+ actual = subject.map(mismatched)
134
+
135
+ expect(actual).to eq(expected)
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,57 @@
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::Matrix do
13
+ let(:examples) do
14
+ {
15
+ {} => [],
16
+ { a: :b } => [
17
+ { a: :b }
18
+ ],
19
+ { a: 'a1', b: 'b1', c: 'c1' } => [
20
+ { a: 'a1', b: 'b1', c: 'c1' }
21
+ ],
22
+ { a: %w[a1 a2], b: 'b1', c: 'c1' } => [
23
+ { a: 'a1', b: 'b1', c: 'c1' },
24
+ { a: 'a2', b: 'b1', c: 'c1' }
25
+ ],
26
+ { a: %w[a1 a2], b: %w[b1 b2], c: 'c1' } => [
27
+ { a: 'a1', b: 'b1', c: 'c1' },
28
+ { a: 'a1', b: 'b2', c: 'c1' },
29
+ { a: 'a2', b: 'b1', c: 'c1' },
30
+ { a: 'a2', b: 'b2', c: 'c1' }
31
+ ],
32
+ { a: %w[a1 a2], b: %w[b1 b2], c: %w[c1 c2] } => [
33
+ { a: 'a1', b: 'b1', c: 'c1' },
34
+ { a: 'a1', b: 'b1', c: 'c2' },
35
+ { a: 'a1', b: 'b2', c: 'c1' },
36
+ { a: 'a1', b: 'b2', c: 'c2' },
37
+
38
+ { a: 'a2', b: 'b1', c: 'c1' },
39
+ { a: 'a2', b: 'b1', c: 'c2' },
40
+ { a: 'a2', b: 'b2', c: 'c1' },
41
+ { a: 'a2', b: 'b2', c: 'c2' }
42
+ ]
43
+ }
44
+ end
45
+
46
+ specify '#produce generates correct matrix-expanded hashes' do
47
+ examples.each_pair do |hash, expanded_hashes|
48
+ subject = described_class.new
49
+
50
+ hash.each_pair do |k, v|
51
+ v.is_a?(Array) ? subject.add_each(k, v) : subject.add(k, v)
52
+ end
53
+
54
+ expect(subject.to_a).to eq(expanded_hashes)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,48 @@
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::Record do
13
+ let(:base_value) { '*' }
14
+
15
+ let(:examples) do
16
+ {
17
+ { a: :b } => { a: :b, c: base_value, 'a' => base_value, 'c' => base_value },
18
+ { c: :d } => { a: base_value, c: :d, 'a' => base_value, 'c' => base_value },
19
+ { 'a' => :b } => { a: base_value, c: base_value, 'a' => :b, 'c' => base_value },
20
+ { 'c' => :d } => { a: base_value, c: base_value, 'a' => base_value, 'c' => :d }
21
+ }
22
+ end
23
+
24
+ subject do
25
+ described_class.new(examples.keys.map(&:keys).flatten, base_value)
26
+ end
27
+
28
+ describe '#initialize' do
29
+ it 'derives prototype' do
30
+ expected = {
31
+ a: base_value,
32
+ c: base_value,
33
+ 'a' => base_value,
34
+ 'c' => base_value
35
+ }
36
+
37
+ expect(subject.make).to eq(expected)
38
+ end
39
+ end
40
+
41
+ describe '#make' do
42
+ it 'generates correct hashes' do
43
+ examples.each_pair do |hash, normalized_hash|
44
+ expect(subject.make(hash)).to eq(normalized_hash)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,91 @@
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::Table do
13
+ RowId = Struct.new(:id1, :id2)
14
+ FieldId = Struct.new(:name1, :name2)
15
+
16
+ let(:base_value) { 'NOT_ADDED' }
17
+
18
+ context 'with simple row and field id types' do
19
+ let(:prototype) do
20
+ HashMath::Record.new(%i[name age location], base_value)
21
+ end
22
+
23
+ subject { described_class.new(prototype) }
24
+
25
+ it 'works for integer row_id and symbol field_id' do
26
+ subject.add(1, :name, 'matt')
27
+ subject.add(2, :age, 990)
28
+ subject.add(3, :location, 'earth')
29
+
30
+ rows = subject.to_a
31
+
32
+ expected = [
33
+ described_class::Row.new(1, name: 'matt', age: base_value, location: base_value),
34
+ described_class::Row.new(2, name: base_value, age: 990, location: base_value),
35
+ described_class::Row.new(3, name: base_value, age: base_value, location: 'earth')
36
+ ]
37
+
38
+ expect(rows).to eq(expected)
39
+ end
40
+
41
+ it 'raises KeyOutOfBoundsError if field_id is not defined in the record' do
42
+ expect { subject.add(4, 'name', '') }.to raise_error(HashMath::KeyOutOfBoundsError)
43
+ expect { subject.add(4, :something_else, '') }.to raise_error(HashMath::KeyOutOfBoundsError)
44
+ end
45
+ end
46
+
47
+ context 'with more complex object subclass row and field id types' do
48
+ let(:prototype) do
49
+ HashMath::Record.new(
50
+ [
51
+ FieldId.new(:name, :first),
52
+ FieldId.new(:address, :st1)
53
+ ],
54
+ base_value
55
+ )
56
+ end
57
+
58
+ subject { described_class.new(prototype) }
59
+
60
+ it 'works for more complex row_id and field_id' do
61
+ subject.add(RowId.new(1, 2), FieldId.new(:name, :first), 'matt')
62
+ subject.add(RowId.new(3, 4), FieldId.new(:name, :first), 'nick')
63
+ subject.add(RowId.new(5, 6), FieldId.new(:name, :first), 'sam')
64
+
65
+ subject.add(RowId.new(1, 2), FieldId.new(:address, :st1), 'mag mile')
66
+ subject.add(RowId.new(3, 4), FieldId.new(:address, :st1), 'saturn ln.')
67
+
68
+ rows = subject.to_a
69
+
70
+ expected = [
71
+ described_class::Row.new(
72
+ RowId.new(1, 2),
73
+ FieldId.new(:name, :first) => 'matt',
74
+ FieldId.new(:address, :st1) => 'mag mile'
75
+ ),
76
+ described_class::Row.new(
77
+ RowId.new(3, 4),
78
+ FieldId.new(:name, :first) => 'nick',
79
+ FieldId.new(:address, :st1) => 'saturn ln.'
80
+ ),
81
+ described_class::Row.new(
82
+ RowId.new(5, 6),
83
+ FieldId.new(:name, :first) => 'sam',
84
+ FieldId.new(:address, :st1) => base_value
85
+ )
86
+ ]
87
+
88
+ expect(rows).to eq(expected)
89
+ end
90
+ end
91
+ end