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