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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +3 -0
- data/.coveralls.yml +2 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.rubocop.yml +30 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +85 -0
- data/Dockerfile +6 -0
- data/Gemfile +7 -0
- data/LICENSE +21 -0
- data/README.md +356 -0
- data/benchmarks/rufus_decision.rb +158 -0
- data/csv_decision2.gemspec +38 -0
- data/doc/CSVDecision/CellValidationError.html +143 -0
- data/doc/CSVDecision/Columns/Default.html +589 -0
- data/doc/CSVDecision/Columns/Dictionary.html +801 -0
- data/doc/CSVDecision/Columns/Entry.html +508 -0
- data/doc/CSVDecision/Columns.html +1259 -0
- data/doc/CSVDecision/Constant.html +254 -0
- data/doc/CSVDecision/Data.html +479 -0
- data/doc/CSVDecision/Decide.html +302 -0
- data/doc/CSVDecision/Decision.html +1011 -0
- data/doc/CSVDecision/Defaults.html +291 -0
- data/doc/CSVDecision/Dictionary/Entry.html +1147 -0
- data/doc/CSVDecision/Dictionary.html +426 -0
- data/doc/CSVDecision/Error.html +139 -0
- data/doc/CSVDecision/FileError.html +143 -0
- data/doc/CSVDecision/Function.html +240 -0
- data/doc/CSVDecision/Guard.html +245 -0
- data/doc/CSVDecision/Header.html +647 -0
- data/doc/CSVDecision/Index.html +741 -0
- data/doc/CSVDecision/Input.html +404 -0
- data/doc/CSVDecision/Load.html +296 -0
- data/doc/CSVDecision/Matchers/Constant.html +484 -0
- data/doc/CSVDecision/Matchers/Function.html +511 -0
- data/doc/CSVDecision/Matchers/Guard.html +503 -0
- data/doc/CSVDecision/Matchers/Matcher.html +507 -0
- data/doc/CSVDecision/Matchers/Numeric.html +415 -0
- data/doc/CSVDecision/Matchers/Pattern.html +491 -0
- data/doc/CSVDecision/Matchers/Proc.html +704 -0
- data/doc/CSVDecision/Matchers/Range.html +379 -0
- data/doc/CSVDecision/Matchers/Symbol.html +426 -0
- data/doc/CSVDecision/Matchers.html +1567 -0
- data/doc/CSVDecision/Numeric.html +259 -0
- data/doc/CSVDecision/Options.html +443 -0
- data/doc/CSVDecision/Parse.html +282 -0
- data/doc/CSVDecision/Paths.html +742 -0
- data/doc/CSVDecision/Result.html +1200 -0
- data/doc/CSVDecision/Scan/InputHashes.html +369 -0
- data/doc/CSVDecision/Scan.html +313 -0
- data/doc/CSVDecision/ScanRow.html +866 -0
- data/doc/CSVDecision/Symbol.html +256 -0
- data/doc/CSVDecision/Table.html +1470 -0
- data/doc/CSVDecision/TableValidationError.html +143 -0
- data/doc/CSVDecision/Validate.html +422 -0
- data/doc/CSVDecision.html +621 -0
- data/doc/_index.html +471 -0
- data/doc/class_list.html +51 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +58 -0
- data/doc/css/style.css +499 -0
- data/doc/file.README.html +421 -0
- data/doc/file_list.html +56 -0
- data/doc/frames.html +17 -0
- data/doc/index.html +421 -0
- data/doc/js/app.js +248 -0
- data/doc/js/full_list.js +216 -0
- data/doc/js/jquery.js +4 -0
- data/doc/method_list.html +1163 -0
- data/doc/top-level-namespace.html +110 -0
- data/docker-compose.yml +13 -0
- data/lib/csv_decision/columns.rb +192 -0
- data/lib/csv_decision/data.rb +92 -0
- data/lib/csv_decision/decision.rb +196 -0
- data/lib/csv_decision/defaults.rb +47 -0
- data/lib/csv_decision/dictionary.rb +180 -0
- data/lib/csv_decision/header.rb +83 -0
- data/lib/csv_decision/index.rb +107 -0
- data/lib/csv_decision/input.rb +121 -0
- data/lib/csv_decision/load.rb +36 -0
- data/lib/csv_decision/matchers/constant.rb +74 -0
- data/lib/csv_decision/matchers/function.rb +56 -0
- data/lib/csv_decision/matchers/guard.rb +142 -0
- data/lib/csv_decision/matchers/numeric.rb +44 -0
- data/lib/csv_decision/matchers/pattern.rb +94 -0
- data/lib/csv_decision/matchers/range.rb +95 -0
- data/lib/csv_decision/matchers/symbol.rb +149 -0
- data/lib/csv_decision/matchers.rb +220 -0
- data/lib/csv_decision/options.rb +124 -0
- data/lib/csv_decision/parse.rb +165 -0
- data/lib/csv_decision/paths.rb +78 -0
- data/lib/csv_decision/result.rb +204 -0
- data/lib/csv_decision/scan.rb +117 -0
- data/lib/csv_decision/scan_row.rb +142 -0
- data/lib/csv_decision/table.rb +101 -0
- data/lib/csv_decision/validate.rb +85 -0
- data/lib/csv_decision.rb +45 -0
- data/spec/csv_decision/columns_spec.rb +251 -0
- data/spec/csv_decision/constant_spec.rb +36 -0
- data/spec/csv_decision/data_spec.rb +50 -0
- data/spec/csv_decision/decision_spec.rb +19 -0
- data/spec/csv_decision/examples_spec.rb +242 -0
- data/spec/csv_decision/index_spec.rb +58 -0
- data/spec/csv_decision/input_spec.rb +55 -0
- data/spec/csv_decision/load_spec.rb +28 -0
- data/spec/csv_decision/matchers/function_spec.rb +82 -0
- data/spec/csv_decision/matchers/guard_spec.rb +170 -0
- data/spec/csv_decision/matchers/numeric_spec.rb +47 -0
- data/spec/csv_decision/matchers/pattern_spec.rb +183 -0
- data/spec/csv_decision/matchers/range_spec.rb +70 -0
- data/spec/csv_decision/matchers/symbol_spec.rb +67 -0
- data/spec/csv_decision/options_spec.rb +94 -0
- data/spec/csv_decision/parse_spec.rb +44 -0
- data/spec/csv_decision/table_spec.rb +683 -0
- data/spec/csv_decision_spec.rb +7 -0
- data/spec/data/invalid/empty.csv +0 -0
- data/spec/data/invalid/invalid_header1.csv +4 -0
- data/spec/data/invalid/invalid_header2.csv +4 -0
- data/spec/data/invalid/invalid_header3.csv +4 -0
- data/spec/data/invalid/invalid_header4.csv +4 -0
- data/spec/data/valid/benchmark_regexp.csv +10 -0
- data/spec/data/valid/index_example.csv +13 -0
- data/spec/data/valid/multi_column_index.csv +10 -0
- data/spec/data/valid/multi_column_index2.csv +12 -0
- data/spec/data/valid/options_in_file1.csv +5 -0
- data/spec/data/valid/options_in_file2.csv +5 -0
- data/spec/data/valid/options_in_file3.csv +13 -0
- data/spec/data/valid/regular_expressions.csv +11 -0
- data/spec/data/valid/simple_constants.csv +5 -0
- data/spec/data/valid/simple_example.csv +10 -0
- data/spec/data/valid/valid.csv +4 -0
- data/spec/spec_helper.rb +106 -0
- 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
|