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.
- checksums.yaml +4 -4
- data/.rubocop.yml +5 -4
- data/.ruby-version +1 -1
- data/.travis.yml +3 -4
- data/CHANGELOG.md +28 -0
- data/README.md +352 -1
- data/hash_math.gemspec +8 -5
- data/lib/hash_math.rb +9 -0
- data/lib/hash_math/mapper.rb +65 -0
- data/lib/hash_math/mapper/lookup.rb +56 -0
- data/lib/hash_math/mapper/mapping.rb +77 -0
- data/lib/hash_math/matrix.rb +60 -0
- data/lib/hash_math/matrix/key_value_pair.rb +38 -0
- data/lib/hash_math/record.rb +57 -0
- data/lib/hash_math/table.rb +61 -0
- data/lib/hash_math/unpivot.rb +54 -0
- data/lib/hash_math/unpivot/pivot.rb +47 -0
- data/lib/hash_math/unpivot/pivot_set.rb +54 -0
- data/lib/hash_math/version.rb +1 -1
- data/spec/hash_math/mapper/mapping_spec.rb +74 -0
- data/spec/hash_math/mapper_spec.rb +139 -0
- data/spec/hash_math/matrix_spec.rb +57 -0
- data/spec/hash_math/record_spec.rb +48 -0
- data/spec/hash_math/table_spec.rb +91 -0
- data/spec/hash_math/unpivot/pivot_spec.rb +53 -0
- data/spec/hash_math/unpivot_spec.rb +121 -0
- metadata +63 -11
@@ -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
|
data/lib/hash_math/version.rb
CHANGED
@@ -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
|