csv_decision 0.4.0 → 0.4.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 +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
|