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