csv_decision 0.0.1 → 0.0.2

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