array_logic 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ Copyright 2012 Rob Nichols
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,32 @@
1
+ == array_logic
2
+
3
+ A system that allows me to define the logic for comparing arrays.
4
+
5
+ The logic for an active record model Answer, looks like this:
6
+
7
+
8
+ a1 = Answer.find(1)
9
+ a2 = Answer.find(2)
10
+ ....
11
+ a5 = Answer.find(5)
12
+
13
+ rule_one.rule = "(a1 and a2) or (a3 and a4)"
14
+
15
+ rule_two.rule = "a1 and not a2"
16
+
17
+ rule_three.rule = "2 in a1 a2 a3"
18
+
19
+ rule_four.rule = "(2 in a1 a2 a3) and (1 in a4 a5)"
20
+
21
+
22
+ rule_one rule_two rule_three rule_four
23
+ [a1, a2] true false true false
24
+ [a3, a4] true false false false
25
+ [a1, a3, a5] false true true true
26
+
27
+ The *match* and *matches* methods allow arrays to be tested against these rules:
28
+
29
+ rule_two.match([a1, a2]) --> false
30
+ rule_two.matches([a1, a2], [a1]) --> [[a1]]
31
+
32
+ See test/array_logic/rule_test for more examples
@@ -0,0 +1,19 @@
1
+
2
+ require 'rubygems'
3
+ require 'rake'
4
+ require 'rake/clean'
5
+ require 'rdoc/task'
6
+ require 'rake/testtask'
7
+
8
+ Rake::RDocTask.new do |rdoc|
9
+ files =['README.rdoc', 'MIT-LICENSE', 'lib/**/*.rb']
10
+ rdoc.rdoc_files.add(files)
11
+ rdoc.main = "README.rdoc" # page to start on
12
+ rdoc.title = "Dibber Docs"
13
+ rdoc.rdoc_dir = 'doc/rdoc' # rdoc output folder
14
+ rdoc.options << '--line-numbers'
15
+ end
16
+
17
+ Rake::TestTask.new do |t|
18
+ t.test_files = FileList['test/**/*.rb']
19
+ end
@@ -0,0 +1,5 @@
1
+ require_relative('array_logic/thing')
2
+ require_relative('array_logic/rule')
3
+ module ArrayLogic
4
+
5
+ end
@@ -0,0 +1,134 @@
1
+
2
+ class Rule
3
+ attr_accessor :rule
4
+ attr_reader :things
5
+
6
+ def initialize
7
+
8
+ end
9
+
10
+ def matches(*array_of_things)
11
+ array_of_things.delete_if{|things| !match(things)}
12
+ end
13
+
14
+ def match(things)
15
+ rule_valid?
16
+ @things = things
17
+ logic
18
+ end
19
+
20
+ def logic
21
+ eval(expression)
22
+ end
23
+
24
+ def rule_valid?
25
+ check_rule_entered
26
+ check_allowed_characters
27
+ end
28
+
29
+ def replace_item(pattern, processor)
30
+ @processed_rule = processed_rule.gsub(pattern) {|x| processor.call(x)}
31
+ end
32
+
33
+ private
34
+ def thing_ids
35
+ things.collect(&:id)
36
+ end
37
+
38
+ def expression
39
+ rule_processing_steps
40
+ return final_processed_rule
41
+ end
42
+
43
+
44
+
45
+ def rule_processing_steps
46
+ add_space_around_puctuation_characters
47
+ make_everything_lower_case
48
+ replace_logic_words_with_operators
49
+ replace_item(thing_id_pattern, true_or_false_for_thing_id_in_thing_ids)
50
+ replace_item(number_in_set_pattern, comparison_of_number_with_true_count)
51
+ end
52
+
53
+ def number_in_set_pattern
54
+ /\d+\s+in\s+((true|false)[\,\s]*)+/
55
+ end
56
+
57
+ def comparison_of_number_with_true_count
58
+ lambda do |string|
59
+ before_in, after_in = string.split(/\s+in\s+/)
60
+ true_count = after_in.split.count('true')
61
+ " ( #{before_in} <= #{true_count} ) "
62
+ end
63
+ end
64
+
65
+ # examples: a1, a2, a33, t1
66
+ def thing_id_pattern
67
+ /\w\d+/
68
+ end
69
+
70
+ def true_or_false_for_thing_id_in_thing_ids
71
+ lambda {|s| thing_ids.include?(s[/\d+/].to_i)}
72
+ end
73
+
74
+ def processed_rule
75
+ @processed_rule ||= rule.clone
76
+ end
77
+
78
+ def add_space_around_puctuation_characters
79
+ @processed_rule = processed_rule.gsub(/(\)|\)|\,)/, ' \1 ')
80
+ end
81
+
82
+ def make_everything_lower_case
83
+ @processed_rule = processed_rule.downcase
84
+ end
85
+
86
+ def replace_logic_words_with_operators
87
+ {
88
+ 'and' => '&&',
89
+ 'or' => '||',
90
+ 'not' => '!'
91
+ }.each{|word, operator| @processed_rule = processed_rule.gsub(Regexp.new(word), operator)}
92
+ end
93
+
94
+ def final_processed_rule
95
+ result = processed_rule.clone
96
+ reset_processed_rule_ready_for_next_comparison
97
+ return result
98
+ end
99
+
100
+ def reset_processed_rule_ready_for_next_comparison
101
+ @processed_rule = nil
102
+ end
103
+
104
+ def check_rule_entered
105
+ raise "You must define a rule before trying to match" unless rule.kind_of? String
106
+ end
107
+
108
+ def check_allowed_characters
109
+ raise_invalid_charachers unless allowed_charachers_pattern =~ rule
110
+ end
111
+
112
+ def allowed_charachers_pattern
113
+ case_insensitive = true
114
+ Regexp.new("^(#{allowed_characters.join('|')})*$", case_insensitive)
115
+ end
116
+
117
+ def allowed_characters
118
+ brackets = ['\(', '\)']
119
+ in_pattern = ['\d+\s+in']
120
+ ids = ['\w\d+']
121
+ logic_words = %w{and or not}
122
+ logic_chrs = ['&&', '\|\|', '!']
123
+ commas = ['\,']
124
+ white_space = ['\s']
125
+
126
+ [brackets, in_pattern, ids, logic_words, logic_chrs, commas, white_space].flatten
127
+ end
128
+
129
+ def raise_invalid_charachers
130
+ invalid = rule.split.collect{|s| (allowed_charachers_pattern =~ s) ? nil : s }.compact
131
+ raise "The rule '#{rule}' is not valid. The problem is within '#{invalid.join(' ')}'"
132
+ end
133
+
134
+ end
@@ -0,0 +1,17 @@
1
+ module ArrayLogic
2
+
3
+ # Basic object used in testing
4
+ class Thing
5
+ attr_accessor :id
6
+
7
+ def initialize(number)
8
+ @id = number
9
+ end
10
+
11
+ def self.make(number)
12
+ things = Hash.new
13
+ (1..number).each{|n| things[n] = new(n)}
14
+ return things
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module ArrayLogic
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,4 @@
1
+ # To change this template, choose Tools | Templates
2
+ # and open the template in the editor.
3
+
4
+ puts "Hello World"
@@ -0,0 +1,214 @@
1
+ require 'test/unit'
2
+ require_relative '../../lib/array_logic'
3
+ require_relative 'test_case'
4
+
5
+ module ArrayLogic
6
+ class RuleTest < TestCase
7
+
8
+ def setup
9
+ @rule = Rule.new
10
+ end
11
+
12
+ def test_simple_and
13
+ @rule.rule = 't1 and t2'
14
+ assert_thing_match([1, 2], @rule)
15
+ assert_thing_match([1, 2, 3], @rule)
16
+ assert_no_thing_match([2, 3], @rule)
17
+ assert_no_thing_match([3], @rule)
18
+ end
19
+
20
+ def test_simple_or
21
+ @rule.rule = 't1 or t2'
22
+ assert_thing_match([1, 2], @rule)
23
+ assert_thing_match([1, 2, 3], @rule)
24
+ assert_thing_match([2, 3], @rule)
25
+ assert_no_thing_match([3], @rule)
26
+ end
27
+
28
+ def test_multiple_and
29
+ @rule.rule = 't1 and t2 and t3'
30
+ assert_no_thing_match([1, 2], @rule)
31
+ assert_thing_match([1, 2, 3], @rule)
32
+ assert_no_thing_match([2, 3], @rule)
33
+ assert_no_thing_match([3], @rule)
34
+ end
35
+
36
+ def test_multiple_or
37
+ @rule.rule = 't1 or t2 or t3'
38
+ assert_thing_match([1, 2], @rule)
39
+ assert_thing_match([1, 2, 3], @rule)
40
+ assert_thing_match([3], @rule)
41
+ assert_no_thing_match([4], @rule)
42
+ end
43
+
44
+ def test_one_or_one_and
45
+ @rule.rule = 't1 or ( t2 and t3 )'
46
+ assert_thing_match([1, 2], @rule)
47
+ assert_thing_match([1, 2, 3], @rule)
48
+ assert_thing_match([2, 3], @rule)
49
+ assert_no_thing_match([3], @rule)
50
+ end
51
+
52
+ def test_one_or_one_and
53
+ @rule.rule = '( t1 or t2 ) and t3'
54
+ assert_no_thing_match([1, 2], @rule)
55
+ assert_thing_match([1, 2, 3], @rule)
56
+ assert_thing_match([2, 3], @rule)
57
+ assert_no_thing_match([3], @rule)
58
+ end
59
+
60
+ def test_one_or_one_and_not
61
+ @rule.rule = 't1 or ( t2 and not t3 )'
62
+ assert_thing_match([1, 3], @rule)
63
+ assert_thing_match([1, 2, 3], @rule)
64
+ assert_no_thing_match([2, 3], @rule)
65
+ assert_thing_match([2], @rule)
66
+ assert_thing_match([2, 4], @rule)
67
+ end
68
+
69
+ def test_one_or_one_and_with_no_space_between_tn_and_bracket
70
+ @rule.rule = 't1 or (t2 and t3)'
71
+ assert_thing_match([1, 2], @rule)
72
+ assert_thing_match([1, 2, 3], @rule)
73
+ assert_thing_match([2, 3], @rule)
74
+ assert_no_thing_match([3], @rule)
75
+ end
76
+
77
+ def test_one_in_three
78
+ @rule.rule = '1 in t1, t2, t3'
79
+ assert_thing_match([1, 2], @rule)
80
+ assert_thing_match([1, 2, 3], @rule)
81
+ assert_thing_match([3], @rule)
82
+ assert_no_thing_match([4], @rule)
83
+ end
84
+
85
+ def test_one_in_three_no_commas
86
+ @rule.rule = '1 in t1 t2 t3'
87
+ assert_thing_match([1, 2], @rule)
88
+ assert_thing_match([1, 2, 3], @rule)
89
+ assert_thing_match([3], @rule)
90
+ assert_no_thing_match([4], @rule)
91
+ end
92
+
93
+ def test_one_in_three_with_and
94
+ @rule.rule = '(1 in t1, t2, t3) and t3'
95
+ assert_no_thing_match([1, 2], @rule)
96
+ assert_thing_match([1, 2, 3], @rule)
97
+ assert_thing_match([3], @rule)
98
+ assert_no_thing_match([4], @rule)
99
+ end
100
+
101
+ def test_one_in_three_with_and_without_brackets
102
+ @rule.rule = '1 in t1, t2, t3 and t3'
103
+ assert_no_thing_match([1, 2], @rule)
104
+ assert_thing_match([1, 2, 3], @rule)
105
+ assert_thing_match([3], @rule)
106
+ assert_no_thing_match([4], @rule)
107
+ end
108
+
109
+ def test_one_in_three_with_and_without_brackets_and_commas
110
+ @rule.rule = '1 in t1 t2 t3 and t3'
111
+ assert_no_thing_match([1, 2], @rule)
112
+ assert_thing_match([1, 2, 3], @rule)
113
+ assert_thing_match([3], @rule)
114
+ assert_no_thing_match([4], @rule)
115
+ end
116
+
117
+ def test_2_in_with_and_at_start
118
+ @rule.rule = 't1 and t2 and 2 in t2 t3 t4'
119
+ assert_no_thing_match([1, 2], @rule)
120
+ assert_thing_match([1, 2, 3], @rule)
121
+ assert_thing_match([1, 2, 4], @rule)
122
+ assert_no_thing_match([2, 3, 4], @rule)
123
+ end
124
+
125
+ def test_in_within_logic_string
126
+ @rule.rule = '(t1 and 1 in t2 t3) or t4'
127
+ assert_thing_match([1, 2], @rule)
128
+ assert_thing_match([1, 2, 3], @rule)
129
+ assert_no_thing_match([3], @rule)
130
+ assert_thing_match([4], @rule)
131
+ end
132
+
133
+ def test_uppercase_in_within_logic_string
134
+ @rule.rule = '(t1 AND 1 IN t2 t3) OR t4'
135
+ assert_thing_match([1, 2], @rule)
136
+ assert_thing_match([1, 2, 3], @rule)
137
+ assert_no_thing_match([3], @rule)
138
+ assert_thing_match([4], @rule)
139
+ end
140
+
141
+ def test_operators_in_within_logic_string
142
+ @rule.rule = '(t1 && 1 in t2 t3) || t4'
143
+ assert_thing_match([1, 2], @rule)
144
+ assert_thing_match([1, 2, 3], @rule)
145
+ assert_no_thing_match([3], @rule)
146
+ assert_thing_match([4], @rule)
147
+ end
148
+
149
+ def test_match_without_rule
150
+ assert_raise RuntimeError do
151
+ @rule.match([1, 2])
152
+ end
153
+ end
154
+
155
+ def test_match_with_number_rule
156
+ @rule.rule = 1
157
+ assert_raise RuntimeError do
158
+ @rule.match([1, 2])
159
+ end
160
+ end
161
+
162
+ def test_replace_item
163
+ @rule.rule = 't1 or ( t2 and t3 )'
164
+ process = lambda {|s| [1, 2].include?(s[/\d+/].to_i)}
165
+ result = @rule.replace_item(/\w\d+/, process)
166
+ assert_equal('true or ( true and false )', result)
167
+ end
168
+
169
+ def test_invalid_input
170
+ @rule.rule = 'invalid'
171
+ assert_raise RuntimeError do
172
+ @rule.match([1])
173
+ end
174
+ end
175
+
176
+ def test_another_invalid_input
177
+ @rule.rule = 'a1 and User.delete_all'
178
+ assert_raise RuntimeError do
179
+ @rule.match([1])
180
+ end
181
+ end
182
+
183
+ def test_matches
184
+ @rule.rule = 't1 and t2'
185
+ t1 = Thing.new(1)
186
+ t2 = Thing.new(2)
187
+ t3 = Thing.new(3)
188
+ match_one = [t1, t2]
189
+ match_two = [t1, t2, t3]
190
+ no_match_one = [t2, t3]
191
+ no_match_two = [t3]
192
+
193
+ result = @rule.matches(match_one, match_two, no_match_one, no_match_two)
194
+ expected = [match_one, match_two]
195
+ assert_equal(expected, result)
196
+ end
197
+
198
+ def test_matches_with_or
199
+ @rule.rule = 't1 or t2'
200
+ t1 = Thing.new(1)
201
+ t2 = Thing.new(2)
202
+ t3 = Thing.new(3)
203
+ match_one = [t1, t2]
204
+ match_two = [t1, t2, t3]
205
+ match_three = [t2, t3]
206
+ no_match_two = [t3]
207
+
208
+ result = @rule.matches(match_one, match_two, match_three, no_match_two)
209
+ expected = [match_one, match_two, match_three]
210
+ assert_equal(expected, result)
211
+ end
212
+
213
+ end
214
+ end
@@ -0,0 +1,25 @@
1
+ require 'test/unit'
2
+ require_relative '../../lib/array_logic'
3
+
4
+ module ArrayLogic
5
+ class TestCase < Test::Unit::TestCase
6
+
7
+ def self.things
8
+ @things ||= Thing.make(10)
9
+ end
10
+
11
+ def get_things(thing_ids)
12
+ @things = thing_ids.collect{|id| self.class.things[id]}
13
+ end
14
+
15
+ def assert_thing_match(thing_ids, rule)
16
+ get_things(thing_ids)
17
+ assert(rule.match(@things), "#{thing_ids.inspect} should match '#{rule.rule}'")
18
+ end
19
+
20
+ def assert_no_thing_match(thing_ids, rule)
21
+ get_things(thing_ids)
22
+ assert(!rule.match(@things), "#{thing_ids.inspect} should not match '#{rule.rule}'")
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,14 @@
1
+ require 'test/unit'
2
+ require_relative '../../lib/array_logic'
3
+
4
+ module ArrayLogic
5
+ class ThingTest < Test::Unit::TestCase
6
+ def test_make
7
+ number = 10
8
+ things = Thing.make(number)
9
+ assert_equal(number, things.length)
10
+ assert_equal((1..number).to_a, things.values.collect(&:id))
11
+ assert_equal((1..number).to_a, things.keys)
12
+ end
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: array_logic
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Rob Nichols
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-26 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Allow a user to define a set of rules, and then test to see if an array
15
+ of object match those rules.
16
+ email:
17
+ - rob@undervale.co.uk
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - lib/array_logic.rb
23
+ - lib/main.rb
24
+ - lib/array_logic/thing.rb
25
+ - lib/array_logic/version.rb
26
+ - lib/array_logic/rule.rb
27
+ - MIT-LICENSE
28
+ - Rakefile
29
+ - README.rdoc
30
+ - test/array_logic/thing_test.rb
31
+ - test/array_logic/rule_test.rb
32
+ - test/array_logic/test_case.rb
33
+ homepage: https://github.com/reggieb/array_logic
34
+ licenses: []
35
+ post_install_message:
36
+ rdoc_options: []
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ! '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ none: false
47
+ requirements:
48
+ - - ! '>='
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubyforge_project:
53
+ rubygems_version: 1.8.24
54
+ signing_key:
55
+ specification_version: 3
56
+ summary: Matches arrays of objects against logical rules.
57
+ test_files:
58
+ - test/array_logic/thing_test.rb
59
+ - test/array_logic/rule_test.rb
60
+ - test/array_logic/test_case.rb