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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/README.md +26 -2
  4. data/csv_decision.gemspec +1 -1
  5. data/doc/CSVDecision/CellValidationError.html +2 -2
  6. data/doc/CSVDecision/Columns/Default.html +203 -23
  7. data/doc/CSVDecision/Columns/Dictionary.html +118 -25
  8. data/doc/CSVDecision/Columns.html +213 -31
  9. data/doc/CSVDecision/Data.html +1 -1
  10. data/doc/CSVDecision/Decide.html +1 -1
  11. data/doc/CSVDecision/Decision.html +1 -1
  12. data/doc/CSVDecision/Defaults.html +291 -0
  13. data/doc/CSVDecision/Dictionary/Entry.html +584 -47
  14. data/doc/CSVDecision/Dictionary.html +20 -20
  15. data/doc/CSVDecision/Error.html +2 -2
  16. data/doc/CSVDecision/FileError.html +1 -1
  17. data/doc/CSVDecision/Header.html +110 -33
  18. data/doc/CSVDecision/Input.html +1 -1
  19. data/doc/CSVDecision/Load.html +1 -1
  20. data/doc/CSVDecision/Matchers/Constant.html +12 -37
  21. data/doc/CSVDecision/Matchers/Function.html +1 -1
  22. data/doc/CSVDecision/Matchers/Guard.html +15 -13
  23. data/doc/CSVDecision/Matchers/Matcher.html +1 -1
  24. data/doc/CSVDecision/Matchers/Numeric.html +13 -21
  25. data/doc/CSVDecision/Matchers/Pattern.html +14 -14
  26. data/doc/CSVDecision/Matchers/Proc.html +1 -1
  27. data/doc/CSVDecision/Matchers/Range.html +13 -58
  28. data/doc/CSVDecision/Matchers/Symbol.html +1 -1
  29. data/doc/CSVDecision/Matchers.html +9 -9
  30. data/doc/CSVDecision/Options.html +1 -1
  31. data/doc/CSVDecision/Parse.html +90 -19
  32. data/doc/CSVDecision/Result.html +1 -1
  33. data/doc/CSVDecision/ScanRow.html +17 -17
  34. data/doc/CSVDecision/Table.html +50 -48
  35. data/doc/CSVDecision/TableValidationError.html +143 -0
  36. data/doc/CSVDecision/Validate.html +422 -0
  37. data/doc/CSVDecision.html +8 -8
  38. data/doc/_index.html +33 -1
  39. data/doc/class_list.html +1 -1
  40. data/doc/file.README.html +27 -3
  41. data/doc/index.html +27 -3
  42. data/doc/method_list.html +193 -89
  43. data/doc/top-level-namespace.html +1 -1
  44. data/lib/csv_decision/columns.rb +28 -27
  45. data/lib/csv_decision/defaults.rb +47 -0
  46. data/lib/csv_decision/dictionary.rb +104 -112
  47. data/lib/csv_decision/header.rb +13 -10
  48. data/lib/csv_decision/input.rb +53 -5
  49. data/lib/csv_decision/matchers/constant.rb +1 -2
  50. data/lib/csv_decision/matchers/guard.rb +3 -2
  51. data/lib/csv_decision/matchers/numeric.rb +4 -6
  52. data/lib/csv_decision/matchers/pattern.rb +6 -8
  53. data/lib/csv_decision/matchers/range.rb +1 -3
  54. data/lib/csv_decision/matchers.rb +7 -7
  55. data/lib/csv_decision/parse.rb +24 -3
  56. data/lib/csv_decision/scan_row.rb +16 -16
  57. data/lib/csv_decision/table.rb +3 -7
  58. data/lib/csv_decision/validate.rb +85 -0
  59. data/lib/csv_decision.rb +3 -1
  60. data/spec/csv_decision/columns_spec.rb +38 -22
  61. data/spec/csv_decision/examples_spec.rb +17 -0
  62. data/spec/csv_decision/matchers/range_spec.rb +0 -32
  63. data/spec/csv_decision/table_spec.rb +39 -0
  64. metadata +7 -2
@@ -100,7 +100,7 @@
100
100
  </div>
101
101
 
102
102
  <div id="footer">
103
- Generated on Sat Jan 6 20:03:01 2018 by
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>
@@ -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
- # TODO: Value object used for any columns with defaults
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 can be interspersed and need not have unique names.
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
- def dictionary
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
- # Table used to build a column dictionary entry.
12
- ENTRY = {
13
- in: { type: :in, eval: nil },
14
- 'in/text': { type: :in, eval: false },
15
- out: { type: :out, eval: nil },
16
- 'out/text': { type: :out, eval: false },
17
- guard: { type: :guard, eval: true },
18
- if: { type: :if, eval: true }
19
- }.freeze
20
- private_constant :ENTRY
21
-
22
- # Value object to hold column dictionary entries.
23
- Entry = Struct.new(:name, :eval, :type) do
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
- %i[in guard].member?(type) ? true : false
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
- # These column types do not need a name.
30
- COLUMN_TYPE_ANONYMOUS = Set.new(%i[guard if]).freeze
31
- private_constant :COLUMN_TYPE_ANONYMOUS
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=>Symbol}] Column dictionary updated with the new name.
110
+ # @return [Hash{Symbol=>[:in, Integer]}] Column dictionary updated with the new name.
52
111
  def self.add_name(columns:, name:, out: false)
53
- validate_name(columns: columns, name: name, out: out)
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 = validate_column(cell: cell, index: index)
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, entry: entry, index: index)
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.ins[index] = entry
130
+ when :in, :guard, :set
131
+ input_entry(dictionary: dictionary, entry: entry, index: index)
126
132
 
127
- when :out
128
- add_name(columns: dictionary.columns, name: entry.name, out: index)
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.validate_name(columns:, name:, out:)
143
- return unless (in_out = columns[name])
144
-
145
- return validate_out_name(in_out: in_out, name: name) if out
146
- validate_in_name(in_out: in_out, name: name)
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
- def self.validate_out_name(in_out:, name:)
151
- if in_out == :in
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
- raise CellValidationError, "output column name '#{name}' is duplicated"
151
+ dictionary.outs[index] = entry
156
152
  end
157
- private_class_method :validate_out_name
153
+ private_class_method :output_entry
154
+
155
+ def self.input_entry(dictionary:, entry:, index:)
156
+ dictionary.ins[index] = entry
158
157
 
159
- def self.validate_in_name(in_out:, name:)
160
- # in: columns may be duped
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
- raise CellValidationError, "output column name '#{name}' is also an input column"
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 :validate_in_name
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
@@ -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|out|in/text|out/text|guard|if)
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| cell.match(COLUMN_TYPE) }
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 and the
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
@@ -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
- # TODO: Does this table have any defaulted columns?
49
- # defaulted_columns = table.columns[:defaults]
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
- value = input[column.name]
69
+ next if column.type == :guard
53
70
 
54
- scan_cols[col] = value
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 :parse_input
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
- # @param (see Matcher#matches?)
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
- value.is_a?(String) && hash[symbol].is_a?(String) &&
52
- Matchers.regexp(value).match(hash[symbol])
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: >= 100 or != 0
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
- # @param (see Matchers::Matcher#matches?)
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
- # @param (see Matchers::Matcher#matches?)
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| pattern != value }.freeze,
22
- '=~' => proc { |pattern, value| !!pattern.match(value) }.freeze,
23
- '!~' => proc { |pattern, value| !pattern.match(value) }.freeze
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 Matchers::Matcher#matches?)
91
- # @return (see Matchers::Matcher#matches?)
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
- # @param (see Matcher#matches?)
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)