csv_decision 0.0.3 → 0.0.4
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 +4 -4
- data/.codeclimate.yml +2 -0
- data/.gitignore +2 -1
- data/.travis.yml +2 -3
- data/CHANGELOG.md +19 -1
- data/README.md +49 -16
- data/{benchmark.rb → benchmarks/rufus_decision.rb} +1 -1
- data/csv_decision.gemspec +1 -1
- data/doc/CSVDecision/CellValidationError.html +143 -0
- data/doc/CSVDecision/Columns/Default.html +409 -0
- data/doc/CSVDecision/Columns/Dictionary.html +410 -0
- data/doc/CSVDecision/Columns/Entry.html +321 -0
- data/doc/CSVDecision/Columns.html +476 -0
- data/doc/CSVDecision/Constant.html +295 -0
- data/doc/CSVDecision/Data.html +344 -0
- data/doc/CSVDecision/Decide.html +434 -0
- data/doc/CSVDecision/Decision.html +604 -0
- data/doc/CSVDecision/Error.html +139 -0
- data/doc/CSVDecision/FileError.html +143 -0
- data/doc/CSVDecision/Function.html +229 -0
- data/doc/CSVDecision/Header.html +520 -0
- data/doc/CSVDecision/Input.html +305 -0
- data/doc/CSVDecision/Load.html +225 -0
- data/doc/CSVDecision/Matchers/Constant.html +242 -0
- data/doc/CSVDecision/Matchers/Function.html +342 -0
- data/doc/CSVDecision/Matchers/Matcher.html +325 -0
- data/doc/CSVDecision/Matchers/Numeric.html +277 -0
- data/doc/CSVDecision/Matchers/Pattern.html +600 -0
- data/doc/CSVDecision/Matchers/Range.html +413 -0
- data/doc/CSVDecision/Matchers/Symbol.html +280 -0
- data/doc/CSVDecision/Matchers.html +1529 -0
- data/doc/CSVDecision/Numeric.html +259 -0
- data/doc/CSVDecision/Options.html +445 -0
- data/doc/CSVDecision/Parse.html +270 -0
- data/doc/CSVDecision/ScanRow.html +746 -0
- data/doc/CSVDecision/Symbol.html +256 -0
- data/doc/CSVDecision/Table.html +1115 -0
- data/doc/CSVDecision.html +652 -0
- data/doc/_index.html +410 -0
- data/doc/class_list.html +51 -0
- data/doc/css/common.css +1 -0
- data/doc/css/full_list.css +58 -0
- data/doc/css/style.css +499 -0
- data/doc/file.README.html +264 -0
- data/doc/file_list.html +56 -0
- data/doc/frames.html +17 -0
- data/doc/index.html +264 -0
- data/doc/js/app.js +248 -0
- data/doc/js/full_list.js +216 -0
- data/doc/js/jquery.js +4 -0
- data/doc/method_list.html +683 -0
- data/doc/top-level-namespace.html +110 -0
- data/lib/csv_decision/columns.rb +15 -12
- data/lib/csv_decision/constant.rb +54 -0
- data/lib/csv_decision/decide.rb +5 -5
- data/lib/csv_decision/decision.rb +3 -1
- data/lib/csv_decision/function.rb +32 -0
- data/lib/csv_decision/header.rb +27 -18
- data/lib/csv_decision/input.rb +11 -8
- data/lib/csv_decision/matchers/constant.rb +18 -0
- data/lib/csv_decision/matchers/function.rb +11 -44
- data/lib/csv_decision/matchers/numeric.rb +5 -33
- data/lib/csv_decision/matchers/pattern.rb +26 -11
- data/lib/csv_decision/matchers/range.rb +21 -5
- data/lib/csv_decision/matchers/symbol.rb +20 -0
- data/lib/csv_decision/matchers.rb +85 -20
- data/lib/csv_decision/numeric.rb +38 -0
- data/lib/csv_decision/options.rb +36 -27
- data/lib/csv_decision/parse.rb +46 -39
- data/lib/csv_decision/scan_row.rb +19 -7
- data/lib/csv_decision/symbol.rb +73 -0
- data/lib/csv_decision/table.rb +24 -18
- data/lib/csv_decision.rb +25 -18
- data/spec/csv_decision/columns_spec.rb +1 -1
- data/spec/csv_decision/constant_spec.rb +60 -0
- data/spec/csv_decision/examples_spec.rb +119 -0
- data/spec/csv_decision/matchers/function_spec.rb +48 -28
- data/spec/csv_decision/matchers/numeric_spec.rb +4 -41
- data/spec/csv_decision/matchers/range_spec.rb +31 -61
- data/spec/csv_decision/matchers/symbol_spec.rb +65 -0
- data/spec/csv_decision/options_spec.rb +14 -2
- data/spec/csv_decision/parse_spec.rb +10 -0
- data/spec/csv_decision/table_spec.rb +112 -6
- data/spec/data/valid/simple_constants.csv +3 -3
- metadata +62 -7
- data/spec/csv_decision/simple_example_spec.rb +0 -75
- /data/spec/{csv_decision.rb → csv_decision_spec.rb} +0 -0
data/lib/csv_decision/table.rb
CHANGED
|
@@ -6,42 +6,47 @@
|
|
|
6
6
|
module CSVDecision
|
|
7
7
|
# Decision Table that accepts input hashes and makes decisions
|
|
8
8
|
class Table
|
|
9
|
-
# CSVDecision::Columns
|
|
9
|
+
# @return [CSVDecision::Columns] Dictionary of all input and output columns.
|
|
10
10
|
attr_accessor :columns
|
|
11
11
|
|
|
12
|
-
# File path name if decision table loaded from a CSV file
|
|
12
|
+
# @return [File, Pathname, nil] File path name if decision table was loaded from a CSV file.
|
|
13
13
|
attr_accessor :file
|
|
14
14
|
|
|
15
|
-
# All options used to parse the table
|
|
15
|
+
# @return [Hash] All options, explicitly set or defaulted, used to parse the table.
|
|
16
16
|
attr_accessor :options
|
|
17
17
|
|
|
18
|
-
# Set if the table has any output functions (planned feature)
|
|
19
|
-
attr_accessor :outs_functions
|
|
18
|
+
# Set if the table row has any output functions (planned feature)
|
|
19
|
+
# attr_accessor :outs_functions
|
|
20
20
|
|
|
21
|
-
# Data rows
|
|
21
|
+
# @return [Array<Array>] Data rows after parsing.
|
|
22
22
|
attr_accessor :rows
|
|
23
23
|
|
|
24
|
-
# Array
|
|
24
|
+
# @return [Array<CSVDecision::ScanRow>] Scanning objects used to implement input matching logic.
|
|
25
25
|
attr_accessor :scan_rows
|
|
26
26
|
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
# @return [Array<CSVDecision::ScanRow>] Used to implement outputting of final results.
|
|
28
|
+
attr_accessor :outs_rows
|
|
29
|
+
|
|
30
|
+
# @return Array<CSVDecision::Table>] pre-loaded tables passed to this decision table
|
|
31
|
+
# at load time. Used to allow this decision table to lookup values in other
|
|
32
|
+
# decision tables. (Planned feature.)
|
|
33
|
+
# attr_reader :tables
|
|
31
34
|
|
|
32
35
|
# Main public method for making decisions.
|
|
33
36
|
#
|
|
34
|
-
# @
|
|
35
|
-
# @
|
|
37
|
+
# @note Input hash keys may or may not be symbolized.
|
|
38
|
+
# @param input [Hash] Input hash.
|
|
39
|
+
# @return [Hash{Symbol => Object, Array<Object>}] Decision hash.
|
|
36
40
|
def decide(input)
|
|
37
41
|
Decide.decide(table: self, input: input, symbolize_keys: true).result
|
|
38
42
|
end
|
|
39
43
|
|
|
40
|
-
# Unsafe version of decide - will mutate the hash if set: column type
|
|
44
|
+
# Unsafe version of decide - will mutate the hash if +set: column+ type
|
|
41
45
|
# is used (planned feature).
|
|
42
46
|
#
|
|
43
|
-
# @param input
|
|
44
|
-
# @
|
|
47
|
+
# @param input (see #decide)
|
|
48
|
+
# @note Input hash must have its keys symbolized.
|
|
49
|
+
# @return (see #decide)
|
|
45
50
|
def decide!(input)
|
|
46
51
|
Decide.decide(table: self, input: input, symbolize_keys: false).result
|
|
47
52
|
end
|
|
@@ -49,8 +54,8 @@ module CSVDecision
|
|
|
49
54
|
# Iterate through all data rows of the decision table, with an optional
|
|
50
55
|
# first and last row index given.
|
|
51
56
|
#
|
|
52
|
-
# @param first [Integer]
|
|
53
|
-
# @param last [Integer, nil]
|
|
57
|
+
# @param first [Integer] Start row.
|
|
58
|
+
# @param last [Integer, nil] Last row.
|
|
54
59
|
def each(first = 0, last = @rows.count - 1)
|
|
55
60
|
index = first
|
|
56
61
|
while index <= (last || first)
|
|
@@ -66,6 +71,7 @@ module CSVDecision
|
|
|
66
71
|
@matchers = []
|
|
67
72
|
@options = nil
|
|
68
73
|
@outs_functions = nil
|
|
74
|
+
@outs_rows = []
|
|
69
75
|
@rows = []
|
|
70
76
|
@scan_rows = []
|
|
71
77
|
@tables = nil
|
data/lib/csv_decision.rb
CHANGED
|
@@ -4,7 +4,8 @@ require 'active_support/core_ext/object'
|
|
|
4
4
|
require 'csv_decision/parse'
|
|
5
5
|
|
|
6
6
|
# CSV Decision: CSV based Ruby decision tables.
|
|
7
|
-
# Created December 2017
|
|
7
|
+
# Created December 2017.
|
|
8
|
+
# @author Brett Vickers <brett@phillips-vickers.com>
|
|
8
9
|
# See LICENSE and README.md for details.
|
|
9
10
|
module CSVDecision
|
|
10
11
|
# @return [String] gem project's root directory
|
|
@@ -12,23 +13,29 @@ module CSVDecision
|
|
|
12
13
|
File.dirname __dir__
|
|
13
14
|
end
|
|
14
15
|
|
|
15
|
-
autoload :
|
|
16
|
-
autoload :
|
|
17
|
-
autoload :
|
|
18
|
-
autoload :
|
|
19
|
-
autoload :
|
|
20
|
-
autoload :
|
|
21
|
-
autoload :
|
|
22
|
-
autoload :
|
|
23
|
-
autoload :
|
|
24
|
-
autoload :
|
|
25
|
-
autoload :
|
|
26
|
-
autoload :
|
|
16
|
+
autoload :Constant, 'csv_decision/constant'
|
|
17
|
+
autoload :Data, 'csv_decision/data'
|
|
18
|
+
autoload :Decide, 'csv_decision/decide'
|
|
19
|
+
autoload :Decision, 'csv_decision/decision'
|
|
20
|
+
autoload :Columns, 'csv_decision/columns'
|
|
21
|
+
autoload :Function, 'csv_decision/function'
|
|
22
|
+
autoload :Header, 'csv_decision/header'
|
|
23
|
+
autoload :Input, 'csv_decision/input'
|
|
24
|
+
autoload :Load, 'csv_decision/load'
|
|
25
|
+
autoload :Matchers, 'csv_decision/matchers'
|
|
26
|
+
autoload :Numeric, 'csv_decision/numeric'
|
|
27
|
+
autoload :Options, 'csv_decision/options'
|
|
28
|
+
autoload :Parse, 'csv_decision/parse'
|
|
29
|
+
autoload :ScanRow, 'csv_decision/scan_row'
|
|
30
|
+
autoload :Symbol, 'csv_decision/symbol'
|
|
31
|
+
autoload :Table, 'csv_decision/table'
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
autoload :
|
|
30
|
-
autoload :
|
|
31
|
-
autoload :
|
|
32
|
-
autoload :
|
|
33
|
+
class Matchers
|
|
34
|
+
autoload :Constant, 'csv_decision/matchers/constant'
|
|
35
|
+
autoload :Function, 'csv_decision/matchers/function'
|
|
36
|
+
autoload :Numeric, 'csv_decision/matchers/numeric'
|
|
37
|
+
autoload :Pattern, 'csv_decision/matchers/pattern'
|
|
38
|
+
autoload :Range, 'csv_decision/matchers/range'
|
|
39
|
+
autoload :Symbol, 'csv_decision/matchers/symbol'
|
|
33
40
|
end
|
|
34
41
|
end
|
|
@@ -74,7 +74,7 @@ describe CSVDecision::Columns do
|
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
context 'rejects invalid CSV decision table columns' do
|
|
77
|
-
Dir[File.join(SPEC_DATA_INVALID, '
|
|
77
|
+
Dir[File.join(SPEC_DATA_INVALID, 'invalid_header*.csv')].each do |file_name|
|
|
78
78
|
pathname = Pathname(file_name)
|
|
79
79
|
|
|
80
80
|
it "rejects CSV file #{pathname.basename}" do
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../lib/csv_decision'
|
|
4
|
+
|
|
5
|
+
describe CSVDecision::Constant do
|
|
6
|
+
context 'cell string value recognition' do
|
|
7
|
+
cells = {
|
|
8
|
+
':= 0.0' => { operator: ':=', value: '0.0' },
|
|
9
|
+
':= 0.' => { operator: ':=', value: '0.' },
|
|
10
|
+
':= .0' => { operator: ':=', value: '.0' },
|
|
11
|
+
'==0.0' => { operator: '==', value: '0.0' },
|
|
12
|
+
'== 0.' => { operator: '==', value: '0.' },
|
|
13
|
+
'==.0' => { operator: '==', value: '.0' },
|
|
14
|
+
'=0.0' => { operator: '=', value: '0.0' },
|
|
15
|
+
'= 0.' => { operator: '=', value: '0.' },
|
|
16
|
+
'=.0' => { operator: '=', value: '.0' },
|
|
17
|
+
'= nil' => { operator: '=', value: 'nil' },
|
|
18
|
+
':= nil' => { operator: ':=', value: 'nil' },
|
|
19
|
+
'==nil' => { operator: '==', value: 'nil' }
|
|
20
|
+
}
|
|
21
|
+
cells.each_pair do |cell, expected|
|
|
22
|
+
it "recognises #{cell} as a constant" do
|
|
23
|
+
match = described_class::EXPRESSION.match(cell)
|
|
24
|
+
expect(match['operator']).to eq expected[:operator]
|
|
25
|
+
expect(match['value']).to eq expected[:value]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe '#matches?' do
|
|
31
|
+
context 'constant matches value' do
|
|
32
|
+
data = [
|
|
33
|
+
['= 1', 1],
|
|
34
|
+
['== 1', 1],
|
|
35
|
+
[':=1', 1],
|
|
36
|
+
['==.1', BigDecimal('0.1')],
|
|
37
|
+
[':= 1.1', BigDecimal('1.1')]
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
data.each do |cell, value|
|
|
41
|
+
it "constant #{cell} matches #{value}" do
|
|
42
|
+
proc = described_class.matches?(cell)
|
|
43
|
+
expect(proc).to be_a(CSVDecision::Proc)
|
|
44
|
+
expect(proc.type).to eq :constant
|
|
45
|
+
expect(proc.function).to eq value
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
context 'does not match strings and non-constants' do
|
|
51
|
+
data = ['true', 'nil', 'false', ':column', '> 0', '!= 1.0', 'abc.*def', '-1..1', '0...3']
|
|
52
|
+
|
|
53
|
+
data.each do |cell|
|
|
54
|
+
it "cell #{cell} is not a non-string constant}" do
|
|
55
|
+
expect(described_class.matches?(cell)).to eq false
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../lib/csv_decision'
|
|
4
|
+
|
|
5
|
+
context 'simple examples' do
|
|
6
|
+
context 'simple example - strings-only' do
|
|
7
|
+
data = <<~DATA
|
|
8
|
+
in :topic, in :region, out :team_member
|
|
9
|
+
sports, Europe, Alice
|
|
10
|
+
sports, , Bob
|
|
11
|
+
finance, America, Charlie
|
|
12
|
+
finance, Europe, Donald
|
|
13
|
+
finance, , Ernest
|
|
14
|
+
politics, Asia, Fujio
|
|
15
|
+
politics, America, Gilbert
|
|
16
|
+
politics, , Henry
|
|
17
|
+
, , Zach
|
|
18
|
+
DATA
|
|
19
|
+
|
|
20
|
+
it 'makes correct decisions for CSV string' do
|
|
21
|
+
table = CSVDecision.parse(data)
|
|
22
|
+
|
|
23
|
+
result = table.decide(topic: 'finance', region: 'Europe')
|
|
24
|
+
expect(result).to eq(team_member: 'Donald')
|
|
25
|
+
|
|
26
|
+
result = table.decide(topic: 'sports', region: nil)
|
|
27
|
+
expect(result).to eq(team_member: 'Bob')
|
|
28
|
+
|
|
29
|
+
result = table.decide(topic: 'culture', region: 'America')
|
|
30
|
+
expect(result).to eq(team_member: 'Zach')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'makes correct decisions for CSV file' do
|
|
34
|
+
table = CSVDecision.parse(Pathname('spec/data/valid/simple_example.csv'))
|
|
35
|
+
|
|
36
|
+
result = table.decide(topic: 'finance', region: 'Europe')
|
|
37
|
+
expect(result).to eq(team_member: 'Donald')
|
|
38
|
+
|
|
39
|
+
result = table.decide(topic: 'sports', region: nil)
|
|
40
|
+
expect(result).to eq(team_member: 'Bob')
|
|
41
|
+
|
|
42
|
+
result = table.decide(topic: 'culture', region: 'America')
|
|
43
|
+
expect(result).to eq(team_member: 'Zach')
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
context 'simple example - constants' do
|
|
48
|
+
data = <<~DATA
|
|
49
|
+
in :constant, out :value
|
|
50
|
+
:=nil, :=nil
|
|
51
|
+
==false, ==false
|
|
52
|
+
=true, =true
|
|
53
|
+
= 0, = 0
|
|
54
|
+
:=100.0, :=100.0
|
|
55
|
+
DATA
|
|
56
|
+
|
|
57
|
+
it 'makes correct decisions for CSV string' do
|
|
58
|
+
table = CSVDecision.parse(data)
|
|
59
|
+
|
|
60
|
+
result = table.decide(constant: nil)
|
|
61
|
+
expect(result).to eq(value: nil)
|
|
62
|
+
|
|
63
|
+
result = table.decide(constant: true)
|
|
64
|
+
expect(result).to eq(value: true)
|
|
65
|
+
|
|
66
|
+
result = table.decide(constant: false)
|
|
67
|
+
expect(result).to eq(value: false)
|
|
68
|
+
|
|
69
|
+
result = table.decide(constant: 0)
|
|
70
|
+
expect(result).to eq(value: 0)
|
|
71
|
+
|
|
72
|
+
result = table.decide(constant: BigDecimal('100.0'))
|
|
73
|
+
expect(result).to eq(value: BigDecimal('100.0'))
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
context 'simple example - symbols' do
|
|
78
|
+
data = <<~DATA
|
|
79
|
+
in :node, in :parent, out :top?
|
|
80
|
+
, ==:node, yes
|
|
81
|
+
, , no
|
|
82
|
+
DATA
|
|
83
|
+
|
|
84
|
+
it 'makes correct decisions' do
|
|
85
|
+
table = CSVDecision.parse(data)
|
|
86
|
+
|
|
87
|
+
result = table.decide(node: 0, parent: 0)
|
|
88
|
+
expect(result).to eq(top?: 'yes')
|
|
89
|
+
|
|
90
|
+
result = table.decide(node: 1, parent: 0)
|
|
91
|
+
expect(result).to eq(top?: 'no')
|
|
92
|
+
|
|
93
|
+
result = table.decide(node: '0', parent: 0)
|
|
94
|
+
expect(result).to eq(top?: 'no')
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
context 'makes correct decision for table with symbol ordered compares' do
|
|
99
|
+
data = <<~DATA
|
|
100
|
+
in :traded, in :settled, out :status
|
|
101
|
+
, :traded, same day
|
|
102
|
+
, >:traded, pending
|
|
103
|
+
, <:traded, invalid trade
|
|
104
|
+
, , invalid data
|
|
105
|
+
DATA
|
|
106
|
+
|
|
107
|
+
it 'decides correctly' do
|
|
108
|
+
table = CSVDecision.parse(data)
|
|
109
|
+
|
|
110
|
+
expect(table.decide(traded: '20171227', settled: '20171227')).to eq(status: 'same day')
|
|
111
|
+
expect(table.decide(traded: 20171227, settled: 20171227 )).to eq(status: 'same day')
|
|
112
|
+
expect(table.decide(traded: '20171227', settled: '20171228')).to eq(status: 'pending')
|
|
113
|
+
expect(table.decide(traded: 20171227, settled: 20171228 )).to eq(status: 'pending')
|
|
114
|
+
expect(table.decide(traded: '20171228', settled: '20171227')).to eq(status: 'invalid trade')
|
|
115
|
+
expect(table.decide(traded: 20171228, settled: 20171227 )).to eq(status: 'invalid trade')
|
|
116
|
+
expect(table.decide(traded: '20171227', settled: 20171228 )).to eq(status: 'invalid data')
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -13,17 +13,17 @@ describe CSVDecision::Matchers::Function do
|
|
|
13
13
|
|
|
14
14
|
context 'cell value recognition' do
|
|
15
15
|
cells = {
|
|
16
|
-
':=
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
16
|
+
':=function' => { operator: ':=', name: 'function' },
|
|
17
|
+
':=function()' => { operator: ':=', name: 'function', args:'()' },
|
|
18
|
+
':=function(arg: value)' => { operator: ':=', name: 'function', args:'(arg: value)' },
|
|
19
|
+
':= !function(arg: value)' => { operator: ':=', negate: '!', name: 'function', args:'(arg: value)' },
|
|
20
|
+
'== ! func(arg: val)' => { operator: '==', negate: '!', name: 'func', args:'(arg: val)' },
|
|
21
21
|
}
|
|
22
22
|
cells.each_pair do |cell, expected|
|
|
23
|
-
it "recognises #{cell} as a
|
|
24
|
-
match =
|
|
23
|
+
it "recognises #{cell} as a function" do
|
|
24
|
+
match = CSVDecision::Function::FUNCTION_RE.match(cell)
|
|
25
25
|
expect(match['operator']).to eq expected[:operator]
|
|
26
|
-
expect(match['name']).to eq expected[:
|
|
26
|
+
expect(match['name']).to eq expected[:name]
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
end
|
|
@@ -31,29 +31,49 @@ describe CSVDecision::Matchers::Function do
|
|
|
31
31
|
describe '#matches?' do
|
|
32
32
|
matcher = described_class.new
|
|
33
33
|
|
|
34
|
-
context '
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
34
|
+
# context 'symbol expression matches value to hash data' do
|
|
35
|
+
# examples = [
|
|
36
|
+
# { cell: ':col', value: 0, hash: { col: 0 }, result: true },
|
|
37
|
+
# { cell: ':col', value: '0', hash: { col: '0' }, result: true },
|
|
38
|
+
# { cell: ':col', value: 0, hash: { col: '0' }, result: false },
|
|
39
|
+
# { cell: ':col', value: '0', hash: { col: 0 }, result: false },
|
|
40
|
+
# { cell: ':col', value: 1, hash: { col: 0 }, result: false },
|
|
41
|
+
# { cell: ':key', value: 0, hash: { col: 0 }, result: false },
|
|
42
|
+
# { cell: '!=:col', value: 0, hash: { col: 0 }, result: false },
|
|
43
|
+
# { cell: '!=:col', value: '0', hash: { col: '0' }, result: false },
|
|
44
|
+
# { cell: '!=:col', value: 0, hash: { col: '0' }, result: true },
|
|
45
|
+
# { cell: '!=:col', value: '0', hash: { col: 0 }, result: true },
|
|
46
|
+
# { cell: '!=:col', value: 1, hash: { col: 0 }, result: true },
|
|
47
|
+
# { cell: '!=:key', value: 0, hash: { col: 0 }, result: true },
|
|
48
|
+
# { cell: '>:col', value: 1, hash: { col: 0 }, result: true },
|
|
49
|
+
# { cell: '>:col', value: 0, hash: { col: 1 }, result: false },
|
|
50
|
+
# { cell: '<:col', value: 0, hash: { col: 1 }, result: true },
|
|
51
|
+
# { cell: '<:col', value: 1, hash: { col: 0 }, result: false },
|
|
52
|
+
# { cell: '= :col', value: 0, hash: { col: 0 }, result: true },
|
|
53
|
+
# { cell: '==:col', value: 0, hash: { col: 0 }, result: true },
|
|
54
|
+
# { cell: ':=:col', value: 0, hash: { col: 0 }, result: true },
|
|
55
|
+
# { cell: '= :col', value: '0', hash: { col: 0 }, result: false },
|
|
56
|
+
# { cell: '>=:col', value: 1, hash: { col: 0 }, result: true },
|
|
57
|
+
# { cell: '>=:col', value: 0, hash: { col: 1 }, result: false },
|
|
58
|
+
# { cell: '<=:col', value: 0, hash: { col: 1 }, result: true },
|
|
59
|
+
# { cell: '<=:col', value: 1, hash: { col: 0 }, result: false },
|
|
60
|
+
# { cell: '<=:col', value: '1', hash: { col: 1 }, result: false },
|
|
61
|
+
# ]
|
|
62
|
+
#
|
|
63
|
+
# examples.each do |ex|
|
|
64
|
+
# it "cell #{ex[:cell]} matches value: #{ex[:value]} to hash: #{ex[:hash]}" do
|
|
65
|
+
# proc = matcher.matches?(ex[:cell])
|
|
66
|
+
# expect(proc).to be_a(CSVDecision::Proc)
|
|
67
|
+
# expect(proc.function.call(ex[:value], ex[:hash])).to eq ex[:result]
|
|
68
|
+
# end
|
|
69
|
+
# end
|
|
70
|
+
# end
|
|
51
71
|
|
|
52
|
-
context 'does not match a function
|
|
53
|
-
data = ['1', '
|
|
72
|
+
context 'does not match a non-function string' do
|
|
73
|
+
data = ['1', 'abc', 'abc.*def', '-1..1', '0...3']
|
|
54
74
|
|
|
55
75
|
data.each do |cell|
|
|
56
|
-
it "cell #{cell} is not a
|
|
76
|
+
it "cell #{cell} is not a function" do
|
|
57
77
|
expect(matcher.matches?(cell)).to eq false
|
|
58
78
|
end
|
|
59
79
|
end
|
|
@@ -10,25 +10,6 @@ describe CSVDecision::Matchers::Numeric do
|
|
|
10
10
|
it { is_expected.to respond_to(:matches?).with(1).argument }
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
context 'cell value recognition' do
|
|
14
|
-
cells = {
|
|
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
|
-
':= 0.0' => { comparator: ':=', value: '0.0' },
|
|
21
|
-
'= 1.0' => { comparator: '=', value: '1.0' }
|
|
22
|
-
}
|
|
23
|
-
cells.each_pair do |cell, expected|
|
|
24
|
-
it "recognises #{cell} as a comparision" do
|
|
25
|
-
match = described_class::COMPARISON.match(cell)
|
|
26
|
-
expect(match['comparator']).to eq expected[:comparator]
|
|
27
|
-
expect(match['value']).to eq expected[:value]
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
13
|
describe '#matches?' do
|
|
33
14
|
matcher = described_class.new
|
|
34
15
|
|
|
@@ -39,8 +20,8 @@ describe CSVDecision::Matchers::Numeric do
|
|
|
39
20
|
['> 1', 5],
|
|
40
21
|
['!= 1', 0],
|
|
41
22
|
['> 1', '5'],
|
|
42
|
-
['>= 1.1', BigDecimal
|
|
43
|
-
['<=-1.1', BigDecimal
|
|
23
|
+
['>= 1.1', BigDecimal('1.1')],
|
|
24
|
+
['<=-1.1', BigDecimal('-12')]
|
|
44
25
|
]
|
|
45
26
|
|
|
46
27
|
data.each do |cell, value|
|
|
@@ -53,26 +34,8 @@ describe CSVDecision::Matchers::Numeric do
|
|
|
53
34
|
end
|
|
54
35
|
end
|
|
55
36
|
|
|
56
|
-
context 'numeric
|
|
57
|
-
data = [
|
|
58
|
-
['== 1', 1],
|
|
59
|
-
[':= 0', 0],
|
|
60
|
-
['==1.1', BigDecimal.new('1.1')],
|
|
61
|
-
['=-1.2', BigDecimal.new('-1.2')]
|
|
62
|
-
]
|
|
63
|
-
|
|
64
|
-
data.each do |cell, value|
|
|
65
|
-
it "constant expression #{cell} evaluates to #{value}" do
|
|
66
|
-
proc = matcher.matches?(cell)
|
|
67
|
-
expect(proc).to be_a(CSVDecision::Proc)
|
|
68
|
-
expect(proc.type).to eq :constant
|
|
69
|
-
expect(proc.function).to eq value
|
|
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']
|
|
37
|
+
context 'does not match non-numeric comparision' do
|
|
38
|
+
data = ['1', ':column', ':= nil', ':= true', ':= 0', 'abc', 'abc.*def', '-1..1', '0...3']
|
|
76
39
|
|
|
77
40
|
data.each do |cell|
|
|
78
41
|
it "cell #{cell} is not a comparision}" do
|
|
@@ -10,67 +10,37 @@ describe CSVDecision::Matchers::Range do
|
|
|
10
10
|
it { is_expected.to respond_to(:matches?).with(1).argument }
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
context 'cell value matching' do
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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
|
|
74
44
|
|
|
75
45
|
describe '#matches?' do
|
|
76
46
|
matcher = described_class.new
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../../lib/csv_decision'
|
|
4
|
+
|
|
5
|
+
describe CSVDecision::Matchers::Symbol do
|
|
6
|
+
subject { described_class.new }
|
|
7
|
+
|
|
8
|
+
describe '#new' do
|
|
9
|
+
it { is_expected.to be_a CSVDecision::Matchers::Symbol }
|
|
10
|
+
it { is_expected.to be_a CSVDecision::Matchers::Matcher }
|
|
11
|
+
it { is_expected.to respond_to(:matches?).with(1).argument }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe '#matches?' do
|
|
15
|
+
matcher = described_class.new
|
|
16
|
+
|
|
17
|
+
context 'symbol expression matches value to hash data' do
|
|
18
|
+
examples = [
|
|
19
|
+
{ cell: ':col', value: 0, hash: { col: 0 }, result: true },
|
|
20
|
+
{ cell: ':col', value: '0', hash: { col: '0' }, result: true },
|
|
21
|
+
{ cell: ':col', value: 0, hash: { col: '0' }, result: false },
|
|
22
|
+
{ cell: ':col', value: '0', hash: { col: 0 }, result: false },
|
|
23
|
+
{ cell: ':col', value: 1, hash: { col: 0 }, result: false },
|
|
24
|
+
{ cell: ':key', value: 0, hash: { col: 0 }, result: false },
|
|
25
|
+
{ cell: '!=:col', value: 0, hash: { col: 0 }, result: false },
|
|
26
|
+
{ cell: '!=:col', value: '0', hash: { col: '0' }, result: false },
|
|
27
|
+
{ cell: '!=:col', value: 0, hash: { col: '0' }, result: true },
|
|
28
|
+
{ cell: '!=:col', value: '0', hash: { col: 0 }, result: true },
|
|
29
|
+
{ cell: '!=:col', value: 1, hash: { col: 0 }, result: true },
|
|
30
|
+
{ cell: '!=:key', value: 0, hash: { col: 0 }, result: true },
|
|
31
|
+
{ cell: '>:col', value: 1, hash: { col: 0 }, result: true },
|
|
32
|
+
{ cell: '>:col', value: 0, hash: { col: 1 }, result: false },
|
|
33
|
+
{ cell: '<:col', value: 0, hash: { col: 1 }, result: true },
|
|
34
|
+
{ cell: '<:col', value: 1, hash: { col: 0 }, result: false },
|
|
35
|
+
{ cell: '= :col', value: 0, hash: { col: 0 }, result: true },
|
|
36
|
+
{ cell: '==:col', value: 0, hash: { col: 0 }, result: true },
|
|
37
|
+
{ cell: ':=:col', value: 0, hash: { col: 0 }, result: true },
|
|
38
|
+
{ cell: '= :col', value: '0', hash: { col: 0 }, result: false },
|
|
39
|
+
{ cell: '>=:col', value: 1, hash: { col: 0 }, result: true },
|
|
40
|
+
{ cell: '>=:col', value: 0, hash: { col: 1 }, result: false },
|
|
41
|
+
{ cell: '<=:col', value: 0, hash: { col: 1 }, result: true },
|
|
42
|
+
{ cell: '<=:col', value: 1, hash: { col: 0 }, result: false },
|
|
43
|
+
{ cell: '<=:col', value: '1', hash: { col: 1 }, result: false },
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
examples.each do |ex|
|
|
47
|
+
it "cell #{ex[:cell]} matches value: #{ex[:value]} to hash: #{ex[:hash]}" do
|
|
48
|
+
proc = matcher.matches?(ex[:cell])
|
|
49
|
+
expect(proc).to be_a(CSVDecision::Proc)
|
|
50
|
+
expect(proc.function.call(ex[:value], ex[:hash])).to eq ex[:result]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
context 'does not match a function' do
|
|
56
|
+
data = ['1', 'abc', 'abc.*def', '-1..1', '0...3', ':= false', ':= lookup?']
|
|
57
|
+
|
|
58
|
+
data.each do |cell|
|
|
59
|
+
it "cell #{cell} is not a function" do
|
|
60
|
+
expect(matcher.matches?(cell)).to eq false
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|