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
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../lib/csv_decision'
4
+
5
+ describe CSVDecision::Load do
6
+ path = Pathname(File.join(CSVDecision.root, 'spec/data/valid'))
7
+
8
+ it "loads all valid CSV files in the directory" do
9
+ tables = CSVDecision.load(path, first_match: false, regexp_implicit: true)
10
+ expect(tables).to be_a Hash
11
+ expect(tables.count).to eq Dir[path.join('*.csv')].count
12
+
13
+ tables.each_pair do |name, table|
14
+ expect(name).to be_a(Symbol)
15
+ expect(table).to be_a(CSVDecision::Table)
16
+ end
17
+ end
18
+
19
+ it 'rejects an invalid path name' do
20
+ expect { CSVDecision.load('path') }
21
+ .to raise_error(ArgumentError, 'path argument must be a Pathname')
22
+ end
23
+
24
+ it 'rejects an invalid folder name' do
25
+ expect { CSVDecision.load(Pathname('path')) }
26
+ .to raise_error(ArgumentError, 'path argument not a valid folder')
27
+ end
28
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../lib/csv_decision'
4
+
5
+ describe CSVDecision::Matchers::Numeric do
6
+ subject { described_class.new }
7
+
8
+ describe '#new' do
9
+ it { is_expected.to be_a CSVDecision::Matchers::Numeric }
10
+ it { is_expected.to respond_to(:matches?).with(1).argument }
11
+ end
12
+
13
+ context 'cell value recognition' do
14
+ ranges = {
15
+ '> -1' => { comparator: '>', value: '-1' },
16
+ '>= 10.0' => { comparator: '>=', value: '10.0' },
17
+ '< .0' => { comparator: '<', value: '.0' },
18
+ '<= +1' => { comparator: '<=', value: '+1' },
19
+ '!= 0.0' => { comparator: '!=', value: '0.0' },
20
+ }
21
+ ranges.each_pair do |cell, expected|
22
+ it "recognises #{cell} as a comparision" do
23
+ match = described_class::COMPARISON.match(cell)
24
+ expect(match['comparator']).to eq expected[:comparator]
25
+ expect(match['value']).to eq expected[:value]
26
+ end
27
+ end
28
+ end
29
+
30
+ describe '#matches?' do
31
+ matcher = described_class.new
32
+
33
+ context 'comparison matches value' do
34
+ data = [
35
+ ['< 1', 0],
36
+ ['< 1', '0'],
37
+ ['> 1', 5],
38
+ ['> 1', '5'],
39
+ ['>= 1.1', BigDecimal.new('1.1')],
40
+ ['<=-1.1', BigDecimal.new('-12')]
41
+ ]
42
+
43
+ data.each do |cell, value|
44
+ it "comparision #{cell} matches #{value}" do
45
+ proc = matcher.matches?(cell)
46
+ expect(proc).to be_a(CSVDecision::Proc)
47
+ expect(proc.type).to eq :proc
48
+ expect(proc.function[value]).to eq true
49
+ end
50
+ end
51
+ end
52
+ #
53
+ # context 'range does not match value' do
54
+ # data = [
55
+ # [ '-1..+4', 5],
56
+ # ['!-1..+4', 2],
57
+ # %w[a...z z],
58
+ # %w[!a..z m],
59
+ # %w[-1..1 1.1],
60
+ # ['-1..1', BigDecimal.new('1.1')],
61
+ # ['-1..1', BigDecimal.new('1.1')]
62
+ # ]
63
+ #
64
+ # data.each do |cell, value|
65
+ # it "range #{cell} does not match #{value}" do
66
+ # proc = matcher.matches?(cell)
67
+ # expect(proc).to be_a(CSVDecision::Proc)
68
+ # expect(proc.type).to eq :proc
69
+ # expect(proc.function[value]).to eq false
70
+ # end
71
+ # end
72
+ # end
73
+ #
74
+ context 'does not match a numeric comparision' do
75
+ data = ['1', ':column', ':= nil', ':= true', 'abc', 'abc.*def', '-1..1', '0...3']
76
+
77
+ data.each do |cell|
78
+ it "cell #{cell} is not a comparision}" do
79
+ expect(matcher.matches?(cell)).to eq false
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../lib/csv_decision'
4
+
5
+ describe CSVDecision::Matchers::Pattern do
6
+ subject { described_class.new }
7
+
8
+ describe '#new' do
9
+ it { is_expected.to be_a CSVDecision::Matchers::Pattern }
10
+ it { is_expected.to respond_to(:matches?).with(1).argument }
11
+ end
12
+
13
+ describe '#matches?' do
14
+ context 'recognises regular expressions with implicit option' do
15
+ matcher = described_class.new(regexp_implicit: true)
16
+
17
+ expressions = [
18
+ '!~Jerk',
19
+ '!=Jerk',
20
+ '=~ Jerk.+',
21
+ 'a.+c',
22
+ '=~AB|BC|CD',
23
+ 'TRP\.CRD',
24
+ '=~'
25
+ ]
26
+
27
+ expressions.each do |cell|
28
+ it "recognises regexp #{cell}" do
29
+ proc = matcher.matches?(cell)
30
+ expect(proc).to be_a CSVDecision::Proc
31
+ expect(proc.type).to eq :proc
32
+ expect(proc.function).to be_a ::Proc
33
+ expect(proc.function.arity).to eq -1
34
+ end
35
+ end
36
+ end
37
+
38
+ context 'recognises regular expressions with explicit option' do
39
+ matcher = described_class.new(regexp_implicit: true)
40
+
41
+ expressions = [
42
+ '!~Jerk',
43
+ '!=Jerk',
44
+ '=~ Jerk.+',
45
+ '=~a.+c',
46
+ '=~AB|BC|CD',
47
+ '=~ TRP\.CRD'
48
+ ]
49
+
50
+ expressions.each do |cell|
51
+ it "recognises regexp #{cell}" do
52
+ proc = matcher.matches?(cell)
53
+ expect(proc).to be_a CSVDecision::Proc
54
+ expect(proc.type).to eq :proc
55
+ expect(proc.function).to be_a ::Proc
56
+ expect(proc.function.arity).to eq -1
57
+ end
58
+ end
59
+ end
60
+
61
+ context 'matches regular expressions with implicit option' do
62
+ matcher = described_class.new(regexp_implicit: true)
63
+
64
+ expressions = [
65
+ %w[!~Jerk Jerks],
66
+ %w[!=Jerk Jerks],
67
+ ['=~ Jerk.+', 'Jerks'],
68
+ %w[a.+c abc],
69
+ %w[=~AB|BC|CD CD],
70
+ %w[TRP\.CRD TRP.CRD],
71
+ %w[=~ =~]
72
+ ]
73
+
74
+ expressions.each do |cell, value|
75
+ it "matches regexp #{cell} to value #{value}" do
76
+ proc = matcher.matches?(cell)
77
+ expect(proc.function[value]).to be_truthy
78
+ end
79
+ end
80
+ end
81
+
82
+ context 'does not match regular expressions with implicit option' do
83
+ matcher = described_class.new(regexp_implicit: true)
84
+
85
+ expressions = [
86
+ %w[!~Jerk Jerk],
87
+ %w[!=Jerk Jerk],
88
+ ['=~ Jerk.+', 'Jerk'],
89
+ %w[a.+c abcd],
90
+ %w[=~AB|BC|CD C],
91
+ %w[TRP\.CRD TRPxCRD]
92
+ ]
93
+
94
+ expressions.each do |cell, value|
95
+ it "matches regexp #{cell} to value #{value}" do
96
+ proc = matcher.matches?(cell)
97
+ expect(proc.function[value]).to be_falsey
98
+ end
99
+ end
100
+ end
101
+
102
+ context 'matches regular expressions with explicit option' do
103
+ matcher = described_class.new(regexp_implicit: false)
104
+
105
+ expressions = [
106
+ %w[!~Jerk Jerks],
107
+ %w[!=Jerk Jerks],
108
+ ['=~ Jerk.+', 'Jerks'],
109
+ %w[=~a.+c abc],
110
+ %w[=~AB|BC|CD CD],
111
+ %w[=~TRP\.CRD TRP.CRD]
112
+ ]
113
+
114
+ expressions.each do |cell, value|
115
+ it "matches regexp #{cell} to value #{value}" do
116
+ proc = matcher.matches?(cell)
117
+ expect(proc.function[value]).to be_truthy
118
+ end
119
+ end
120
+ end
121
+
122
+ context 'does not match regular expressions with explicit option' do
123
+ matcher = described_class.new(regexp_implicit: false)
124
+
125
+ expressions = [
126
+ %w[!~Jerk Jerk],
127
+ %w[!=Jerk Jerk],
128
+ ['=~ Jerk.+', 'Jerk'],
129
+ %w[=~a.+c abcd],
130
+ %w[=~AB|BC|CD C],
131
+ %w[=~TRP\.CRD TRPxCRD]
132
+ ]
133
+
134
+ expressions.each do |cell, value|
135
+ it "matches regexp #{cell} to value #{value}" do
136
+ proc = matcher.matches?(cell)
137
+ expect(proc.function[value]).to be_falsey
138
+ end
139
+ end
140
+ end
141
+
142
+ context 'does not recognise non-regular expressions with implicit option' do
143
+ matcher = described_class.new(regexp_implicit: true)
144
+
145
+ expressions = [
146
+ 'Jerk',
147
+ ':Jerk',
148
+ '=~:Jerk',
149
+ ':= nil',
150
+ ':= 100.0'
151
+ ]
152
+
153
+ expressions.each do |cell|
154
+ it "does not match string #{cell}" do
155
+ proc = matcher.matches?(cell)
156
+ expect(proc).to be_falsey
157
+ end
158
+ end
159
+ end
160
+
161
+ context 'does not recognise non-regular expressions with explicit option' do
162
+ matcher = described_class.new(regexp_implicit: false)
163
+
164
+ expressions = [
165
+ 'Jerk',
166
+ ':Jerk',
167
+ '=~:Jerk',
168
+ 'a.+c',
169
+ '*.OPT.*',
170
+ ':= nil',
171
+ ':= 100.0',
172
+ '=~'
173
+ ]
174
+
175
+ expressions.each do |cell|
176
+ it "does not match string #{cell}" do
177
+ proc = matcher.matches?(cell)
178
+ expect(proc).to be_falsey
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../lib/csv_decision'
4
+
5
+ describe CSVDecision::Matchers::Range do
6
+ subject { described_class.new }
7
+
8
+ describe '#new' do
9
+ it { is_expected.to be_a CSVDecision::Matchers::Range }
10
+ it { is_expected.to respond_to(:matches?).with(1).argument }
11
+ end
12
+
13
+ context 'cell value matching' do
14
+ ranges = {
15
+ '-1..1' => { min: '-1', type: '..', max: '1', negate: '' },
16
+ '! -1..1' => { min: '-1', type: '..', max: '1', negate: '!' },
17
+ '!-1.0..1.1' => { min: '-1.0', type: '..', max: '1.1', negate: '!' },
18
+ '!-1.0...1.1' => { min: '-1.0', type: '...', max: '1.1', negate: '!' }
19
+ }
20
+ ranges.each_pair do |range, expected|
21
+ it "matches #{range} as a numeric range" do
22
+ match = described_class::NUMERIC_RANGE.match(range)
23
+ expect(match['min']).to eq expected[:min]
24
+ expect(match['max']).to eq expected[:max]
25
+ expect(match['type']).to eq expected[:type]
26
+ expect(match['negate']).to eq expected[:negate]
27
+ end
28
+ end
29
+
30
+ ranges = {
31
+ 'a..z' => { min: 'a', type: '..', max: 'z', negate: '' },
32
+ '!1...9' => { min: '1', type: '...', max: '9', negate: '!' },
33
+ }
34
+ ranges.each_pair do |range, expected|
35
+ it "matches #{range} as an alphanumeric range" do
36
+ match = described_class::ALNUM_RANGE.match(range)
37
+ expect(match['min']).to eq expected[:min]
38
+ expect(match['max']).to eq expected[:max]
39
+ expect(match['type']).to eq expected[:type]
40
+ expect(match['negate']).to eq expected[:negate]
41
+ end
42
+ end
43
+ end
44
+
45
+ describe '#range' do
46
+ it 'constructs various numeric ranges' do
47
+ ranges = {
48
+ '-1..1' => { range: -1..1, negate: false },
49
+ '! -1..1' => { range: -1..1, negate: true },
50
+ '!-1.0..1.1' => { range: BigDecimal.new('-1.0')..BigDecimal.new('1.1'), negate: true },
51
+ '!-1.0...1' => { range: BigDecimal.new('-1.0')...1, negate: true }
52
+ }
53
+ ranges.each_pair do |cell, expected|
54
+ match = described_class::NUMERIC_RANGE.match(cell)
55
+ negate, range = CSVDecision::Matchers::Range.range(match, coerce: :to_numeric)
56
+ expect(negate).to eq expected[:negate]
57
+ expect(range).to eq expected[:range]
58
+ end
59
+ end
60
+
61
+ it 'constructs various alphanumeric ranges' do
62
+ ranges = {
63
+ 'a..z' => { range: 'a'..'z', negate: false },
64
+ '!1...9' => { range: '1'...'9', negate: true },
65
+ }
66
+ ranges.each_pair do |cell, expected|
67
+ match = described_class::ALNUM_RANGE.match(cell)
68
+ negate, range = described_class.range(match)
69
+ expect(negate).to eq expected[:negate]
70
+ expect(range).to eq expected[:range]
71
+ end
72
+ end
73
+ end
74
+
75
+ describe '#matches?' do
76
+ matcher = described_class.new
77
+
78
+ context 'range matches value' do
79
+ data = [
80
+ [ '-1..+4', 0],
81
+ ['!-1..+4', 5],
82
+ ['1.1...4', 3],
83
+ %w[a..z a],
84
+ %w[a..z z],
85
+ %w[a..z m],
86
+ %w[!-1..1 1.1],
87
+ ['! -1..1', BigDecimal.new('1.1')],
88
+ [ '-1..1', BigDecimal.new('1')]
89
+ ]
90
+
91
+ data.each do |cell, value|
92
+ it "range #{cell} matches #{value}" do
93
+ proc = matcher.matches?(cell)
94
+ expect(proc).to be_a(CSVDecision::Proc)
95
+ expect(proc.type).to eq :proc
96
+ expect(proc.function[value]).to eq true
97
+ end
98
+ end
99
+ end
100
+
101
+ context 'range does not match value' do
102
+ data = [
103
+ [ '-1..+4', 5],
104
+ ['!-1..+4', 2],
105
+ %w[a...z z],
106
+ %w[!a..z m],
107
+ %w[-1..1 1.1],
108
+ ['-1..1', BigDecimal.new('1.1')],
109
+ ['-1..1', BigDecimal.new('1.1')]
110
+ ]
111
+
112
+ data.each do |cell, value|
113
+ it "range #{cell} does not match #{value}" do
114
+ proc = matcher.matches?(cell)
115
+ expect(proc).to be_a(CSVDecision::Proc)
116
+ expect(proc.type).to eq :proc
117
+ expect(proc.function[value]).to eq false
118
+ end
119
+ end
120
+ end
121
+
122
+ context 'does not match a range' do
123
+ data = ['1', ':column', ':= nil', ':= true', 'abc', 'abc.*def']
124
+
125
+ data.each do |cell|
126
+ it "cell #{cell} is not a range}" do
127
+ expect(matcher.matches?(cell)).to eq false
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../lib/csv_decision'
4
+
5
+ SPEC_DATA_VALID ||= File.join(CSVDecision.root, 'spec', 'data', 'valid')
6
+
7
+ describe CSVDecision::Options do
8
+ it 'sets the default options' do
9
+ data = <<~DATA
10
+ IN :input, OUT :output
11
+ input0, output0
12
+ DATA
13
+
14
+ result = CSVDecision.parse(data)
15
+
16
+ expected = {
17
+ first_match: true,
18
+ regexp_implicit: false,
19
+ text_only: false,
20
+ matchers: CSVDecision::DEFAULT_MATCHERS
21
+ }
22
+ expect(result.options).to eql expected
23
+ end
24
+
25
+ it 'overrides the default options' do
26
+ data = <<~DATA
27
+ IN :input, OUT :output
28
+ input0, output0
29
+ DATA
30
+
31
+ result = CSVDecision.parse(data, first_match: false)
32
+
33
+ expected = {
34
+ first_match: false,
35
+ regexp_implicit: false,
36
+ text_only: false,
37
+ matchers: CSVDecision::DEFAULT_MATCHERS
38
+ }
39
+ expect(result.options).to eql expected
40
+ end
41
+
42
+ it 'parses options from a CSV file' do
43
+ file = Pathname(File.join(SPEC_DATA_VALID, 'options_in_file1.csv'))
44
+ result = CSVDecision.parse(file)
45
+
46
+ expected = {
47
+ first_match: false,
48
+ regexp_implicit: false,
49
+ text_only: false,
50
+ matchers: CSVDecision::DEFAULT_MATCHERS
51
+ }
52
+ expect(result.options).to eql expected
53
+ end
54
+
55
+ it 'options from the CSV file override method options' do
56
+ file = Pathname(File.join(SPEC_DATA_VALID, 'options_in_file2.csv'))
57
+ result = CSVDecision.parse(file, first_match: true, regexp_implicit: nil)
58
+
59
+ expected = {
60
+ first_match: false,
61
+ regexp_implicit: true,
62
+ text_only: false,
63
+ matchers: CSVDecision::DEFAULT_MATCHERS
64
+ }
65
+ expect(result.options).to eql expected
66
+ end
67
+ end