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.
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)