csv_decision2 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|