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 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