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
data/lib/hash_math.rb
CHANGED
@@ -7,6 +7,15 @@
|
|
7
7
|
# LICENSE file in the root directory of this source tree.
|
8
8
|
#
|
9
9
|
|
10
|
+
require 'acts_as_hashable'
|
11
|
+
|
12
|
+
require_relative 'hash_math/mapper'
|
13
|
+
require_relative 'hash_math/matrix'
|
14
|
+
require_relative 'hash_math/record'
|
15
|
+
require_relative 'hash_math/table'
|
16
|
+
require_relative 'hash_math/unpivot'
|
17
|
+
|
10
18
|
# Top-level namespace
|
11
19
|
module HashMath
|
20
|
+
class KeyOutOfBoundsError < StandardError; end
|
12
21
|
end
|
@@ -0,0 +1,65 @@
|
|
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
|
+
# Raises ArgumentError if name is blank.
|
30
|
+
def add_each(name, objects)
|
31
|
+
raise ArgumentError, 'name is required' if name.to_s.empty?
|
32
|
+
|
33
|
+
tap { objects.each { |o| add(name, o) } }
|
34
|
+
end
|
35
|
+
|
36
|
+
# Add a lookup record to this instance's lookup dataset.
|
37
|
+
# Raises ArgumentError if name is blank.
|
38
|
+
def add(name, object)
|
39
|
+
raise ArgumentError, 'name is required' if name.to_s.empty?
|
40
|
+
|
41
|
+
tap { mappings_by_name.fetch(name.to_s).add(object) }
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns a new hash with the added/updated key-value pairs. Note that this only does a
|
45
|
+
# shallow copy using Hash#merge.
|
46
|
+
def map(hash)
|
47
|
+
map!({}.merge(hash || {}))
|
48
|
+
end
|
49
|
+
|
50
|
+
# Mutates the inpuuted hash with the added/updated key-value pairs.
|
51
|
+
def map!(hash)
|
52
|
+
mappings_by_name.values.each_with_object(hash) do |mapping, _memo|
|
53
|
+
mapping.map!(hash)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def pivot_by_name(array)
|
60
|
+
array.each_with_object({}) do |object, memo|
|
61
|
+
memo[object.name.to_s] = object
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
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
|
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
|
@@ -0,0 +1,60 @@
|
|
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 'matrix/key_value_pair'
|
11
|
+
|
12
|
+
module HashMath
|
13
|
+
# A Matrix allows you to build up a hash of key and values, then it will generate the
|
14
|
+
# product of all values.
|
15
|
+
class Matrix
|
16
|
+
extend Forwardable
|
17
|
+
include Enumerable
|
18
|
+
|
19
|
+
def_delegators :pair_products, :each
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@pairs_by_key = {}
|
23
|
+
|
24
|
+
freeze
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_each(key, vals)
|
28
|
+
tap { kvp(key).add_each(vals) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def add(key, val)
|
32
|
+
tap { kvp(key).add(val) }
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
attr_reader :pairs_by_key
|
38
|
+
|
39
|
+
def kvp(key)
|
40
|
+
pairs_by_key[key] ||= KeyValuePair.new(key)
|
41
|
+
end
|
42
|
+
|
43
|
+
def make_pair_groups
|
44
|
+
pairs_by_key.values.map(&:pairs)
|
45
|
+
end
|
46
|
+
|
47
|
+
def pair_products
|
48
|
+
pair_groups = make_pair_groups
|
49
|
+
|
50
|
+
products = pair_groups.inject(pair_groups.shift) { |memo, f| memo.product(f) }
|
51
|
+
&.map { |f| f.is_a?(KeyValuePair::Pair) ? [f] : f.flatten } || []
|
52
|
+
|
53
|
+
products.map { |pairs| recombine(pairs) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def recombine(pairs)
|
57
|
+
pairs.each_with_object({}) { |p, memo| memo[p.key] = p.value }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,38 @@
|
|
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 Matrix
|
12
|
+
# A hash-like structure that allows you to gradually build up keys.
|
13
|
+
class KeyValuePair
|
14
|
+
Pair = Struct.new(:key, :value)
|
15
|
+
|
16
|
+
attr_reader :key, :value
|
17
|
+
|
18
|
+
def initialize(key)
|
19
|
+
@key = key
|
20
|
+
@value = Set.new
|
21
|
+
|
22
|
+
freeze
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_each(vals)
|
26
|
+
tap { vals.each { |val| add(val) } }
|
27
|
+
end
|
28
|
+
|
29
|
+
def add(val)
|
30
|
+
tap { value << val }
|
31
|
+
end
|
32
|
+
|
33
|
+
def pairs
|
34
|
+
value.map { |value| Pair.new(key, value) }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
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
|
+
module HashMath
|
11
|
+
# A Record serves as a prototype for a Hash. It will allow the output of hashes
|
12
|
+
# conforming to a strict (#make!) or non-strict (#make) shape.
|
13
|
+
class Record
|
14
|
+
extend Forwardable
|
15
|
+
|
16
|
+
def_delegators :prototype, :key?, :keys
|
17
|
+
|
18
|
+
def initialize(keys = [], base_value = nil)
|
19
|
+
@prototype = keys.map { |key| [key, base_value] }.to_h
|
20
|
+
|
21
|
+
freeze
|
22
|
+
end
|
23
|
+
|
24
|
+
def make!(hash = {})
|
25
|
+
make(hash, true)
|
26
|
+
end
|
27
|
+
|
28
|
+
def make(hash = {}, bound = false)
|
29
|
+
hash.each_with_object(shallow_copy_prototype) do |(key, value), memo|
|
30
|
+
next unless assert_key_in_bounds(key, bound)
|
31
|
+
|
32
|
+
memo[key] = value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
attr_reader :prototype
|
39
|
+
|
40
|
+
# raise error if key is not in key set and bound is true
|
41
|
+
# return true if key is in key set and bound is false
|
42
|
+
# return false if key is not in key set and bound is false
|
43
|
+
def assert_key_in_bounds(key, bound)
|
44
|
+
raise KeyOutOfBoundsError, "[#{key}] for: #{keys}" if not_key?(key) && bound
|
45
|
+
|
46
|
+
key?(key)
|
47
|
+
end
|
48
|
+
|
49
|
+
def not_key?(key)
|
50
|
+
!key?(key)
|
51
|
+
end
|
52
|
+
|
53
|
+
def shallow_copy_prototype
|
54
|
+
{}.merge(prototype)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,61 @@
|
|
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
|
+
# The main data structure for a virtual table that can be treated as a key-value builder.
|
12
|
+
# Basically, it is a hash with a default 'prototype' assigned to it, which serves as the
|
13
|
+
# base record. Then, #add is called over and over passing in row_id, field_id, and value,
|
14
|
+
# which gives it enough information to pinpoint where to insert the data (memory-wise.)
|
15
|
+
# Imagine a two-dimensional table where X is the field_id axis and row is the Y axis.
|
16
|
+
# Since it is essentially backed by a hash, the row_id and field_id can be anything that
|
17
|
+
# implements #hash, #eql? and #== properly.
|
18
|
+
class Table
|
19
|
+
extend Forwardable
|
20
|
+
include Enumerable
|
21
|
+
|
22
|
+
Row = Struct.new(:row_id, :fields)
|
23
|
+
|
24
|
+
attr_reader :lookup, :record
|
25
|
+
|
26
|
+
def_delegators :record, :keys, :key?
|
27
|
+
|
28
|
+
def initialize(record)
|
29
|
+
raise ArgumentError, 'record is required' unless record
|
30
|
+
|
31
|
+
@lookup = {}
|
32
|
+
@record = record
|
33
|
+
|
34
|
+
freeze
|
35
|
+
end
|
36
|
+
|
37
|
+
def add(row_id, field_id, value)
|
38
|
+
raise KeyOutOfBoundsError, "field_id: #{field_id} not allowed." unless key?(field_id)
|
39
|
+
|
40
|
+
tap { set(row_id, field_id, value) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def each
|
44
|
+
return enum_for(:each) unless block_given?
|
45
|
+
|
46
|
+
lookup.map do |row_id, fields|
|
47
|
+
Row.new(row_id, record.make!(fields)).tap { |row| yield(row) }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def row(row_id)
|
54
|
+
lookup[row_id] ||= {}
|
55
|
+
end
|
56
|
+
|
57
|
+
def set(row_id, field_id, value)
|
58
|
+
row(row_id)[field_id] = value
|
59
|
+
end
|
60
|
+
end
|
61
|
+
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 'unpivot/pivot_set'
|
11
|
+
|
12
|
+
module HashMath
|
13
|
+
# This class has the ability to extrapolate one hash (row) into multiple hashes (rows) while
|
14
|
+
# unpivoting specific keys into key-value pairs.
|
15
|
+
class Unpivot
|
16
|
+
extend Forwardable
|
17
|
+
|
18
|
+
attr_reader :pivot_set
|
19
|
+
|
20
|
+
def_delegators :pivot_set, :add
|
21
|
+
|
22
|
+
def initialize(pivot_set = PivotSet.new)
|
23
|
+
@pivot_set = PivotSet.make(pivot_set, nullable: false)
|
24
|
+
|
25
|
+
freeze
|
26
|
+
end
|
27
|
+
|
28
|
+
# The main method for this class that performs the un-pivoting and hash expansion.
|
29
|
+
# Pass in a hash and it will return an array of hashes.
|
30
|
+
def expand(hash)
|
31
|
+
return [hash] unless pivot_set.any?
|
32
|
+
|
33
|
+
all_combinations = pivot_set.expand(hash)
|
34
|
+
|
35
|
+
products = all_combinations.inject(all_combinations.shift) do |memo, array|
|
36
|
+
memo.product(array)
|
37
|
+
end
|
38
|
+
|
39
|
+
recombine(products)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def recombine(products)
|
45
|
+
products.map do |pairs|
|
46
|
+
if pairs.is_a?(Array)
|
47
|
+
pairs.inject(pairs.shift) { |memo, p| memo.merge(p) }
|
48
|
+
else
|
49
|
+
pairs
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|