csv_decision2 0.5.1

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 (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