csv_decision 0.4.0 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +8 -7
- data/csv_decision.gemspec +1 -1
- data/lib/csv_decision/matchers/constant.rb +1 -2
- data/lib/csv_decision/matchers/guard.rb +1 -10
- data/lib/csv_decision/matchers/numeric.rb +2 -5
- data/lib/csv_decision/matchers/pattern.rb +0 -1
- data/lib/csv_decision/matchers/symbol.rb +40 -26
- data/lib/csv_decision/matchers.rb +16 -2
- data/spec/csv_decision/matchers/symbol_spec.rb +2 -2
- data/spec/csv_decision/table_spec.rb +2 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4fc2bc37813bc6b1bf8cff959e559b808dc5125b
|
4
|
+
data.tar.gz: 75446892df8690d268627583c46df139b0fea96c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2def585d31f8dc19baa761c9407520b74be312da434f3521fc7e27c64bf035572c1f174b7271a66159f0746253897681735f0102580ec25479eba5401fb21725
|
7
|
+
data.tar.gz: 27084f128c9ccf26dd81b932e9f9498a165dca08530c9c064f862267ceaab147ded8918bea1a8d037c7ffd5ea30368e7cf4ce3e024485379cabe7988201dcd45
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
## v0.4.1, 10 February 2018.
|
2
|
+
*Additions*
|
3
|
+
- For consistency, input columns may now use the form `!nil?` to negate 0-arity Ruby methods.
|
4
|
+
(Typical SME users can be somewhat lax with syntax.)
|
5
|
+
|
1
6
|
## v0.4.0, 28 January 2018.
|
2
7
|
*Additions*
|
3
8
|
- Input columns may now use 0-arity Ruby methods to implement conditional logic.
|
data/README.md
CHANGED
@@ -20,10 +20,10 @@ producing a decision as an output hash.
|
|
20
20
|
|
21
21
|
Typical "business logic" is notoriously illogical - full of corner cases and one-off
|
22
22
|
exceptions.
|
23
|
-
A decision table can express data-based decisions in a way that comes
|
24
|
-
to subject matter experts, who typically
|
25
|
-
Business logic
|
26
|
-
|
23
|
+
A decision table can express data-based decisions in a way that comes naturally
|
24
|
+
to subject matter experts, who typically use spreadsheet models.
|
25
|
+
Business logic can be encapsulated in a table, avoiding the need for tortuous conditional
|
26
|
+
expressions.
|
27
27
|
|
28
28
|
This gem and the examples below take inspiration from
|
29
29
|
[rufus/decision](https://github.com/jmettraux/rufus-decision).
|
@@ -75,7 +75,7 @@ the world, except for `America` and `Europe`, *must* come after his colleagues
|
|
75
75
|
`Charlie` and `Donald`. `Zach` has been placed last, catching all the input combos
|
76
76
|
not matching any other row.
|
77
77
|
|
78
|
-
Here
|
78
|
+
Here's the example as code:
|
79
79
|
|
80
80
|
```ruby
|
81
81
|
# Valid CSV string
|
@@ -127,7 +127,7 @@ Complete documentation of all table parameters is in the code - see
|
|
127
127
|
* Either returns the first matching row as a hash (default), or accumulates all matches as an
|
128
128
|
array of hashes (i.e., `parse` option `first_match: false` or CSV file option `accumulate`).
|
129
129
|
* Fast decision-time performance (see `benchmarks` folder). Automatically indexes all
|
130
|
-
|
130
|
+
constants-only columns that do not contain any empty strings.
|
131
131
|
* In addition to simple strings, `csv_decision` can match basic Ruby constants (e.g., `=nil`),
|
132
132
|
regular expressions (e.g., `=~ on|off`), comparisons (e.g., `> 100.0` ) and
|
133
133
|
Ruby-style ranges (e.g., `1..10`)
|
@@ -198,7 +198,8 @@ may be simplified to:
|
|
198
198
|
```
|
199
199
|
These comparison operators are also supported: `!=`, `>`, `>=`, `<`, `<=`.
|
200
200
|
In addition, you can also apply a Ruby 0-arity method - e.g., `.present?` or `.nil?`. Negation is
|
201
|
-
also supported - e.g., `!.nil?`.
|
201
|
+
also supported - e.g., `!.nil?`. Note that `.nil?` can also be written as `:= nil?`, and `!.nil?`
|
202
|
+
as `:= !nil?`, depending on preference.
|
202
203
|
|
203
204
|
For more simple examples see `spec/csv_decision/examples_spec.rb`.
|
204
205
|
|
data/csv_decision.gemspec
CHANGED
@@ -5,7 +5,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = 'csv_decision'
|
8
|
-
spec.version = '0.4.
|
8
|
+
spec.version = '0.4.1'
|
9
9
|
spec.authors = ['Brett Vickers']
|
10
10
|
spec.email = ['brett@phillips-vickers.com']
|
11
11
|
spec.description = 'CSV based Ruby decision tables.'
|
@@ -43,8 +43,7 @@ module CSVDecision
|
|
43
43
|
private_class_method :proc
|
44
44
|
|
45
45
|
def self.numeric?(match)
|
46
|
-
value = Matchers.to_numeric(match['value'])
|
47
|
-
return false unless value
|
46
|
+
return false unless (value = Matchers.to_numeric(match['value']))
|
48
47
|
|
49
48
|
proc(function: value)
|
50
49
|
end
|
@@ -69,20 +69,11 @@ module CSVDecision
|
|
69
69
|
}.freeze
|
70
70
|
private_constant :SYMBOL_PROC
|
71
71
|
|
72
|
-
def self.compare?(lhs:, compare:, rhs:)
|
73
|
-
# Is the rhs the same class or a superclass of lhs, and does rhs respond to the
|
74
|
-
# compare method?
|
75
|
-
return lhs.send(compare, rhs) if lhs.is_a?(rhs.class) && rhs.respond_to?(compare)
|
76
|
-
|
77
|
-
nil
|
78
|
-
end
|
79
|
-
private_class_method :compare?
|
80
|
-
|
81
72
|
def self.non_numeric(method)
|
82
73
|
proc = FUNCTION[method]
|
83
74
|
return proc if proc
|
84
75
|
|
85
|
-
proc { |symbol, value, hash| compare?(lhs: hash[symbol], compare: method, rhs: value) }
|
76
|
+
proc { |symbol, value, hash| Matchers.compare?(lhs: hash[symbol], compare: method, rhs: value) }
|
86
77
|
end
|
87
78
|
private_class_method :non_numeric
|
88
79
|
|
@@ -27,11 +27,8 @@ module CSVDecision
|
|
27
27
|
|
28
28
|
# (see Matcher#matches?)
|
29
29
|
def self.matches?(cell)
|
30
|
-
match = COMPARISON.match(cell)
|
31
|
-
return false unless match
|
32
|
-
|
33
|
-
numeric_cell = Matchers.to_numeric(match['value'])
|
34
|
-
return false unless numeric_cell
|
30
|
+
return false unless (match = COMPARISON.match(cell))
|
31
|
+
return false unless (numeric_cell = Matchers.to_numeric(match['value']))
|
35
32
|
|
36
33
|
comparator = match['comparator']
|
37
34
|
Matchers::Proc.new(type: :proc,
|
@@ -17,7 +17,7 @@ module CSVDecision
|
|
17
17
|
# Column symbol comparison - e.g., > :column or != :column.
|
18
18
|
# Can also be a method call - e.g., .present? or .blank?
|
19
19
|
SYMBOL_COMPARE =
|
20
|
-
"(?<comparator>#{SYMBOL_COMPARATORS})?\\s*(?<type>[
|
20
|
+
"(?<comparator>#{SYMBOL_COMPARATORS})?\\s*(?<type>[.:!])?(?<name>#{Header::COLUMN_NAME})"
|
21
21
|
private_constant :SYMBOL_COMPARE
|
22
22
|
|
23
23
|
# Symbol comparision regular expression.
|
@@ -33,8 +33,10 @@ module CSVDecision
|
|
33
33
|
}.freeze
|
34
34
|
private_constant :EQUALITY
|
35
35
|
|
36
|
-
def self.compare_proc(
|
37
|
-
proc
|
36
|
+
def self.compare_proc(sym)
|
37
|
+
proc do |symbol, value, hash|
|
38
|
+
Matchers.compare?(lhs: value, compare: sym, rhs: hash[symbol])
|
39
|
+
end
|
38
40
|
end
|
39
41
|
private_class_method :compare_proc
|
40
42
|
|
@@ -73,14 +75,6 @@ module CSVDecision
|
|
73
75
|
}.freeze
|
74
76
|
private_constant :COMPARE
|
75
77
|
|
76
|
-
def self.compare?(lhs:, compare:, rhs:)
|
77
|
-
# Is the rhs a superclass of lhs, and does rhs respond to the compare method?
|
78
|
-
return lhs.public_send(compare, rhs) if lhs.is_a?(rhs.class) && rhs.respond_to?(compare)
|
79
|
-
|
80
|
-
false
|
81
|
-
end
|
82
|
-
private_class_method :compare?
|
83
|
-
|
84
78
|
# E.g., > :col, we get comparator: >, name: col
|
85
79
|
def self.comparison(comparator:, name:)
|
86
80
|
function = COMPARE[comparator]
|
@@ -88,16 +82,29 @@ module CSVDecision
|
|
88
82
|
end
|
89
83
|
private_class_method :comparison
|
90
84
|
|
91
|
-
# E.g., !.nil?, we get comparator: !, name: nil
|
92
|
-
def self.method_call(comparator:, name:)
|
93
|
-
|
94
|
-
|
95
|
-
return false unless equality || inequality
|
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?
|
96
89
|
|
97
|
-
|
90
|
+
# Check for double negation - e.g., != !blank?
|
91
|
+
negate = type == '!' ? !negate : negate
|
92
|
+
method_function(name: name, negate: negate)
|
98
93
|
end
|
99
94
|
private_class_method :method_call
|
100
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
|
+
|
101
108
|
# E.g., !.nil?, we get comparator: !, name: nil?
|
102
109
|
def self.method_function(name:, negate:)
|
103
110
|
# Allowed Ruby method names are a bit stricter than allowed decision table column names.
|
@@ -108,22 +115,29 @@ module CSVDecision
|
|
108
115
|
end
|
109
116
|
private_class_method :method_function
|
110
117
|
|
111
|
-
|
112
|
-
|
113
|
-
def self.matches?(cell)
|
114
|
-
return false unless (match = SYMBOL_COMPARE_RE.match(cell))
|
115
|
-
|
116
|
-
comparator = match['comparator'] || '='
|
117
|
-
name = match['name'].to_sym
|
118
|
-
if match['type'] == ':'
|
118
|
+
def self.comparator_type(comparator:, name:, type:)
|
119
|
+
if type == ':'
|
119
120
|
comparison(comparator: comparator, name: name)
|
120
121
|
|
121
122
|
# Method call - e.g, .blank? or !.present?
|
122
123
|
# Can also take the forms: := .blank? or !=.present?
|
123
124
|
else
|
124
|
-
method_call(comparator: comparator, name: name)
|
125
|
+
method_call(comparator: comparator, name: name, type: type || '.')
|
125
126
|
end
|
126
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
|
127
141
|
|
128
142
|
# @param (see Matcher#matches?)
|
129
143
|
# @return (see Matcher#matches?)
|
@@ -75,14 +75,14 @@ module CSVDecision
|
|
75
75
|
# Symbols used for inequality
|
76
76
|
INEQUALITY = '!=|!'
|
77
77
|
|
78
|
-
# Match
|
78
|
+
# Match Regexp for inequality
|
79
79
|
INEQUALITY_RE = regexp(INEQUALITY)
|
80
80
|
|
81
81
|
# Equality, cell constants and functions specified by prefixing the value with
|
82
82
|
# one of these 3 symbols.
|
83
83
|
EQUALS = '==|:=|='
|
84
84
|
|
85
|
-
# Match
|
85
|
+
# Match Regexp for equality
|
86
86
|
EQUALS_RE = regexp(EQUALS)
|
87
87
|
|
88
88
|
# Method names are stricter than CSV column names.
|
@@ -124,6 +124,20 @@ module CSVDecision
|
|
124
124
|
BigDecimal(value.chomp('.'))
|
125
125
|
end
|
126
126
|
|
127
|
+
# Compare one object with another if they both respond to the compare method.
|
128
|
+
#
|
129
|
+
# @param lhs [Object]
|
130
|
+
# @param compare [Object]
|
131
|
+
# @param rhs [Object]
|
132
|
+
# @return [nil, Boolean]
|
133
|
+
def self.compare?(lhs:, compare:, rhs:)
|
134
|
+
# Is the rhs the same class or a superclass of lhs, and does rhs respond to the
|
135
|
+
# compare method?
|
136
|
+
return lhs.send(compare, rhs) if lhs.is_a?(rhs.class) && rhs.respond_to?(compare)
|
137
|
+
|
138
|
+
nil
|
139
|
+
end
|
140
|
+
|
127
141
|
# Parse the supplied input columns for the row supplied using an array of matchers.
|
128
142
|
#
|
129
143
|
# @param columns [Hash{Integer=>Columns::Entry}] Input columns hash.
|
@@ -42,7 +42,7 @@ describe CSVDecision::Matchers::Symbol do
|
|
42
42
|
{ cell: '<=:col', value: 0, hash: { col: 1 }, result: true },
|
43
43
|
{ cell: '<=:col', value: 1, hash: { col: 0 }, result: false },
|
44
44
|
{ cell: '<=:col', value: 1, hash: { col: 1 }, result: true },
|
45
|
-
{ cell: '<=:col', value: '1', hash: { col: 1 }, result:
|
45
|
+
{ cell: '<=:col', value: '1', hash: { col: 1 }, result: nil },
|
46
46
|
{ cell: '<=:col', value: '1', hash: { col: '1' }, result: true },
|
47
47
|
]
|
48
48
|
examples.each do |ex|
|
@@ -55,7 +55,7 @@ describe CSVDecision::Matchers::Symbol do
|
|
55
55
|
end
|
56
56
|
|
57
57
|
context 'does not match a function' do
|
58
|
-
data = ['1', 'abc', 'abc.*def', '-1..1', '0...3', ':= false', ':= lookup?']
|
58
|
+
data = ['1', 'abc', 'abc.*def', '-1..1', '0...3', ':= false()', ':= lookup?()']
|
59
59
|
|
60
60
|
data.each do |cell|
|
61
61
|
it "cell #{cell} is not a function" do
|
@@ -568,10 +568,10 @@ describe CSVDecision::Table do
|
|
568
568
|
!= .present?, , integer, none, :=nil
|
569
569
|
=.present?, , integer, one, :=1
|
570
570
|
==.blank?, , integer, one, :=nil
|
571
|
-
,
|
571
|
+
, != !blank?, string, none, 0
|
572
572
|
.blank?, , string, none, :=nil
|
573
573
|
, !.blank?, string, one, 1
|
574
|
-
|
574
|
+
!present?, , string, one, :=nil
|
575
575
|
DATA
|
576
576
|
},
|
577
577
|
{ example: 'evaluates multi-column index CSV file',
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: csv_decision
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.4.
|
4
|
+
version: 0.4.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brett Vickers
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-02-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|