csv_decision2 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (134) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +3 -0
  3. data/.coveralls.yml +2 -0
  4. data/.gitignore +14 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +30 -0
  7. data/.travis.yml +6 -0
  8. data/CHANGELOG.md +85 -0
  9. data/Dockerfile +6 -0
  10. data/Gemfile +7 -0
  11. data/LICENSE +21 -0
  12. data/README.md +356 -0
  13. data/benchmarks/rufus_decision.rb +158 -0
  14. data/csv_decision2.gemspec +38 -0
  15. data/doc/CSVDecision/CellValidationError.html +143 -0
  16. data/doc/CSVDecision/Columns/Default.html +589 -0
  17. data/doc/CSVDecision/Columns/Dictionary.html +801 -0
  18. data/doc/CSVDecision/Columns/Entry.html +508 -0
  19. data/doc/CSVDecision/Columns.html +1259 -0
  20. data/doc/CSVDecision/Constant.html +254 -0
  21. data/doc/CSVDecision/Data.html +479 -0
  22. data/doc/CSVDecision/Decide.html +302 -0
  23. data/doc/CSVDecision/Decision.html +1011 -0
  24. data/doc/CSVDecision/Defaults.html +291 -0
  25. data/doc/CSVDecision/Dictionary/Entry.html +1147 -0
  26. data/doc/CSVDecision/Dictionary.html +426 -0
  27. data/doc/CSVDecision/Error.html +139 -0
  28. data/doc/CSVDecision/FileError.html +143 -0
  29. data/doc/CSVDecision/Function.html +240 -0
  30. data/doc/CSVDecision/Guard.html +245 -0
  31. data/doc/CSVDecision/Header.html +647 -0
  32. data/doc/CSVDecision/Index.html +741 -0
  33. data/doc/CSVDecision/Input.html +404 -0
  34. data/doc/CSVDecision/Load.html +296 -0
  35. data/doc/CSVDecision/Matchers/Constant.html +484 -0
  36. data/doc/CSVDecision/Matchers/Function.html +511 -0
  37. data/doc/CSVDecision/Matchers/Guard.html +503 -0
  38. data/doc/CSVDecision/Matchers/Matcher.html +507 -0
  39. data/doc/CSVDecision/Matchers/Numeric.html +415 -0
  40. data/doc/CSVDecision/Matchers/Pattern.html +491 -0
  41. data/doc/CSVDecision/Matchers/Proc.html +704 -0
  42. data/doc/CSVDecision/Matchers/Range.html +379 -0
  43. data/doc/CSVDecision/Matchers/Symbol.html +426 -0
  44. data/doc/CSVDecision/Matchers.html +1567 -0
  45. data/doc/CSVDecision/Numeric.html +259 -0
  46. data/doc/CSVDecision/Options.html +443 -0
  47. data/doc/CSVDecision/Parse.html +282 -0
  48. data/doc/CSVDecision/Paths.html +742 -0
  49. data/doc/CSVDecision/Result.html +1200 -0
  50. data/doc/CSVDecision/Scan/InputHashes.html +369 -0
  51. data/doc/CSVDecision/Scan.html +313 -0
  52. data/doc/CSVDecision/ScanRow.html +866 -0
  53. data/doc/CSVDecision/Symbol.html +256 -0
  54. data/doc/CSVDecision/Table.html +1470 -0
  55. data/doc/CSVDecision/TableValidationError.html +143 -0
  56. data/doc/CSVDecision/Validate.html +422 -0
  57. data/doc/CSVDecision.html +621 -0
  58. data/doc/_index.html +471 -0
  59. data/doc/class_list.html +51 -0
  60. data/doc/css/common.css +1 -0
  61. data/doc/css/full_list.css +58 -0
  62. data/doc/css/style.css +499 -0
  63. data/doc/file.README.html +421 -0
  64. data/doc/file_list.html +56 -0
  65. data/doc/frames.html +17 -0
  66. data/doc/index.html +421 -0
  67. data/doc/js/app.js +248 -0
  68. data/doc/js/full_list.js +216 -0
  69. data/doc/js/jquery.js +4 -0
  70. data/doc/method_list.html +1163 -0
  71. data/doc/top-level-namespace.html +110 -0
  72. data/docker-compose.yml +13 -0
  73. data/lib/csv_decision/columns.rb +192 -0
  74. data/lib/csv_decision/data.rb +92 -0
  75. data/lib/csv_decision/decision.rb +196 -0
  76. data/lib/csv_decision/defaults.rb +47 -0
  77. data/lib/csv_decision/dictionary.rb +180 -0
  78. data/lib/csv_decision/header.rb +83 -0
  79. data/lib/csv_decision/index.rb +107 -0
  80. data/lib/csv_decision/input.rb +121 -0
  81. data/lib/csv_decision/load.rb +36 -0
  82. data/lib/csv_decision/matchers/constant.rb +74 -0
  83. data/lib/csv_decision/matchers/function.rb +56 -0
  84. data/lib/csv_decision/matchers/guard.rb +142 -0
  85. data/lib/csv_decision/matchers/numeric.rb +44 -0
  86. data/lib/csv_decision/matchers/pattern.rb +94 -0
  87. data/lib/csv_decision/matchers/range.rb +95 -0
  88. data/lib/csv_decision/matchers/symbol.rb +149 -0
  89. data/lib/csv_decision/matchers.rb +220 -0
  90. data/lib/csv_decision/options.rb +124 -0
  91. data/lib/csv_decision/parse.rb +165 -0
  92. data/lib/csv_decision/paths.rb +78 -0
  93. data/lib/csv_decision/result.rb +204 -0
  94. data/lib/csv_decision/scan.rb +117 -0
  95. data/lib/csv_decision/scan_row.rb +142 -0
  96. data/lib/csv_decision/table.rb +101 -0
  97. data/lib/csv_decision/validate.rb +85 -0
  98. data/lib/csv_decision.rb +45 -0
  99. data/spec/csv_decision/columns_spec.rb +251 -0
  100. data/spec/csv_decision/constant_spec.rb +36 -0
  101. data/spec/csv_decision/data_spec.rb +50 -0
  102. data/spec/csv_decision/decision_spec.rb +19 -0
  103. data/spec/csv_decision/examples_spec.rb +242 -0
  104. data/spec/csv_decision/index_spec.rb +58 -0
  105. data/spec/csv_decision/input_spec.rb +55 -0
  106. data/spec/csv_decision/load_spec.rb +28 -0
  107. data/spec/csv_decision/matchers/function_spec.rb +82 -0
  108. data/spec/csv_decision/matchers/guard_spec.rb +170 -0
  109. data/spec/csv_decision/matchers/numeric_spec.rb +47 -0
  110. data/spec/csv_decision/matchers/pattern_spec.rb +183 -0
  111. data/spec/csv_decision/matchers/range_spec.rb +70 -0
  112. data/spec/csv_decision/matchers/symbol_spec.rb +67 -0
  113. data/spec/csv_decision/options_spec.rb +94 -0
  114. data/spec/csv_decision/parse_spec.rb +44 -0
  115. data/spec/csv_decision/table_spec.rb +683 -0
  116. data/spec/csv_decision_spec.rb +7 -0
  117. data/spec/data/invalid/empty.csv +0 -0
  118. data/spec/data/invalid/invalid_header1.csv +4 -0
  119. data/spec/data/invalid/invalid_header2.csv +4 -0
  120. data/spec/data/invalid/invalid_header3.csv +4 -0
  121. data/spec/data/invalid/invalid_header4.csv +4 -0
  122. data/spec/data/valid/benchmark_regexp.csv +10 -0
  123. data/spec/data/valid/index_example.csv +13 -0
  124. data/spec/data/valid/multi_column_index.csv +10 -0
  125. data/spec/data/valid/multi_column_index2.csv +12 -0
  126. data/spec/data/valid/options_in_file1.csv +5 -0
  127. data/spec/data/valid/options_in_file2.csv +5 -0
  128. data/spec/data/valid/options_in_file3.csv +13 -0
  129. data/spec/data/valid/regular_expressions.csv +11 -0
  130. data/spec/data/valid/simple_constants.csv +5 -0
  131. data/spec/data/valid/simple_example.csv +10 -0
  132. data/spec/data/valid/valid.csv +4 -0
  133. data/spec/spec_helper.rb +106 -0
  134. metadata +352 -0
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ # CSV Decision: CSV based Ruby decision tables.
4
+ # Created December 2017.
5
+ # @author Brett Vickers <brett@phillips-vickers.com>
6
+ # See LICENSE and README.md for details.
7
+ module CSVDecision
8
+ # Recognise expressions in table data cells.
9
+ # @api private
10
+ class Matchers
11
+ # Match cell against a column symbol guard expression -
12
+ # e.g., +>:column.present?+ or +:column == 100.0+.
13
+ class Guard < Matcher
14
+ # Column symbol expression - e.g., +>:column+ or +:!column+.
15
+ SYMBOL_RE =
16
+ Matchers.regexp("(?<negate>#{Matchers::NEGATE}?)\\s*:(?<name>#{Header::COLUMN_NAME})")
17
+ private_constant :SYMBOL_RE
18
+
19
+ # Column symbol guard expression - e.g., +>:column.present?+ or +:column == 100.0+.
20
+ GUARD_RE = Matchers.regexp(
21
+ "(?<negate>#{Matchers::NEGATE}?)\\s*" \
22
+ ":(?<name>#{Header::COLUMN_NAME})\\s*" \
23
+ "(?<method>!=|=~|!~|<=|>=|>|<|#{Matchers::EQUALS}|\\.)\\s*" \
24
+ "(?<param>\\S.*)"
25
+ )
26
+ private_constant :GUARD_RE
27
+
28
+ # Negated methods
29
+ NEGATION = { '=' => '!=', '==' => '!=', ':=' => '!=', '!=' => '=',
30
+ '>' => '<=', '>=' => '<', '<' => '>=', '<=' => '>',
31
+ '.' => '!.',
32
+ '=~' => '!~', '!~' => '=~' }.freeze
33
+ private_constant :NEGATION
34
+
35
+ # Note: value has already been converted to an Integer or BigDecimal.
36
+ NUMERIC_COMPARE = {
37
+ '==' => proc { |symbol, value, hash| Matchers.numeric(hash[symbol]) == value },
38
+ '!=' => proc { |symbol, value, hash| Matchers.numeric(hash[symbol]) != value },
39
+ '>' => proc { |symbol, value, hash| Matchers.numeric(hash[symbol]) &.> value },
40
+ '>=' => proc { |symbol, value, hash| Matchers.numeric(hash[symbol]) &.>= value },
41
+ '<' => proc { |symbol, value, hash| Matchers.numeric(hash[symbol]) &.< value },
42
+ '<=' => proc { |symbol, value, hash| Matchers.numeric(hash[symbol]) &.<= value }
43
+ }.freeze
44
+ private_constant :NUMERIC_COMPARE
45
+
46
+ def self.symbol_function(symbol, method, hash)
47
+ hash[symbol].respond_to?(method) && hash[symbol].send(method)
48
+ end
49
+ private_class_method :symbol_function
50
+
51
+ def self.regexp_match(symbol, value, hash)
52
+ return false unless value.is_a?(String)
53
+ data = hash[symbol]
54
+ data.is_a?(String) && Matchers.regexp(value).match?(data)
55
+ end
56
+ private_class_method :regexp_match
57
+
58
+ FUNCTION = {
59
+ '.' => proc { |symbol, method, hash| symbol_function(symbol, method, hash) },
60
+ '!.' => proc { |symbol, method, hash| !symbol_function(symbol, method, hash) },
61
+ '=~' => proc { |symbol, value, hash| regexp_match(symbol, value, hash) },
62
+ '!~' => proc { |symbol, value, hash| !regexp_match(symbol, value, hash) }
63
+ }.freeze
64
+ private_constant :FUNCTION
65
+
66
+ SYMBOL_PROC = {
67
+ ':' => proc { |symbol, hash| hash[symbol] },
68
+ '!:' => proc { |symbol, hash| !hash[symbol] }
69
+ }.freeze
70
+ private_constant :SYMBOL_PROC
71
+
72
+ def self.non_numeric(method)
73
+ proc = FUNCTION[method]
74
+ return proc if proc
75
+
76
+ proc { |symbol, value, hash| Matchers.compare?(lhs: hash[symbol], compare: method, rhs: value) }
77
+ end
78
+ private_class_method :non_numeric
79
+
80
+ def self.method(match)
81
+ method = match['method']
82
+ match['negate'].present? ? NEGATION[method] : Matchers.normalize_operator(method)
83
+ end
84
+ private_class_method :method
85
+
86
+ def self.guard_proc(match)
87
+ method = method(match)
88
+ param = match['param']
89
+
90
+ # If the parameter is a numeric value then use numeric compares rather than string compares.
91
+ if (value = Matchers.to_numeric(param))
92
+ return [NUMERIC_COMPARE[method], value]
93
+ end
94
+
95
+ # Process a non-numeric method where the param is just a string
96
+ [non_numeric(method), param]
97
+ end
98
+ private_class_method :guard_proc
99
+
100
+ def self.symbol_proc(cell)
101
+ match = SYMBOL_RE.match(cell)
102
+ return false unless match
103
+
104
+ method = match['negate'].present? ? '!:' : ':'
105
+ proc = SYMBOL_PROC[method]
106
+ symbol = match['name'].to_sym
107
+ Matchers::Proc.new(type: :guard, symbols: symbol, function: proc.curry[symbol].freeze)
108
+ end
109
+ private_class_method :symbol_proc
110
+
111
+ def self.symbol_guard(cell)
112
+ match = GUARD_RE.match(cell)
113
+ return false unless match
114
+
115
+ proc, value = guard_proc(match)
116
+ symbol = match['name'].to_sym
117
+ Matchers::Proc.new(type: :guard, symbols: symbol,
118
+ function: proc.curry[symbol][value].freeze)
119
+ end
120
+ private_class_method :symbol_guard
121
+
122
+ # (see Matcher#matches?)
123
+ def self.matches?(cell)
124
+ proc = symbol_proc(cell)
125
+ return proc if proc
126
+
127
+ symbol_guard(cell)
128
+ end
129
+
130
+ # @param (see Matcher#matches?)
131
+ # @return (see Matcher#matches?)
132
+ def matches?(cell)
133
+ Guard.matches?(cell)
134
+ end
135
+
136
+ # @return (see Matcher#outs?)
137
+ def outs?
138
+ true
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ # CSV Decision: CSV based Ruby decision tables.
4
+ # Created December 2017.
5
+ # @author Brett Vickers <brett@phillips-vickers.com>
6
+ # See LICENSE and README.md for details.
7
+ module CSVDecision
8
+ # Methods to assign a matcher to data cells.
9
+ # @api private
10
+ class Matchers
11
+ # Recognise numeric comparison expressions - e.g., +> 100+ or +!= 0+.
12
+ class Numeric < Matcher
13
+ # For example: +>= 100+ or +!= 0+.
14
+ COMPARISON = /\A(?<comparator><=|>=|<|>|!=)\s*(?<value>\S.*)\z/
15
+ private_constant :COMPARISON
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
+ private_constant :COMPARATORS
27
+
28
+ # (see Matcher#matches?)
29
+ def self.matches?(cell)
30
+ return false unless (match = COMPARISON.match(cell))
31
+ return false unless (numeric_cell = Matchers.to_numeric(match['value']))
32
+
33
+ comparator = match['comparator']
34
+ Matchers::Proc.new(type: :proc,
35
+ function: COMPARATORS[comparator].curry[numeric_cell].freeze)
36
+ end
37
+
38
+ # (see Matcher#matches?)
39
+ def matches?(cell)
40
+ Numeric.matches?(cell)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ # CSV Decision: CSV based Ruby decision tables.
4
+ # Created December 2017.
5
+ # @author Brett Vickers <brett@phillips-vickers.com>
6
+ # See LICENSE and README.md for details.
7
+ module CSVDecision
8
+ # Methods to assign a matcher to data cells
9
+ # @api private
10
+ class Matchers
11
+ # Match cell against a regular expression pattern - e.g., +=~ hot|col+ or +.*OPT.*+
12
+ class Pattern < Matcher
13
+ EXPLICIT_COMPARISON = Matchers.regexp("(?<comparator>=~|!~|!=)\\s*(?<value>\\S.*)")
14
+ private_constant :EXPLICIT_COMPARISON
15
+
16
+ IMPLICIT_COMPARISON = Matchers.regexp("(?<comparator>=~|!~|!=)?\\s*(?<value>\\S.*)")
17
+ private_constant :IMPLICIT_COMPARISON
18
+
19
+ PATTERN_LAMBDAS = {
20
+ '!=' => proc { |pattern, value| pattern != value }.freeze,
21
+ '=~' => proc { |pattern, value| pattern.match?(value) }.freeze,
22
+ '!~' => proc { |pattern, value| !pattern.match?(value) }.freeze
23
+ }.freeze
24
+ private_constant :PATTERN_LAMBDAS
25
+
26
+ def self.regexp?(cell:, explicit:)
27
+ # By default a regexp pattern must use an explicit comparator
28
+ match = explicit ? EXPLICIT_COMPARISON.match(cell) : IMPLICIT_COMPARISON.match(cell)
29
+ return false if match.nil?
30
+
31
+ comparator = match['comparator']
32
+
33
+ # Comparator may be omitted if the regexp_explicit option is off.
34
+ return false if explicit && comparator.nil?
35
+
36
+ parse(comparator: comparator, value: match['value'])
37
+ end
38
+ private_class_method :regexp?
39
+
40
+ def self.parse(comparator:, value:)
41
+ return false if value.blank?
42
+
43
+ # We cannot do a regexp comparison against a symbol name.
44
+ return if value[0] == ':'
45
+
46
+ # If no comparator then the implicit option must be on
47
+ comparator = regexp_implicit(value) if comparator.nil?
48
+
49
+ [comparator, value]
50
+ end
51
+ private_class_method :parse
52
+
53
+ def self.regexp_implicit(value)
54
+ # rubocop: disable Style/CaseEquality
55
+ return unless /\W/ === value
56
+ # rubocop: enable Style/CaseEquality
57
+
58
+ # Make the implicit comparator explicit
59
+ '=~'
60
+ end
61
+ private_class_method :regexp_implicit
62
+
63
+ # @api private
64
+ # (see Pattern#matches)
65
+ def self.matches?(cell, regexp_explicit:)
66
+ comparator, value = regexp?(cell: cell, explicit: regexp_explicit)
67
+
68
+ # We could not find a regexp pattern - maybe it's a simple string or something else?
69
+ return false unless comparator
70
+
71
+ # No need for a regular expression if we have simple string inequality
72
+ pattern = comparator == '!=' ? value : Matchers.regexp(value)
73
+
74
+ Proc.new(type: :proc, function: PATTERN_LAMBDAS[comparator].curry[pattern].freeze)
75
+ end
76
+
77
+ # @param options [Hash{Symbol=>Object}] Used to determine the value of regexp_implicit:.
78
+ def initialize(options = {})
79
+ # By default regexp's must have an explicit comparator.
80
+ @regexp_explicit = !options[:regexp_implicit]
81
+ end
82
+
83
+ # Recognise a regular expression pattern - e.g., +=~ on|off+ or +!~ OPT.*+.
84
+ # If the option regexp_implicit: true has been set, then cells may omit the +=~+ comparator
85
+ # so long as they contain non-word characters typically used in regular expressions such as
86
+ # +*+ and +.+.
87
+ # @param (see Matcher#matches?)
88
+ # @return (see Matcher#matches?)
89
+ def matches?(cell)
90
+ Pattern.matches?(cell, regexp_explicit: @regexp_explicit)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ # CSV Decision: CSV based Ruby decision tables.
4
+ # Created December 2017.
5
+ # @author Brett Vickers <brett@phillips-vickers.com>
6
+ # See LICENSE and README.md for details.
7
+ module CSVDecision
8
+ # Methods to assign a matcher to data cells.
9
+ # @api private
10
+ class Matchers
11
+ # Match cells against Ruby-like range expressions or their negation -
12
+ # e.g., +0...10+ or +!a..z+.
13
+ class Range < Matcher
14
+ # Match a table data cell string against a Ruby-like range expression.
15
+ #
16
+ # (see Matcher#matches?)
17
+ def self.matches?(cell)
18
+ if (match = NUMERIC_RANGE.match(cell))
19
+ return range_proc(match: match, coerce: :to_numeric)
20
+ end
21
+
22
+ if (match = ALNUM_RANGE.match(cell))
23
+ return range_proc(match: match)
24
+ end
25
+
26
+ false
27
+ end
28
+
29
+ # Range types are +..+ or +...+.
30
+ TYPE = '(\.\.\.|\.\.)'
31
+ private_constant :TYPE
32
+
33
+ # Range expression looks like +0...10+ or +a..z+.
34
+ # Can also be negated - e.g., +! 0..10+ or +!a..z+.
35
+ def self.range_re(value)
36
+ Matchers.regexp(
37
+ "(?<negate>#{NEGATE}?)\\s*(?<min>#{value})(?<type>#{TYPE})(?<max>#{value})"
38
+ )
39
+ end
40
+ private_class_method :range_re
41
+
42
+ NUMERIC_RANGE = range_re(Matchers::NUMERIC)
43
+ private_constant :NUMERIC_RANGE
44
+
45
+ # Alphanumeric range, e.g., +a...z+ or +!a..c+.
46
+ ALNUM_RANGE = range_re('[[:alnum:]][[:alnum:]]*')
47
+ private_constant :ALNUM_RANGE
48
+
49
+ # Coerce the string into a numeric value if required.
50
+ def self.convert(value, method)
51
+ method ? Matchers.send(method, value) : value
52
+ end
53
+ private_class_method :convert
54
+
55
+ def self.range(match, coerce: nil)
56
+ negate = match['negate'] == Matchers::NEGATE
57
+ min = convert(match['min'], coerce)
58
+ type = match['type']
59
+ max = convert(match['max'], coerce)
60
+
61
+ [negate, type == '...' ? min...max : min..max]
62
+ end
63
+ private_class_method :range
64
+
65
+ # Build the lambda proc for a numeric range.
66
+ def self.numeric_range(negate, range)
67
+ return ->(value) { range.include?(Matchers.numeric(value)) } unless negate
68
+ ->(value) { !range.include?(Matchers.numeric(value)) }
69
+ end
70
+ private_class_method :numeric_range
71
+
72
+ # Build the lambda proc for an alphanumeric range.
73
+ def self.alnum_range(negate, range)
74
+ return ->(value) { range.include?(value) } unless negate
75
+ ->(value) { !range.include?(value) }
76
+ end
77
+ private_class_method :alnum_range
78
+
79
+ def self.range_proc(match:, coerce: nil)
80
+ negate, range = range(match, coerce: coerce)
81
+ method = coerce ? :numeric_range : :alnum_range
82
+ function = Range.send(method, negate, range).freeze
83
+ Proc.new(type: :proc, function: function)
84
+ end
85
+ private_class_method :range_proc
86
+
87
+ # Ruby-like range expressions or their negation - e.g., +0...10+ or +!a..z+.
88
+ #
89
+ # @return (see Matcher#matches?)
90
+ def matches?(cell)
91
+ Range.matches?(cell)
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ # CSV Decision: CSV based Ruby decision tables.
4
+ # Created December 2017.
5
+ # @author Brett Vickers <brett@phillips-vickers.com>
6
+ # See LICENSE and README.md for details.
7
+ module CSVDecision
8
+ # Recognise expressions in table data cells.
9
+ # @api private
10
+ class Matchers
11
+ # Match cell against a symbolic expression - e.g., :column, > :column.
12
+ # Can also call a Ruby method pn the column value - e.g, .blank? or !.blank?
13
+ class Symbol < Matcher
14
+ SYMBOL_COMPARATORS = "#{INEQUALITY}|>=|<=|<|>|#{EQUALS}"
15
+ private_constant :SYMBOL_COMPARATORS
16
+
17
+ # Column symbol comparison - e.g., > :column or != :column.
18
+ # Can also be a method call - e.g., .present? or .blank?
19
+ SYMBOL_COMPARE =
20
+ "(?<comparator>#{SYMBOL_COMPARATORS})?\\s*(?<type>[.:!])?(?<name>#{Header::COLUMN_NAME})"
21
+ private_constant :SYMBOL_COMPARE
22
+
23
+ # Symbol comparision regular expression.
24
+ SYMBOL_COMPARE_RE = Matchers.regexp(SYMBOL_COMPARE)
25
+ private_constant :SYMBOL_COMPARE_RE
26
+
27
+ # These procs compare one input hash value to another, and so do not coerce numeric values.
28
+ # Note that we do *not* check +hash.key?(symbol)+, so a +nil+ value will match a missing
29
+ # hash key.
30
+ EQUALITY = {
31
+ ':=' => proc { |symbol, value, hash| value == hash[symbol] },
32
+ '!=' => proc { |symbol, value, hash| value != hash[symbol] }
33
+ }.freeze
34
+ private_constant :EQUALITY
35
+
36
+ def self.compare_proc(sym)
37
+ proc do |symbol, value, hash|
38
+ Matchers.compare?(lhs: value, compare: sym, rhs: hash[symbol])
39
+ end
40
+ end
41
+ private_class_method :compare_proc
42
+
43
+ def self.value_method(value, method)
44
+ value.respond_to?(method) && value.send(method)
45
+ end
46
+ private_class_method :value_method
47
+
48
+ def self.method_proc(negate:)
49
+ if negate
50
+ proc { |symbol, value| !value_method(value, symbol) }
51
+ else
52
+ proc { |symbol, value| value_method(value, symbol) }
53
+ end
54
+ end
55
+ private_class_method :method_proc
56
+
57
+ COMPARE = {
58
+ # Equality and inequality - create a lambda proc by calling with the actual column name
59
+ # symbol.
60
+ ':=' => ->(symbol) { EQUALITY[':='].curry[symbol].freeze },
61
+ '=' => ->(symbol) { EQUALITY[':='].curry[symbol].freeze },
62
+ '==' => ->(symbol) { EQUALITY[':='].curry[symbol].freeze },
63
+ '!=' => ->(symbol) { EQUALITY['!='].curry[symbol].freeze },
64
+ '!' => ->(symbol) { EQUALITY['!='].curry[symbol].freeze },
65
+
66
+ # Comparisons - create a lambda proc by calling with the actual column name symbol.
67
+ '>' => ->(symbol) { compare_proc(:'>').curry[symbol].freeze },
68
+ '>=' => ->(symbol) { compare_proc(:'>=').curry[symbol].freeze },
69
+ '<' => ->(symbol) { compare_proc(:'<').curry[symbol].freeze },
70
+ '<=' => ->(symbol) { compare_proc(:'<=').curry[symbol].freeze },
71
+
72
+ # 0-arity Ruby method calls applied to an input column value.
73
+ '.' => ->(symbol) { method_proc(negate: false).curry[symbol].freeze },
74
+ '!.' => ->(symbol) { method_proc(negate: true).curry[symbol].freeze }
75
+ }.freeze
76
+ private_constant :COMPARE
77
+
78
+ # E.g., > :col, we get comparator: >, name: col
79
+ def self.comparison(comparator:, name:)
80
+ function = COMPARE[comparator]
81
+ Matchers::Proc.new(type: :symbol, function: function[name], symbols: name)
82
+ end
83
+ private_class_method :comparison
84
+
85
+ # E.g., !.nil?, we get comparator: !, name: nil?, type: .
86
+ def self.method_call(comparator:, name:, type:)
87
+ negate = negated_comparator?(comparator: comparator)
88
+ return false if negate.nil?
89
+
90
+ # Check for double negation - e.g., != !blank?
91
+ negate = type == '!' ? !negate : negate
92
+ method_function(name: name, negate: negate)
93
+ end
94
+ private_class_method :method_call
95
+
96
+ def self.negated_comparator?(comparator:)
97
+ # Do we have an equality comparator?
98
+ if EQUALS_RE.match?(comparator)
99
+ false
100
+
101
+ # If do not have equality, do we have inequality?
102
+ elsif INEQUALITY_RE.match?(comparator)
103
+ true
104
+ end
105
+ end
106
+ private_class_method :negated_comparator?
107
+
108
+ # E.g., !.nil?, we get comparator: !, name: nil?
109
+ def self.method_function(name:, negate:)
110
+ # Allowed Ruby method names are a bit stricter than allowed decision table column names.
111
+ return false unless METHOD_NAME_RE.match?(name)
112
+
113
+ function = COMPARE[negate ? '!.' : '.']
114
+ Matchers::Proc.new(type: :proc, function: function[name])
115
+ end
116
+ private_class_method :method_function
117
+
118
+ def self.comparator_type(comparator:, name:, type:)
119
+ if type == ':'
120
+ comparison(comparator: comparator, name: name)
121
+
122
+ # Method call - e.g, .blank? or !.present?
123
+ # Can also take the forms: := .blank? or !=.present?
124
+ else
125
+ method_call(comparator: comparator, name: name, type: type || '.')
126
+ end
127
+ end
128
+ private_class_method :comparator_type
129
+
130
+ # @param (see Matchers::Matcher#matches?)
131
+ # @return (see Matchers::Matcher#matches?)
132
+ def self.matches?(cell)
133
+ return false unless (match = SYMBOL_COMPARE_RE.match(cell))
134
+
135
+ comparator = match['comparator']
136
+ type = match['type']
137
+ return false if comparator.nil? && type.nil?
138
+
139
+ comparator_type(comparator: comparator || '=', type: type, name: match['name'].to_sym)
140
+ end
141
+
142
+ # @param (see Matcher#matches?)
143
+ # @return (see Matcher#matches?)
144
+ def matches?(cell)
145
+ Symbol.matches?(cell)
146
+ end
147
+ end
148
+ end
149
+ end