mendel 1.0.0

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,256 @@
1
+ require_relative "../spec_helper"
2
+ require "mendel/combiner"
3
+ require "fixtures/example_input"
4
+ require "support/foosball_team"
5
+
6
+ describe Mendel::Combiner do
7
+
8
+ let(:combiner_class) {
9
+ Class.new do
10
+ include Mendel::Combiner
11
+ def score_combination(items)
12
+ items.reduce(0) { |sum, item| sum += item }
13
+ end
14
+ end
15
+
16
+ }
17
+ let(:combiner) { combiner_class.new(list1, list2) }
18
+ let(:list1) { [1.0, 2.0, 3.0] }
19
+ let(:list2) { [1.1, 2.1, 3.1] }
20
+
21
+ describe "requiring the including class to define `score_combination`" do
22
+
23
+ let(:combiner_class) {
24
+ Class.new do
25
+ include Mendel::Combiner
26
+ end
27
+ }
28
+
29
+ it "raises NotImplementedError otherwise" do
30
+ expect{combiner.take(1)}.to raise_error(NotImplementedError)
31
+ end
32
+
33
+ end
34
+
35
+ describe "producing correct output" do
36
+
37
+ let(:list1) { EXAMPLE_INPUT[:incrementing_integers] }
38
+ let(:all_results) { combiner.to_a }
39
+
40
+ describe "when both lists increment smoothly" do
41
+ let(:list2) { EXAMPLE_INPUT[:incrementing_decimals] }
42
+
43
+ it "has a complete result set that is ordered correctly" do
44
+ require "fixtures/example_output/inc_integers_w_inc_decimals"
45
+ expect(all_results).to be_sorted_like($inc_integers_w_inc_decimals)
46
+ end
47
+
48
+ end
49
+
50
+ describe "when the second list has repeats" do
51
+ let(:list2) { EXAMPLE_INPUT[:repeats] }
52
+
53
+ it "has a complete result set that is ordered correctly" do
54
+ require "fixtures/example_output/inc_integers_w_repeats"
55
+ expect(all_results).to be_sorted_like($inc_integers_w_repeats)
56
+ end
57
+
58
+ end
59
+
60
+ describe "when the second list has skips" do
61
+ let(:list2) { EXAMPLE_INPUT[:skips] }
62
+
63
+ it "has a complete result set that is ordered correctly" do
64
+ require "fixtures/example_output/inc_integers_w_skips"
65
+ expect(all_results).to be_sorted_like($inc_integers_w_skips)
66
+ end
67
+
68
+ end
69
+
70
+ describe "when the second list has repeats AND skips" do
71
+ let(:list2) { EXAMPLE_INPUT[:repeats_and_skips] }
72
+
73
+ it "has a complete result set that is ordered correctly" do
74
+ require "fixtures/example_output/inc_integers_w_repeats_and_skips"
75
+ expect(all_results).to be_sorted_like($inc_integers_w_repeats_and_skips)
76
+ end
77
+
78
+ end
79
+
80
+ describe "when the lists are different lengths" do
81
+ let(:list2) { EXAMPLE_INPUT[:short_list] }
82
+
83
+ it "has a complete result set that is ordered correctly" do
84
+ require "fixtures/example_output/different_lengths"
85
+ expect(all_results).to be_sorted_like($different_lengths)
86
+ end
87
+
88
+ end
89
+
90
+ context "when there are more than 2 lists" do
91
+
92
+ let(:list3) { [1.2, 2.2, 3.2] }
93
+ let(:list4) { [1.3, 2.3, 3.3] }
94
+
95
+ let(:combiner) { combiner_class.new(list1, list2, list3, list4) }
96
+
97
+ it "can produce valid combinations" do
98
+ expect(combiner.first).to eq([[1.0, 1.1, 1.2, 1.3], 4.6])
99
+ end
100
+
101
+ end
102
+
103
+ context "when given lists of non-numeric items" do
104
+
105
+ let(:list1) { [{name: 'Jimmy', age: 10}, {name: 'Susan', age: 12}] }
106
+ let(:list2) { [{name: 'Roger', age: 8}, {name: 'Carla', age: 14}] }
107
+ let(:sorted_combos) {
108
+ combos = []
109
+ list1.each do |player_1|
110
+ list2.each do |player_2|
111
+ combos << [
112
+ FoosballTeam.new(player_1, player_2),
113
+ (player_1[:age] + player_2[:age]) / 2.0
114
+ ]
115
+ end
116
+ end
117
+ combos.sort_by {|c| c.last}
118
+ }
119
+
120
+ context "when the combiner class has custom combination and scoring methods" do
121
+
122
+ let(:combiner_class) {
123
+ Class.new do
124
+ include Mendel::Combiner
125
+
126
+ def build_combination(items)
127
+ FoosballTeam.new(*items)
128
+ end
129
+
130
+ def score_combination(combination)
131
+ combination.average_age
132
+ end
133
+ end
134
+ }
135
+
136
+ it "has a complete result set that is ordered correctly" do
137
+ expect(all_results).to be_sorted_like(sorted_combos)
138
+ end
139
+
140
+ end
141
+
142
+ end
143
+
144
+ context "when the input lists are not sorted in ascending order" do
145
+
146
+ let(:list1) { EXAMPLE_INPUT[:incrementing_integers].reverse }
147
+ let(:list2) { EXAMPLE_INPUT[:incrementing_decimals].shuffle }
148
+
149
+ it "does not produce correct output" do
150
+ require "fixtures/example_output/inc_integers_w_inc_decimals"
151
+ expect(all_results).not_to be_sorted_like($inc_integers_w_inc_decimals)
152
+ end
153
+
154
+ end
155
+
156
+ end
157
+
158
+ describe "enumeration" do
159
+
160
+ it "is Enumerable" do
161
+ expect(combiner).to be_a(Enumerable)
162
+ end
163
+
164
+ it "supports normal enumerable operations" do
165
+ expect(combiner.take(3).map {|c| c[-1] }).to eq([2.1, 3.1, 3.1])
166
+ end
167
+
168
+ end
169
+
170
+ describe "dumping and loading state" do
171
+
172
+ context "when it has produced some combinations" do
173
+
174
+ before :each do
175
+ combiner.take(3)
176
+ end
177
+
178
+ let(:dumped) {
179
+ {
180
+ 'input' => [list1, list2], 'seen' => [[0, 0], [1, 0], [0, 1], [2, 0], [1, 1], [0, 2]],
181
+ 'queued' => [
182
+ [{'combo'=>[2.0, 2.1], "coordinates"=>[1, 1]}, 4.1],
183
+ [{"combo"=>[3.0, 1.1], "coordinates"=>[2, 0]}, 4.1],
184
+ [{"combo"=>[1.0, 3.1], "coordinates"=>[0, 2]}, 4.1],
185
+ ]
186
+ }
187
+ }
188
+
189
+ it "can dump its state" do
190
+ expect(combiner.dump).to eq(dumped)
191
+ end
192
+
193
+ it "can dump its state as JSON" do
194
+ expect(combiner.dump_json).to eq(JSON.dump(dumped))
195
+ end
196
+
197
+ context "when state has been dumped somewhere" do
198
+
199
+ context "as a hash" do
200
+
201
+ let!(:dumped_data) { combiner.dump }
202
+
203
+ it "can load state" do
204
+ expect(combiner_class.load(dumped_data)).to be_a(combiner_class)
205
+ end
206
+
207
+ it "can begin producing combinations again from that point" do
208
+ combiner = combiner_class.load(dumped_data)
209
+ expect(combiner.take(3)).to be_sorted_like([[[2.0, 2.1], 4.1], [[3.0, 1.1], 4.1], [[1.0, 3.1], 4.1]])
210
+ end
211
+
212
+ end
213
+
214
+ context "as json" do
215
+
216
+ let!(:dumped_json) { combiner.dump_json }
217
+
218
+ it "can load state" do
219
+ expect(combiner_class.load_json(dumped_json)).to be_a(combiner_class)
220
+ end
221
+
222
+ it "can begin producing combinations again from that point" do
223
+ combiner = combiner_class.load_json(dumped_json)
224
+ expect(combiner.take(3)).to be_sorted_like([[[2.0, 2.1], 4.1], [[3.0, 1.1], 4.1], [[1.0, 3.1], 4.1]])
225
+ end
226
+
227
+ end
228
+
229
+ end
230
+
231
+ end
232
+
233
+ end
234
+
235
+ describe "other methods" do
236
+
237
+ it "can return its queue length" do
238
+ expect(combiner.queue_length).to eq(1) # item at 0,0
239
+ combiner.take(1)
240
+ expect(combiner.queue_length).to eq(2) # queued its children
241
+ end
242
+
243
+ end
244
+
245
+ describe "when given empty lists" do
246
+
247
+ let(:list1) { [] }
248
+ let(:list2) { [] }
249
+
250
+ it "raises an error" do
251
+ expect{combiner}.to raise_error(Mendel::Combiner::EmptyList)
252
+ end
253
+
254
+ end
255
+
256
+ end
@@ -0,0 +1,70 @@
1
+ require_relative "../spec_helper"
2
+ require "mendel/min_priority_queue"
3
+
4
+ describe Mendel::MinPriorityQueue do
5
+
6
+ let(:queue) { described_class.new }
7
+
8
+ describe "basic functionality" do
9
+
10
+ it "can add items and pop an item of the lowest priority" do
11
+ queue.push('b', 2)
12
+ queue.push('a', 1)
13
+ queue.push('c', 3)
14
+ expect(queue.pop).to eq(['a', 1])
15
+ end
16
+
17
+ it "can report its length" do
18
+ expect(queue.length).to eq(0)
19
+ queue.push('b', 2)
20
+ queue.push('a', 1)
21
+ expect(queue.length).to eq(2)
22
+ queue.pop
23
+ expect(queue.length).to eq(1)
24
+ 2.times { queue.pop }
25
+ expect(queue.length).to eq(0) # not -1
26
+ end
27
+
28
+ end
29
+
30
+ describe "dumping and loading" do
31
+
32
+ describe "dumping" do
33
+
34
+ let(:queue) {
35
+ described_class.new.tap { |q|
36
+ q.push('c', 3)
37
+ q.push('a', 1)
38
+ q.push('b', 2)
39
+ }
40
+ }
41
+
42
+ it "can dump its contents as an array" do
43
+ expect(queue.dump).to eq([['a', 1], ['b', 2], ['c', 3]])
44
+ end
45
+
46
+ it "can dump its contents as json" do
47
+ expect(queue.dump_json).to eq("[[\"a\",1],[\"b\",2],[\"c\",3]]")
48
+ end
49
+
50
+ end
51
+
52
+ describe "loading" do
53
+
54
+ it "can load its contents from an array" do
55
+ queue.load([['a', 1], ['b', 2], ['c', 3]])
56
+ expect(queue.length).to eq(3)
57
+ expect(queue.pop).to eq(['a', 1])
58
+ end
59
+
60
+ it "can load its contents from JSON" do
61
+ queue.load_json("[[\"a\",1],[\"b\",2],[\"c\",3]]")
62
+ expect(queue.length).to eq(3)
63
+ expect(queue.pop).to eq(['a', 1])
64
+ end
65
+
66
+ end
67
+
68
+ end
69
+
70
+ end
@@ -0,0 +1,42 @@
1
+ require_relative "../spec_helper"
2
+ require "mendel/observable_combiner"
3
+
4
+ describe Mendel::ObservableCombiner do
5
+
6
+ let(:combiner_class) {
7
+ Class.new(Mendel::ObservableCombiner) do
8
+ def score_combination(items)
9
+ items.reduce(0) { |sum, item| sum += item }
10
+ end
11
+ end
12
+ }
13
+ let(:combiner) { combiner_class.new(list1, list2) }
14
+ let(:list1) { [1.0, 2.0, 3.0] }
15
+ let(:list2) { [1.1, 2.1, 3.1] }
16
+
17
+ describe "notification of events" do
18
+
19
+ describe "when a combination is returned" do
20
+
21
+ it "notifies that the combo's children have been scored and that the combo has been returned" do
22
+ # 2 times because with two lists there are two child coordinates
23
+ expect(combiner).to receive(:notify).exactly(2).times.with(
24
+ :scored, a_hash_including(
25
+ 'coordinates' => an_instance_of(Array),
26
+ 'score' => a_kind_of(Numeric)
27
+ )
28
+ )
29
+ expect(combiner).to receive(:notify).exactly(1).times.with(
30
+ :returned, a_hash_including(
31
+ 'coordinates' => an_instance_of(Array),
32
+ 'score' => a_kind_of(Numeric)
33
+ )
34
+ )
35
+ combiner.take(1)
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,42 @@
1
+ File.dirname(__FILE__).tap {|this_dir| $LOAD_PATH << this_dir unless $LOAD_PATH.include?(this_dir) }
2
+
3
+ # This file was generated by the `rspec --init` command. Conventionally, all
4
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
5
+ # Require this file using `require "spec_helper"` to ensure that it is only
6
+ # loaded once.
7
+ RSpec::Matchers.define :be_sorted_like do |expected_array|
8
+ require 'set'
9
+ # Items with same totals are ordered unpredictably; we only care that
10
+ # items with different totals are ordered correctly
11
+ match do |actual_array|
12
+ same_length = actual_array.length == expected_array.length
13
+ same_uniques = Set.new(actual_array) == Set.new(expected_array)
14
+ same_sort_keys = actual_array.map(&:last) == expected_array.map(&:last)
15
+ # puts "#{same_length} && #{same_uniques} && #{same_sort_keys}"
16
+ same_length && same_uniques && same_sort_keys
17
+ end
18
+ end
19
+ #
20
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
21
+ RSpec.configure do |config|
22
+ config.raise_errors_for_deprecations!
23
+ config.run_all_when_everything_filtered = true
24
+ config.filter_run :focus
25
+
26
+ # Run specs in random order to surface order dependencies. If you find an
27
+ # order dependency and want to debug it, you can fix the order by providing
28
+ # the seed, which is printed after each run.
29
+ # --seed 1234
30
+ config.order = 'random'
31
+ end
32
+
33
+ def sorted_combos(list1, list2)
34
+ results = []
35
+ list1.each do |l1|
36
+ list2.each do |l2|
37
+ results << [l1, l2, l1 + l2]
38
+ end
39
+ end
40
+ results.sort_by(&:last)
41
+ end
42
+
@@ -0,0 +1,24 @@
1
+ class FoosballTeam
2
+ attr_accessor :players
3
+
4
+ def initialize(*players)
5
+ self.players = players
6
+ end
7
+
8
+ def average_age
9
+ players.reduce(0){ |total, player|
10
+ total += player.fetch(:age)
11
+ } / 2.0
12
+ end
13
+
14
+ # So that one Set of FoosballTeams will be equal to another
15
+ # if the teams have the same players
16
+ def eql?(other)
17
+ other.class == self.class && other.players == players
18
+ end
19
+
20
+ def hash
21
+ players.hash
22
+ end
23
+
24
+ end
@@ -0,0 +1,119 @@
1
+ require_relative "../../spec_helper"
2
+ require "mendel"
3
+ require "mendel/observable_combiner"
4
+ require "mendel/visualizers/ascii"
5
+
6
+ describe Mendel::Visualizers::ASCII do
7
+
8
+ let(:klass) { described_class }
9
+ let(:combiner_class) {
10
+ Class.new(Mendel::ObservableCombiner) do
11
+ def score_combination(items)
12
+ items.reduce(0) { |sum, item| sum += item }
13
+ end
14
+ end
15
+ }
16
+
17
+ let(:combiner) { combiner_class.new(list1, list2) }
18
+ let(:list1) { (1..10).to_a }
19
+ let(:list2) { (1..10).map { |i| i + 0.1} }
20
+ let(:klass) { described_class }
21
+ let!(:visualizer) { klass.new(combiner) }
22
+
23
+ describe "after initialization" do
24
+
25
+ describe "output" do
26
+
27
+ describe "axis labels" do
28
+
29
+ it "converts the list item to a string" do
30
+ item = '1'
31
+ expect(item).to receive(:to_s).and_call_original
32
+ visualizer.axis_label_for(item)
33
+ end
34
+
35
+ it "pads the output to be 8 characters wide" do
36
+ item = 1
37
+ expect(visualizer.axis_label_for(item)).to eq(
38
+ ' 1'
39
+ )
40
+ end
41
+
42
+ it "limits the output to 8 characters" do
43
+ item = '0123456789'
44
+ expect(visualizer.axis_label_for(item)).to eq(
45
+ '01234567'
46
+ )
47
+ end
48
+
49
+ end
50
+
51
+ describe "grid points" do
52
+
53
+ it "represents :unscored as a blank 8 spaces wide" do
54
+ expect(visualizer.grid_point_for(:unscored)).to eq(' ')
55
+ end
56
+
57
+ it "represents :scored as a blue number" do
58
+ expect(visualizer.grid_point_for(:scored, 5)).to eq(' 5'.blue)
59
+ end
60
+
61
+ it "represents :returned as a green number" do
62
+ expect(visualizer.grid_point_for(:returned, 873)).to eq(' 873'.green)
63
+ end
64
+
65
+ end
66
+
67
+ describe "output" do
68
+
69
+ let(:empty_grid) {
70
+ <<-WOO
71
+ 10
72
+
73
+
74
+ 9
75
+
76
+
77
+ 8
78
+
79
+
80
+ 7
81
+
82
+
83
+ 6
84
+
85
+
86
+ 5
87
+
88
+
89
+ 4
90
+
91
+
92
+ 3
93
+
94
+
95
+ 2
96
+
97
+
98
+ 1
99
+ 1.1 2.1 3.1 4.1 5.1 6.1 7.1 8.1 9.1 10.1
100
+ WOO
101
+ }
102
+
103
+ it "shows an empty grid when initialized" do
104
+ expect(visualizer.output).to eq(empty_grid)
105
+ end
106
+
107
+ it "shows something else after enumerating" do
108
+ combiner.take(3)
109
+ expect(visualizer.output).not_to eq(empty_grid)
110
+ end
111
+
112
+ end
113
+
114
+ end
115
+
116
+ end
117
+
118
+ end
119
+