csv_decision 0.1.0 → 0.2.0
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 +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)
|