csv_decision2 0.5.1

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.
Files changed (134) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +3 -0
  3. data/.coveralls.yml +2 -0
  4. data/.gitignore +14 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +30 -0
  7. data/.travis.yml +6 -0
  8. data/CHANGELOG.md +85 -0
  9. data/Dockerfile +6 -0
  10. data/Gemfile +7 -0
  11. data/LICENSE +21 -0
  12. data/README.md +356 -0
  13. data/benchmarks/rufus_decision.rb +158 -0
  14. data/csv_decision2.gemspec +38 -0
  15. data/doc/CSVDecision/CellValidationError.html +143 -0
  16. data/doc/CSVDecision/Columns/Default.html +589 -0
  17. data/doc/CSVDecision/Columns/Dictionary.html +801 -0
  18. data/doc/CSVDecision/Columns/Entry.html +508 -0
  19. data/doc/CSVDecision/Columns.html +1259 -0
  20. data/doc/CSVDecision/Constant.html +254 -0
  21. data/doc/CSVDecision/Data.html +479 -0
  22. data/doc/CSVDecision/Decide.html +302 -0
  23. data/doc/CSVDecision/Decision.html +1011 -0
  24. data/doc/CSVDecision/Defaults.html +291 -0
  25. data/doc/CSVDecision/Dictionary/Entry.html +1147 -0
  26. data/doc/CSVDecision/Dictionary.html +426 -0
  27. data/doc/CSVDecision/Error.html +139 -0
  28. data/doc/CSVDecision/FileError.html +143 -0
  29. data/doc/CSVDecision/Function.html +240 -0
  30. data/doc/CSVDecision/Guard.html +245 -0
  31. data/doc/CSVDecision/Header.html +647 -0
  32. data/doc/CSVDecision/Index.html +741 -0
  33. data/doc/CSVDecision/Input.html +404 -0
  34. data/doc/CSVDecision/Load.html +296 -0
  35. data/doc/CSVDecision/Matchers/Constant.html +484 -0
  36. data/doc/CSVDecision/Matchers/Function.html +511 -0
  37. data/doc/CSVDecision/Matchers/Guard.html +503 -0
  38. data/doc/CSVDecision/Matchers/Matcher.html +507 -0
  39. data/doc/CSVDecision/Matchers/Numeric.html +415 -0
  40. data/doc/CSVDecision/Matchers/Pattern.html +491 -0
  41. data/doc/CSVDecision/Matchers/Proc.html +704 -0
  42. data/doc/CSVDecision/Matchers/Range.html +379 -0
  43. data/doc/CSVDecision/Matchers/Symbol.html +426 -0
  44. data/doc/CSVDecision/Matchers.html +1567 -0
  45. data/doc/CSVDecision/Numeric.html +259 -0
  46. data/doc/CSVDecision/Options.html +443 -0
  47. data/doc/CSVDecision/Parse.html +282 -0
  48. data/doc/CSVDecision/Paths.html +742 -0
  49. data/doc/CSVDecision/Result.html +1200 -0
  50. data/doc/CSVDecision/Scan/InputHashes.html +369 -0
  51. data/doc/CSVDecision/Scan.html +313 -0
  52. data/doc/CSVDecision/ScanRow.html +866 -0
  53. data/doc/CSVDecision/Symbol.html +256 -0
  54. data/doc/CSVDecision/Table.html +1470 -0
  55. data/doc/CSVDecision/TableValidationError.html +143 -0
  56. data/doc/CSVDecision/Validate.html +422 -0
  57. data/doc/CSVDecision.html +621 -0
  58. data/doc/_index.html +471 -0
  59. data/doc/class_list.html +51 -0
  60. data/doc/css/common.css +1 -0
  61. data/doc/css/full_list.css +58 -0
  62. data/doc/css/style.css +499 -0
  63. data/doc/file.README.html +421 -0
  64. data/doc/file_list.html +56 -0
  65. data/doc/frames.html +17 -0
  66. data/doc/index.html +421 -0
  67. data/doc/js/app.js +248 -0
  68. data/doc/js/full_list.js +216 -0
  69. data/doc/js/jquery.js +4 -0
  70. data/doc/method_list.html +1163 -0
  71. data/doc/top-level-namespace.html +110 -0
  72. data/docker-compose.yml +13 -0
  73. data/lib/csv_decision/columns.rb +192 -0
  74. data/lib/csv_decision/data.rb +92 -0
  75. data/lib/csv_decision/decision.rb +196 -0
  76. data/lib/csv_decision/defaults.rb +47 -0
  77. data/lib/csv_decision/dictionary.rb +180 -0
  78. data/lib/csv_decision/header.rb +83 -0
  79. data/lib/csv_decision/index.rb +107 -0
  80. data/lib/csv_decision/input.rb +121 -0
  81. data/lib/csv_decision/load.rb +36 -0
  82. data/lib/csv_decision/matchers/constant.rb +74 -0
  83. data/lib/csv_decision/matchers/function.rb +56 -0
  84. data/lib/csv_decision/matchers/guard.rb +142 -0
  85. data/lib/csv_decision/matchers/numeric.rb +44 -0
  86. data/lib/csv_decision/matchers/pattern.rb +94 -0
  87. data/lib/csv_decision/matchers/range.rb +95 -0
  88. data/lib/csv_decision/matchers/symbol.rb +149 -0
  89. data/lib/csv_decision/matchers.rb +220 -0
  90. data/lib/csv_decision/options.rb +124 -0
  91. data/lib/csv_decision/parse.rb +165 -0
  92. data/lib/csv_decision/paths.rb +78 -0
  93. data/lib/csv_decision/result.rb +204 -0
  94. data/lib/csv_decision/scan.rb +117 -0
  95. data/lib/csv_decision/scan_row.rb +142 -0
  96. data/lib/csv_decision/table.rb +101 -0
  97. data/lib/csv_decision/validate.rb +85 -0
  98. data/lib/csv_decision.rb +45 -0
  99. data/spec/csv_decision/columns_spec.rb +251 -0
  100. data/spec/csv_decision/constant_spec.rb +36 -0
  101. data/spec/csv_decision/data_spec.rb +50 -0
  102. data/spec/csv_decision/decision_spec.rb +19 -0
  103. data/spec/csv_decision/examples_spec.rb +242 -0
  104. data/spec/csv_decision/index_spec.rb +58 -0
  105. data/spec/csv_decision/input_spec.rb +55 -0
  106. data/spec/csv_decision/load_spec.rb +28 -0
  107. data/spec/csv_decision/matchers/function_spec.rb +82 -0
  108. data/spec/csv_decision/matchers/guard_spec.rb +170 -0
  109. data/spec/csv_decision/matchers/numeric_spec.rb +47 -0
  110. data/spec/csv_decision/matchers/pattern_spec.rb +183 -0
  111. data/spec/csv_decision/matchers/range_spec.rb +70 -0
  112. data/spec/csv_decision/matchers/symbol_spec.rb +67 -0
  113. data/spec/csv_decision/options_spec.rb +94 -0
  114. data/spec/csv_decision/parse_spec.rb +44 -0
  115. data/spec/csv_decision/table_spec.rb +683 -0
  116. data/spec/csv_decision_spec.rb +7 -0
  117. data/spec/data/invalid/empty.csv +0 -0
  118. data/spec/data/invalid/invalid_header1.csv +4 -0
  119. data/spec/data/invalid/invalid_header2.csv +4 -0
  120. data/spec/data/invalid/invalid_header3.csv +4 -0
  121. data/spec/data/invalid/invalid_header4.csv +4 -0
  122. data/spec/data/valid/benchmark_regexp.csv +10 -0
  123. data/spec/data/valid/index_example.csv +13 -0
  124. data/spec/data/valid/multi_column_index.csv +10 -0
  125. data/spec/data/valid/multi_column_index2.csv +12 -0
  126. data/spec/data/valid/options_in_file1.csv +5 -0
  127. data/spec/data/valid/options_in_file2.csv +5 -0
  128. data/spec/data/valid/options_in_file3.csv +13 -0
  129. data/spec/data/valid/regular_expressions.csv +11 -0
  130. data/spec/data/valid/simple_constants.csv +5 -0
  131. data/spec/data/valid/simple_example.csv +10 -0
  132. data/spec/data/valid/valid.csv +4 -0
  133. data/spec/spec_helper.rb +106 -0
  134. metadata +352 -0
@@ -0,0 +1,110 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>
7
+ Top Level Namespace
8
+
9
+ &mdash; Documentation by YARD 0.9.12
10
+
11
+ </title>
12
+
13
+ <link rel="stylesheet" href="css/style.css" type="text/css" charset="utf-8" />
14
+
15
+ <link rel="stylesheet" href="css/common.css" type="text/css" charset="utf-8" />
16
+
17
+ <script type="text/javascript" charset="utf-8">
18
+ pathId = "";
19
+ relpath = '';
20
+ </script>
21
+
22
+
23
+ <script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
24
+
25
+ <script type="text/javascript" charset="utf-8" src="js/app.js"></script>
26
+
27
+
28
+ </head>
29
+ <body>
30
+ <div class="nav_wrap">
31
+ <iframe id="nav" src="class_list.html?1"></iframe>
32
+ <div id="resizer"></div>
33
+ </div>
34
+
35
+ <div id="main" tabindex="-1">
36
+ <div id="header">
37
+ <div id="menu">
38
+
39
+ <a href="_index.html">Index</a> &raquo;
40
+
41
+
42
+ <span class="title">Top Level Namespace</span>
43
+
44
+ </div>
45
+
46
+ <div id="search">
47
+
48
+ <a class="full_list_link" id="class_list_link"
49
+ href="class_list.html">
50
+
51
+ <svg width="24" height="24">
52
+ <rect x="0" y="4" width="24" height="4" rx="1" ry="1"></rect>
53
+ <rect x="0" y="12" width="24" height="4" rx="1" ry="1"></rect>
54
+ <rect x="0" y="20" width="24" height="4" rx="1" ry="1"></rect>
55
+ </svg>
56
+ </a>
57
+
58
+ </div>
59
+ <div class="clear"></div>
60
+ </div>
61
+
62
+ <div id="content"><h1>Top Level Namespace
63
+
64
+
65
+
66
+ </h1>
67
+ <div class="box_info">
68
+
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+ </div>
80
+
81
+ <h2>Defined Under Namespace</h2>
82
+ <p class="children">
83
+
84
+
85
+ <strong class="modules">Modules:</strong> <span class='object_link'><a href="CSVDecision.html" title="CSVDecision (module)">CSVDecision</a></span>
86
+
87
+
88
+
89
+
90
+ </p>
91
+
92
+
93
+
94
+
95
+
96
+
97
+
98
+
99
+
100
+ </div>
101
+
102
+ <div id="footer">
103
+ Generated on Sun Feb 11 10:26:07 2018 by
104
+ <a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
105
+ 0.9.12 (ruby-2.4.0).
106
+ </div>
107
+
108
+ </div>
109
+ </body>
110
+ </html>
@@ -0,0 +1,13 @@
1
+ version: "3.8"
2
+ services:
3
+ app:
4
+ container_name: csv_decision_app
5
+ build: .
6
+ command: bash
7
+ volumes:
8
+ - .:/app
9
+ - bundle:/usr/local/bundle
10
+ stdin_open: true
11
+ tty: true
12
+ volumes:
13
+ bundle:
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ # CSV Decision: CSV based Ruby decision tables.
4
+ # Created December 2017.
5
+ # @author Brett Vickers <brett@phillips-vickers.com>
6
+ # See LICENSE and README.md for details.
7
+ module CSVDecision
8
+ # Dictionary of all this table's columns - inputs, outputs etc.
9
+ # @api private
10
+ class Columns
11
+ # @param columns [CSVDecision::Columns] Table's columns dictionary.
12
+ # @param row [Array] Data row.
13
+ # @return [void]
14
+ def self.outs_dictionary(columns:, row:)
15
+ row.each_with_index do |cell, index|
16
+ outs_check_cell(columns: columns, cell: cell, index: index)
17
+ end
18
+ end
19
+
20
+ # @param columns [CSVDecision::Columns] Table's columns dictionary.
21
+ # @param row [Array] Data row.
22
+ # @return [void]
23
+ def self.ins_dictionary(columns:, row:)
24
+ row.each { |cell| ins_cell_dictionary(columns: columns, cell: cell) }
25
+ end
26
+
27
+ # @param columns [CSVDecision::Columns] Table's columns dictionary.
28
+ # @param cell [Object] Data row cell.
29
+ # @return [void]
30
+ def self.ins_cell_dictionary(columns:, cell:)
31
+ return unless cell.is_a?(Matchers::Proc)
32
+ return if cell.symbols.nil?
33
+
34
+ add_ins_symbols(columns: columns, cell: cell)
35
+ end
36
+
37
+ def self.outs_check_cell(columns:, cell:, index:)
38
+ return unless cell.is_a?(Matchers::Proc)
39
+ return if cell.symbols.nil?
40
+
41
+ check_outs_symbols(columns: columns, cell: cell, index: index)
42
+ end
43
+ private_class_method :outs_check_cell
44
+
45
+ def self.check_outs_symbols(columns:, cell:, index:)
46
+ Array(cell.symbols).each do |symbol|
47
+ check_outs_symbol(columns: columns, symbol: symbol, index: index)
48
+ end
49
+ end
50
+ private_class_method :check_outs_symbols
51
+
52
+ def self.check_outs_symbol(columns:, symbol:, index:)
53
+ in_out = columns.dictionary[symbol]
54
+
55
+ # If its an input column symbol then we're good.
56
+ return if ins_symbol?(columns: columns, symbol: symbol, in_out: in_out)
57
+
58
+ # Check if this output symbol reference is on or after this cell's column
59
+ invalid_out_ref?(columns, index, in_out)
60
+ end
61
+ private_class_method :check_outs_symbol
62
+
63
+ # If the symbol exists either as an input or does not exist then we're good.
64
+ def self.ins_symbol?(columns:, symbol:, in_out:)
65
+ return true if in_out == :in
66
+
67
+ # It must an input symbol, as all the output symbols have been parsed.
68
+ return columns.dictionary[symbol] = :in if in_out.nil?
69
+
70
+ false
71
+ end
72
+ private_class_method :ins_symbol?
73
+
74
+ def self.invalid_out_ref?(columns, index, in_out)
75
+ return false if in_out < index
76
+
77
+ that_column = if in_out == index
78
+ 'reference to itself'
79
+ else
80
+ "an out of order reference to output column '#{columns.outs[in_out].name}'"
81
+ end
82
+ raise CellValidationError,
83
+ "output column '#{columns.outs[index].name}' makes #{that_column}"
84
+ end
85
+ private_class_method :invalid_out_ref?
86
+
87
+ def self.add_ins_symbols(columns:, cell:)
88
+ Array(cell.symbols).each do |symbol|
89
+ CSVDecision::Dictionary.add_name(columns: columns, name: symbol)
90
+ end
91
+ end
92
+ private_class_method :add_ins_symbols
93
+
94
+ # Dictionary of all table data columns.
95
+ # The key of each hash is the header cell's array column index.
96
+ # Note that input and output columns may be interspersed, and multiple input columns
97
+ # may refer to the same input hash key symbol.
98
+ # However, output columns must have unique symbols, which cannot overlap with input
99
+ # column symbols.
100
+ class Dictionary
101
+ # @return [Hash{Integer=>Entry}] All column names.
102
+ attr_accessor :columns
103
+
104
+ # @return [Hash{Integer=>Entry}] All input column dictionary entries.
105
+ attr_accessor :ins
106
+
107
+ # @return [Hash{Integer=>Entry}] All defaulted input column dictionary
108
+ # entries. This is actually just a subset of :ins.
109
+ attr_accessor :defaults
110
+
111
+ # @return [Hash{Integer=>Entry}] All output column dictionary entries.
112
+ attr_accessor :outs
113
+
114
+ # @return [Hash{Integer=>Entry}] All if: column dictionary entries.
115
+ # This is actually just a subset of :outs.
116
+ attr_accessor :ifs
117
+
118
+ # @return [Hash{Integer=>Symbol}] All path columns.
119
+ # This is actually just a subset of :outs.
120
+ attr_accessor :paths
121
+
122
+ def initialize
123
+ @columns = {}
124
+ @defaults = {}
125
+ @ifs = {}
126
+ @ins = {}
127
+ @outs = {}
128
+ @paths = {}
129
+ end
130
+ end
131
+
132
+ # Input columns with defaults specified
133
+ def defaults
134
+ @dictionary&.defaults
135
+ end
136
+
137
+ # Set defaults for columns with defaults specified
138
+ def defaults=(value)
139
+ @dictionary.defaults = value
140
+ end
141
+
142
+ # @return [Hash{Symbol=>[false, Integer]}] Dictionary of all
143
+ # input and output column names.
144
+ def dictionary
145
+ @dictionary.columns
146
+ end
147
+
148
+ # Input columns hash keyed by column index.
149
+ # @return [Hash{Index=>Entry}]
150
+ def ins
151
+ @dictionary.ins
152
+ end
153
+
154
+ # Output columns hash keyed by column index.
155
+ # @return [Hash{Index=>Entry}]
156
+ def outs
157
+ @dictionary&.outs
158
+ end
159
+
160
+ # if: columns hash keyed by column index.
161
+ # @return [Hash{Index=>Entry}]
162
+ def ifs
163
+ @dictionary.ifs
164
+ end
165
+
166
+ # path: columns hash keyed by column index.
167
+ # @return [Hash{Index=>Entry}]
168
+ def paths
169
+ @dictionary.paths
170
+ end
171
+
172
+ # @return [Array<Symbol>] All input column symbols.
173
+ def input_keys
174
+ @dictionary.columns.select { |_k, v| v == :in }.keys
175
+ end
176
+
177
+ # @param table [Table] Decision table being constructed.
178
+ def initialize(table)
179
+ # If a column does not have a valid header cell, then it's empty of data.
180
+ # Return the stripped header row, and remove it from the data array.
181
+ row = Header.strip_empty_columns(rows: table.rows)
182
+
183
+ # No header row found?
184
+ raise TableValidationError, 'table has no header row' unless row
185
+
186
+ # Build a dictionary of all valid data columns from the header row.
187
+ @dictionary = CSVDecision::Dictionary.build(header: row, dictionary: Dictionary.new)
188
+
189
+ freeze
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'csv'
4
+
5
+ # CSV Decision: CSV based Ruby decision tables.
6
+ # Created December 2017.
7
+ # @author Brett Vickers <brett@phillips-vickers.com>
8
+ # See LICENSE and README.md for details..
9
+ module CSVDecision
10
+ # All cells starting with this character are comments, and treated as a blank cell.
11
+ COMMENT_CHARACTER = '#'
12
+ private_constant :COMMENT_CHARACTER
13
+
14
+ # Methods to load data from a file, CSV string or an array of arrays.
15
+ # @api private
16
+ module Data
17
+ # Options passed to CSV.parse and CSV.read.
18
+ CSV_OPTIONS = { encoding: 'UTF-8', skip_blanks: true }.freeze
19
+ private_constant :CSV_OPTIONS
20
+
21
+ # Parse the input data which may either be a file path name, CSV string or
22
+ # array of arrays. Strips out empty columns/rows and comment cells.
23
+ #
24
+ # @param data (see Parse.parse)
25
+ # @return [Array<Array<String>>] Data array stripped of empty rows.
26
+ def self.to_array(data:)
27
+ strip_rows(data: data_array(data))
28
+ end
29
+
30
+ # If the input is a file name return true, otherwise false.
31
+ #
32
+ # @param data (see Parse.parse)
33
+ # @return [Boolean] Set to true if the input data is passed as a File or Pathname.
34
+ def self.input_file?(data)
35
+ data.is_a?(Pathname) || data.is_a?(File)
36
+ end
37
+
38
+ # Strip the empty columns from the input data rows.
39
+ #
40
+ # @param data (see Parse.parse)
41
+ # @param empty_columns [Array<Index>]
42
+ # @return [Array<Array<String>>] Data array stripped of empty columns.
43
+ def self.strip_columns(data:, empty_columns:)
44
+ # Adjust column indices as we delete columns the rest shift to the left by 1
45
+ empty_columns.map!.with_index { |col, index| col - index }
46
+
47
+ # Delete all empty columns from the array of arrays
48
+ empty_columns.each { |col| data.each_index { |row| data[row].delete_at(col) } }
49
+ end
50
+
51
+ # Parse the input data which may either be a file path name, CSV string or
52
+ # array of arrays
53
+ def self.data_array(input)
54
+ return CSV.read(input, **CSV_OPTIONS) if input_file?(input)
55
+ return input.deep_dup if input.is_a?(Array) && input[0].is_a?(Array)
56
+ return CSV.parse(input, **CSV_OPTIONS) if input.is_a?(String)
57
+
58
+ raise ArgumentError,
59
+ "#{input.class} input invalid; " \
60
+ 'input must be a file path name, CSV string or array of arrays'
61
+ end
62
+ private_class_method :data_array
63
+
64
+ def self.strip_rows(data:)
65
+ rows = []
66
+ data.each do |row|
67
+ row = strip_cells(row: row)
68
+ rows << row if row.find { |cell| cell != '' }
69
+ end
70
+ rows
71
+ end
72
+ private_class_method :strip_rows
73
+
74
+ # Strip cells of leading/trailing spaces; treat comments as an empty cell.
75
+ # Non string values treated as empty cells.
76
+ # Non-ascii strings treated as empty cells by default.
77
+ def self.strip_cells(row:)
78
+ row.map! { |cell| strip_cell(cell) }
79
+ end
80
+ private_class_method :strip_cells
81
+
82
+ def self.strip_cell(cell)
83
+ return '' unless cell.is_a?(String)
84
+ cell = cell.force_encoding('UTF-8')
85
+ return '' unless cell.ascii_only?
86
+ return '' if cell.lstrip[0] == COMMENT_CHARACTER
87
+
88
+ cell.strip
89
+ end
90
+ private_class_method :strip_cell
91
+ end
92
+ end
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ # CSV Decision: CSV based Ruby decision tables.
4
+ # Created December 2017.
5
+ # @author Brett Vickers <brett@phillips-vickers.com>
6
+ # See LICENSE and README.md for details.
7
+ module CSVDecision
8
+ # Accumulate the matching row(s) and calculate the final result.
9
+ # @api private
10
+ class Decision
11
+ # Main method for making decisions without a path.
12
+ #
13
+ # @param table [CSVDecision::Table] Decision table.
14
+ # @param input [Hash] Input hash (keys may or may not be symbolized)
15
+ # @param symbolize_keys [Boolean] Set to false if keys are symbolized and it's
16
+ # OK to mutate the input hash. Otherwise a copy of the input hash is symbolized.
17
+ # @return [Hash{Symbol=>Object}] Decision result.
18
+ def self.make(table:, input:, symbolize_keys:)
19
+ # Parse and transform the hash supplied as input
20
+ data = Input.parse(table: table, input: input, symbolize_keys: symbolize_keys)
21
+
22
+ # The decision object collects the results of the search and
23
+ # calculates the final result.
24
+ Decision.new(table: table).scan(data)
25
+ end
26
+
27
+ # @return [Boolean] True if a first match decision table.
28
+ attr_reader :first_match
29
+
30
+ # @return [CSVDecision::Table] Decision table object.
31
+ attr_reader :table
32
+
33
+ # @param table [CSVDecision::Table] Decision table being processed.
34
+ def initialize(table:)
35
+ # The result object is a hash of values, and each value will be an array if this is
36
+ # a multi-row result for the +first_match: false+ option.
37
+ @result = Result.new(table: table)
38
+ @first_match = table.options[:first_match]
39
+ @table = table
40
+ end
41
+
42
+ # Initialize the input data used to make the decision.
43
+ #
44
+ # @param data [Hash{Symbol=>Object}] Input hash data structure.
45
+ # @return [void]
46
+ def input(data)
47
+ @result.input(data[:hash])
48
+
49
+ # All rows picked by the matching process. An array if +first_match: false+,
50
+ # otherwise a single row.
51
+ @rows_picked = []
52
+
53
+ @input = data
54
+ end
55
+
56
+ # Scan the decision table and produce an output decision.
57
+ #
58
+ # @param data [Hash{Symbol=>Object}] Input hash data structure.
59
+ # @return (see .make)
60
+ def scan(data)
61
+ input(data)
62
+ # Use the table's index if present
63
+ @table.index ? index_scan : table_scan
64
+ end
65
+
66
+ # Scan the index for a first match result.
67
+ #
68
+ # @param scan_cols [Hash{Integer=>Object}]
69
+ # @param hash [Hash{Symbol=>Object}]
70
+ # @param index_rows [Array<Integer>]
71
+ # @return [Hash{Symbol=>Object}]
72
+ def index_scan_first_match(scan_cols:, hash:, index_rows:)
73
+ index_rows.each do |start_row, end_row|
74
+ @table.each(start_row, end_row || start_row) do |row, index|
75
+ next unless @table.scan_rows[index].match?(row: row, hash: hash, scan_cols: scan_cols)
76
+
77
+ return @result.attributes if first_match_found(row)
78
+ end
79
+ end
80
+
81
+ {}
82
+ end
83
+
84
+ # Scan the index for an accumulated result.
85
+ #
86
+ # @param scan_cols [Hash{Integer=>Object}]
87
+ # @param hash [Hash{Symbol=>Object}]
88
+ # @param index_rows [Array<Integer>]
89
+ # @return [Hash{Symbol=>Object}]
90
+ def index_scan_accumulate(scan_cols:, hash:, index_rows:)
91
+ index_rows.each do |start_row, end_row|
92
+ @table.each(start_row, end_row || start_row) do |row, index|
93
+ next unless @table.scan_rows[index].match?(row: row, hash: hash, scan_cols: scan_cols)
94
+
95
+ # Accumulate output rows.
96
+ @rows_picked << row
97
+ @result.accumulate_outs(row)
98
+ end
99
+ end
100
+
101
+ @rows_picked.empty? ? {} : accumulated_result
102
+ end
103
+
104
+ private
105
+
106
+ # Use an index to scan the decision table up against the input hash.
107
+ def index_scan_rows(rows:)
108
+ if @first_match
109
+ index_scan_first_match(scan_cols: @input[:scan_cols], hash: @input[:hash], index_rows: rows)
110
+ else
111
+ index_scan_accumulate(scan_cols: @input[:scan_cols], hash: @input[:hash], index_rows: rows)
112
+ end
113
+ end
114
+
115
+ def scan_first_match(hash:, scan_cols:)
116
+ @table.each do |row, index|
117
+ next unless @table.scan_rows[index].match?(row: row, hash: hash, scan_cols: scan_cols)
118
+
119
+ return @result.attributes if first_match_found(row)
120
+ end
121
+
122
+ {}
123
+ end
124
+
125
+ def scan_accumulate(hash:, scan_cols:)
126
+ @table.each do |row, index|
127
+ next unless @table.scan_rows[index].match?(row: row, hash: hash, scan_cols: scan_cols)
128
+
129
+ # Accumulate output rows
130
+ @rows_picked << row
131
+ @result.accumulate_outs(row)
132
+ end
133
+
134
+ @rows_picked.empty? ? {} : accumulated_result
135
+ end
136
+
137
+ # Scan the decision table up against the input hash.
138
+ def table_scan
139
+ if @first_match
140
+ scan_first_match(hash: @input[:hash], scan_cols: @input[:scan_cols])
141
+ else
142
+ scan_accumulate(hash: @input[:hash], scan_cols: @input[:scan_cols])
143
+ end
144
+ end
145
+
146
+ # Use an index to scan the decision table up against the input hash.
147
+ def index_scan
148
+ # If the index lookup fails, there's no match.
149
+ return {} unless (index_rows = Array(@table.index.hash[@input[:key]]))
150
+
151
+ index_scan_rows(rows: index_rows)
152
+ end
153
+
154
+ def accumulated_result
155
+ return @result.final_result unless @result.outs_functions
156
+ return @result.eval_outs(@rows_picked.first) unless @result.multi_result
157
+
158
+ multi_row_result
159
+ end
160
+
161
+ def multi_row_result
162
+ # Scan each output column that contains functions
163
+ @result.outs.each_pair { |col, column| eval_procs(col: col, column: column) if column.eval }
164
+
165
+ @result.final_result
166
+ end
167
+
168
+ def eval_procs(col:, column:)
169
+ @rows_picked.each_with_index do |row, index|
170
+ cell = row[col]
171
+ next unless cell.is_a?(Matchers::Proc)
172
+
173
+ # Evaluate the proc and update the result
174
+ @result.eval_cell_proc(proc: cell, column_name: column.name, index: index)
175
+ end
176
+ end
177
+
178
+ def first_match_found(row)
179
+ # This decision row may contain procs, which if present will need to be evaluated.
180
+ # If this row contains if: columns then this row may be filtered out, in which case
181
+ # this method call will return false.
182
+ return eval_single_row(row) if @result.outs_functions
183
+
184
+ # Common case is just copying output column values to the final result.
185
+ @rows_picked = row
186
+ @result.add_outs(row)
187
+ end
188
+
189
+ def eval_single_row(row)
190
+ return false unless (result = @result.eval_outs(row))
191
+
192
+ @rows_picked = row
193
+ result
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # CSV Decision: CSV based Ruby decision tables.
4
+ # Created December 2017.
5
+ # @author Brett Vickers.
6
+ # See LICENSE and README.md for details.
7
+ module CSVDecision
8
+ # Parse the default row beneath the header row if present
9
+ # @api private
10
+ module Defaults
11
+ # Parse the defaults row that (optionally) appears just after the header row.
12
+ # We have already determined that this row must be present.
13
+ # @param columns [{Integer=>Dictionary::Entry}] Hash of header columns with defaults.
14
+ # @param matchers [Array<Matchers>] Output cell special matchers.
15
+ # @param row [Array<String>] Defaults row that appears just after the header row.
16
+ # @raise [TableValidationError] Missing defaults row.
17
+ def self.parse(columns:, matchers:, row:)
18
+ raise TableValidationError, 'Missing defaults row' if row.nil?
19
+
20
+ defaults = columns.defaults
21
+
22
+ # Scan the default row for procs and constants
23
+ scan_row = ScanRow.new.scan_columns(row: row, columns: defaults, matchers: matchers)
24
+
25
+ parse_columns(defaults: defaults, columns: columns.dictionary, row: scan_row)
26
+ end
27
+
28
+ def self.parse_columns(defaults:, columns:, row:)
29
+ defaults.each_pair do |col, entry|
30
+ parse_cell(cell: row[col], columns: columns, entry: entry)
31
+ end
32
+
33
+ defaults
34
+ end
35
+ private_class_method :parse_columns
36
+
37
+ def self.parse_cell(cell:, columns:, entry:)
38
+ return entry.function = cell unless cell.is_a?(Matchers::Proc)
39
+
40
+ entry.function = cell.function
41
+
42
+ # Add any referenced input column symbols to the column name dictionary
43
+ Columns.ins_cell_dictionary(columns: columns, cell: cell)
44
+ end
45
+ private_class_method :parse_cell
46
+ end
47
+ end