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