csv_decision 0.0.6 → 0.0.7
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/CHANGELOG.md +9 -1
- data/README.md +112 -93
- data/benchmarks/rufus_decision.rb +9 -1
- data/csv_decision.gemspec +6 -6
- data/doc/CSVDecision.html +54 -90
- data/doc/CSVDecision/CellValidationError.html +1 -1
- data/doc/CSVDecision/Columns.html +104 -45
- data/doc/CSVDecision/Columns/Dictionary.html +40 -24
- data/doc/CSVDecision/Columns/Entry.html +209 -22
- data/doc/CSVDecision/Constant.html +9 -50
- data/doc/CSVDecision/Data.html +182 -47
- data/doc/CSVDecision/Decide.html +97 -93
- data/doc/CSVDecision/Decision.html +105 -294
- data/doc/CSVDecision/Error.html +1 -1
- data/doc/CSVDecision/FileError.html +1 -1
- data/doc/CSVDecision/Function.html +18 -7
- data/doc/CSVDecision/Guard.html +245 -0
- data/doc/CSVDecision/Header.html +58 -50
- data/doc/CSVDecision/Input.html +20 -12
- data/doc/CSVDecision/Load.html +80 -14
- data/doc/CSVDecision/Matchers.html +237 -279
- data/doc/CSVDecision/Matchers/Constant.html +280 -13
- data/doc/CSVDecision/Matchers/Function.html +188 -19
- data/doc/CSVDecision/Matchers/Guard.html +568 -0
- data/doc/CSVDecision/Matchers/Matcher.html +200 -14
- data/doc/CSVDecision/Matchers/Numeric.html +165 -13
- data/doc/CSVDecision/Matchers/Pattern.html +56 -163
- data/doc/CSVDecision/Matchers/Range.html +48 -37
- data/doc/CSVDecision/Matchers/Symbol.html +161 -16
- data/doc/CSVDecision/Numeric.html +4 -4
- data/doc/CSVDecision/Options.html +53 -55
- data/doc/CSVDecision/Parse.html +23 -13
- data/doc/CSVDecision/ScanRow.html +461 -73
- data/doc/CSVDecision/Symbol.html +4 -4
- data/doc/CSVDecision/Table.html +185 -79
- data/doc/_index.html +15 -28
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +105 -82
- data/doc/index.html +105 -82
- data/doc/method_list.html +137 -113
- data/doc/top-level-namespace.html +1 -1
- data/lib/csv_decision.rb +2 -5
- data/lib/csv_decision/columns.rb +14 -5
- data/lib/csv_decision/data.rb +24 -6
- data/lib/csv_decision/decide.rb +18 -20
- data/lib/csv_decision/decision.rb +106 -42
- data/lib/csv_decision/header.rb +44 -23
- data/lib/csv_decision/input.rb +4 -2
- data/lib/csv_decision/load.rb +7 -3
- data/lib/csv_decision/matchers.rb +49 -41
- data/lib/csv_decision/matchers/constant.rb +62 -4
- data/lib/csv_decision/matchers/function.rb +33 -2
- data/lib/csv_decision/matchers/guard.rb +143 -0
- data/lib/csv_decision/matchers/numeric.rb +34 -3
- data/lib/csv_decision/matchers/pattern.rb +11 -4
- data/lib/csv_decision/matchers/range.rb +34 -26
- data/lib/csv_decision/matchers/symbol.rb +71 -5
- data/lib/csv_decision/options.rb +31 -20
- data/lib/csv_decision/parse.rb +28 -9
- data/lib/csv_decision/scan_row.rb +79 -13
- data/lib/csv_decision/table.rb +34 -23
- data/spec/csv_decision/columns_spec.rb +32 -7
- data/spec/csv_decision/constant_spec.rb +2 -26
- data/spec/csv_decision/decision_spec.rb +0 -9
- data/spec/csv_decision/examples_spec.rb +33 -16
- data/spec/csv_decision/matchers/function_spec.rb +1 -1
- data/spec/csv_decision/matchers/guard_spec.rb +153 -0
- data/spec/csv_decision/matchers/numeric_spec.rb +1 -1
- data/spec/csv_decision/matchers/pattern_spec.rb +2 -2
- data/spec/csv_decision/matchers/range_spec.rb +2 -2
- data/spec/csv_decision/matchers/symbol_spec.rb +1 -1
- data/spec/csv_decision/options_spec.rb +3 -3
- data/spec/csv_decision/table_spec.rb +96 -12
- data/spec/data/valid/benchmark_regexp.csv +10 -0
- data/spec/data/valid/regular_expressions.csv +11 -0
- metadata +14 -9
- data/lib/csv_decision/constant.rb +0 -54
- data/lib/csv_decision/function.rb +0 -32
- data/lib/csv_decision/numeric.rb +0 -38
- data/lib/csv_decision/symbol.rb +0 -73
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
</div>
|
|
101
101
|
|
|
102
102
|
<div id="footer">
|
|
103
|
-
Generated on
|
|
103
|
+
Generated on Sat Dec 30 13:04:04 2017 by
|
|
104
104
|
<a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
|
|
105
105
|
0.9.12 (ruby-2.3.0).
|
|
106
106
|
</div>
|
data/lib/csv_decision.rb
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'active_support/core_ext/object'
|
|
4
4
|
require 'csv_decision/parse'
|
|
@@ -13,26 +13,23 @@ module CSVDecision
|
|
|
13
13
|
File.dirname __dir__
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
autoload :Constant, 'csv_decision/constant'
|
|
17
16
|
autoload :Data, 'csv_decision/data'
|
|
18
17
|
autoload :Decide, 'csv_decision/decide'
|
|
19
18
|
autoload :Decision, 'csv_decision/decision'
|
|
20
19
|
autoload :Columns, 'csv_decision/columns'
|
|
21
|
-
autoload :Function, 'csv_decision/function'
|
|
22
20
|
autoload :Header, 'csv_decision/header'
|
|
23
21
|
autoload :Input, 'csv_decision/input'
|
|
24
22
|
autoload :Load, 'csv_decision/load'
|
|
25
23
|
autoload :Matchers, 'csv_decision/matchers'
|
|
26
|
-
autoload :Numeric, 'csv_decision/numeric'
|
|
27
24
|
autoload :Options, 'csv_decision/options'
|
|
28
25
|
autoload :Parse, 'csv_decision/parse'
|
|
29
26
|
autoload :ScanRow, 'csv_decision/scan_row'
|
|
30
|
-
autoload :Symbol, 'csv_decision/symbol'
|
|
31
27
|
autoload :Table, 'csv_decision/table'
|
|
32
28
|
|
|
33
29
|
class Matchers
|
|
34
30
|
autoload :Constant, 'csv_decision/matchers/constant'
|
|
35
31
|
autoload :Function, 'csv_decision/matchers/function'
|
|
32
|
+
autoload :Guard, 'csv_decision/matchers/guard'
|
|
36
33
|
autoload :Numeric, 'csv_decision/matchers/numeric'
|
|
37
34
|
autoload :Pattern, 'csv_decision/matchers/pattern'
|
|
38
35
|
autoload :Range, 'csv_decision/matchers/range'
|
data/lib/csv_decision/columns.rb
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# CSV Decision: CSV based Ruby decision tables.
|
|
4
|
-
# Created December 2017
|
|
4
|
+
# Created December 2017.
|
|
5
|
+
# @author Brett Vickers <brett@phillips-vickers.com>
|
|
5
6
|
# See LICENSE and README.md for details.
|
|
6
7
|
module CSVDecision
|
|
7
8
|
# Dictionary of all this table's columns - inputs, outputs etc.
|
|
9
|
+
# @api private
|
|
8
10
|
class Columns
|
|
9
11
|
# Value object to hold column dictionary entries.
|
|
10
|
-
Entry = Struct.new(:name, :
|
|
12
|
+
Entry = Struct.new(:name, :eval, :type) do
|
|
13
|
+
def ins?
|
|
14
|
+
%i[in guard].member?(type) ? true : false
|
|
15
|
+
end
|
|
16
|
+
end
|
|
11
17
|
|
|
12
18
|
# TODO: Value object used for any columns with defaults
|
|
13
19
|
# Default = Struct.new(:name, :function, :default_if)
|
|
@@ -38,15 +44,18 @@ module CSVDecision
|
|
|
38
44
|
end
|
|
39
45
|
end
|
|
40
46
|
|
|
41
|
-
# Dictionary of all data columns
|
|
47
|
+
# Dictionary of all data columns.
|
|
48
|
+
# @return [Columns::Dictionary]
|
|
42
49
|
attr_reader :dictionary
|
|
43
50
|
|
|
44
|
-
# Input columns
|
|
51
|
+
# Input columns hash keyed by column index.
|
|
52
|
+
# @return [Hash{Index=>Entry}]
|
|
45
53
|
def ins
|
|
46
54
|
@dictionary.ins
|
|
47
55
|
end
|
|
48
56
|
|
|
49
|
-
# Output columns
|
|
57
|
+
# Output columns hash keyed by column index.
|
|
58
|
+
# @return [Hash{Index=>Entry}]
|
|
50
59
|
def outs
|
|
51
60
|
@dictionary.outs
|
|
52
61
|
end
|
data/lib/csv_decision/data.rb
CHANGED
|
@@ -3,25 +3,43 @@
|
|
|
3
3
|
require 'csv'
|
|
4
4
|
|
|
5
5
|
# CSV Decision: CSV based Ruby decision tables.
|
|
6
|
-
# Created December 2017
|
|
7
|
-
#
|
|
6
|
+
# Created December 2017.
|
|
7
|
+
# @author Brett Vickers <brett@phillips-vickers.com>
|
|
8
|
+
# See LICENSE and README.md for details..
|
|
8
9
|
module CSVDecision
|
|
10
|
+
# All cells starting with this character are comments, and treated as a blank cell.
|
|
9
11
|
COMMENT_CHARACTER = '#'
|
|
12
|
+
private_constant :COMMENT_CHARACTER
|
|
10
13
|
|
|
11
|
-
# Methods to load data from a file, CSV string or array of arrays
|
|
14
|
+
# Methods to load data from a file, CSV string or an array of arrays.
|
|
15
|
+
# @api private
|
|
12
16
|
module Data
|
|
17
|
+
# Options passed to CSV.parse and CSV.read.
|
|
13
18
|
CSV_OPTIONS = { encoding: 'UTF-8', skip_blanks: true }.freeze
|
|
19
|
+
private_constant :CSV_OPTIONS
|
|
14
20
|
|
|
15
21
|
# Parse the input data which may either be a file path name, CSV string or
|
|
16
|
-
# array of arrays. Strips out empty columns/rows and comment cells
|
|
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.
|
|
17
26
|
def self.to_array(data:)
|
|
18
27
|
strip_rows(data: data_array(data))
|
|
19
28
|
end
|
|
20
29
|
|
|
21
|
-
|
|
22
|
-
|
|
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)
|
|
23
36
|
end
|
|
24
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.
|
|
25
43
|
def self.strip_columns(data:, empty_columns:)
|
|
26
44
|
# Adjust column indices as we delete columns the rest shift to the left by 1
|
|
27
45
|
empty_columns.map!.with_index { |col, index| col - index }
|
data/lib/csv_decision/decide.rb
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# CSV Decision: CSV based Ruby decision tables.
|
|
4
|
-
# Created December 2017
|
|
4
|
+
# Created December 2017.
|
|
5
|
+
# @author Brett Vickers <brett@phillips-vickers.com>
|
|
5
6
|
# See LICENSE and README.md for details.
|
|
6
7
|
module CSVDecision
|
|
7
8
|
# Main module for searching the decision table looking for one or more matches
|
|
9
|
+
# @api private
|
|
8
10
|
module Decide
|
|
11
|
+
# Match the table row against the input hash.
|
|
12
|
+
#
|
|
13
|
+
# @param row [Array] Table row.
|
|
14
|
+
# @param input [Hash{Symbol=>Object}] Input hash data structure.
|
|
15
|
+
# @param scan_row [ScanRow]
|
|
16
|
+
# @return [Boolean] Returns true if a match, false otherwise.
|
|
17
|
+
def self.matches?(row:, input:, scan_row:)
|
|
18
|
+
match = scan_row.match_constants?(row: row, scan_cols: input[:scan_cols])
|
|
19
|
+
return false unless match
|
|
20
|
+
|
|
21
|
+
return true if scan_row.procs.empty?
|
|
22
|
+
|
|
23
|
+
scan_row.match_procs?(row: row, input: input)
|
|
24
|
+
end
|
|
25
|
+
|
|
9
26
|
# Main method for making decisions.
|
|
10
27
|
#
|
|
11
28
|
# @param table [CSVDecision::Table] Decision table.
|
|
@@ -24,24 +41,5 @@ module CSVDecision
|
|
|
24
41
|
# table_scan(table: table, input: parsed_input, decision: decision)
|
|
25
42
|
decision.scan(table: table, input: parsed_input)
|
|
26
43
|
end
|
|
27
|
-
|
|
28
|
-
def self.matches?(row:, input:, scan_row:)
|
|
29
|
-
match = scan_row.match_constants?(row: row, scan_cols: input[:scan_cols])
|
|
30
|
-
return false unless match
|
|
31
|
-
|
|
32
|
-
return true if scan_row.procs.empty?
|
|
33
|
-
|
|
34
|
-
scan_row.match_procs?(row: row, input: input)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def self.eval_matcher(proc:, value:, hash:)
|
|
38
|
-
function = proc.function
|
|
39
|
-
|
|
40
|
-
# A symbol guard expression just needs to be passed the input hash
|
|
41
|
-
return function[hash] if proc.type == :expression
|
|
42
|
-
|
|
43
|
-
# All other procs can take one or two args
|
|
44
|
-
function.arity == 1 ? function[value] : function[value, hash]
|
|
45
|
-
end
|
|
46
44
|
end
|
|
47
45
|
end
|
|
@@ -1,80 +1,90 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# CSV Decision: CSV based Ruby decision tables.
|
|
4
|
-
# Created December 2017
|
|
4
|
+
# Created December 2017.
|
|
5
|
+
# @author Brett Vickers <brett@phillips-vickers.com>
|
|
5
6
|
# See LICENSE and README.md for details.
|
|
6
7
|
module CSVDecision
|
|
7
|
-
# Accumulate the matching row(s) and calculate the final result
|
|
8
|
+
# Accumulate the matching row(s) and calculate the final result.
|
|
9
|
+
# @api private
|
|
8
10
|
class Decision
|
|
11
|
+
# @param table [CSVDecision::Table] Decision table being processed.
|
|
12
|
+
# @param input [Hash{Symbol=>Object}] Input hash data structure.
|
|
9
13
|
def initialize(table:, input:)
|
|
10
14
|
@result = {}
|
|
11
15
|
|
|
12
16
|
# Relevant table attributes
|
|
13
17
|
@first_match = table.options[:first_match]
|
|
14
18
|
@outs = table.columns.outs
|
|
15
|
-
|
|
16
|
-
# TODO: Planned feature
|
|
17
|
-
# @outs_functions = table.outs_functions
|
|
19
|
+
@outs_functions = table.outs_functions
|
|
18
20
|
|
|
19
21
|
# Partial result always includes the input hash for calculating output functions
|
|
20
22
|
@partial_result = input[:hash].dup if @outs_functions
|
|
21
23
|
|
|
22
|
-
@row_picked = nil
|
|
23
|
-
return if @first_match
|
|
24
|
-
|
|
25
|
-
# Extra attributes for the accumulate option
|
|
26
24
|
@rows_picked = []
|
|
27
|
-
@multi_result = nil
|
|
28
25
|
end
|
|
29
26
|
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
# Scan the decision table up against the input hash.
|
|
28
|
+
#
|
|
29
|
+
# @param table [CSVDecision::Table] Decision table being processed.
|
|
30
|
+
# @param input (see #initialize)
|
|
31
|
+
# @return [self] Decision object built so far.
|
|
32
|
+
def scan(table:, input:)
|
|
33
|
+
table.each do |row, index|
|
|
34
|
+
return result if row_scan(input: input, row: row, scan_row: table.scan_rows[index])
|
|
35
|
+
end
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
!empty?
|
|
37
|
+
result
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
return {} if empty?
|
|
42
|
-
return final_result unless @outs_functions
|
|
40
|
+
private
|
|
43
41
|
|
|
44
|
-
|
|
42
|
+
# Calculate the final result.
|
|
43
|
+
# @return [nil, Hash{Symbol=>Object}] Final result hash if found, otherwise nil for no result.
|
|
44
|
+
def result
|
|
45
|
+
return {} if @rows_picked.blank?
|
|
46
|
+
@first_match ? final_result : accumulated_result
|
|
45
47
|
end
|
|
46
48
|
|
|
47
|
-
def
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
table.each do |row, index|
|
|
51
|
-
done = row_scan(input: input, row: row, scan_row: scan_rows[index])
|
|
52
|
-
|
|
53
|
-
return self if done
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
self
|
|
49
|
+
def row_scan(input:, row:, scan_row:)
|
|
50
|
+
add(row) if Decide.matches?(row: row, input: input, scan_row: scan_row)
|
|
57
51
|
end
|
|
58
52
|
|
|
53
|
+
# Add a matched row to the decision object being built.
|
|
54
|
+
#
|
|
55
|
+
# @param row [Array]
|
|
59
56
|
def add(row)
|
|
60
57
|
return add_first_match(row) if @first_match
|
|
61
58
|
|
|
62
59
|
# Accumulate output rows
|
|
63
60
|
@rows_picked << row
|
|
64
|
-
@outs.each_pair
|
|
65
|
-
accumulate_outs(column_name: column.name, cell: row[col])
|
|
66
|
-
end
|
|
61
|
+
@outs.each_pair { |col, column| accumulate_outs(column_name: column.name, cell: row[col]) }
|
|
67
62
|
|
|
68
63
|
# Not done
|
|
69
64
|
false
|
|
70
65
|
end
|
|
71
66
|
|
|
72
|
-
|
|
67
|
+
def accumulated_result
|
|
68
|
+
return final_result unless @outs_functions
|
|
69
|
+
return eval_outs(@rows_picked.first) unless @multi_result
|
|
73
70
|
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
multi_row_result
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def multi_row_result
|
|
75
|
+
# Scan each output column that contains functions
|
|
76
|
+
@outs.each_pair do |col, column|
|
|
77
|
+
# Does this column have any functions defined?
|
|
78
|
+
next unless column.eval
|
|
79
|
+
|
|
80
|
+
eval_column_procs(col, column)
|
|
81
|
+
end
|
|
76
82
|
|
|
77
|
-
|
|
83
|
+
final_result
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def accumulate_outs(column_name:, cell:)
|
|
87
|
+
case (current = @result[column_name])
|
|
78
88
|
when nil
|
|
79
89
|
@result[column_name] = cell
|
|
80
90
|
|
|
@@ -87,10 +97,30 @@ module CSVDecision
|
|
|
87
97
|
end
|
|
88
98
|
end
|
|
89
99
|
|
|
90
|
-
def
|
|
91
|
-
|
|
100
|
+
def eval_column_procs(col, column)
|
|
101
|
+
@rows_picked.each_with_index do |row, index|
|
|
102
|
+
proc = row[col]
|
|
103
|
+
next unless proc.is_a?(Matchers::Proc)
|
|
104
|
+
|
|
105
|
+
# Evaluate the proc and update the result
|
|
106
|
+
eval_cell_proc(proc: proc, column_name: column.name, index: index)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
92
109
|
|
|
93
|
-
|
|
110
|
+
# Update the partial result calculated so far and call the function
|
|
111
|
+
def eval_cell_proc(proc:, column_name:, index:)
|
|
112
|
+
value = proc.function[partial_result(index)]
|
|
113
|
+
@multi_result ? @result[column_name][index] = value : @result[column_name] = value
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def partial_result(index)
|
|
117
|
+
@result.each_pair do |column_name, value|
|
|
118
|
+
# Delete this column from the partial result in case there is data from a prior result row
|
|
119
|
+
next @partial_result.delete(column_name) if value[index].is_a?(Matchers::Proc)
|
|
120
|
+
@partial_result[column_name] = value[index]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
@partial_result
|
|
94
124
|
end
|
|
95
125
|
|
|
96
126
|
def final_result
|
|
@@ -98,10 +128,44 @@ module CSVDecision
|
|
|
98
128
|
end
|
|
99
129
|
|
|
100
130
|
def add_first_match(row)
|
|
101
|
-
@
|
|
131
|
+
@rows_picked = row
|
|
132
|
+
|
|
133
|
+
return eval_outs(row) if @outs_functions
|
|
102
134
|
|
|
103
135
|
# Common case is just copying output column values to the final result
|
|
104
136
|
@outs.each_pair { |col, column| @result[column.name] = row[col] }
|
|
105
137
|
end
|
|
138
|
+
|
|
139
|
+
def eval_outs(row)
|
|
140
|
+
# Set the constants first, in case the functions refer to them
|
|
141
|
+
eval_outs_constants(row)
|
|
142
|
+
|
|
143
|
+
# Then evaluate the functions, left to right
|
|
144
|
+
eval_outs_procs(row)
|
|
145
|
+
|
|
146
|
+
final_result
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def eval_outs_constants(row)
|
|
150
|
+
@outs.each_pair do |col, column|
|
|
151
|
+
value = row[col]
|
|
152
|
+
next if value.is_a?(Matchers::Proc)
|
|
153
|
+
|
|
154
|
+
@partial_result[column.name] = value
|
|
155
|
+
@result[column.name] = value
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def eval_outs_procs(row)
|
|
160
|
+
@outs.each_pair do |col, column|
|
|
161
|
+
proc = row[col]
|
|
162
|
+
next unless proc.is_a?(Matchers::Proc)
|
|
163
|
+
|
|
164
|
+
value = proc.function[@partial_result]
|
|
165
|
+
|
|
166
|
+
@partial_result[column.name] = value
|
|
167
|
+
@result[column.name] = value
|
|
168
|
+
end
|
|
169
|
+
end
|
|
106
170
|
end
|
|
107
171
|
end
|
data/lib/csv_decision/header.rb
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# CSV Decision: CSV based Ruby decision tables.
|
|
4
|
-
# Created December 2017
|
|
4
|
+
# Created December 2017.
|
|
5
|
+
# @author Brett Vickers <brett@phillips-vickers.com>
|
|
5
6
|
# See LICENSE and README.md for details.
|
|
6
7
|
module CSVDecision
|
|
7
8
|
# Parse the CSV file's header row. These methods are only required at table load time.
|
|
9
|
+
# @api private
|
|
8
10
|
module Header
|
|
9
11
|
# TODO: implement all column types
|
|
10
12
|
# COLUMN_TYPE = %r{
|
|
@@ -12,22 +14,26 @@ module CSVDecision
|
|
|
12
14
|
# \s*:\s*(?<name>\S?.*)\z
|
|
13
15
|
# }xi
|
|
14
16
|
|
|
15
|
-
# Column types
|
|
17
|
+
# Column types recognised in the header row.
|
|
16
18
|
COLUMN_TYPE = %r{
|
|
17
|
-
\A(?<type>in|out|in/text|out/text)
|
|
19
|
+
\A(?<type>in|out|in/text|out/text|guard)
|
|
18
20
|
\s*:\s*(?<name>\S?.*)\z
|
|
19
21
|
}xi
|
|
20
22
|
|
|
21
|
-
#
|
|
22
|
-
# TODO: implement anonymous column types
|
|
23
|
+
# TODO: implement all anonymous column types
|
|
23
24
|
# COLUMN_TYPE_ANONYMOUS = Set.new(%i[path if guard]).freeze
|
|
25
|
+
# These column types do not need a name
|
|
26
|
+
COLUMN_TYPE_ANONYMOUS = Set.new(%i[guard]).freeze
|
|
27
|
+
private_constant :COLUMN_TYPE_ANONYMOUS
|
|
24
28
|
|
|
25
29
|
# Regular expression string for a column name.
|
|
26
|
-
# More lenient than a Ruby method name - note any spaces will have been replaced with
|
|
30
|
+
# More lenient than a Ruby method name - note any spaces will have been replaced with
|
|
31
|
+
# underscores.
|
|
27
32
|
COLUMN_NAME = "\\w[\\w:/!?]*"
|
|
28
33
|
|
|
29
34
|
# Column name regular expression.
|
|
30
35
|
COLUMN_NAME_RE = Matchers.regexp(COLUMN_NAME)
|
|
36
|
+
private_constant :COLUMN_NAME_RE
|
|
31
37
|
|
|
32
38
|
# Check if the given row contains a recognisable header cell.
|
|
33
39
|
#
|
|
@@ -61,8 +67,26 @@ module CSVDecision
|
|
|
61
67
|
dictionary = parse_cell(cell: cell, index: index, dictionary: dictionary)
|
|
62
68
|
end
|
|
63
69
|
|
|
70
|
+
validate(dictionary: dictionary)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.validate(dictionary:)
|
|
74
|
+
dictionary.outs.each_value do |column|
|
|
75
|
+
next unless input_column?(dictionary: dictionary, column_name: column.name)
|
|
76
|
+
|
|
77
|
+
raise CellValidationError, "output column name '#{column.name}' is also an input column"
|
|
78
|
+
end
|
|
79
|
+
|
|
64
80
|
dictionary
|
|
65
81
|
end
|
|
82
|
+
private_class_method :validate
|
|
83
|
+
|
|
84
|
+
def self.input_column?(dictionary:, column_name:)
|
|
85
|
+
dictionary.ins.each_value { |column| return true if column_name == column.name }
|
|
86
|
+
|
|
87
|
+
false
|
|
88
|
+
end
|
|
89
|
+
private_class_method :input_column?
|
|
66
90
|
|
|
67
91
|
def self.validate_header_column(cell:)
|
|
68
92
|
match = COLUMN_TYPE.match(cell)
|
|
@@ -73,8 +97,7 @@ module CSVDecision
|
|
|
73
97
|
|
|
74
98
|
[column_type, column_name]
|
|
75
99
|
rescue CellValidationError => exp
|
|
76
|
-
raise CellValidationError,
|
|
77
|
-
"header column '#{cell}' is not valid as the #{exp.message}"
|
|
100
|
+
raise CellValidationError, "header column '#{cell}' is not valid as the #{exp.message}"
|
|
78
101
|
end
|
|
79
102
|
private_class_method :validate_header_column
|
|
80
103
|
|
|
@@ -90,8 +113,7 @@ module CSVDecision
|
|
|
90
113
|
def self.column_name(type:, name:)
|
|
91
114
|
return format_column_name(name) if name.present?
|
|
92
115
|
|
|
93
|
-
|
|
94
|
-
# return if COLUMN_TYPE_ANONYMOUS.member?(type)
|
|
116
|
+
return if COLUMN_TYPE_ANONYMOUS.member?(type)
|
|
95
117
|
|
|
96
118
|
raise CellValidationError, 'column name is missing'
|
|
97
119
|
end
|
|
@@ -107,22 +129,21 @@ module CSVDecision
|
|
|
107
129
|
private_class_method :format_column_name
|
|
108
130
|
|
|
109
131
|
# Returns the normalized column type, along with an indication if
|
|
110
|
-
# the column
|
|
111
|
-
def self.column_type(type)
|
|
132
|
+
# the column requires evaluation
|
|
133
|
+
def self.column_type(column_name, type)
|
|
112
134
|
case type
|
|
113
135
|
when :'in/text'
|
|
114
|
-
|
|
136
|
+
Columns::Entry.new(column_name, false, :in)
|
|
115
137
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
# [:in, false]
|
|
138
|
+
when :guard
|
|
139
|
+
Columns::Entry.new(column_name, true, :guard)
|
|
119
140
|
|
|
120
141
|
when :'out/text'
|
|
121
|
-
|
|
142
|
+
Columns::Entry.new(column_name, false, :out)
|
|
122
143
|
|
|
123
|
-
# Column may turn out to be
|
|
144
|
+
# Column may turn out to be constants only, or not
|
|
124
145
|
else
|
|
125
|
-
|
|
146
|
+
Columns::Entry.new(column_name, nil, type.to_sym)
|
|
126
147
|
end
|
|
127
148
|
end
|
|
128
149
|
private_class_method :column_type
|
|
@@ -130,11 +151,11 @@ module CSVDecision
|
|
|
130
151
|
def self.parse_cell(cell:, index:, dictionary:)
|
|
131
152
|
column_type, column_name = validate_header_column(cell: cell)
|
|
132
153
|
|
|
133
|
-
|
|
154
|
+
entry = column_type(column_name, column_type)
|
|
134
155
|
|
|
135
156
|
dictionary_entry(dictionary: dictionary,
|
|
136
|
-
type: type,
|
|
137
|
-
entry:
|
|
157
|
+
type: entry.type,
|
|
158
|
+
entry: entry,
|
|
138
159
|
index: index)
|
|
139
160
|
end
|
|
140
161
|
private_class_method :parse_cell
|
|
@@ -150,7 +171,7 @@ module CSVDecision
|
|
|
150
171
|
# # Treat set: as an in: column
|
|
151
172
|
# dictionary.ins[index] = entry
|
|
152
173
|
|
|
153
|
-
when :in
|
|
174
|
+
when :in, :guard
|
|
154
175
|
dictionary.ins[index] = entry
|
|
155
176
|
|
|
156
177
|
when :out
|