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