csv_decision2 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
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