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
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>
|
|
7
|
+
Top Level Namespace
|
|
8
|
+
|
|
9
|
+
— Documentation by YARD 0.9.12
|
|
10
|
+
|
|
11
|
+
</title>
|
|
12
|
+
|
|
13
|
+
<link rel="stylesheet" href="css/style.css" type="text/css" charset="utf-8" />
|
|
14
|
+
|
|
15
|
+
<link rel="stylesheet" href="css/common.css" type="text/css" charset="utf-8" />
|
|
16
|
+
|
|
17
|
+
<script type="text/javascript" charset="utf-8">
|
|
18
|
+
pathId = "";
|
|
19
|
+
relpath = '';
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
<script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
|
|
24
|
+
|
|
25
|
+
<script type="text/javascript" charset="utf-8" src="js/app.js"></script>
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
</head>
|
|
29
|
+
<body>
|
|
30
|
+
<div class="nav_wrap">
|
|
31
|
+
<iframe id="nav" src="class_list.html?1"></iframe>
|
|
32
|
+
<div id="resizer"></div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div id="main" tabindex="-1">
|
|
36
|
+
<div id="header">
|
|
37
|
+
<div id="menu">
|
|
38
|
+
|
|
39
|
+
<a href="_index.html">Index</a> »
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
<span class="title">Top Level Namespace</span>
|
|
43
|
+
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div id="search">
|
|
47
|
+
|
|
48
|
+
<a class="full_list_link" id="class_list_link"
|
|
49
|
+
href="class_list.html">
|
|
50
|
+
|
|
51
|
+
<svg width="24" height="24">
|
|
52
|
+
<rect x="0" y="4" width="24" height="4" rx="1" ry="1"></rect>
|
|
53
|
+
<rect x="0" y="12" width="24" height="4" rx="1" ry="1"></rect>
|
|
54
|
+
<rect x="0" y="20" width="24" height="4" rx="1" ry="1"></rect>
|
|
55
|
+
</svg>
|
|
56
|
+
</a>
|
|
57
|
+
|
|
58
|
+
</div>
|
|
59
|
+
<div class="clear"></div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div id="content"><h1>Top Level Namespace
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
</h1>
|
|
67
|
+
<div class="box_info">
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<h2>Defined Under Namespace</h2>
|
|
82
|
+
<p class="children">
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
<strong class="modules">Modules:</strong> <span class='object_link'><a href="CSVDecision.html" title="CSVDecision (module)">CSVDecision</a></span>
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
</p>
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div id="footer">
|
|
103
|
+
Generated on Tue Dec 26 18:31:37 2017 by
|
|
104
|
+
<a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
|
|
105
|
+
0.9.12 (ruby-2.3.0).
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
</div>
|
|
109
|
+
</body>
|
|
110
|
+
</html>
|
data/lib/csv_decision/columns.rb
CHANGED
|
@@ -6,33 +6,35 @@
|
|
|
6
6
|
module CSVDecision
|
|
7
7
|
# Dictionary of all this table's columns - inputs, outputs etc.
|
|
8
8
|
class Columns
|
|
9
|
-
# Value object
|
|
9
|
+
# Value object to hold column dictionary entries.
|
|
10
10
|
Entry = Struct.new(:name, :text_only)
|
|
11
11
|
|
|
12
|
-
# Value object used for any columns with defaults
|
|
13
|
-
Default = Struct.new(:name, :function, :default_if)
|
|
12
|
+
# TODO: Value object used for any columns with defaults
|
|
13
|
+
# Default = Struct.new(:name, :function, :default_if)
|
|
14
14
|
|
|
15
15
|
# Dictionary of all data columns.
|
|
16
|
-
#
|
|
16
|
+
# The key of each hash is the header cell's array column index.
|
|
17
17
|
# Note that input and output columns can be interspersed and need not have unique names.
|
|
18
18
|
class Dictionary
|
|
19
|
-
# Input columns
|
|
19
|
+
# Input columns.
|
|
20
|
+
# @return [Hash{Integer=>Entry}] All input column dictionary entries.
|
|
20
21
|
attr_accessor :ins
|
|
21
22
|
|
|
22
|
-
# Output columns
|
|
23
|
+
# Output columns.
|
|
24
|
+
# @return [Hash{Integer=>Entry}] All output column dictionary entries.
|
|
23
25
|
attr_accessor :outs
|
|
24
26
|
|
|
25
|
-
# Input hash path - optional (planned feature)
|
|
26
|
-
attr_accessor :path
|
|
27
|
+
# TODO: Input hash path - optional (planned feature)
|
|
28
|
+
# attr_accessor :path
|
|
27
29
|
|
|
28
|
-
# Input columns with a default value (planned feature)
|
|
29
|
-
attr_accessor :defaults
|
|
30
|
+
# TODO: Input columns with a default value (planned feature)
|
|
31
|
+
# attr_accessor :defaults
|
|
30
32
|
|
|
31
33
|
def initialize
|
|
32
34
|
@ins = {}
|
|
33
35
|
@outs = {}
|
|
34
|
-
@path = {}
|
|
35
|
-
@defaults = {}
|
|
36
|
+
# TODO: @path = {}
|
|
37
|
+
# TODO: @defaults = {}
|
|
36
38
|
end
|
|
37
39
|
end
|
|
38
40
|
|
|
@@ -59,6 +61,7 @@ module CSVDecision
|
|
|
59
61
|
# @dictionary.path
|
|
60
62
|
# end
|
|
61
63
|
|
|
64
|
+
# @param table [Table] Decision table being constructed.
|
|
62
65
|
def initialize(table)
|
|
63
66
|
# If a column does not have a valid header cell, then it's empty of data.
|
|
64
67
|
# Return the stripped header row, and remove it from the data array.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# CSV Decision: CSV based Ruby decision tables.
|
|
4
|
+
# Created December 2017 by Brett Vickers
|
|
5
|
+
# See LICENSE and README.md for details.
|
|
6
|
+
module CSVDecision
|
|
7
|
+
# Recognise constant expressions in table data cells.
|
|
8
|
+
module Constant
|
|
9
|
+
# Cell constant expression specified by prefixing the value with one of the three equality symbols.
|
|
10
|
+
EXPRESSION = Matchers.regexp("(?<operator>#{Matchers::EQUALS})\\s*(?<value>\\S.*)")
|
|
11
|
+
|
|
12
|
+
# rubocop: disable Lint/BooleanSymbol
|
|
13
|
+
|
|
14
|
+
# Non-numeric constants recognised by CSV Decision.
|
|
15
|
+
NON_NUMERIC = {
|
|
16
|
+
nil: nil,
|
|
17
|
+
true: true,
|
|
18
|
+
false: false
|
|
19
|
+
}.freeze
|
|
20
|
+
# rubocop: enable Lint/BooleanSymbol
|
|
21
|
+
|
|
22
|
+
# @param (see Matchers::Matcher#matches?)
|
|
23
|
+
# @return (see Matchers::Matcher#matches?)
|
|
24
|
+
def self.matches?(cell)
|
|
25
|
+
return false unless (match = EXPRESSION.match(cell))
|
|
26
|
+
|
|
27
|
+
proc = non_numeric?(match)
|
|
28
|
+
return proc if proc
|
|
29
|
+
|
|
30
|
+
numeric?(match)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.proc(function:)
|
|
34
|
+
Proc.with(type: :constant, function: function)
|
|
35
|
+
end
|
|
36
|
+
private_class_method :proc
|
|
37
|
+
|
|
38
|
+
def self.numeric?(match)
|
|
39
|
+
value = Matchers.to_numeric(match['value'])
|
|
40
|
+
return false unless value
|
|
41
|
+
|
|
42
|
+
proc(function: value)
|
|
43
|
+
end
|
|
44
|
+
private_class_method :numeric?
|
|
45
|
+
|
|
46
|
+
def self.non_numeric?(match)
|
|
47
|
+
name = match['value'].to_sym
|
|
48
|
+
return false unless NON_NUMERIC.key?(name)
|
|
49
|
+
|
|
50
|
+
proc(function: NON_NUMERIC[name])
|
|
51
|
+
end
|
|
52
|
+
private_class_method :non_numeric?
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/csv_decision/decide.rb
CHANGED
|
@@ -8,11 +8,11 @@ module CSVDecision
|
|
|
8
8
|
module Decide
|
|
9
9
|
# Main method for making decisions.
|
|
10
10
|
#
|
|
11
|
-
# @param table [CSVDecision::Table]
|
|
12
|
-
# @param input [Hash]
|
|
13
|
-
# @param symbolize_keys [true, false]
|
|
11
|
+
# @param table [CSVDecision::Table] Decision table.
|
|
12
|
+
# @param input [Hash] Input hash (keys may or may not be symbolized)
|
|
13
|
+
# @param symbolize_keys [true, false] Set to false if keys are symbolized and it's
|
|
14
14
|
# OK to mutate the input hash. Otherwise a copy of the input hash is symbolized.
|
|
15
|
-
# @return [Hash]
|
|
15
|
+
# @return [Hash] Decision result.
|
|
16
16
|
def self.decide(table:, input:, symbolize_keys:)
|
|
17
17
|
# Parse and transform the hash supplied as input
|
|
18
18
|
parsed_input = Input.parse(table: table, input: input, symbolize_keys: symbolize_keys)
|
|
@@ -37,7 +37,7 @@ module CSVDecision
|
|
|
37
37
|
def self.eval_matcher(proc:, value:, hash:)
|
|
38
38
|
function = proc.function
|
|
39
39
|
|
|
40
|
-
# A symbol expression just needs to be passed the input hash
|
|
40
|
+
# A symbol guard expression just needs to be passed the input hash
|
|
41
41
|
return function[hash] if proc.type == :expression
|
|
42
42
|
|
|
43
43
|
# All other procs can take one or two args
|
|
@@ -12,7 +12,9 @@ module CSVDecision
|
|
|
12
12
|
# Relevant table attributes
|
|
13
13
|
@first_match = table.options[:first_match]
|
|
14
14
|
@outs = table.columns.outs
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
# TODO: Planned feature
|
|
17
|
+
# @outs_functions = table.outs_functions
|
|
16
18
|
|
|
17
19
|
# Partial result always includes the input hash for calculating output functions
|
|
18
20
|
@partial_result = input[:hash].dup if @outs_functions
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# CSV Decision: CSV based Ruby decision tables.
|
|
4
|
+
# Created December 2017 by Brett Vickers
|
|
5
|
+
# See LICENSE and README.md for details.
|
|
6
|
+
module CSVDecision
|
|
7
|
+
# Methods to recognise various function expressions
|
|
8
|
+
# TODO: fully implement
|
|
9
|
+
module Function
|
|
10
|
+
# Looks like a function call or symbol expressions, e.g.,
|
|
11
|
+
# == true
|
|
12
|
+
# := function(arg: symbol)
|
|
13
|
+
# == :column_name
|
|
14
|
+
FUNCTION_CALL =
|
|
15
|
+
"(?<operator>=|:=|==|=|<|>|!=|>=|<=|:|!\\s*:)\\s*" \
|
|
16
|
+
"(?<negate>!?)\\s*" \
|
|
17
|
+
"(?<name>#{Header::COLUMN_NAME}|:)(?<args>.*)"
|
|
18
|
+
private_constant :FUNCTION_CALL
|
|
19
|
+
|
|
20
|
+
FUNCTION_RE = Matchers.regexp(FUNCTION_CALL)
|
|
21
|
+
|
|
22
|
+
def self.matches?(cell)
|
|
23
|
+
match = FUNCTION_RE.match(cell)
|
|
24
|
+
return false unless match
|
|
25
|
+
|
|
26
|
+
# operator = match['operator']&.gsub(/\s+/, '')
|
|
27
|
+
# name = match['name'].to_sym
|
|
28
|
+
# args = match['args'].strip
|
|
29
|
+
# negate = match['negate'] == Matchers::NEGATE
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/csv_decision/header.rb
CHANGED
|
@@ -6,32 +6,41 @@
|
|
|
6
6
|
module CSVDecision
|
|
7
7
|
# Parse the CSV file's header row. These methods are only required at table load time.
|
|
8
8
|
module Header
|
|
9
|
-
#
|
|
9
|
+
# TODO: implement all column types
|
|
10
|
+
# COLUMN_TYPE = %r{
|
|
11
|
+
# \A(?<type>in|out|in/text|out/text|set|set/nil|set/blank|path|guard|if)
|
|
12
|
+
# \s*:\s*(?<name>\S?.*)\z
|
|
13
|
+
# }xi
|
|
14
|
+
|
|
15
|
+
# Column types recognise din the header row.
|
|
10
16
|
COLUMN_TYPE = %r{
|
|
11
|
-
\A(?<type>in|out|in/text|out/text
|
|
17
|
+
\A(?<type>in|out|in/text|out/text)
|
|
12
18
|
\s*:\s*(?<name>\S?.*)\z
|
|
13
19
|
}xi
|
|
14
20
|
|
|
15
21
|
# These column types do not need a name
|
|
16
|
-
|
|
22
|
+
# TODO: implement anonymous column types
|
|
23
|
+
# COLUMN_TYPE_ANONYMOUS = Set.new(%i[path if guard]).freeze
|
|
17
24
|
|
|
18
|
-
#
|
|
19
|
-
# any spaces will have been replaced with underscores
|
|
25
|
+
# Regular expression string for a column name.
|
|
26
|
+
# More lenient than a Ruby method name - note any spaces will have been replaced with underscores.
|
|
20
27
|
COLUMN_NAME = "\\w[\\w:/!?]*"
|
|
28
|
+
|
|
29
|
+
# Column name regular expression.
|
|
21
30
|
COLUMN_NAME_RE = Matchers.regexp(COLUMN_NAME)
|
|
22
31
|
|
|
23
|
-
#
|
|
32
|
+
# Check if the given row contains a recognisable header cell.
|
|
24
33
|
#
|
|
25
|
-
# @param row [Array<String>]
|
|
26
|
-
# @return [Boolean] true if the row looks like a header
|
|
34
|
+
# @param row [Array<String>] Header row.
|
|
35
|
+
# @return [Boolean] Return true if the row looks like a header.
|
|
27
36
|
def self.row?(row)
|
|
28
37
|
row.find { |cell| cell.match(COLUMN_TYPE) }
|
|
29
38
|
end
|
|
30
39
|
|
|
31
40
|
# Strip empty columns from all data rows.
|
|
32
41
|
#
|
|
33
|
-
# @param rows [Array<Array<String>>]
|
|
34
|
-
# @return [Array<Array<String>>]
|
|
42
|
+
# @param rows [Array<Array<String>>] Data rows.
|
|
43
|
+
# @return [Array<Array<String>>] Data array after removing any empty columns and the
|
|
35
44
|
# header row.
|
|
36
45
|
def self.strip_empty_columns(rows:)
|
|
37
46
|
empty_cols = empty_columns?(row: rows.first)
|
|
@@ -43,8 +52,8 @@ module CSVDecision
|
|
|
43
52
|
|
|
44
53
|
# Classify and build a dictionary of all input and output columns.
|
|
45
54
|
#
|
|
46
|
-
# @param row [Array<String>]
|
|
47
|
-
# @return [Hash<Hash>]
|
|
55
|
+
# @param row [Array<String>] The header row after removing any empty columns.
|
|
56
|
+
# @return [Hash<Hash>] Column dictionary is a hash of hashes.
|
|
48
57
|
def self.dictionary(row:)
|
|
49
58
|
dictionary = Columns::Dictionary.new
|
|
50
59
|
|
|
@@ -80,7 +89,9 @@ module CSVDecision
|
|
|
80
89
|
|
|
81
90
|
def self.column_name(type:, name:)
|
|
82
91
|
return format_column_name(name) if name.present?
|
|
83
|
-
|
|
92
|
+
|
|
93
|
+
# TODO: implement anonymous column types
|
|
94
|
+
# return if COLUMN_TYPE_ANONYMOUS.member?(type)
|
|
84
95
|
|
|
85
96
|
raise CellValidationError, 'column name is missing'
|
|
86
97
|
end
|
|
@@ -102,8 +113,9 @@ module CSVDecision
|
|
|
102
113
|
when :'in/text'
|
|
103
114
|
[:in, true]
|
|
104
115
|
|
|
105
|
-
|
|
106
|
-
|
|
116
|
+
# TODO: planned feature
|
|
117
|
+
# when :guard
|
|
118
|
+
# [:in, false]
|
|
107
119
|
|
|
108
120
|
when :'out/text'
|
|
109
121
|
[:out, true]
|
|
@@ -143,9 +155,6 @@ module CSVDecision
|
|
|
143
155
|
|
|
144
156
|
when :out
|
|
145
157
|
dictionary.outs[index] = entry
|
|
146
|
-
|
|
147
|
-
else
|
|
148
|
-
raise "internal error - column type #{type} not recognised"
|
|
149
158
|
end
|
|
150
159
|
|
|
151
160
|
dictionary
|
data/lib/csv_decision/input.rb
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'ice_nine'
|
|
4
|
-
require 'ice_nine/core_ext/object'
|
|
5
|
-
|
|
6
3
|
# CSV Decision: CSV based Ruby decision tables.
|
|
7
4
|
# Created December 2017 by Brett Vickers
|
|
8
5
|
# See LICENSE and README.md for details.
|
|
9
6
|
module CSVDecision
|
|
10
|
-
# Parse the input hash
|
|
7
|
+
# Parse the input hash.
|
|
11
8
|
module Input
|
|
9
|
+
# @param (see Decide.decide)
|
|
10
|
+
# @return [Hash{Symbol => Hash{Symbol=>Object}, Hash{Integer=>Object}}]
|
|
11
|
+
# Returns a hash of two hashes:
|
|
12
|
+
# * hash: either a copy with keys symbolized or the original input object
|
|
13
|
+
# * scan_cols: Picks out the value in the input hash for each table input column.
|
|
14
|
+
# Defaults to nil if the key is missing in the input hash.
|
|
12
15
|
def self.parse(table:, input:, symbolize_keys:)
|
|
13
16
|
validate(input)
|
|
14
17
|
|
|
15
18
|
# For safety the default is to symbolize keys and make a copy of the hash.
|
|
16
|
-
# However, if this is turned off
|
|
17
|
-
|
|
18
|
-
input = symbolize_keys ? input.deep_symbolize_keys : input
|
|
19
|
+
# However, if this is turned off then the keys must already symbolized.
|
|
20
|
+
input = symbolize_keys ? input.symbolize_keys : input
|
|
19
21
|
|
|
20
22
|
parsed_input = parse_input(table: table, input: input)
|
|
21
23
|
|
|
24
|
+
# Freeze the copy of the input hash we just created.
|
|
22
25
|
parsed_input[:hash].freeze if symbolize_keys
|
|
23
26
|
|
|
24
27
|
parsed_input
|
|
@@ -33,7 +36,7 @@ module CSVDecision
|
|
|
33
36
|
def self.parse_input(table:, input:)
|
|
34
37
|
scan_cols = {}
|
|
35
38
|
|
|
36
|
-
# Does this table have any defaulted columns?
|
|
39
|
+
# TODO: Does this table have any defaulted columns?
|
|
37
40
|
# defaulted_columns = table.columns[:defaults]
|
|
38
41
|
|
|
39
42
|
table.columns.ins.each_pair do |col, column|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# CSV Decision: CSV based Ruby decision tables.
|
|
4
|
+
# Created December 2017 by Brett Vickers
|
|
5
|
+
# See LICENSE and README.md for details.
|
|
6
|
+
module CSVDecision
|
|
7
|
+
# Methods to assign a matcher to data cells
|
|
8
|
+
class Matchers
|
|
9
|
+
# Cell constant matcher - e.g., := true, = nil.
|
|
10
|
+
class Constant < Matcher
|
|
11
|
+
# @param (see Matchers::Matcher)
|
|
12
|
+
# @return (see Matchers::Matcher)
|
|
13
|
+
def matches?(cell)
|
|
14
|
+
CSVDecision::Constant.matches?(cell)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -5,53 +5,20 @@
|
|
|
5
5
|
# See LICENSE and README.md for details.
|
|
6
6
|
module CSVDecision
|
|
7
7
|
# Methods to assign a matcher to data cells
|
|
8
|
-
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
nil: nil
|
|
14
|
-
}.freeze
|
|
15
|
-
# rubocop: enable Lint/BooleanSymbol
|
|
16
|
-
|
|
17
|
-
def self.input_cell_constant?(match)
|
|
18
|
-
return false unless CELL_CONSTANT.member?(match['operator'])
|
|
19
|
-
return false unless match['args'] == ''
|
|
20
|
-
return false unless match['negate'] == ''
|
|
21
|
-
|
|
22
|
-
name = match['name'].to_sym
|
|
23
|
-
return false unless NON_NUMERIC_CONSTANTS.key?(name)
|
|
24
|
-
|
|
25
|
-
Proc.with(type: :constant, function: NON_NUMERIC_CONSTANTS[name])
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Match cell against a function call or symbolic expression.
|
|
8
|
+
class Matchers
|
|
9
|
+
# Match cell against a function call
|
|
10
|
+
# * no arguments - e.g., := present?
|
|
11
|
+
# * with arguments - e.g., :=lookup?(:table)
|
|
12
|
+
# TODO: fully implement
|
|
29
13
|
class Function < Matcher
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
# == :column_name
|
|
34
|
-
FUNCTION_CALL =
|
|
35
|
-
"(?<operator>=|:=|==|<|>|!=|>=|<=|:|!\\s*:)\s*(?<negate>!?)\\s*(?<name>#{Header::COLUMN_NAME}|:)(?<args>.*)"
|
|
36
|
-
FUNCTION_RE = Matchers.regexp(FUNCTION_CALL)
|
|
37
|
-
|
|
38
|
-
# COMPARATORS = {
|
|
39
|
-
# '>' => proc { |numeric_cell, value| Matchers.numeric(value) &.> numeric_cell },
|
|
40
|
-
# '>=' => proc { |numeric_cell, value| Matchers.numeric(value) &.>= numeric_cell },
|
|
41
|
-
# '<' => proc { |numeric_cell, value| Matchers.numeric(value) &.< numeric_cell },
|
|
42
|
-
# '<=' => proc { |numeric_cell, value| Matchers.numeric(value) &.<= numeric_cell },
|
|
43
|
-
# '!=' => proc { |numeric_cell, value| Matchers.numeric(value) &.!= numeric_cell }
|
|
44
|
-
# }.freeze
|
|
14
|
+
def initialize(options = {})
|
|
15
|
+
@options = options
|
|
16
|
+
end
|
|
45
17
|
|
|
18
|
+
# @param (see Matchers::Matcher#matches?)
|
|
19
|
+
# @return (see Matchers::Matcher#matches?)
|
|
46
20
|
def matches?(cell)
|
|
47
|
-
|
|
48
|
-
return false unless match
|
|
49
|
-
|
|
50
|
-
# Check if the guard condition is a cell constant
|
|
51
|
-
proc = Matchers.input_cell_constant?(match)
|
|
52
|
-
return proc if proc
|
|
53
|
-
|
|
54
|
-
false
|
|
21
|
+
CSVDecision::Function.matches?(cell)
|
|
55
22
|
end
|
|
56
23
|
end
|
|
57
24
|
end
|
|
@@ -5,41 +5,13 @@
|
|
|
5
5
|
# See LICENSE and README.md for details.
|
|
6
6
|
module CSVDecision
|
|
7
7
|
# Methods to assign a matcher to data cells
|
|
8
|
-
|
|
9
|
-
#
|
|
10
|
-
CELL_CONSTANT = Set.new(%w[== := =]).freeze
|
|
11
|
-
|
|
12
|
-
# Match cell against a Ruby-like numeric comparison or a numeric constant
|
|
8
|
+
class Matchers
|
|
9
|
+
# Recognise numeric comparison expressions - e.g., +> 100+ or +!= 0+
|
|
13
10
|
class Numeric < Matcher
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
# Coerce the input value to a numeric representation before invoking the comparison.
|
|
18
|
-
# If the coercion fails, it will produce a nil value which always fails to match.
|
|
19
|
-
COMPARATORS = {
|
|
20
|
-
'>' => proc { |numeric_cell, value| Matchers.numeric(value) &.> numeric_cell },
|
|
21
|
-
'>=' => proc { |numeric_cell, value| Matchers.numeric(value) &.>= numeric_cell },
|
|
22
|
-
'<' => proc { |numeric_cell, value| Matchers.numeric(value) &.< numeric_cell },
|
|
23
|
-
'<=' => proc { |numeric_cell, value| Matchers.numeric(value) &.<= numeric_cell },
|
|
24
|
-
'!=' => proc { |numeric_cell, value| Matchers.numeric(value) &.!= numeric_cell }
|
|
25
|
-
}.freeze
|
|
26
|
-
|
|
11
|
+
# @param (see Matchers::Matcher#matches?)
|
|
12
|
+
# @return (see Matchers::Matcher#matches?)
|
|
27
13
|
def matches?(cell)
|
|
28
|
-
|
|
29
|
-
return false unless match
|
|
30
|
-
|
|
31
|
-
numeric_cell = Matchers.numeric(match['value'])
|
|
32
|
-
return false unless numeric_cell
|
|
33
|
-
|
|
34
|
-
comparator = match['comparator']
|
|
35
|
-
|
|
36
|
-
# If the comparator is assignment/equality, then just treat as a simple constant
|
|
37
|
-
if CELL_CONSTANT.member?(comparator)
|
|
38
|
-
return Proc.with(type: :constant, function: numeric_cell)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
Proc.with(type: :proc,
|
|
42
|
-
function: COMPARATORS[comparator].curry[numeric_cell])
|
|
14
|
+
CSVDecision::Numeric.matches?(cell)
|
|
43
15
|
end
|
|
44
16
|
end
|
|
45
17
|
end
|
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
# See LICENSE and README.md for details.
|
|
6
6
|
module CSVDecision
|
|
7
7
|
# Methods to assign a matcher to data cells
|
|
8
|
-
|
|
9
|
-
# Match cell against a regular expression pattern
|
|
8
|
+
class Matchers
|
|
9
|
+
# Match cell against a regular expression pattern - e.g., +=~ hot|col+ or +.*OPT.*+
|
|
10
10
|
class Pattern < Matcher
|
|
11
|
-
EXPLICIT_COMPARISON =
|
|
12
|
-
|
|
11
|
+
EXPLICIT_COMPARISON = Matchers.regexp("(?<comparator>=~|!~|!=)\\s*(?<value>\\S.*)")
|
|
12
|
+
private_constant :EXPLICIT_COMPARISON
|
|
13
|
+
|
|
14
|
+
IMPLICIT_COMPARISON = Matchers.regexp("(?<comparator>=~|!~|!=)?\\s*(?<value>\\S.*)")
|
|
15
|
+
private_constant :IMPLICIT_COMPARISON
|
|
13
16
|
|
|
14
17
|
# rubocop: disable Style/DoubleNegation
|
|
15
18
|
PATTERN_LAMBDAS = {
|
|
@@ -17,6 +20,7 @@ module CSVDecision
|
|
|
17
20
|
'=~' => proc { |pattern, value| !!pattern.match(value) }.freeze,
|
|
18
21
|
'!~' => proc { |pattern, value| !pattern.match(value) }.freeze
|
|
19
22
|
}.freeze
|
|
23
|
+
private_constant :PATTERN_LAMBDAS
|
|
20
24
|
# rubocop: enable Style/DoubleNegation
|
|
21
25
|
|
|
22
26
|
def self.regexp?(cell:, explicit:)
|
|
@@ -31,6 +35,7 @@ module CSVDecision
|
|
|
31
35
|
|
|
32
36
|
parse(comparator: comparator, value: match['value'])
|
|
33
37
|
end
|
|
38
|
+
private_class_method :regexp?
|
|
34
39
|
|
|
35
40
|
def self.parse(comparator:, value:)
|
|
36
41
|
return false if value.blank?
|
|
@@ -54,13 +59,8 @@ module CSVDecision
|
|
|
54
59
|
'=~'
|
|
55
60
|
end
|
|
56
61
|
|
|
57
|
-
def
|
|
58
|
-
|
|
59
|
-
@regexp_explicit = !options[:regexp_implicit]
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def matches?(cell)
|
|
63
|
-
comparator, value = Pattern.regexp?(cell: cell, explicit: @regexp_explicit)
|
|
62
|
+
def self.matches?(cell, regexp_explicit:)
|
|
63
|
+
comparator, value = regexp?(cell: cell, explicit: regexp_explicit)
|
|
64
64
|
|
|
65
65
|
# We could not find a regexp pattern - maybe it's a simple string or something else?
|
|
66
66
|
return false unless comparator
|
|
@@ -71,6 +71,21 @@ module CSVDecision
|
|
|
71
71
|
Proc.with(type: :proc,
|
|
72
72
|
function: PATTERN_LAMBDAS[comparator].curry[pattern].freeze)
|
|
73
73
|
end
|
|
74
|
+
|
|
75
|
+
# @param options [Hash{Symbol=>Object}] Used to determine the value of regexp_implicit:.
|
|
76
|
+
def initialize(options = {})
|
|
77
|
+
# By default regexp's must have an explicit comparator
|
|
78
|
+
@regexp_explicit = !options[:regexp_implicit]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Recognise a regular expression pattern - e.g., +=~ on|off+ or +!~ OPT.*+.
|
|
82
|
+
# If the option regexp_implicit: true has been set, then cells may omit the +=~+ comparator so long as they
|
|
83
|
+
# contain non-word characters typically used in regular expressions such as +*+ and +.+.
|
|
84
|
+
# @param (see Matchers::Matcher#matches?)
|
|
85
|
+
# @return (see Matchers::Matcher#matches?)
|
|
86
|
+
def matches?(cell)
|
|
87
|
+
Pattern.matches?(cell, regexp_explicit: @regexp_explicit)
|
|
88
|
+
end
|
|
74
89
|
end
|
|
75
90
|
end
|
|
76
91
|
end
|