csv_decision 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +26 -2
- data/csv_decision.gemspec +1 -1
- data/doc/CSVDecision/CellValidationError.html +2 -2
- data/doc/CSVDecision/Columns/Default.html +203 -23
- data/doc/CSVDecision/Columns/Dictionary.html +118 -25
- data/doc/CSVDecision/Columns.html +213 -31
- data/doc/CSVDecision/Data.html +1 -1
- data/doc/CSVDecision/Decide.html +1 -1
- data/doc/CSVDecision/Decision.html +1 -1
- data/doc/CSVDecision/Defaults.html +291 -0
- data/doc/CSVDecision/Dictionary/Entry.html +584 -47
- data/doc/CSVDecision/Dictionary.html +20 -20
- data/doc/CSVDecision/Error.html +2 -2
- data/doc/CSVDecision/FileError.html +1 -1
- data/doc/CSVDecision/Header.html +110 -33
- data/doc/CSVDecision/Input.html +1 -1
- data/doc/CSVDecision/Load.html +1 -1
- data/doc/CSVDecision/Matchers/Constant.html +12 -37
- data/doc/CSVDecision/Matchers/Function.html +1 -1
- data/doc/CSVDecision/Matchers/Guard.html +15 -13
- data/doc/CSVDecision/Matchers/Matcher.html +1 -1
- data/doc/CSVDecision/Matchers/Numeric.html +13 -21
- data/doc/CSVDecision/Matchers/Pattern.html +14 -14
- data/doc/CSVDecision/Matchers/Proc.html +1 -1
- data/doc/CSVDecision/Matchers/Range.html +13 -58
- data/doc/CSVDecision/Matchers/Symbol.html +1 -1
- data/doc/CSVDecision/Matchers.html +9 -9
- data/doc/CSVDecision/Options.html +1 -1
- data/doc/CSVDecision/Parse.html +90 -19
- data/doc/CSVDecision/Result.html +1 -1
- data/doc/CSVDecision/ScanRow.html +17 -17
- data/doc/CSVDecision/Table.html +50 -48
- data/doc/CSVDecision/TableValidationError.html +143 -0
- data/doc/CSVDecision/Validate.html +422 -0
- data/doc/CSVDecision.html +8 -8
- data/doc/_index.html +33 -1
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +27 -3
- data/doc/index.html +27 -3
- data/doc/method_list.html +193 -89
- data/doc/top-level-namespace.html +1 -1
- data/lib/csv_decision/columns.rb +28 -27
- data/lib/csv_decision/defaults.rb +47 -0
- data/lib/csv_decision/dictionary.rb +104 -112
- data/lib/csv_decision/header.rb +13 -10
- data/lib/csv_decision/input.rb +53 -5
- data/lib/csv_decision/matchers/constant.rb +1 -2
- data/lib/csv_decision/matchers/guard.rb +3 -2
- data/lib/csv_decision/matchers/numeric.rb +4 -6
- data/lib/csv_decision/matchers/pattern.rb +6 -8
- data/lib/csv_decision/matchers/range.rb +1 -3
- data/lib/csv_decision/matchers.rb +7 -7
- data/lib/csv_decision/parse.rb +24 -3
- data/lib/csv_decision/scan_row.rb +16 -16
- data/lib/csv_decision/table.rb +3 -7
- data/lib/csv_decision/validate.rb +85 -0
- data/lib/csv_decision.rb +3 -1
- data/spec/csv_decision/columns_spec.rb +38 -22
- data/spec/csv_decision/examples_spec.rb +17 -0
- data/spec/csv_decision/matchers/range_spec.rb +0 -32
- data/spec/csv_decision/table_spec.rb +39 -0
- metadata +7 -2
@@ -100,7 +100,7 @@
|
|
100
100
|
</div>
|
101
101
|
|
102
102
|
<div id="footer">
|
103
|
-
Generated on Sat Jan
|
103
|
+
Generated on Sat Jan 13 10:02:46 2018 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.4.0).
|
106
106
|
</div>
|
data/lib/csv_decision/columns.rb
CHANGED
@@ -8,12 +8,12 @@ module CSVDecision
|
|
8
8
|
# Dictionary of all this table's columns - inputs, outputs etc.
|
9
9
|
# @api private
|
10
10
|
class Columns
|
11
|
-
#
|
12
|
-
# Default = Struct.new(:name, :function, :default_if)
|
13
|
-
|
14
|
-
# Dictionary of all data columns.
|
11
|
+
# Dictionary of all table data columns.
|
15
12
|
# The key of each hash is the header cell's array column index.
|
16
|
-
# Note that input and output columns
|
13
|
+
# Note that input and output columns may be interspersed, and multiple input columns
|
14
|
+
# may refer to the same input hash key symbol.
|
15
|
+
# However, output columns must have unique symbols, which cannot overlap with input
|
16
|
+
# column symbols.
|
17
17
|
class Dictionary
|
18
18
|
# @return [Hash{Integer=>Entry}] All column names.
|
19
19
|
attr_accessor :columns
|
@@ -21,28 +21,42 @@ module CSVDecision
|
|
21
21
|
# @return [Hash{Integer=>Entry}] All input column dictionary entries.
|
22
22
|
attr_accessor :ins
|
23
23
|
|
24
|
+
# @return [Hash{Integer=>Entry}] All defaulted input column dictionary
|
25
|
+
# entries. This is actually just a subset of :ins.
|
26
|
+
attr_accessor :defaults
|
27
|
+
|
24
28
|
# @return [Hash{Integer=>Entry}] All output column dictionary entries.
|
25
29
|
attr_accessor :outs
|
26
30
|
|
27
31
|
# @return [Hash{Integer=>Entry}] All if: column dictionary entries.
|
32
|
+
# This is actually just a subset of :outs.
|
28
33
|
attr_accessor :ifs
|
29
34
|
|
30
|
-
# TODO: Input hash path - optional (planned feature)
|
31
|
-
# attr_accessor :path
|
32
|
-
|
33
|
-
# TODO: Input columns with a default value (planned feature)
|
34
|
-
# attr_accessor :defaults
|
35
|
-
|
36
35
|
def initialize
|
37
36
|
@columns = {}
|
37
|
+
@defaults = {}
|
38
38
|
@ifs = {}
|
39
39
|
@ins = {}
|
40
40
|
@outs = {}
|
41
|
-
# TODO: @path = {}
|
42
|
-
# TODO: @defaults = {}
|
43
41
|
end
|
44
42
|
end
|
45
43
|
|
44
|
+
# Input columns with defaults specified
|
45
|
+
def defaults
|
46
|
+
@dictionary&.defaults
|
47
|
+
end
|
48
|
+
|
49
|
+
# Set defaults for columns with defaults specified
|
50
|
+
def defaults=(value)
|
51
|
+
@dictionary.defaults = value
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [Hash{Symbol=>[false, Integer]}] Dictionary of all
|
55
|
+
# input and output column names.
|
56
|
+
def dictionary
|
57
|
+
@dictionary.columns
|
58
|
+
end
|
59
|
+
|
46
60
|
# Input columns hash keyed by column index.
|
47
61
|
# @return [Hash{Index=>Entry}]
|
48
62
|
def ins
|
@@ -61,24 +75,11 @@ module CSVDecision
|
|
61
75
|
@dictionary.ifs
|
62
76
|
end
|
63
77
|
|
64
|
-
|
65
|
-
@dictionary.columns
|
66
|
-
end
|
67
|
-
|
78
|
+
# @return [Array<Symbol>] All input column symbols.
|
68
79
|
def input_keys
|
69
80
|
@dictionary.columns.select { |_k, v| v == :in }.keys
|
70
81
|
end
|
71
82
|
|
72
|
-
# Input columns with defaults specified (planned feature)
|
73
|
-
# def defaults
|
74
|
-
# @dictionary.defaults
|
75
|
-
# end
|
76
|
-
|
77
|
-
# Input hash path (planned feature)
|
78
|
-
# def path
|
79
|
-
# @dictionary.path
|
80
|
-
# end
|
81
|
-
|
82
83
|
# @param table [Table] Decision table being constructed.
|
83
84
|
def initialize(table)
|
84
85
|
# If a column does not have a valid header cell, then it's empty of data.
|
@@ -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
|
+
Parse.ins_cell_dictionary(columns: columns, cell: cell)
|
44
|
+
end
|
45
|
+
private_class_method :parse_cell
|
46
|
+
end
|
47
|
+
end
|
@@ -8,27 +8,86 @@ module CSVDecision
|
|
8
8
|
# Parse the CSV file's header row. These methods are only required at table load time.
|
9
9
|
# @api private
|
10
10
|
module Dictionary
|
11
|
-
#
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
11
|
+
# Column dictionary entries.
|
12
|
+
class Entry
|
13
|
+
# Table used to build a column dictionary entry.
|
14
|
+
ENTRY = {
|
15
|
+
in: { type: :in, eval: nil },
|
16
|
+
'in/text': { type: :in, eval: false },
|
17
|
+
set: { type: :set, eval: nil, set_if: true },
|
18
|
+
'set/nil?': { type: :set, eval: nil, set_if: :nil? },
|
19
|
+
'set/blank?': { type: :set, eval: nil, set_if: :blank? },
|
20
|
+
out: { type: :out, eval: nil },
|
21
|
+
'out/text': { type: :out, eval: false },
|
22
|
+
guard: { type: :guard, eval: true },
|
23
|
+
if: { type: :if, eval: true }
|
24
|
+
}.freeze
|
25
|
+
private_constant :ENTRY
|
26
|
+
|
27
|
+
# Input column types.
|
28
|
+
INS_TYPES = %i[in guard set].freeze
|
29
|
+
private_constant :INS_TYPES
|
30
|
+
|
31
|
+
# Create a new column dictionary entry defaulting attributes from the column type,
|
32
|
+
# which is looked up in +ENTRY+ table.
|
33
|
+
#
|
34
|
+
# @param name [Symbol] Column name.
|
35
|
+
# @param type [Symbol] Column type.
|
36
|
+
# @return [Entry] Column dictionary entry.
|
37
|
+
def self.create(name:, type:)
|
38
|
+
entry = ENTRY[type]
|
39
|
+
new(name: name, eval: entry[:eval], type: entry[:type], set_if: entry[:set_if])
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [Boolean] Return true is this is an input column, false otherwise.
|
24
43
|
def ins?
|
25
|
-
|
44
|
+
@ins
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [Symbol] Column name.
|
48
|
+
attr_reader :name
|
49
|
+
|
50
|
+
# @return [Symbol] Column type.
|
51
|
+
attr_reader :type
|
52
|
+
|
53
|
+
# @return [nil, Boolean] If set to true then this column has procs that
|
54
|
+
# need evaluating, otherwise it only contains constants.
|
55
|
+
attr_accessor :eval
|
56
|
+
|
57
|
+
# @return [nil, true, Symbol] Defined for columns of type :set, nil otherwise.
|
58
|
+
# If true, then default is set unconditionally, otherwise the method symbol
|
59
|
+
# sent to the input hash value that must evaluate to a truthy value.
|
60
|
+
attr_reader :set_if
|
61
|
+
|
62
|
+
# @return [Matchers::Proc, Object] For a column of type set: gives the proc that must be
|
63
|
+
# evaluated to set the default value. If not a proc, then it's some type of constant.
|
64
|
+
attr_accessor :function
|
65
|
+
|
66
|
+
# @param name (see #name)
|
67
|
+
# @param type (see #type)
|
68
|
+
# @param eval (see #eval)
|
69
|
+
# @param set_if (see #set_if)
|
70
|
+
def initialize(name:, type:, eval: nil, set_if: nil)
|
71
|
+
@name = name
|
72
|
+
@type = type
|
73
|
+
@eval = eval
|
74
|
+
@set_if = set_if
|
75
|
+
@function = nil
|
76
|
+
@ins = INS_TYPES.member?(type)
|
26
77
|
end
|
27
|
-
end
|
28
78
|
|
29
|
-
|
30
|
-
|
31
|
-
|
79
|
+
# Convert the object's attributes to a hash.
|
80
|
+
#
|
81
|
+
# @return [Hash{Symbol=>[nil, Boolean, Symbol]}]
|
82
|
+
def to_h
|
83
|
+
{
|
84
|
+
name: @name,
|
85
|
+
type: @type,
|
86
|
+
eval: @eval,
|
87
|
+
set_if: @set_if
|
88
|
+
}
|
89
|
+
end
|
90
|
+
end
|
32
91
|
|
33
92
|
# Classify and build a dictionary of all input and output columns by
|
34
93
|
# parsing the header row.
|
@@ -48,127 +107,60 @@ module CSVDecision
|
|
48
107
|
# @param columns [{Symbol=>Symbol}] Hash of column names with key values :in or :out.
|
49
108
|
# @param name [Symbol] Symbolized column name.
|
50
109
|
# @param out [false, Index] False if an input column, otherwise the index of the output column.
|
51
|
-
# @return [{Symbol=>
|
110
|
+
# @return [Hash{Symbol=>[:in, Integer]}] Column dictionary updated with the new name.
|
52
111
|
def self.add_name(columns:, name:, out: false)
|
53
|
-
|
112
|
+
Validate.name(columns: columns, name: name, out: out)
|
54
113
|
|
55
114
|
columns[name] = out ? out : :in
|
56
115
|
columns
|
57
116
|
end
|
58
117
|
|
59
|
-
def self.validate_column(cell:, index:)
|
60
|
-
match = Header::COLUMN_TYPE.match(cell)
|
61
|
-
raise CellValidationError, 'column name is not well formed' unless match
|
62
|
-
|
63
|
-
column_type = match['type']&.downcase&.to_sym
|
64
|
-
column_name = column_name(type: column_type, name: match['name'], index: index)
|
65
|
-
|
66
|
-
[column_type, column_name]
|
67
|
-
rescue CellValidationError => exp
|
68
|
-
raise CellValidationError, "header column '#{cell}' is not valid as the #{exp.message}"
|
69
|
-
end
|
70
|
-
private_class_method :validate_column
|
71
|
-
|
72
|
-
def self.column_name(type:, name:, index:)
|
73
|
-
# if: columns are named after their index, which is an integer and so cannot
|
74
|
-
# clash with other column name types, which are symbols.
|
75
|
-
return index if type == :if
|
76
|
-
|
77
|
-
return format_column_name(name) if name.present?
|
78
|
-
|
79
|
-
return if COLUMN_TYPE_ANONYMOUS.member?(type)
|
80
|
-
raise CellValidationError, 'column name is missing'
|
81
|
-
end
|
82
|
-
private_class_method :column_name
|
83
|
-
|
84
|
-
def self.format_column_name(name)
|
85
|
-
column_name = name.strip.tr("\s", '_')
|
86
|
-
|
87
|
-
return column_name.to_sym if Header::COLUMN_NAME_RE.match(column_name)
|
88
|
-
raise CellValidationError, "column name '#{name}' contains invalid characters"
|
89
|
-
end
|
90
|
-
private_class_method :format_column_name
|
91
|
-
|
92
|
-
# Returns the normalized column type, along with an indication if
|
93
|
-
# the column requires evaluation
|
94
|
-
def self.column_type(column_name, entry)
|
95
|
-
Entry.new(column_name, entry[:eval], entry[:type])
|
96
|
-
end
|
97
|
-
private_class_method :column_type
|
98
|
-
|
99
118
|
def self.parse_cell(cell:, index:, dictionary:)
|
100
|
-
column_type, column_name =
|
101
|
-
|
102
|
-
entry = column_type(column_name, ENTRY[column_type])
|
119
|
+
column_type, column_name = Validate.column(cell: cell, index: index)
|
103
120
|
|
104
|
-
dictionary_entry(dictionary: dictionary,
|
121
|
+
dictionary_entry(dictionary: dictionary,
|
122
|
+
entry: Entry.create(name: column_name, type: column_type),
|
123
|
+
index: index)
|
105
124
|
end
|
106
125
|
private_class_method :parse_cell
|
107
126
|
|
108
127
|
def self.dictionary_entry(dictionary:, entry:, index:)
|
109
128
|
case entry.type
|
110
|
-
# Header column that has a function for setting the value (planned feature)
|
111
|
-
# when :set, :'set/nil?', :'set/blank?'
|
112
|
-
# # Default function will set the input value unconditionally or conditionally
|
113
|
-
# dictionary.defaults[index] =
|
114
|
-
# Columns::Default.new(entry.name, nil, default_if(type))
|
115
|
-
#
|
116
|
-
# # Treat set: as an in: column
|
117
|
-
# dictionary.ins[index] = entry
|
118
|
-
|
119
|
-
when :in
|
120
|
-
add_name(columns: dictionary.columns, name: entry.name)
|
121
|
-
dictionary.ins[index] = entry
|
122
|
-
|
123
129
|
# A guard column is still added to the ins hash for parsing as an input column.
|
124
|
-
when :guard
|
125
|
-
dictionary
|
130
|
+
when :in, :guard, :set
|
131
|
+
input_entry(dictionary: dictionary, entry: entry, index: index)
|
126
132
|
|
127
|
-
when :out
|
128
|
-
|
129
|
-
dictionary.outs[index] = entry
|
130
|
-
|
131
|
-
# Add an if: column to both the +outs+ hash for output column parsing, and also
|
132
|
-
# a specialized +ifs+ hash used for evaluating them for row filtering.
|
133
|
-
when :if
|
134
|
-
dictionary.outs[index] = entry
|
135
|
-
dictionary.ifs[index] = entry
|
133
|
+
when :out, :if
|
134
|
+
output_entry(dictionary: dictionary, entry: entry, index: index)
|
136
135
|
end
|
137
136
|
|
138
137
|
dictionary
|
139
138
|
end
|
140
139
|
private_class_method :dictionary_entry
|
141
140
|
|
142
|
-
def self.
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
end
|
148
|
-
private_class_method :validate_name
|
141
|
+
def self.output_entry(dictionary:, entry:, index:)
|
142
|
+
case entry.type
|
143
|
+
# if: columns are anonymous
|
144
|
+
when :if
|
145
|
+
dictionary.ifs[index] = entry
|
149
146
|
|
150
|
-
|
151
|
-
|
152
|
-
raise CellValidationError, "output column name '#{name}' is also an input column"
|
147
|
+
when :out
|
148
|
+
add_name(columns: dictionary.columns, name: entry.name, out: index)
|
153
149
|
end
|
154
150
|
|
155
|
-
|
151
|
+
dictionary.outs[index] = entry
|
156
152
|
end
|
157
|
-
private_class_method :
|
153
|
+
private_class_method :output_entry
|
154
|
+
|
155
|
+
def self.input_entry(dictionary:, entry:, index:)
|
156
|
+
dictionary.ins[index] = entry
|
158
157
|
|
159
|
-
|
160
|
-
|
161
|
-
return if in_out == :in
|
158
|
+
# Default function will set the input value unconditionally or conditionally.
|
159
|
+
dictionary.defaults[index] = entry if entry.type == :set
|
162
160
|
|
163
|
-
|
161
|
+
# guard: columns are anonymous
|
162
|
+
add_name(columns: dictionary.columns, name: entry.name) unless entry.type == :guard
|
164
163
|
end
|
165
|
-
private_class_method :
|
166
|
-
|
167
|
-
# def self.default_if(type)
|
168
|
-
# return nil if type == :set
|
169
|
-
# return :nil? if type == :'set/nil'
|
170
|
-
# :blank?
|
171
|
-
# end
|
172
|
-
# private_class_method :default_if
|
164
|
+
private_class_method :input_entry
|
173
165
|
end
|
174
166
|
end
|
data/lib/csv_decision/header.rb
CHANGED
@@ -8,15 +8,9 @@ module CSVDecision
|
|
8
8
|
# Parse the CSV file's header row. These methods are only required at table load time.
|
9
9
|
# @api private
|
10
10
|
module Header
|
11
|
-
# TODO: implement all column types
|
12
|
-
# COLUMN_TYPE = %r{
|
13
|
-
# \A(?<type>in|out|in/text|out/text|set|set/nil|set/blank|path|guard|if)
|
14
|
-
# \s*:\s*(?<name>\S?.*)\z
|
15
|
-
# }xi
|
16
|
-
|
17
11
|
# Column types recognised in the header row.
|
18
12
|
COLUMN_TYPE = %r{
|
19
|
-
\A(?<type>in
|
13
|
+
\A(?<type>in/text|in|out/text|out|guard|if|set/nil\?|set/blank\?|set)
|
20
14
|
\s*:\s*(?<name>\S?.*)\z
|
21
15
|
}xi
|
22
16
|
|
@@ -27,20 +21,29 @@ module CSVDecision
|
|
27
21
|
|
28
22
|
# Regular expression for matching a column name.
|
29
23
|
COLUMN_NAME_RE = Matchers.regexp(Header::COLUMN_NAME)
|
24
|
+
private_constant :COLUMN_NAME_RE
|
25
|
+
|
26
|
+
# Return true if column name is valid.
|
27
|
+
#
|
28
|
+
# @param column_name [String]
|
29
|
+
# @return [Boolean]
|
30
|
+
def self.column_name?(column_name)
|
31
|
+
COLUMN_NAME_RE.match?(column_name)
|
32
|
+
end
|
30
33
|
|
31
34
|
# Check if the given row contains a recognisable header cell.
|
32
35
|
#
|
33
36
|
# @param row [Array<String>] Header row.
|
34
37
|
# @return [Boolean] Return true if the row looks like a header.
|
35
38
|
def self.row?(row)
|
36
|
-
row.any? { |cell|
|
39
|
+
row.any? { |cell| COLUMN_TYPE.match?(cell) }
|
37
40
|
end
|
38
41
|
|
39
42
|
# Strip empty columns from all data rows.
|
40
43
|
#
|
41
44
|
# @param rows [Array<Array<String>>] Data rows.
|
42
|
-
# @return [Array<Array<String>>] Data array after removing any empty columns
|
43
|
-
# header row.
|
45
|
+
# @return [Array<Array<String>>] Data array after removing any empty columns
|
46
|
+
# and the header row.
|
44
47
|
def self.strip_empty_columns(rows:)
|
45
48
|
empty_cols = empty_columns?(row: rows.first)
|
46
49
|
Data.strip_columns(data: rows, empty_columns: empty_cols) if empty_cols
|
data/lib/csv_decision/input.rb
CHANGED
@@ -43,19 +43,67 @@ module CSVDecision
|
|
43
43
|
private_class_method :validate
|
44
44
|
|
45
45
|
def self.parse_input(table:, input:)
|
46
|
+
defaulted_columns = table.columns.defaults
|
47
|
+
parse_cells(table: table, input: input) if defaulted_columns.empty?
|
48
|
+
|
49
|
+
parse_defaulted(table: table, input: input, defaulted_columns: defaulted_columns)
|
50
|
+
end
|
51
|
+
private_class_method :parse_input
|
52
|
+
|
53
|
+
def self.parse_cells(table:, input:)
|
46
54
|
scan_cols = {}
|
55
|
+
table.columns.ins.each_pair do |col, column|
|
56
|
+
next if column.type == :guard
|
47
57
|
|
48
|
-
|
49
|
-
|
58
|
+
scan_cols[col] = input[column.name]
|
59
|
+
end
|
60
|
+
|
61
|
+
{ hash: input, scan_cols: scan_cols }
|
62
|
+
end
|
63
|
+
private_class_method :parse_cells
|
64
|
+
|
65
|
+
def self.parse_defaulted(table:, input:, defaulted_columns:)
|
66
|
+
scan_cols = {}
|
50
67
|
|
51
68
|
table.columns.ins.each_pair do |col, column|
|
52
|
-
|
69
|
+
next if column.type == :guard
|
53
70
|
|
54
|
-
scan_cols[col] =
|
71
|
+
scan_cols[col] =
|
72
|
+
default_value(default: defaulted_columns[col], input: input, column: column)
|
73
|
+
|
74
|
+
# Also update the input hash with the default value.
|
75
|
+
input[column.name] = scan_cols[col]
|
55
76
|
end
|
56
77
|
|
57
78
|
{ hash: input, scan_cols: scan_cols }
|
58
79
|
end
|
59
|
-
private_class_method :
|
80
|
+
private_class_method :parse_defaulted
|
81
|
+
|
82
|
+
def self.default_value(default:, input:, column:)
|
83
|
+
value = input[column.name]
|
84
|
+
|
85
|
+
# Do we even have a default entry for this column?
|
86
|
+
return value if default.nil?
|
87
|
+
|
88
|
+
# Has the set condition been met, or is it unconditional?
|
89
|
+
return value unless default_if?(default.set_if, value)
|
90
|
+
|
91
|
+
# Expression may be a Proc that needs evaluating against the input hash,
|
92
|
+
# or else a constant.
|
93
|
+
eval_default(default.function, input)
|
94
|
+
end
|
95
|
+
private_class_method :default_value
|
96
|
+
|
97
|
+
def self.default_if?(set_if, value)
|
98
|
+
set_if == true || (value.respond_to?(set_if) && value.send(set_if))
|
99
|
+
end
|
100
|
+
private_class_method :default_if?
|
101
|
+
|
102
|
+
# Expression may be a Proc that needs evaluating against the input hash,
|
103
|
+
# or else a constant.
|
104
|
+
def self.eval_default(expression, input)
|
105
|
+
expression.is_a?(::Proc) ? expression[input] : expression
|
106
|
+
end
|
107
|
+
private_class_method :eval_default
|
60
108
|
end
|
61
109
|
end
|
@@ -61,8 +61,7 @@ module CSVDecision
|
|
61
61
|
# If a constant expression returns a Proc of type :constant,
|
62
62
|
# otherwise return false.
|
63
63
|
#
|
64
|
-
#
|
65
|
-
# @return (see Matcher#matches?)
|
64
|
+
# (see Matcher#matches?)
|
66
65
|
def matches?(cell)
|
67
66
|
Matchers::Constant.matches?(cell)
|
68
67
|
end
|
@@ -48,8 +48,9 @@ module CSVDecision
|
|
48
48
|
end
|
49
49
|
|
50
50
|
def self.regexp_match(symbol, value, hash)
|
51
|
-
|
52
|
-
|
51
|
+
return false unless value.is_a?(String)
|
52
|
+
data = hash[symbol]
|
53
|
+
data.is_a?(String) && Matchers.regexp(value).match?(data)
|
53
54
|
end
|
54
55
|
|
55
56
|
FUNCTION = {
|
@@ -8,9 +8,9 @@ module CSVDecision
|
|
8
8
|
# Methods to assign a matcher to data cells.
|
9
9
|
# @api private
|
10
10
|
class Matchers
|
11
|
-
# Recognise numeric comparison expressions - e.g., +> 100+ or +!= 0
|
11
|
+
# Recognise numeric comparison expressions - e.g., +> 100+ or +!= 0+.
|
12
12
|
class Numeric < Matcher
|
13
|
-
# For example:
|
13
|
+
# For example: +>= 100+ or +!= 0+.
|
14
14
|
COMPARISON = /\A(?<comparator><=|>=|<|>|!=)\s*(?<value>\S.*)\z/
|
15
15
|
private_constant :COMPARISON
|
16
16
|
|
@@ -25,8 +25,7 @@ module CSVDecision
|
|
25
25
|
}.freeze
|
26
26
|
private_constant :COMPARATORS
|
27
27
|
|
28
|
-
#
|
29
|
-
# @return (see Matchers::Matcher#matches?)
|
28
|
+
# (see Matcher#matches?)
|
30
29
|
def self.matches?(cell)
|
31
30
|
match = COMPARISON.match(cell)
|
32
31
|
return false unless match
|
@@ -39,8 +38,7 @@ module CSVDecision
|
|
39
38
|
function: COMPARATORS[comparator].curry[numeric_cell].freeze)
|
40
39
|
end
|
41
40
|
|
42
|
-
#
|
43
|
-
# @return (see Matchers::Matcher#matches?)
|
41
|
+
# (see Matcher#matches?)
|
44
42
|
def matches?(cell)
|
45
43
|
Numeric.matches?(cell)
|
46
44
|
end
|
@@ -16,14 +16,12 @@ module CSVDecision
|
|
16
16
|
IMPLICIT_COMPARISON = Matchers.regexp("(?<comparator>=~|!~|!=)?\\s*(?<value>\\S.*)")
|
17
17
|
private_constant :IMPLICIT_COMPARISON
|
18
18
|
|
19
|
-
# rubocop: disable Style/DoubleNegation
|
20
19
|
PATTERN_LAMBDAS = {
|
21
|
-
'!=' => proc { |pattern, value|
|
22
|
-
'=~' => proc { |pattern, value|
|
23
|
-
'!~' => proc { |pattern, value|
|
20
|
+
'!=' => proc { |pattern, value| pattern != value }.freeze,
|
21
|
+
'=~' => proc { |pattern, value| pattern.match?(value) }.freeze,
|
22
|
+
'!~' => proc { |pattern, value| !pattern.match?(value) }.freeze
|
24
23
|
}.freeze
|
25
24
|
private_constant :PATTERN_LAMBDAS
|
26
|
-
# rubocop: enable Style/DoubleNegation
|
27
25
|
|
28
26
|
def self.regexp?(cell:, explicit:)
|
29
27
|
# By default a regexp pattern must use an explicit comparator
|
@@ -79,7 +77,7 @@ module CSVDecision
|
|
79
77
|
|
80
78
|
# @param options [Hash{Symbol=>Object}] Used to determine the value of regexp_implicit:.
|
81
79
|
def initialize(options = {})
|
82
|
-
# By default regexp's must have an explicit comparator
|
80
|
+
# By default regexp's must have an explicit comparator.
|
83
81
|
@regexp_explicit = !options[:regexp_implicit]
|
84
82
|
end
|
85
83
|
|
@@ -87,8 +85,8 @@ module CSVDecision
|
|
87
85
|
# If the option regexp_implicit: true has been set, then cells may omit the +=~+ comparator
|
88
86
|
# so long as they contain non-word characters typically used in regular expressions such as
|
89
87
|
# +*+ and +.+.
|
90
|
-
# @param (see
|
91
|
-
# @return (see
|
88
|
+
# @param (see Matcher#matches?)
|
89
|
+
# @return (see Matcher#matches?)
|
92
90
|
def matches?(cell)
|
93
91
|
Pattern.matches?(cell, regexp_explicit: @regexp_explicit)
|
94
92
|
end
|
@@ -13,8 +13,7 @@ module CSVDecision
|
|
13
13
|
class Range < Matcher
|
14
14
|
# Match a table data cell string against a Ruby-like range expression.
|
15
15
|
#
|
16
|
-
#
|
17
|
-
# @return (see Matcher#matches?)
|
16
|
+
# (see Matcher#matches?)
|
18
17
|
def self.matches?(cell)
|
19
18
|
if (match = NUMERIC_RANGE.match(cell))
|
20
19
|
return range_proc(match: match, coerce: :to_numeric)
|
@@ -87,7 +86,6 @@ module CSVDecision
|
|
87
86
|
|
88
87
|
# Ruby-like range expressions or their negation - e.g., +0...10+ or +!a..z+.
|
89
88
|
#
|
90
|
-
# @param (see Matcher#matches?)
|
91
89
|
# @return (see Matcher#matches?)
|
92
90
|
def matches?(cell)
|
93
91
|
Range.matches?(cell)
|