csv_decision 0.0.3 → 0.0.4

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +2 -0
  3. data/.gitignore +2 -1
  4. data/.travis.yml +2 -3
  5. data/CHANGELOG.md +19 -1
  6. data/README.md +49 -16
  7. data/{benchmark.rb → benchmarks/rufus_decision.rb} +1 -1
  8. data/csv_decision.gemspec +1 -1
  9. data/doc/CSVDecision/CellValidationError.html +143 -0
  10. data/doc/CSVDecision/Columns/Default.html +409 -0
  11. data/doc/CSVDecision/Columns/Dictionary.html +410 -0
  12. data/doc/CSVDecision/Columns/Entry.html +321 -0
  13. data/doc/CSVDecision/Columns.html +476 -0
  14. data/doc/CSVDecision/Constant.html +295 -0
  15. data/doc/CSVDecision/Data.html +344 -0
  16. data/doc/CSVDecision/Decide.html +434 -0
  17. data/doc/CSVDecision/Decision.html +604 -0
  18. data/doc/CSVDecision/Error.html +139 -0
  19. data/doc/CSVDecision/FileError.html +143 -0
  20. data/doc/CSVDecision/Function.html +229 -0
  21. data/doc/CSVDecision/Header.html +520 -0
  22. data/doc/CSVDecision/Input.html +305 -0
  23. data/doc/CSVDecision/Load.html +225 -0
  24. data/doc/CSVDecision/Matchers/Constant.html +242 -0
  25. data/doc/CSVDecision/Matchers/Function.html +342 -0
  26. data/doc/CSVDecision/Matchers/Matcher.html +325 -0
  27. data/doc/CSVDecision/Matchers/Numeric.html +277 -0
  28. data/doc/CSVDecision/Matchers/Pattern.html +600 -0
  29. data/doc/CSVDecision/Matchers/Range.html +413 -0
  30. data/doc/CSVDecision/Matchers/Symbol.html +280 -0
  31. data/doc/CSVDecision/Matchers.html +1529 -0
  32. data/doc/CSVDecision/Numeric.html +259 -0
  33. data/doc/CSVDecision/Options.html +445 -0
  34. data/doc/CSVDecision/Parse.html +270 -0
  35. data/doc/CSVDecision/ScanRow.html +746 -0
  36. data/doc/CSVDecision/Symbol.html +256 -0
  37. data/doc/CSVDecision/Table.html +1115 -0
  38. data/doc/CSVDecision.html +652 -0
  39. data/doc/_index.html +410 -0
  40. data/doc/class_list.html +51 -0
  41. data/doc/css/common.css +1 -0
  42. data/doc/css/full_list.css +58 -0
  43. data/doc/css/style.css +499 -0
  44. data/doc/file.README.html +264 -0
  45. data/doc/file_list.html +56 -0
  46. data/doc/frames.html +17 -0
  47. data/doc/index.html +264 -0
  48. data/doc/js/app.js +248 -0
  49. data/doc/js/full_list.js +216 -0
  50. data/doc/js/jquery.js +4 -0
  51. data/doc/method_list.html +683 -0
  52. data/doc/top-level-namespace.html +110 -0
  53. data/lib/csv_decision/columns.rb +15 -12
  54. data/lib/csv_decision/constant.rb +54 -0
  55. data/lib/csv_decision/decide.rb +5 -5
  56. data/lib/csv_decision/decision.rb +3 -1
  57. data/lib/csv_decision/function.rb +32 -0
  58. data/lib/csv_decision/header.rb +27 -18
  59. data/lib/csv_decision/input.rb +11 -8
  60. data/lib/csv_decision/matchers/constant.rb +18 -0
  61. data/lib/csv_decision/matchers/function.rb +11 -44
  62. data/lib/csv_decision/matchers/numeric.rb +5 -33
  63. data/lib/csv_decision/matchers/pattern.rb +26 -11
  64. data/lib/csv_decision/matchers/range.rb +21 -5
  65. data/lib/csv_decision/matchers/symbol.rb +20 -0
  66. data/lib/csv_decision/matchers.rb +85 -20
  67. data/lib/csv_decision/numeric.rb +38 -0
  68. data/lib/csv_decision/options.rb +36 -27
  69. data/lib/csv_decision/parse.rb +46 -39
  70. data/lib/csv_decision/scan_row.rb +19 -7
  71. data/lib/csv_decision/symbol.rb +73 -0
  72. data/lib/csv_decision/table.rb +24 -18
  73. data/lib/csv_decision.rb +25 -18
  74. data/spec/csv_decision/columns_spec.rb +1 -1
  75. data/spec/csv_decision/constant_spec.rb +60 -0
  76. data/spec/csv_decision/examples_spec.rb +119 -0
  77. data/spec/csv_decision/matchers/function_spec.rb +48 -28
  78. data/spec/csv_decision/matchers/numeric_spec.rb +4 -41
  79. data/spec/csv_decision/matchers/range_spec.rb +31 -61
  80. data/spec/csv_decision/matchers/symbol_spec.rb +65 -0
  81. data/spec/csv_decision/options_spec.rb +14 -2
  82. data/spec/csv_decision/parse_spec.rb +10 -0
  83. data/spec/csv_decision/table_spec.rb +112 -6
  84. data/spec/data/valid/simple_constants.csv +3 -3
  85. metadata +62 -7
  86. data/spec/csv_decision/simple_example_spec.rb +0 -75
  87. /data/spec/{csv_decision.rb → csv_decision_spec.rb} +0 -0
@@ -0,0 +1,110 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>
7
+ Top Level Namespace
8
+
9
+ &mdash; Documentation by YARD 0.9.12
10
+
11
+ </title>
12
+
13
+ <link rel="stylesheet" href="css/style.css" type="text/css" charset="utf-8" />
14
+
15
+ <link rel="stylesheet" href="css/common.css" type="text/css" charset="utf-8" />
16
+
17
+ <script type="text/javascript" charset="utf-8">
18
+ pathId = "";
19
+ relpath = '';
20
+ </script>
21
+
22
+
23
+ <script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
24
+
25
+ <script type="text/javascript" charset="utf-8" src="js/app.js"></script>
26
+
27
+
28
+ </head>
29
+ <body>
30
+ <div class="nav_wrap">
31
+ <iframe id="nav" src="class_list.html?1"></iframe>
32
+ <div id="resizer"></div>
33
+ </div>
34
+
35
+ <div id="main" tabindex="-1">
36
+ <div id="header">
37
+ <div id="menu">
38
+
39
+ <a href="_index.html">Index</a> &raquo;
40
+
41
+
42
+ <span class="title">Top Level Namespace</span>
43
+
44
+ </div>
45
+
46
+ <div id="search">
47
+
48
+ <a class="full_list_link" id="class_list_link"
49
+ href="class_list.html">
50
+
51
+ <svg width="24" height="24">
52
+ <rect x="0" y="4" width="24" height="4" rx="1" ry="1"></rect>
53
+ <rect x="0" y="12" width="24" height="4" rx="1" ry="1"></rect>
54
+ <rect x="0" y="20" width="24" height="4" rx="1" ry="1"></rect>
55
+ </svg>
56
+ </a>
57
+
58
+ </div>
59
+ <div class="clear"></div>
60
+ </div>
61
+
62
+ <div id="content"><h1>Top Level Namespace
63
+
64
+
65
+
66
+ </h1>
67
+ <div class="box_info">
68
+
69
+
70
+
71
+
72
+
73
+
74
+
75
+
76
+
77
+
78
+
79
+ </div>
80
+
81
+ <h2>Defined Under Namespace</h2>
82
+ <p class="children">
83
+
84
+
85
+ <strong class="modules">Modules:</strong> <span class='object_link'><a href="CSVDecision.html" title="CSVDecision (module)">CSVDecision</a></span>
86
+
87
+
88
+
89
+
90
+ </p>
91
+
92
+
93
+
94
+
95
+
96
+
97
+
98
+
99
+
100
+ </div>
101
+
102
+ <div id="footer">
103
+ Generated on Tue Dec 26 18:31:37 2017 by
104
+ <a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
105
+ 0.9.12 (ruby-2.3.0).
106
+ </div>
107
+
108
+ </div>
109
+ </body>
110
+ </html>
@@ -6,33 +6,35 @@
6
6
  module CSVDecision
7
7
  # Dictionary of all this table's columns - inputs, outputs etc.
8
8
  class Columns
9
- # Value object used for column dictionary entries
9
+ # Value object to hold column dictionary entries.
10
10
  Entry = Struct.new(:name, :text_only)
11
11
 
12
- # Value object used for any columns with defaults
13
- Default = Struct.new(:name, :function, :default_if)
12
+ # TODO: Value object used for any columns with defaults
13
+ # Default = Struct.new(:name, :function, :default_if)
14
14
 
15
15
  # Dictionary of all data columns.
16
- # # Note that the key of each hash is the header cell's array column index.
16
+ # The key of each hash is the header cell's array column index.
17
17
  # Note that input and output columns can be interspersed and need not have unique names.
18
18
  class Dictionary
19
- # Input columns
19
+ # Input columns.
20
+ # @return [Hash{Integer=>Entry}] All input column dictionary entries.
20
21
  attr_accessor :ins
21
22
 
22
- # Output columns
23
+ # Output columns.
24
+ # @return [Hash{Integer=>Entry}] All output column dictionary entries.
23
25
  attr_accessor :outs
24
26
 
25
- # Input hash path - optional (planned feature)
26
- attr_accessor :path
27
+ # TODO: Input hash path - optional (planned feature)
28
+ # attr_accessor :path
27
29
 
28
- # Input columns with a default value (planned feature)
29
- attr_accessor :defaults
30
+ # TODO: Input columns with a default value (planned feature)
31
+ # attr_accessor :defaults
30
32
 
31
33
  def initialize
32
34
  @ins = {}
33
35
  @outs = {}
34
- @path = {}
35
- @defaults = {}
36
+ # TODO: @path = {}
37
+ # TODO: @defaults = {}
36
38
  end
37
39
  end
38
40
 
@@ -59,6 +61,7 @@ module CSVDecision
59
61
  # @dictionary.path
60
62
  # end
61
63
 
64
+ # @param table [Table] Decision table being constructed.
62
65
  def initialize(table)
63
66
  # If a column does not have a valid header cell, then it's empty of data.
64
67
  # Return the stripped header row, and remove it from the data array.
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ # CSV Decision: CSV based Ruby decision tables.
4
+ # Created December 2017 by Brett Vickers
5
+ # See LICENSE and README.md for details.
6
+ module CSVDecision
7
+ # Recognise constant expressions in table data cells.
8
+ module Constant
9
+ # Cell constant expression specified by prefixing the value with one of the three equality symbols.
10
+ EXPRESSION = Matchers.regexp("(?<operator>#{Matchers::EQUALS})\\s*(?<value>\\S.*)")
11
+
12
+ # rubocop: disable Lint/BooleanSymbol
13
+
14
+ # Non-numeric constants recognised by CSV Decision.
15
+ NON_NUMERIC = {
16
+ nil: nil,
17
+ true: true,
18
+ false: false
19
+ }.freeze
20
+ # rubocop: enable Lint/BooleanSymbol
21
+
22
+ # @param (see Matchers::Matcher#matches?)
23
+ # @return (see Matchers::Matcher#matches?)
24
+ def self.matches?(cell)
25
+ return false unless (match = EXPRESSION.match(cell))
26
+
27
+ proc = non_numeric?(match)
28
+ return proc if proc
29
+
30
+ numeric?(match)
31
+ end
32
+
33
+ def self.proc(function:)
34
+ Proc.with(type: :constant, function: function)
35
+ end
36
+ private_class_method :proc
37
+
38
+ def self.numeric?(match)
39
+ value = Matchers.to_numeric(match['value'])
40
+ return false unless value
41
+
42
+ proc(function: value)
43
+ end
44
+ private_class_method :numeric?
45
+
46
+ def self.non_numeric?(match)
47
+ name = match['value'].to_sym
48
+ return false unless NON_NUMERIC.key?(name)
49
+
50
+ proc(function: NON_NUMERIC[name])
51
+ end
52
+ private_class_method :non_numeric?
53
+ end
54
+ end
@@ -8,11 +8,11 @@ module CSVDecision
8
8
  module Decide
9
9
  # Main method for making decisions.
10
10
  #
11
- # @param table [CSVDecision::Table]
12
- # @param input [Hash] - input hash (keys may or may not be symbolized)
13
- # @param symbolize_keys [true, false] - set to true if keys are symbolized and it's
11
+ # @param table [CSVDecision::Table] Decision table.
12
+ # @param input [Hash] Input hash (keys may or may not be symbolized)
13
+ # @param symbolize_keys [true, false] Set to false if keys are symbolized and it's
14
14
  # OK to mutate the input hash. Otherwise a copy of the input hash is symbolized.
15
- # @return [Hash]
15
+ # @return [Hash] Decision result.
16
16
  def self.decide(table:, input:, symbolize_keys:)
17
17
  # Parse and transform the hash supplied as input
18
18
  parsed_input = Input.parse(table: table, input: input, symbolize_keys: symbolize_keys)
@@ -37,7 +37,7 @@ module CSVDecision
37
37
  def self.eval_matcher(proc:, value:, hash:)
38
38
  function = proc.function
39
39
 
40
- # A symbol expression just needs to be passed the input hash
40
+ # A symbol guard expression just needs to be passed the input hash
41
41
  return function[hash] if proc.type == :expression
42
42
 
43
43
  # All other procs can take one or two args
@@ -12,7 +12,9 @@ module CSVDecision
12
12
  # Relevant table attributes
13
13
  @first_match = table.options[:first_match]
14
14
  @outs = table.columns.outs
15
- @outs_functions = table.outs_functions
15
+
16
+ # TODO: Planned feature
17
+ # @outs_functions = table.outs_functions
16
18
 
17
19
  # Partial result always includes the input hash for calculating output functions
18
20
  @partial_result = input[:hash].dup if @outs_functions
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # CSV Decision: CSV based Ruby decision tables.
4
+ # Created December 2017 by Brett Vickers
5
+ # See LICENSE and README.md for details.
6
+ module CSVDecision
7
+ # Methods to recognise various function expressions
8
+ # TODO: fully implement
9
+ module Function
10
+ # Looks like a function call or symbol expressions, e.g.,
11
+ # == true
12
+ # := function(arg: symbol)
13
+ # == :column_name
14
+ FUNCTION_CALL =
15
+ "(?<operator>=|:=|==|=|<|>|!=|>=|<=|:|!\\s*:)\\s*" \
16
+ "(?<negate>!?)\\s*" \
17
+ "(?<name>#{Header::COLUMN_NAME}|:)(?<args>.*)"
18
+ private_constant :FUNCTION_CALL
19
+
20
+ FUNCTION_RE = Matchers.regexp(FUNCTION_CALL)
21
+
22
+ def self.matches?(cell)
23
+ match = FUNCTION_RE.match(cell)
24
+ return false unless match
25
+
26
+ # operator = match['operator']&.gsub(/\s+/, '')
27
+ # name = match['name'].to_sym
28
+ # args = match['args'].strip
29
+ # negate = match['negate'] == Matchers::NEGATE
30
+ end
31
+ end
32
+ end
@@ -6,32 +6,41 @@
6
6
  module CSVDecision
7
7
  # Parse the CSV file's header row. These methods are only required at table load time.
8
8
  module Header
9
- # Column header looks like IN :col_name or cond:
9
+ # TODO: implement all column types
10
+ # COLUMN_TYPE = %r{
11
+ # \A(?<type>in|out|in/text|out/text|set|set/nil|set/blank|path|guard|if)
12
+ # \s*:\s*(?<name>\S?.*)\z
13
+ # }xi
14
+
15
+ # Column types recognise din the header row.
10
16
  COLUMN_TYPE = %r{
11
- \A(?<type>in|out|in/text|out/text|set|set/nil|set/blank|path|cond|if)
17
+ \A(?<type>in|out|in/text|out/text)
12
18
  \s*:\s*(?<name>\S?.*)\z
13
19
  }xi
14
20
 
15
21
  # These column types do not need a name
16
- COLUMN_TYPE_ANONYMOUS = Set.new(%i[path if cond]).freeze
22
+ # TODO: implement anonymous column types
23
+ # COLUMN_TYPE_ANONYMOUS = Set.new(%i[path if guard]).freeze
17
24
 
18
- # More lenient than a Ruby method name -
19
- # any spaces will have been replaced with underscores
25
+ # Regular expression string for a column name.
26
+ # More lenient than a Ruby method name - note any spaces will have been replaced with underscores.
20
27
  COLUMN_NAME = "\\w[\\w:/!?]*"
28
+
29
+ # Column name regular expression.
21
30
  COLUMN_NAME_RE = Matchers.regexp(COLUMN_NAME)
22
31
 
23
- # Does this row contain a recognisable header cell?
32
+ # Check if the given row contains a recognisable header cell.
24
33
  #
25
- # @param row [Array<String>] header row
26
- # @return [Boolean] true if the row looks like a header
34
+ # @param row [Array<String>] Header row.
35
+ # @return [Boolean] Return true if the row looks like a header.
27
36
  def self.row?(row)
28
37
  row.find { |cell| cell.match(COLUMN_TYPE) }
29
38
  end
30
39
 
31
40
  # Strip empty columns from all data rows.
32
41
  #
33
- # @param rows [Array<Array<String>>]
34
- # @return [Array<Array<String>>] - data array after removing any empty columns and the
42
+ # @param rows [Array<Array<String>>] Data rows.
43
+ # @return [Array<Array<String>>] Data array after removing any empty columns and the
35
44
  # header row.
36
45
  def self.strip_empty_columns(rows:)
37
46
  empty_cols = empty_columns?(row: rows.first)
@@ -43,8 +52,8 @@ module CSVDecision
43
52
 
44
53
  # Classify and build a dictionary of all input and output columns.
45
54
  #
46
- # @param row [Array<String>] - the header row after removing any empty columns.
47
- # @return [Hash<Hash>] - Column dictionary if a hash of hashes.
55
+ # @param row [Array<String>] The header row after removing any empty columns.
56
+ # @return [Hash<Hash>] Column dictionary is a hash of hashes.
48
57
  def self.dictionary(row:)
49
58
  dictionary = Columns::Dictionary.new
50
59
 
@@ -80,7 +89,9 @@ module CSVDecision
80
89
 
81
90
  def self.column_name(type:, name:)
82
91
  return format_column_name(name) if name.present?
83
- return if COLUMN_TYPE_ANONYMOUS.member?(type)
92
+
93
+ # TODO: implement anonymous column types
94
+ # return if COLUMN_TYPE_ANONYMOUS.member?(type)
84
95
 
85
96
  raise CellValidationError, 'column name is missing'
86
97
  end
@@ -102,8 +113,9 @@ module CSVDecision
102
113
  when :'in/text'
103
114
  [:in, true]
104
115
 
105
- when :cond
106
- [:in, false]
116
+ # TODO: planned feature
117
+ # when :guard
118
+ # [:in, false]
107
119
 
108
120
  when :'out/text'
109
121
  [:out, true]
@@ -143,9 +155,6 @@ module CSVDecision
143
155
 
144
156
  when :out
145
157
  dictionary.outs[index] = entry
146
-
147
- else
148
- raise "internal error - column type #{type} not recognised"
149
158
  end
150
159
 
151
160
  dictionary
@@ -1,24 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'ice_nine'
4
- require 'ice_nine/core_ext/object'
5
-
6
3
  # CSV Decision: CSV based Ruby decision tables.
7
4
  # Created December 2017 by Brett Vickers
8
5
  # See LICENSE and README.md for details.
9
6
  module CSVDecision
10
- # Parse the input hash
7
+ # Parse the input hash.
11
8
  module Input
9
+ # @param (see Decide.decide)
10
+ # @return [Hash{Symbol => Hash{Symbol=>Object}, Hash{Integer=>Object}}]
11
+ # Returns a hash of two hashes:
12
+ # * hash: either a copy with keys symbolized or the original input object
13
+ # * scan_cols: Picks out the value in the input hash for each table input column.
14
+ # Defaults to nil if the key is missing in the input hash.
12
15
  def self.parse(table:, input:, symbolize_keys:)
13
16
  validate(input)
14
17
 
15
18
  # For safety the default is to symbolize keys and make a copy of the hash.
16
- # However, if this is turned off assume keys are symbolized
17
- # TODO: Is it OK to mutate the hash in this case?
18
- input = symbolize_keys ? input.deep_symbolize_keys : input
19
+ # However, if this is turned off then the keys must already symbolized.
20
+ input = symbolize_keys ? input.symbolize_keys : input
19
21
 
20
22
  parsed_input = parse_input(table: table, input: input)
21
23
 
24
+ # Freeze the copy of the input hash we just created.
22
25
  parsed_input[:hash].freeze if symbolize_keys
23
26
 
24
27
  parsed_input
@@ -33,7 +36,7 @@ module CSVDecision
33
36
  def self.parse_input(table:, input:)
34
37
  scan_cols = {}
35
38
 
36
- # Does this table have any defaulted columns?
39
+ # TODO: Does this table have any defaulted columns?
37
40
  # defaulted_columns = table.columns[:defaults]
38
41
 
39
42
  table.columns.ins.each_pair do |col, column|
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # CSV Decision: CSV based Ruby decision tables.
4
+ # Created December 2017 by Brett Vickers
5
+ # See LICENSE and README.md for details.
6
+ module CSVDecision
7
+ # Methods to assign a matcher to data cells
8
+ class Matchers
9
+ # Cell constant matcher - e.g., := true, = nil.
10
+ class Constant < Matcher
11
+ # @param (see Matchers::Matcher)
12
+ # @return (see Matchers::Matcher)
13
+ def matches?(cell)
14
+ CSVDecision::Constant.matches?(cell)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -5,53 +5,20 @@
5
5
  # See LICENSE and README.md for details.
6
6
  module CSVDecision
7
7
  # Methods to assign a matcher to data cells
8
- module Matchers
9
- # rubocop: disable Lint/BooleanSymbol
10
- NON_NUMERIC_CONSTANTS = {
11
- true: true,
12
- false: false,
13
- nil: nil
14
- }.freeze
15
- # rubocop: enable Lint/BooleanSymbol
16
-
17
- def self.input_cell_constant?(match)
18
- return false unless CELL_CONSTANT.member?(match['operator'])
19
- return false unless match['args'] == ''
20
- return false unless match['negate'] == ''
21
-
22
- name = match['name'].to_sym
23
- return false unless NON_NUMERIC_CONSTANTS.key?(name)
24
-
25
- Proc.with(type: :constant, function: NON_NUMERIC_CONSTANTS[name])
26
- end
27
-
28
- # Match cell against a function call or symbolic expression.
8
+ class Matchers
9
+ # Match cell against a function call
10
+ # * no arguments - e.g., := present?
11
+ # * with arguments - e.g., :=lookup?(:table)
12
+ # TODO: fully implement
29
13
  class Function < Matcher
30
- # Looks like a function call or symbol expressions, e.g.,
31
- # == true
32
- # := function(arg: symbol)
33
- # == :column_name
34
- FUNCTION_CALL =
35
- "(?<operator>=|:=|==|<|>|!=|>=|<=|:|!\\s*:)\s*(?<negate>!?)\\s*(?<name>#{Header::COLUMN_NAME}|:)(?<args>.*)"
36
- FUNCTION_RE = Matchers.regexp(FUNCTION_CALL)
37
-
38
- # COMPARATORS = {
39
- # '>' => proc { |numeric_cell, value| Matchers.numeric(value) &.> numeric_cell },
40
- # '>=' => proc { |numeric_cell, value| Matchers.numeric(value) &.>= numeric_cell },
41
- # '<' => proc { |numeric_cell, value| Matchers.numeric(value) &.< numeric_cell },
42
- # '<=' => proc { |numeric_cell, value| Matchers.numeric(value) &.<= numeric_cell },
43
- # '!=' => proc { |numeric_cell, value| Matchers.numeric(value) &.!= numeric_cell }
44
- # }.freeze
14
+ def initialize(options = {})
15
+ @options = options
16
+ end
45
17
 
18
+ # @param (see Matchers::Matcher#matches?)
19
+ # @return (see Matchers::Matcher#matches?)
46
20
  def matches?(cell)
47
- match = FUNCTION_RE.match(cell)
48
- return false unless match
49
-
50
- # Check if the guard condition is a cell constant
51
- proc = Matchers.input_cell_constant?(match)
52
- return proc if proc
53
-
54
- false
21
+ CSVDecision::Function.matches?(cell)
55
22
  end
56
23
  end
57
24
  end
@@ -5,41 +5,13 @@
5
5
  # See LICENSE and README.md for details.
6
6
  module CSVDecision
7
7
  # Methods to assign a matcher to data cells
8
- module Matchers
9
- # Cell constant specified by prefixing the value with these symbols
10
- CELL_CONSTANT = Set.new(%w[== := =]).freeze
11
-
12
- # Match cell against a Ruby-like numeric comparison or a numeric constant
8
+ class Matchers
9
+ # Recognise numeric comparison expressions - e.g., +> 100+ or +!= 0+
13
10
  class Numeric < Matcher
14
- # For example: >= 100 or != 0
15
- COMPARISON = /\A(?<comparator><=|>=|<|>|!=|:=|==|=)\s*(?<value>\S.*)\z/
16
-
17
- # Coerce the input value to a numeric representation before invoking the comparison.
18
- # If the coercion fails, it will produce a nil value which always fails to match.
19
- COMPARATORS = {
20
- '>' => proc { |numeric_cell, value| Matchers.numeric(value) &.> numeric_cell },
21
- '>=' => proc { |numeric_cell, value| Matchers.numeric(value) &.>= numeric_cell },
22
- '<' => proc { |numeric_cell, value| Matchers.numeric(value) &.< numeric_cell },
23
- '<=' => proc { |numeric_cell, value| Matchers.numeric(value) &.<= numeric_cell },
24
- '!=' => proc { |numeric_cell, value| Matchers.numeric(value) &.!= numeric_cell }
25
- }.freeze
26
-
11
+ # @param (see Matchers::Matcher#matches?)
12
+ # @return (see Matchers::Matcher#matches?)
27
13
  def matches?(cell)
28
- match = COMPARISON.match(cell)
29
- return false unless match
30
-
31
- numeric_cell = Matchers.numeric(match['value'])
32
- return false unless numeric_cell
33
-
34
- comparator = match['comparator']
35
-
36
- # If the comparator is assignment/equality, then just treat as a simple constant
37
- if CELL_CONSTANT.member?(comparator)
38
- return Proc.with(type: :constant, function: numeric_cell)
39
- end
40
-
41
- Proc.with(type: :proc,
42
- function: COMPARATORS[comparator].curry[numeric_cell])
14
+ CSVDecision::Numeric.matches?(cell)
43
15
  end
44
16
  end
45
17
  end
@@ -5,11 +5,14 @@
5
5
  # See LICENSE and README.md for details.
6
6
  module CSVDecision
7
7
  # Methods to assign a matcher to data cells
8
- module Matchers
9
- # Match cell against a regular expression pattern
8
+ class Matchers
9
+ # Match cell against a regular expression pattern - e.g., +=~ hot|col+ or +.*OPT.*+
10
10
  class Pattern < Matcher
11
- EXPLICIT_COMPARISON = /\A(?<comparator>=~|!~|!=)\s*(?<value>\S.*)\z/
12
- IMPLICIT_COMPARISON = /\A(?<comparator>=~|!~|!=)?\s*(?<value>\S.*)\z/
11
+ EXPLICIT_COMPARISON = Matchers.regexp("(?<comparator>=~|!~|!=)\\s*(?<value>\\S.*)")
12
+ private_constant :EXPLICIT_COMPARISON
13
+
14
+ IMPLICIT_COMPARISON = Matchers.regexp("(?<comparator>=~|!~|!=)?\\s*(?<value>\\S.*)")
15
+ private_constant :IMPLICIT_COMPARISON
13
16
 
14
17
  # rubocop: disable Style/DoubleNegation
15
18
  PATTERN_LAMBDAS = {
@@ -17,6 +20,7 @@ module CSVDecision
17
20
  '=~' => proc { |pattern, value| !!pattern.match(value) }.freeze,
18
21
  '!~' => proc { |pattern, value| !pattern.match(value) }.freeze
19
22
  }.freeze
23
+ private_constant :PATTERN_LAMBDAS
20
24
  # rubocop: enable Style/DoubleNegation
21
25
 
22
26
  def self.regexp?(cell:, explicit:)
@@ -31,6 +35,7 @@ module CSVDecision
31
35
 
32
36
  parse(comparator: comparator, value: match['value'])
33
37
  end
38
+ private_class_method :regexp?
34
39
 
35
40
  def self.parse(comparator:, value:)
36
41
  return false if value.blank?
@@ -54,13 +59,8 @@ module CSVDecision
54
59
  '=~'
55
60
  end
56
61
 
57
- def initialize(options = {})
58
- # By default regexp's must have an explicit comparator
59
- @regexp_explicit = !options[:regexp_implicit]
60
- end
61
-
62
- def matches?(cell)
63
- comparator, value = Pattern.regexp?(cell: cell, explicit: @regexp_explicit)
62
+ def self.matches?(cell, regexp_explicit:)
63
+ comparator, value = regexp?(cell: cell, explicit: regexp_explicit)
64
64
 
65
65
  # We could not find a regexp pattern - maybe it's a simple string or something else?
66
66
  return false unless comparator
@@ -71,6 +71,21 @@ module CSVDecision
71
71
  Proc.with(type: :proc,
72
72
  function: PATTERN_LAMBDAS[comparator].curry[pattern].freeze)
73
73
  end
74
+
75
+ # @param options [Hash{Symbol=>Object}] Used to determine the value of regexp_implicit:.
76
+ def initialize(options = {})
77
+ # By default regexp's must have an explicit comparator
78
+ @regexp_explicit = !options[:regexp_implicit]
79
+ end
80
+
81
+ # Recognise a regular expression pattern - e.g., +=~ on|off+ or +!~ OPT.*+.
82
+ # If the option regexp_implicit: true has been set, then cells may omit the +=~+ comparator so long as they
83
+ # contain non-word characters typically used in regular expressions such as +*+ and +.+.
84
+ # @param (see Matchers::Matcher#matches?)
85
+ # @return (see Matchers::Matcher#matches?)
86
+ def matches?(cell)
87
+ Pattern.matches?(cell, regexp_explicit: @regexp_explicit)
88
+ end
74
89
  end
75
90
  end
76
91
  end