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