array_logic 0.0.1

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