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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7238268643ad9dc69c91b0367550cd0ed2577b7d
4
- data.tar.gz: d00d7a55a444b8026cb2bf477ce2812f16c2084d
3
+ metadata.gz: 4fc2bc37813bc6b1bf8cff959e559b808dc5125b
4
+ data.tar.gz: 75446892df8690d268627583c46df139b0fea96c
5
5
  SHA512:
6
- metadata.gz: 89b76ce96dd906ab748158e6a96bb0f02e40abf6b4df4a5fafa5809dac1663a1b7ea97cbbc1cdfad7e10456e5b78b3e674e9e2032dcd24ac6009d8dcc8467b82
7
- data.tar.gz: bde8179729614e89c2fa5fa5861e5ca55fe34a2038b752e3d4e27968f9eb83fb8a5979d18bd5353089482e54315355190e28792a1008618d169267df15a7790c
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 more naturally
24
- to subject matter experts, who typically prefer spreadsheet models.
25
- Business logic may then be encapsulated, avoiding the need to write tortuous
26
- conditional expressions in Ruby that draw the ire of `rubocop` and its ilk.
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 is the example as code:
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
- text-only columns that do not contain any empty strings.
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.0'
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,
@@ -41,7 +41,6 @@ module CSVDecision
41
41
  return false if value.blank?
42
42
 
43
43
  # We cannot do a regexp comparison against a symbol name.
44
- # (Maybe we should add this feature?)
45
44
  return if value[0] == ':'
46
45
 
47
46
  # If no comparator then the implicit option must be on
@@ -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>[.:])(?<name>#{Header::COLUMN_NAME})"
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(compare)
37
- proc { |symbol, value, hash| compare?(lhs: value, compare: compare, rhs: hash[symbol]) }
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
- equality = EQUALS_RE.match?(comparator)
94
- inequality = !equality && INEQUALITY_RE.match?(comparator)
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
- method_function(name: name, negate: inequality)
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
- # @param (see Matchers::Matcher#matches?)
112
- # @return (see Matchers::Matcher#matches?)
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 string for inequality
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 string for equality
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: false },
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
- , .present?, string, none, 0
571
+ , != !blank?, string, none, 0
572
572
  .blank?, , string, none, :=nil
573
573
  , !.blank?, string, one, 1
574
- .blank?, , string, one, :=nil
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.0
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-01-28 00:00:00.000000000 Z
11
+ date: 2018-02-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport