csv_decision 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.coveralls.yml +2 -0
  3. data/.rubocop.yml +16 -4
  4. data/.travis.yml +10 -0
  5. data/CHANGELOG.md +2 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE +21 -0
  8. data/README.md +133 -19
  9. data/benchmark.rb +143 -0
  10. data/csv_decision.gemspec +8 -6
  11. data/lib/csv_decision.rb +18 -4
  12. data/lib/csv_decision/columns.rb +69 -0
  13. data/lib/csv_decision/data.rb +31 -16
  14. data/lib/csv_decision/decide.rb +47 -0
  15. data/lib/csv_decision/decision.rb +105 -0
  16. data/lib/csv_decision/header.rb +143 -8
  17. data/lib/csv_decision/input.rb +49 -0
  18. data/lib/csv_decision/load.rb +31 -0
  19. data/lib/csv_decision/matchers.rb +131 -0
  20. data/lib/csv_decision/matchers/numeric.rb +37 -0
  21. data/lib/csv_decision/matchers/pattern.rb +76 -0
  22. data/lib/csv_decision/matchers/range.rb +76 -0
  23. data/lib/csv_decision/options.rb +80 -50
  24. data/lib/csv_decision/parse.rb +77 -23
  25. data/lib/csv_decision/scan_row.rb +68 -0
  26. data/lib/csv_decision/table.rb +34 -6
  27. data/spec/csv_decision/columns_spec.rb +86 -0
  28. data/spec/csv_decision/data_spec.rb +16 -3
  29. data/spec/csv_decision/decision_spec.rb +30 -0
  30. data/spec/csv_decision/input_spec.rb +54 -0
  31. data/spec/csv_decision/load_spec.rb +28 -0
  32. data/spec/csv_decision/matchers/numeric_spec.rb +84 -0
  33. data/spec/csv_decision/matchers/pattern_spec.rb +183 -0
  34. data/spec/csv_decision/matchers/range_spec.rb +132 -0
  35. data/spec/csv_decision/options_spec.rb +67 -0
  36. data/spec/csv_decision/parse_spec.rb +2 -3
  37. data/spec/csv_decision/simple_example_spec.rb +45 -0
  38. data/spec/csv_decision/table_spec.rb +151 -0
  39. data/spec/data/invalid/invalid_header1.csv +4 -0
  40. data/spec/data/invalid/invalid_header2.csv +4 -0
  41. data/spec/data/invalid/invalid_header3.csv +4 -0
  42. data/spec/data/invalid/invalid_header4.csv +4 -0
  43. data/spec/data/valid/options_in_file1.csv +5 -0
  44. data/spec/data/valid/options_in_file2.csv +5 -0
  45. data/spec/data/valid/simple_example.csv +10 -0
  46. data/spec/data/valid/valid.csv +4 -4
  47. data/spec/spec_helper.rb +6 -0
  48. metadata +89 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 486bc130f39ee0893ac24493bb1e4f866e23171a
4
- data.tar.gz: 1b0b4467391b2358e0bdb6e380a9fc7cbfa815d9
3
+ metadata.gz: 824f8e4c688f507bc8e0ea1c65c37d7a615e7d1d
4
+ data.tar.gz: 644623f44ba59db10bc98470e113673f0482fbcd
5
5
  SHA512:
6
- metadata.gz: c8dae72ca353f917f4511735e5a5d5d7f9eccc7627a41cdf53e9a92c8d63374cf69f4c84834da1e8cd9ae08178ae13aaa71443bbed5e60394507645296f5b931
7
- data.tar.gz: b292cd9287309a084fefd21d78f6b04d90448188ff85008b17c75dc6aab66cc902e3bfddcfe66d1c266b7c924bbe5ee9d33dc73609da9bcddffccbd306217808
6
+ metadata.gz: 005e743b69070d458d93d8fef1fd1e75e6328d3046e6adb7e2461d0d3201848d011922e3536b4942949697d3cbb04f985af21da48b3464690c2373490ffffd5c
7
+ data.tar.gz: e3dd1c8cc027ae660a82ad8f5c9be5f1307741aa329ca03c109bc28486acd59dac4ecc18ed497e75294f162fda596d91cd1e937f3ccbb494af4bd6fcf57e8692
data/.coveralls.yml ADDED
@@ -0,0 +1,2 @@
1
+ service_name: travis-ci
2
+ repo_token: Rr2wjGPimrqmxVhIHHN29kGxcaQnSh4Xs
data/.rubocop.yml CHANGED
@@ -1,15 +1,27 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.3.1
2
+ TargetRubyVersion: 2.3.0
3
3
 
4
4
  Metrics/MethodLength:
5
5
  CountComments: false
6
6
  Max: 15
7
7
 
8
- Metrics/AbcSize:
9
- Max: 18
10
-
11
8
  Metrics/LineLength:
12
9
  Max: 100
13
10
 
14
11
  Layout/TrailingWhitespace:
15
12
  Enabled: false
13
+
14
+ Layout/SpaceInsideArrayPercentLiteral:
15
+ Enabled: false
16
+
17
+ Layout/AlignArray:
18
+ Enabled: false
19
+
20
+ Layout/IdentArray:
21
+ Enabled: false
22
+
23
+ Style/StringLiterals:
24
+ Enabled: false
25
+
26
+ Style/WordArray:
27
+ Enabled: false
data/.travis.yml ADDED
@@ -0,0 +1,10 @@
1
+ env:
2
+ global:
3
+ - CC_TEST_REPORTER_ID=787a2f89b15c637323c7340d65ec17e898ac44480706b4b4122ea040c2a88f1d
4
+ language: ruby
5
+ rvm:
6
+ - 2.3.0
7
+ - 2.3.6
8
+ - 2.4.3
9
+ script:
10
+ - bundle exec rspec
data/CHANGELOG.md ADDED
@@ -0,0 +1,2 @@
1
+ ## v0.0.1, 28 December 2017.
2
+ - Initial release
data/Gemfile CHANGED
@@ -1,4 +1,7 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
+ gem 'coveralls', require: false
4
+
3
5
  gemspec
4
6
 
7
+
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Brett Vickers
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md CHANGED
@@ -1,20 +1,134 @@
1
- # csv_decision - Work In Progress
2
- `csv_decision` is a Ruby gem for CSV based decision tables. It accepts decision table logic encoded in
3
- a CSV file, which can then be used to implement complex conditional logic.
1
+ CSV Decision
2
+ ============
4
3
 
5
- `csv_decision` has many useful features:
6
- * able to parse and load into memory many CSV files for subsequent processing
7
- * can return the first matching row as a hash, or accumulate all matches as an array of hashes
8
- * input columns may be indexed for fast lookup performance
9
- * can use regular expressions, Ruby-style ranges and function calls to implement complex decision logic
10
- * all CSV cells are parsed for correctness, and friendly error messages generated for bad input
11
- * can be safely extended with user defined Ruby functions for tailored logic
12
- * good decision time performance
13
- * flexible options for tailoring behavior and defaults
14
-
15
- ### Why use CSV Decision?
16
-
17
- Typical "business logic" is notoriously illogical -- full of corner cases and irregular exceptions.
18
- A decision table can capture data-based decisions in a way that comes naturally to analysts and subject matter
19
- experts, who typically use spreadsheet models. Business logic can be encapsulated, avoiding the need to write
20
- tortuous conditional expressions in Ruby that draws the ire of `rubocop` and its ilk.
4
+ <a href="https://codeclimate.com/github/bpvickers/csv_decision/maintainability"><img src="https://api.codeclimate.com/v1/badges/466a6c52e8f6a3840967/maintainability" /></a>
5
+ [![Build Status](https://travis-ci.org/bpvickers/csv_decision.svg?branch=master)](https://travis-ci.org/bpvickers/csv_decision)
6
+ [![Coverage Status](https://coveralls.io/repos/github/bpvickers/csv_decision/badge.svg?branch=master)](https://coveralls.io/github/bpvickers/csv_decision?branch=master)
7
+ [![Gem Version](https://badge.fury.io/rb/csv_decision.svg)](http://badge.fury.io/rb/csv_decision)
8
+
9
+ # CSV based Ruby decision tables
10
+
11
+ `csv_decision` is a Ruby gem for CSV (comma separated values) based
12
+ [decision tables](https://en.wikipedia.org/wiki/Decision_table).
13
+ It accepts decision tables written in a CSV file, which can then be used to execute
14
+ complex conditional logic against an input hash, producing a decision as an output hash.
15
+
16
+ ### `csv_decision` features
17
+ * fast decision-time performance
18
+ * can use regular expressions, numeric comparisons and Ruby-style ranges
19
+ * accepts data as a file, CSV string or an array of arrays.
20
+ * all CSV cells are parsed for correctness, and helpful error messages generated for bad
21
+ inputs
22
+
23
+ ### Planned features
24
+ * input columns may be indexed for faster lookup performance
25
+ * either returns the first matching row as a hash, or accumulates all matches as an
26
+ array of hashes.
27
+ * use of output functions to formulate the final decision
28
+ * can use column symbol references or built-in guard functions for matching
29
+ * may be extended with user-defined Ruby functions for tailored logic
30
+ * can use if conditions to filter the results of multi-row decision output
31
+
32
+ ### Why use `csv_decision`?
33
+
34
+ Typical "business logic" is notoriously illogical -- full of corner cases and one-off
35
+ exceptions.
36
+ A decision table can capture data-based decisions in a way that comes more naturally
37
+ to subject matter experts, who typically prefer spreadsheet models.
38
+ Business logic may then be encapsulated, avoiding the need to write tortuous
39
+ conditional expressions in Ruby that draw the ire of `rubocop` and its ilk.
40
+
41
+ This gem takes its inspiration from
42
+ [rufus/decision](https://github.com/jmettraux/rufus-decision).
43
+ (That gem is no longer maintained and has issues with execution performance.)
44
+
45
+ ### Installation
46
+
47
+ To get started, just add `csv_decision` to your `Gemfile`, and then run `bundle`:
48
+
49
+ ```ruby
50
+ gem 'csv_decision', '~> 0.0.1'
51
+ ```
52
+
53
+ ### Simple example
54
+
55
+ A decision table may be as simple or as complex as you like (although very complex
56
+ tables defeat the whole purpose).
57
+ Basic usage will be illustrated by an example taken from:
58
+ https://jmettraux.wordpress.com/2009/04/25/rufus-decision-11-ruby-decision-tables/.
59
+
60
+ This example considers two input conditions: `topic` and `region`.
61
+ These are labeled `in`. Certain combinations yield an output value for `team_member`,
62
+ labeled `out`.
63
+
64
+ ```
65
+ in :topic | in :region | out :team_member
66
+ ----------+-------------+-----------------
67
+ sports | Europe | Alice
68
+ sports | | Bob
69
+ finance | America | Charlie
70
+ finance | Europe | Donald
71
+ finance | | Ernest
72
+ politics | Asia | Fujio
73
+ politics | America | Gilbert
74
+ politics | | Henry
75
+ | | Zach
76
+ ```
77
+
78
+ When the topic is `finance` and the region is `Europe` the team member `Donald`
79
+ is selected.
80
+
81
+ This is a "first match" decision table in that as soon as a match is made execution
82
+ stops and a single output value (hash) is returned.
83
+
84
+ The ordering of rows matters. `Ernest`, who is in charge of `finance` for the rest of
85
+ the world, except for `America` and `Europe`, *must* come after his colleagues
86
+ `Charlie` and `Donald`. `Zach` has been placed last, catching all the input combos
87
+ not matching any other row.
88
+
89
+ Now for some code.
90
+
91
+ ```ruby
92
+ data = <<~DATA
93
+ in :topic, in :region, out :team_member
94
+ sports, Europe, Alice
95
+ sports, , Bob
96
+ finance, America, Charlie
97
+ finance, Europe, Donald
98
+ finance, , Ernest
99
+ politics, Asia, Fujio
100
+ politics, America, Gilbert
101
+ politics, , Henry
102
+ , , Zach
103
+ DATA
104
+
105
+ table = CSVDecision.parse(data)
106
+
107
+ table.decide(topic: 'finance', region: 'Europe') # returns team_member: 'Donald'
108
+ table.decide(topic: 'sports', region: nil) # returns team_member: 'Bob'
109
+ table.decide(topic: 'culture', region: 'America') # team_member: 'Zach'
110
+ ```
111
+
112
+ An empty `in` cell means "matches any value".
113
+
114
+ If you have cloned this gem's git repo, then this example can also be run by loading
115
+ the table from a CSV file:
116
+
117
+ ```ruby
118
+ table = CSVDecision.parse(Pathname('spec/data/valid/simple_example.csv'))
119
+ ```
120
+
121
+ For more examples see `spec/csv_decision/table_spec.rb`.
122
+ Complete documentation of all table parameters is in the code - see
123
+ `lib/csv_decision/parse.rb` and `lib/csv_decision/table.rb`.
124
+
125
+
126
+ ### Testing
127
+
128
+ `csv_decision` includes thorough [RSpec](http://rspec.info) tests:
129
+
130
+ ```bash
131
+ # Execute within a clone of the csv_decision Git repository:
132
+ bundle install
133
+ rspec
134
+ ```
data/benchmark.rb ADDED
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'benchmark/ips'
4
+ require 'benchmark/memory'
5
+ require 'rufus/decision'
6
+ require 'ice_nine'
7
+ require 'ice_nine/core_ext/object'
8
+
9
+ require_relative 'lib/csv_decision'
10
+
11
+ SPEC_DATA_VALID ||= File.join(CSVDecision.root, 'spec', 'data', 'valid')
12
+
13
+ CSV_OPTIONS = { regexp_implicit: true }.freeze
14
+ RUFUS_OPTIONS = { open_uri: false, ruby_eval: false }.freeze
15
+
16
+ benchmarks = [
17
+ {
18
+ name: 'String compares only (no index)',
19
+ data: 'simple_example.csv',
20
+ input: { 'topic' => 'culture', 'region' => 'America' },
21
+ # Expected results for first_match and accumulate
22
+ first_match: { 'team_member' => 'Zach' }
23
+ }
24
+ ].deep_freeze
25
+
26
+ tag_width = 70
27
+
28
+ puts ""
29
+ puts "Benchmarking Decisions per Second"
30
+ puts '=' * tag_width
31
+ puts ""
32
+
33
+ # First match true and false run options
34
+ [true].each do |first_match|
35
+ puts "Table Decision Option: first_match: #{first_match}"
36
+ puts '-' * tag_width
37
+
38
+ csv_options = CSV_OPTIONS.merge(first_match: first_match)
39
+ rufus_options = RUFUS_OPTIONS.merge(first_match: first_match)
40
+
41
+ benchmarks.each do |test|
42
+ name = test[:name]
43
+ data = Pathname(File.join(SPEC_DATA_VALID, test[:data]))
44
+
45
+ rufus_table = Rufus::Decision::Table.new(data.to_s, rufus_options)
46
+ csv_table = CSVDecision.parse(data, csv_options)
47
+
48
+ # Prepare input hash
49
+ input = test[:input].deep_dup
50
+ input_symbolized = input.symbolize_keys
51
+
52
+ # Test expected results
53
+ expected = first_match ? test[:first_match] : test[:accumulate]
54
+
55
+ result = rufus_table.transform!(input)
56
+
57
+ unless result.slice(*expected.keys).eql?(expected)
58
+ raise "Rufus expected results check failed for test: #{name}"
59
+ end
60
+
61
+ result = csv_table.decide!(input_symbolized)
62
+
63
+ unless result.eql?(expected.symbolize_keys)
64
+ raise "CSV Decision expected results check failed for test: #{name}"
65
+ end
66
+
67
+ Benchmark.ips do |x|
68
+ GC.start
69
+ x.report("CSV decision (first_match: #{first_match}) - #{name}: ") do |count|
70
+ count.times { csv_table.decide!(input_symbolized) }
71
+ end
72
+
73
+ GC.start
74
+ x.report("Rufus decision (first_match: #{first_match}) - #{name}: ") do |count|
75
+ count.times { rufus_table.transform!(input) }
76
+ end
77
+
78
+ x.compare!
79
+ end
80
+ end
81
+ end
82
+
83
+ puts ""
84
+ puts "Benchmarking Memory"
85
+ puts '=' * tag_width
86
+ puts ""
87
+
88
+ def benchmark_memory(test, quiet: false)
89
+ name = test[:name]
90
+ data = Pathname(File.join(SPEC_DATA_VALID, test[:data]))
91
+ file_name = data.to_s
92
+
93
+ rufus_tables = {}
94
+ csv_tables = {}
95
+ key = File.basename(file_name, '.csv').to_sym
96
+
97
+ Benchmark.memory(quiet: quiet) do |x|
98
+ GC.start
99
+ x.report("Rufus new table - #{name} ") do
100
+ rufus_tables[key] = Rufus::Decision::Table.new(file_name, RUFUS_OPTIONS)
101
+ end
102
+
103
+ GC.start
104
+ x.report("CSV Decision new table - #{name} ") do
105
+ csv_tables[key] = CSVDecision.parse(data, CSV_OPTIONS)
106
+ end
107
+
108
+ x.compare!
109
+ end
110
+ end
111
+
112
+ # Warmup
113
+ benchmarks.each { |test| benchmark_memory(test, quiet: true) }
114
+
115
+ # Run the test
116
+ benchmarks.each { |test| benchmark_memory(test, quiet: false) }
117
+
118
+ puts ""
119
+ puts "Benchmarking Table Loads per Second"
120
+ puts '=' * tag_width
121
+ puts ""
122
+
123
+ benchmarks.each do |test|
124
+ name = test[:name]
125
+ data = Pathname(File.join(SPEC_DATA_VALID, test[:data]))
126
+ file_name = data.to_s
127
+
128
+ Benchmark.ips do |x|
129
+ GC.start
130
+ x.report("CSV new table - #{name}: ") do |count|
131
+ count.times { CSVDecision.parse(data) }
132
+ end
133
+
134
+ GC.start
135
+ x.report("Rufus new table - #{name}: ") do |count|
136
+ count.times { Rufus::Decision::Table.new(file_name, RUFUS_OPTIONS) }
137
+ end
138
+
139
+ x.compare!
140
+ end
141
+ end
142
+
143
+
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.0.1'
8
+ spec.version = '0.0.2'
9
9
  spec.authors = ['Brett Vickers']
10
10
  spec.email = ['brett@phillips-vickers.com']
11
11
  spec.description = 'CSV based Ruby decision tables.'
@@ -18,17 +18,19 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ['lib']
20
20
 
21
- spec.required_ruby_version = '>= 2.3.1'
21
+ spec.required_ruby_version = '>= 2.3.0'
22
22
 
23
- spec.add_dependency 'activesupport', '~> 5.0'
23
+ spec.add_dependency 'activesupport', '~> 5.1'
24
+ spec.add_dependency 'ice_nine', '~> 0.11'
25
+ spec.add_dependency 'values', '~> 1.8'
24
26
 
25
27
  spec.add_development_dependency 'benchmark-ips', '~> 2.7'
26
28
  spec.add_development_dependency 'benchmark-memory', '~> 0.1'
27
29
  spec.add_development_dependency 'bundler', '~> 1.3'
28
30
  spec.add_development_dependency 'oj', '~> 3.3'
29
31
  spec.add_development_dependency 'rake', '~> 12.3'
30
- spec.add_development_dependency 'rspec', '~> 3.5'
31
- spec.add_development_dependency 'rubocop', '~> 0.51'
32
+ spec.add_development_dependency 'rspec', '~> 3.7'
33
+ spec.add_development_dependency 'rubocop', '~> 0.52'
32
34
  spec.add_development_dependency 'rufus-decision', '~> 1.3'
33
- spec.add_development_dependency 'simplecov', '~> 0.12'
35
+ spec.add_development_dependency 'simplecov', '~> 0.15'
34
36
  end
data/lib/csv_decision.rb CHANGED
@@ -1,12 +1,11 @@
1
1
  # frozen_string_literal: true\
2
2
 
3
+ require 'active_support/core_ext/object'
4
+ require 'csv_decision/parse'
5
+
3
6
  # CSV Decision: CSV based Ruby decision tables.
4
7
  # Created December 2017 by Brett Vickers
5
8
  # See LICENSE and README.md for details.
6
-
7
- require 'active_support/core_ext/object'
8
- require_relative '../lib/csv_decision/table'
9
-
10
9
  module CSVDecision
11
10
  # @return [String] gem project's root directory
12
11
  def self.root
@@ -14,6 +13,21 @@ module CSVDecision
14
13
  end
15
14
 
16
15
  autoload :Data, 'csv_decision/data'
16
+ autoload :Decide, 'csv_decision/decide'
17
+ autoload :Decision, 'csv_decision/decision'
18
+ autoload :Columns, 'csv_decision/columns'
19
+ autoload :Header, 'csv_decision/header'
20
+ autoload :Input, 'csv_decision/input'
21
+ autoload :Load, 'csv_decision/load'
22
+ autoload :Matchers, 'csv_decision/matchers'
17
23
  autoload :Options, 'csv_decision/options'
18
24
  autoload :Parse, 'csv_decision/parse'
25
+ autoload :ScanRow, 'csv_decision/scan_row'
26
+ autoload :Table, 'csv_decision/table'
27
+
28
+ module Matchers
29
+ autoload :Numeric, 'csv_decision/matchers/numeric'
30
+ autoload :Pattern, 'csv_decision/matchers/pattern'
31
+ autoload :Range, 'csv_decision/matchers/range'
32
+ end
19
33
  end