hash_math 0.0.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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