conditional_sample 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 66f4bd90cb090564aa82b8fcf2542ee09be6d90e
4
+ data.tar.gz: d92cfeb38c8587e14a787c22865dd393b1ca953f
5
+ SHA512:
6
+ metadata.gz: 4849a931094f7dc5577632fc630216ae22a6c28dd1f9fff08e7aaf8303f52b10a9f13e1ee5495e2a698bcd98adef440e099b7875f37dae760ace83a0db3ae161
7
+ data.tar.gz: 76a8a68232ec62e2a83e52d23708b669e41d7fcfe009a1e6b09689621fdd021e69a8edfeaa84f1f14955dc1eacc82de7544ba0e7ff8d5f2649fa56f7b4134510
data/.gitignore ADDED
@@ -0,0 +1,68 @@
1
+
2
+ ################################################################################
3
+ # Ruby specific files
4
+
5
+ *.gem
6
+ *.rbc
7
+ /.config
8
+ /coverage/
9
+ /InstalledFiles
10
+ /pkg/
11
+ /spec/reports/
12
+ /test/tmp/
13
+ /test/version_tmp/
14
+ /tmp/
15
+
16
+ ## Specific to RubyMotion:
17
+ .dat*
18
+ .repl_history
19
+ build/
20
+
21
+ ## Documentation cache and generated files:
22
+ /.yardoc/
23
+ /_yardoc/
24
+ /doc/
25
+ /rdoc/
26
+
27
+ ## Environment normalisation:
28
+ /.bundle/
29
+ /vendor/bundle
30
+ /lib/bundler/man/
31
+
32
+ # for a library or gem, you might want to ignore these files since the code is
33
+ # intended to run in multiple environments; otherwise, check them in:
34
+ Gemfile.lock
35
+ .ruby-version
36
+ .ruby-gemset
37
+
38
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
39
+ .rvmrc
40
+
41
+ ################################################################################
42
+ # Rails stuff
43
+
44
+ # Ignore all logfiles and tempfiles.
45
+ /log/*
46
+ /tmp/*
47
+ !/log/.keep
48
+ !/tmp/.keep
49
+
50
+ # Ignore Byebug command history file.
51
+ .byebug_history
52
+
53
+ # Ignore application configuration
54
+ /config/application.yml
55
+
56
+ ################################################################################
57
+ # System and config files
58
+
59
+ desktop.ini
60
+ .agignore
61
+ .ignore
62
+
63
+ ################################################################################
64
+ # App specific files
65
+
66
+ # Development files
67
+ work*.rb
68
+ /~/
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1 @@
1
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (C) 2017 Paul Thompson
2
+
3
+ This program is free software: you can redistribute it and/or modify
4
+ it under the terms of the GNU General Public License as published by
5
+ the Free Software Foundation, either version 3 of the License, or
6
+ (at your option) any later version.
7
+
8
+ This program is distributed in the hope that it will be useful,
9
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ GNU General Public License for more details.
12
+
13
+ Full text of this licence: <https://www.gnu.org/licenses/gpl.html>.
data/README.md ADDED
@@ -0,0 +1,348 @@
1
+ # Conditional Sample
2
+
3
+ by Paul Thompson - nossidge@gmail.com
4
+
5
+ This is a Ruby gem that will patch the Array class with a couple of
6
+ nice methods for sampling based on the results of an array of Boolean
7
+ procs. Array is sampled using the procs as conditions that each specific
8
+ array index element must conform to.
9
+
10
+ I'm using this primarily for procedural generation, where I have an
11
+ array of possible values and a certain sample I need, or order in which
12
+ I want the values arranged.
13
+
14
+ This code was spun off into a gem from my poetry generation project
15
+ https://github.com/nossidge/poefy
16
+
17
+
18
+ ## Installation
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ ```ruby
23
+ gem 'conditional_sample'
24
+ ```
25
+
26
+ And then execute:
27
+
28
+ $ bundle
29
+
30
+ Or install it yourself as:
31
+
32
+ $ gem install conditional_sample
33
+
34
+
35
+ ## Usage
36
+
37
+ When you require the gem, two extra methods are added to Array.
38
+
39
+ ```ruby
40
+ array.conditional_permutation(conditions, seconds)
41
+ array.conditional_sample(conditions, seconds)
42
+ ```
43
+
44
+ `#conditional_permutation` returns a permutation of the array where
45
+ each element validates to the same index in a 'conditions' array of
46
+ procs that return Boolean. At the end of the 'conditions' array, if
47
+ there are any elements in the array that have not been assigned, they
48
+ are appended without comparison.
49
+
50
+ `#conditional_sample` returns values from 'array' where each element
51
+ validates to the same index in a 'conditions' array of procs that
52
+ return Boolean. At the end of the 'conditions' array, no further
53
+ elements from the input array are appended.
54
+
55
+
56
+ ### The 'conditions' argument
57
+
58
+ This is an array of boolean procs that evaluate true if an element is
59
+ allowed to be placed in that position.
60
+ The arguments for each proc are |arr, elem|
61
+ * **arr** is a reference to the current array that has been built up
62
+ through the recursion chain.
63
+ * **elem** is a reference to the current element being considered.
64
+
65
+
66
+ ### The 'seconds' argument
67
+
68
+ This optional argument will force the method to give up and return an
69
+ empty array after a given number of seconds.
70
+
71
+ If you've ever tried to run `Array#permutation` on an array of even a
72
+ seemingly moderate size, you will know that it is very computationally
73
+ expensive. The results are [factorial][1], and get exponentially larger
74
+ the more elements there are in the input array.
75
+
76
+ For example, this code takes my machine two whole minutes to run.
77
+ ```ruby
78
+ puts (1..12).to_a.permutation.count
79
+ ```
80
+
81
+ These methods are not usually as computationally expensive, as there is
82
+ only one array for the output, but they can take a very long time to run
83
+ depending on how many rejected permutations there are before a valid one.
84
+ If there are no valid permutations for the given conditions array, all
85
+ permutations will be compared before simply outputting an empty array.
86
+ And that will take even longer than `Array#permutation`.
87
+
88
+ The methods have an optional argument to assuage this. This takes a
89
+ number that represents a time in seconds (which can be a fraction) and
90
+ returns an empty array if a valid sample is not found in that time.
91
+
92
+ Below is an example. If it fails to resolve in, say, two seconds, then
93
+ it's probably not possible to fit the conditions to the lines:
94
+
95
+ ```ruby
96
+ lines.shuffle.conditional_sample(conditions, 2)
97
+ ```
98
+
99
+ This is **strongly** recommended when first developing a project, as it
100
+ may not be transparent how computationally expensive a condition array
101
+ will be, especially on input arrays larger than about 10.
102
+
103
+ **It's really useful, honest. I wouldn't have built this in to the
104
+ methods if it wasn't super needed.**
105
+
106
+ Example programs can be found in the `spec` directory.
107
+
108
+ [1]: https://en.wikipedia.org/wiki/Factorial
109
+
110
+
111
+ ## Examples
112
+
113
+ All of these examples can be found in `spec/examples_spec.rb`, and
114
+ are evaluated when using the `$ rspec` or `$ rake test` commands.
115
+
116
+
117
+ ### Basic example
118
+
119
+ ```ruby
120
+ # 5 element input array.
121
+ numbers = (1..5).to_a
122
+
123
+ # 3 element conditions array.
124
+ conditions = [
125
+ proc { |arr, elem| elem < 3 },
126
+ proc { |arr, elem| elem > 3 },
127
+ proc { |arr, elem| elem > 1 }
128
+ ]
129
+
130
+ # These will always return the below output
131
+ # because they are the first values that match.
132
+ permut = numbers.conditional_permutation(conditions)
133
+ sample = numbers.conditional_sample(conditions)
134
+ p permut # => [1, 4, 2, 3, 5]
135
+ p sample # => [1, 4, 2]
136
+
137
+ # To get a random sample, #shuffle the array first.
138
+ # These results will vary based on the shuffle.
139
+ shuf = numbers.shuffle
140
+ shuf_permut = shuf.conditional_permutation(conditions)
141
+ shuf_sample = shuf.conditional_sample(conditions)
142
+ p shuf_permut # => [2, 5, 4, 1, 3]
143
+ p shuf_sample # => [2, 5, 4]
144
+ ```
145
+
146
+
147
+ ### Random rhyming lines
148
+
149
+ A really simple way to extract rhyming lines from an input array.
150
+
151
+ ```ruby
152
+ # Use this gem to get the rhyme of the line's final word.
153
+ require 'ruby_rhymes'
154
+
155
+ # Input lines, just two limericks concat together.
156
+ lines = [
157
+ "There was a young rustic named Mallory,",
158
+ "who drew but a very small salary.",
159
+ "When he went to the show,",
160
+ "his purse made him go",
161
+ "to a seat in the uppermost gallery.",
162
+ "There was an Old Man with a beard,",
163
+ "Who said, 'It is just as I feared!—",
164
+ "Two Owls and a Hen,",
165
+ "four Larks and a Wren,",
166
+ "Have all built their nests in my beard.'"
167
+ ]
168
+
169
+ # Output a couplet of any two lines that rhyme.
170
+ couplet_conditions = [
171
+ proc { |arr, elem| true },
172
+ proc { |arr, elem| elem.to_phrase.rhyme_key == arr.last.to_phrase.rhyme_key }
173
+ ]
174
+ puts lines.shuffle.conditional_sample(couplet_conditions)
175
+ # Who said, 'It is just as I feared!—
176
+ # There was an Old Man with a beard,
177
+
178
+ # Output a jumbled limerick from the input lines.
179
+ limerick_conditions = [
180
+ proc { |arr, elem| true },
181
+ proc { |arr, elem| elem.to_phrase.rhyme_key == arr[0].to_phrase.rhyme_key },
182
+ proc { |arr, elem| elem.to_phrase.rhyme_key != arr[0].to_phrase.rhyme_key },
183
+ proc { |arr, elem| elem.to_phrase.rhyme_key == arr[2].to_phrase.rhyme_key },
184
+ proc { |arr, elem| elem.to_phrase.rhyme_key == arr[0].to_phrase.rhyme_key }
185
+ ]
186
+ puts lines.shuffle.conditional_sample(limerick_conditions)
187
+ # to a seat in the uppermost gallery.
188
+ # who drew but a very small salary.
189
+ # Two Owls and a Hen,
190
+ # four Larks and a Wren,
191
+ # There was a young rustic named Mallory,
192
+ ```
193
+
194
+
195
+ ### Logic puzzle
196
+
197
+ You can use this to solve simple one-dimensional logic puzzles, such as
198
+ determining seating order, or racehorse results. Here's an example I just
199
+ made up:
200
+
201
+ **It's E's birthday!** Her good buddies have invited her for a big ol'
202
+ birthday meal at the fanciest restaurant in the whole prefecture. They'll
203
+ be sitting at the Top Bench, getting to look down at all the nonbirthdaying
204
+ plebs. E's gonna love it! All that needs sorting out now is the seating
205
+ arrangement, but A isn't worried. He knows he just has to follow the simple
206
+ rules below:
207
+
208
+ 1. E has to be in the middle, obviously. It's her birthday!
209
+ 2. B and D are besties. They must always sit together.
210
+ 3. B and E are beasties. They can't sit together, or they'll beast out
211
+ all over each other.
212
+ 4. For religious reasons, C and D must have exactly two people between them.
213
+ 5. For different (but equally culturally valid) religious reasons, A must
214
+ always sit to the left of C.
215
+
216
+ ```ruby
217
+ # Create an array of procs, one for each rule.
218
+ rules = []
219
+
220
+ # E has to be in the middle.
221
+ rules << proc do |arr, elem|
222
+ !(arr.count == 2) || elem == 'e'
223
+ end
224
+
225
+ # B and D must always sit together.
226
+ rules << proc do |arr, elem|
227
+ if elem == 'b' and arr.include?('d')
228
+ arr.last == 'd'
229
+ elsif elem == 'd' and arr.include?('b')
230
+ arr.last == 'b'
231
+ else
232
+ true
233
+ end
234
+ end
235
+
236
+ # B and E can't sit together.
237
+ rules << proc do |arr, elem|
238
+ if elem == 'b'
239
+ arr.last != 'e'
240
+ elsif elem == 'e'
241
+ arr.last != 'b'
242
+ else
243
+ true
244
+ end
245
+ end
246
+
247
+ # C and D must have exactly two people between them.
248
+ rules << proc do |arr, elem|
249
+ if elem == 'c' and arr.include?('d')
250
+ arr[-3] == 'd'
251
+ elsif elem == 'd' and arr.include?('c')
252
+ arr[-3] == 'c'
253
+ else
254
+ true
255
+ end
256
+ end
257
+
258
+ # A must always sit to the left of C.
259
+ rules << proc do |arr, elem|
260
+ !(elem == 'c') || arr.include?('a')
261
+ end
262
+
263
+ # Method to apply all the rules to a given |arr, elem|
264
+ def apply_all(rules, arr, elem)
265
+ rules.all? { |p| p.call(arr, elem) }
266
+ end
267
+
268
+ # The names of E and her friends.
269
+ people = ['a', 'b', 'c', 'd', 'e']
270
+
271
+ # Conditions array that implements all the rules.
272
+ conditions = people.count.times.map do
273
+ proc { |arr, elem| apply_all(rules, arr, elem) }
274
+ end
275
+
276
+ # Output the permutation that satisfies all rules.
277
+ p people.conditional_permutation(conditions)
278
+ # ['b', 'd', 'e', 'a', 'c']
279
+ ```
280
+
281
+
282
+ ### Logic puzzle 2
283
+
284
+ Puzzle found at: http://www.braingle.com/brainteasers/teaser.php?id=20962
285
+
286
+ At the wedding reception, there are five guests, Colin, Emily, Kate,
287
+ Fred, and Irene, who are not sure where to sit at the dinner table.
288
+ They ask the bride's mother, who responds, "As I remember, Colin is
289
+ not next to Kate, Emily is not next to Fred or Kate. Neither Kate or
290
+ Emily are next to Irene. And Fred should sit on Irene's left." As you
291
+ look at them from the opposite side of the table, can you correctly
292
+ seat the guests from left to right?
293
+
294
+ ```ruby
295
+ # Make sure certain people don't sit next to each other.
296
+ # e.g. "A is not next to B (or C, or D)"
297
+ def person_not_next_to(arr, elem, subject, *people)
298
+ if elem == subject
299
+ people.all? do |person|
300
+ !arr.include?(person) || arr.last != person
301
+ end
302
+ elsif people.include?(elem)
303
+ !arr.include?(subject) || arr.last != subject
304
+ else
305
+ true
306
+ end
307
+ end
308
+
309
+ # Create an array of procs, one for each rule.
310
+ rules = []
311
+
312
+ # Colin is not next to Kate.
313
+ rules << proc do |arr, elem|
314
+ person_not_next_to(arr, elem, 'Colin', 'Kate')
315
+ end
316
+
317
+ # Emily is not next to Fred or Kate.
318
+ rules << proc do |arr, elem|
319
+ person_not_next_to(arr, elem, 'Emily', 'Fred', 'Kate')
320
+ end
321
+
322
+ # Neither Kate or Emily are next to Irene.
323
+ rules << proc do |arr, elem|
324
+ person_not_next_to(arr, elem, 'Irene', 'Kate', 'Emily')
325
+ end
326
+
327
+ # And Fred should sit on Irene's left.
328
+ rules << proc do |arr, elem|
329
+ !(elem == 'Irene') || arr.last == 'Fred'
330
+ end
331
+
332
+ # Method to apply all the rules to a given |arr, elem|
333
+ def apply_all(rules, arr, elem)
334
+ rules.all? { |p| p.call(arr, elem) }
335
+ end
336
+
337
+ # The names of the wedding guests.
338
+ people = ['Colin', 'Emily', 'Kate', 'Fred', 'Irene']
339
+
340
+ # Conditions array that implements all the rules.
341
+ conditions = people.count.times.map do
342
+ proc { |arr, elem| apply_all(rules, arr, elem) }
343
+ end
344
+
345
+ # Output the permutation that satisfies all rules.
346
+ p people.conditional_permutation(conditions).reverse
347
+ # ['Emily', 'Colin', 'Irene', 'Fred', 'Kate']
348
+ ```
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec) do |t|
5
+ t.verbose = false
6
+ end
7
+ task :default => :spec
8
+ task :test => :spec
@@ -0,0 +1,29 @@
1
+ # Encoding: UTF-8
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'conditional_sample/version.rb'
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = 'conditional_sample'
9
+ s.authors = ['Paul Thompson']
10
+ s.email = ['nossidge@gmail.com']
11
+
12
+ s.summary = %q{Array sampling based on an input array of Boolean procs}
13
+ s.description = %q{Patch the Array with a couple of nice methods for sampling based on the results of an array of Boolean procs. Array is sampled using the procs as conditions that each specific array index element must conform to.}
14
+ s.homepage = 'https://github.com/nossidge/conditional_sample'
15
+
16
+ s.version = ConditionalSample.version_number
17
+ s.date = ConditionalSample.version_date
18
+ s.license = 'GPL-3.0'
19
+
20
+ s.files = `git ls-files`.split("\n")
21
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
22
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
23
+ s.require_paths = ['lib']
24
+
25
+ s.add_development_dependency('bundler', '~> 1.13')
26
+ s.add_development_dependency('rake', '~> 10.0')
27
+ s.add_development_dependency('rspec', '~> 3.0')
28
+ s.add_development_dependency('ruby_rhymes', '~> 0.1')
29
+ end
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env ruby
2
+ # Encoding: UTF-8
3
+
4
+ module ConditionalSample
5
+
6
+ module MixMe
7
+
8
+ private
9
+
10
+ ##
11
+ # Run a bloc in a given number of 'seconds'. If the bloc completes in
12
+ # time, then return the result of the bloc, else return 'rescue_value'.
13
+ #
14
+ def timeout_rescue seconds, rescue_value = nil
15
+ begin
16
+ Timeout::timeout(seconds.to_f) do
17
+ yield
18
+ end
19
+ rescue Timeout::Error
20
+ rescue_value
21
+ end
22
+ end
23
+
24
+ ##
25
+ # Delete the first matching value in an array.
26
+ # Destructive to the first argument.
27
+ #
28
+ def delete_first! array, value
29
+ array.delete_at(array.index(value) || array.length)
30
+ end
31
+
32
+ ##
33
+ # Private recursive method.
34
+ # #conditional_permutation is the public interface.
35
+ #
36
+ def conditional_permutation_recurse (
37
+ array,
38
+ conditions,
39
+ current_iter = 0,
40
+ current_array = [])
41
+
42
+ output = []
43
+
44
+ # Get the current conditional.
45
+ cond = conditions[current_iter]
46
+
47
+ # Loop through and return the first element that validates.
48
+ valid = false
49
+ array.each do |elem|
50
+
51
+ # Test the condition. If we've run out of elements
52
+ # in the condition array, then allow any value.
53
+ valid = cond ? cond.call(current_array, elem) : true
54
+ if valid
55
+
56
+ # Remove this element from the array, and recurse.
57
+ remain = array.dup
58
+ delete_first!(remain, elem)
59
+
60
+ # If the remaining array is empty, no need to recurse.
61
+ new_val = nil
62
+ if !remain.empty?
63
+ new_val = conditional_permutation_recurse(
64
+ remain,
65
+ conditions,
66
+ current_iter + 1,
67
+ current_array + [elem])
68
+ end
69
+
70
+ # If we cannot use this value, because it breaks future conditions.
71
+ if !remain.empty? && new_val.empty?
72
+ valid = false
73
+ else
74
+ output << elem << new_val
75
+ end
76
+ end
77
+
78
+ break if valid
79
+ end
80
+
81
+ output.flatten.compact
82
+ end
83
+
84
+ ##
85
+ # Private recursive method.
86
+ # #conditional_sample is the public interface.
87
+ #
88
+ def conditional_sample_recurse (
89
+ array,
90
+ conditions,
91
+ current_iter = 0,
92
+ current_array = [])
93
+
94
+ output = []
95
+
96
+ # Get the current conditional.
97
+ cond = conditions[current_iter]
98
+
99
+ # Return nil if we have reached the end of the conditionals.
100
+ return nil if cond.nil?
101
+
102
+ # Loop through and return the first element that validates.
103
+ valid = false
104
+ array.each do |elem|
105
+
106
+ # Test the condition. If we've run out of elements
107
+ # in the condition array, then allow any value.
108
+ valid = cond.call(current_array, elem)
109
+ if valid
110
+
111
+ # Remove this element from the array, and recurse.
112
+ remain = array.dup
113
+ delete_first!(remain, elem)
114
+
115
+ # If the remaining array is empty, no need to recurse.
116
+ new_val = conditional_sample_recurse(
117
+ remain,
118
+ conditions,
119
+ current_iter + 1,
120
+ current_array + [elem])
121
+
122
+ # If we cannot use this value, because it breaks future conditions.
123
+ if new_val and new_val.empty?
124
+ valid = false
125
+ else
126
+ output << elem << new_val
127
+ end
128
+ end
129
+
130
+ break if valid
131
+ end
132
+
133
+ output.flatten.compact
134
+ end
135
+
136
+ end
137
+
138
+ end
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env ruby
2
+ # Encoding: UTF-8
3
+
4
+ module ConditionalSample
5
+
6
+ ##
7
+ # This module is suitable as a mixin, using the results of self#to_a
8
+ #
9
+ # It is automatically included in Array, so each of these methods are added
10
+ # to Array when you require 'conditional_sample'
11
+ #
12
+ # For both methods, the 'conditions' array must contain boolean procs
13
+ # using args |arr, elem|
14
+ # arr:: a reference to the current array that has been built up
15
+ # through the recursion chain.
16
+ # elem:: a reference to the current element being considered.
17
+ #
18
+ module MixMe
19
+
20
+ ##
21
+ # Return a permutation of 'array' where each element validates to the
22
+ # same index in a 'conditions' array of procs that return Boolean.
23
+ #
24
+ # The output is an array that is a complete permutation of the input array.
25
+ # i.e. output.length == array.length
26
+ #
27
+ # Any elements in the array that are extra to the number of conditions
28
+ # will be assumed valid.
29
+ #
30
+ # array = [1,2,3,4,5].shuffle
31
+ # conditions = [
32
+ # proc { |arr, elem| elem < 2},
33
+ # proc { |arr, elem| elem > 2},
34
+ # proc { |arr, elem| elem > 1}
35
+ # ]
36
+ # array.conditional_permutation(conditions)
37
+ #
38
+ # possible output => [1, 3, 4, 5, 2]
39
+ #
40
+ # Will not work on arrays that contain nil values.
41
+ #
42
+ def conditional_permutation conditions, seconds = nil
43
+ timeout_rescue(seconds, []) do
44
+ conditional_permutation_recurse(self.to_a, conditions)
45
+ end
46
+ end
47
+
48
+ ##
49
+ # Return values from 'array' where each element validates to the same
50
+ # index in a 'conditions' array of procs that return Boolean.
51
+ #
52
+ # The output is an array of conditions.length that is a partial
53
+ # permutation of the input array, where satisfies only the conditions.
54
+ #
55
+ # Any elements in the array that are extra to the number of conditions
56
+ # will not be output.
57
+ #
58
+ # array = [1,2,3,4,5].shuffle
59
+ # conditions = [
60
+ # proc { |arr, elem| elem < 2},
61
+ # proc { |arr, elem| elem > 2},
62
+ # proc { |arr, elem| elem > 1}
63
+ # ]
64
+ # array.conditional_sample(conditions)
65
+ #
66
+ # possible output => [1, 5, 3]
67
+ #
68
+ # Will not work on arrays that contain nil values.
69
+ #
70
+ def conditional_sample conditions, seconds = nil
71
+ timeout_rescue(seconds, []) do
72
+ conditional_sample_recurse(self.to_a, conditions)
73
+ end
74
+ end
75
+
76
+ end
77
+
78
+ end
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env ruby
2
+ # Encoding: UTF-8
3
+
4
+ module ConditionalSample
5
+
6
+ ##
7
+ # The number of the current version.
8
+ #
9
+ def self.version_number
10
+ major = 0
11
+ minor = 1
12
+ tiny = 0
13
+ pre = nil
14
+
15
+ string = [major, minor, tiny, pre].compact.join('.')
16
+ end
17
+
18
+ ##
19
+ # The date of the current version.
20
+ #
21
+ def self.version_date
22
+ '2017-06-27'
23
+ end
24
+ end
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+ # Encoding: UTF-8
3
+
4
+ require 'timeout'
5
+
6
+ module ConditionalSample
7
+
8
+ ##
9
+ # Require everything in the subdirectory.
10
+ #
11
+ Dir[File.dirname(__FILE__) + '/*/*.rb'].each { |file| require file }
12
+
13
+ ##
14
+ # Add the instance methods to the Array class.
15
+ #
16
+ Array.include ConditionalSample::MixMe
17
+
18
+ end
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env ruby
2
+ # Encoding: UTF-8
3
+
4
+ ################################################################################
5
+
6
+ require File.dirname(__FILE__) + '/spec_helper.rb'
7
+
8
+ ################################################################################
9
+
10
+ describe ConditionalSample, "basic behaviour" do
11
+
12
+ # Input values.
13
+ let(:numbers) { (1..5).to_a }
14
+ let(:conditions) {
15
+ [
16
+ proc { |arr, elem| elem < 2},
17
+ proc { |arr, elem| elem > 4},
18
+ proc { |arr, elem| elem > 1}
19
+ ]
20
+ }
21
+ let(:output_sample) { [1, 5, 2] }
22
+ let(:output_permutation) { [1, 5, 2, 3, 4] }
23
+
24
+ it "should output the correct results" do
25
+ result = numbers.conditional_sample(conditions)
26
+ expect(result).to eq output_sample
27
+
28
+ result = numbers.conditional_permutation(conditions)
29
+ expect(result).to eq output_permutation
30
+ end
31
+
32
+ it "when shuffled, should output an array of the correct length" do
33
+ result = numbers.shuffle.conditional_sample(conditions)
34
+ expect(result.count).to be conditions.count
35
+ expect(result[0]).to be 1
36
+ expect(result[1]).to be 5
37
+
38
+ result = numbers.shuffle.conditional_permutation(conditions)
39
+ expect(result.count).to be numbers.count
40
+ expect(result[0]).to be 1
41
+ expect(result[1]).to be 5
42
+ end
43
+ end
44
+
45
+ ################################################################################
46
+
47
+ describe ConditionalSample, "mixin behaviour" do
48
+
49
+ # Input and output values.
50
+ let(:numbers) { (1..5).to_a }
51
+ let(:conditions) {
52
+ [
53
+ proc { |arr, elem| elem < 2},
54
+ proc { |arr, elem| elem > 4},
55
+ proc { |arr, elem| elem > 1}
56
+ ]
57
+ }
58
+ let(:output_sample) { [1, 5, 2] }
59
+ let(:output_permutation) { [1, 5, 2, 3, 4] }
60
+
61
+ it "should correctly work with a Struct implementing #to_a" do
62
+
63
+ # Attempt to mix in using extend on a Struct instance.
64
+ struct = Struct.new(:contents) do
65
+ def to_a
66
+ contents.to_a
67
+ end
68
+ end.new numbers
69
+ struct.extend ConditionalSample::MixMe
70
+
71
+ # Run the methods, and compare results.
72
+ result = struct.conditional_sample(conditions)
73
+ expect(result).to eq output_sample
74
+
75
+ result = struct.conditional_permutation(conditions)
76
+ expect(result).to eq output_permutation
77
+ end
78
+
79
+ it "should fail on a Struct not implementing #to_a" do
80
+
81
+ # Attempt to mix in using extend on a Struct instance.
82
+ struct = Struct.new(:contents).new numbers
83
+ struct.extend ConditionalSample::MixMe
84
+
85
+ # Run the methods, and expect that they will fail.
86
+ expect do
87
+ struct.conditional_sample(conditions)
88
+ end.to raise_error NoMethodError
89
+
90
+ expect do
91
+ struct.conditional_permutation(conditions)
92
+ end.to raise_error NoMethodError
93
+ end
94
+ end
95
+
96
+ ################################################################################
@@ -0,0 +1,223 @@
1
+ #!/usr/bin/env ruby
2
+ # Encoding: UTF-8
3
+
4
+ ################################################################################
5
+
6
+ require File.dirname(__FILE__) + '/spec_helper.rb'
7
+
8
+ ################################################################################
9
+
10
+ describe ConditionalSample, "examples" do
11
+
12
+ it "should output the correct results: Basic example" do
13
+
14
+ # 5 element input array.
15
+ numbers = (1..5).to_a
16
+
17
+ # 3 element conditions array.
18
+ conditions = [
19
+ proc { |arr, elem| elem < 3 },
20
+ proc { |arr, elem| elem > 3 },
21
+ proc { |arr, elem| elem > 1 }
22
+ ]
23
+
24
+ # These will always return the below output
25
+ # because they are the first values that match.
26
+ permut = numbers.conditional_permutation(conditions)
27
+ sample = numbers.conditional_sample(conditions)
28
+ expect(permut).to eq [1, 4, 2, 3, 5]
29
+ expect(sample).to eq [1, 4, 2]
30
+
31
+ # To get a random sample, #shuffle the array first.
32
+ # These results will vary based on the shuffle.
33
+ srand(42)
34
+ shuf = numbers.shuffle
35
+ shuf_permut = shuf.conditional_permutation(conditions)
36
+ shuf_sample = shuf.conditional_sample(conditions)
37
+ expect(shuf_permut).to eq [2, 5, 3, 1, 4]
38
+ expect(shuf_sample).to eq [2, 5, 3]
39
+ end
40
+
41
+ ##############################################################################
42
+
43
+ it "should output the correct results: Random rhyming lines" do
44
+
45
+ # Use this gem to get the rhyme of the line's final word.
46
+ require 'ruby_rhymes'
47
+
48
+ # Input lines, just two limericks concat together.
49
+ lines = [
50
+ "There was a young rustic named Mallory,",
51
+ "who drew but a very small salary.",
52
+ "When he went to the show,",
53
+ "his purse made him go",
54
+ "to a seat in the uppermost gallery.",
55
+ "There was an Old Man with a beard,",
56
+ "Who said, 'It is just as I feared!—",
57
+ "Two Owls and a Hen,",
58
+ "four Larks and a Wren,",
59
+ "Have all built their nests in my beard.'"
60
+ ]
61
+
62
+ # Output a couplet of any two lines that rhyme.
63
+ couplet_conditions = [
64
+ proc { |arr, elem| true },
65
+ proc { |arr, elem| elem.to_phrase.rhyme_key == arr.last.to_phrase.rhyme_key }
66
+ ]
67
+ srand(42)
68
+ results = lines.shuffle.conditional_sample(couplet_conditions)
69
+ expect(results).to eq ["four Larks and a Wren,", "Two Owls and a Hen,"]
70
+
71
+ # Output a jumbled limerick from the input lines.
72
+ limerick_conditions = [
73
+ proc { |arr, elem| true },
74
+ proc { |arr, elem| elem.to_phrase.rhyme_key == arr[0].to_phrase.rhyme_key },
75
+ proc { |arr, elem| elem.to_phrase.rhyme_key != arr[0].to_phrase.rhyme_key },
76
+ proc { |arr, elem| elem.to_phrase.rhyme_key == arr[2].to_phrase.rhyme_key },
77
+ proc { |arr, elem| elem.to_phrase.rhyme_key == arr[0].to_phrase.rhyme_key }
78
+ ]
79
+ srand(42)
80
+ results = lines.shuffle.conditional_sample(limerick_conditions)
81
+ expect(results).to eq ["who drew but a very small salary.",
82
+ "There was a young rustic named Mallory,",
83
+ "four Larks and a Wren,",
84
+ "Two Owls and a Hen,",
85
+ "to a seat in the uppermost gallery."
86
+ ]
87
+
88
+ end
89
+
90
+ ##############################################################################
91
+
92
+ it "should output the correct results: Logic puzzle 1" do
93
+
94
+ # Create an array of procs, one for each rule.
95
+ rules = []
96
+
97
+ # E has to be in the middle.
98
+ rules << proc do |arr, elem|
99
+ !(arr.count == 2) || elem == 'e'
100
+ end
101
+
102
+ # B and D must always sit together.
103
+ rules << proc do |arr, elem|
104
+ if elem == 'b' and arr.include?('d')
105
+ arr.last == 'd'
106
+ elsif elem == 'd' and arr.include?('b')
107
+ arr.last == 'b'
108
+ else
109
+ true
110
+ end
111
+ end
112
+
113
+ # B and E can't sit together.
114
+ rules << proc do |arr, elem|
115
+ if elem == 'b'
116
+ arr.last != 'e'
117
+ elsif elem == 'e'
118
+ arr.last != 'b'
119
+ else
120
+ true
121
+ end
122
+ end
123
+
124
+ # C and D must have exactly two people between them.
125
+ rules << proc do |arr, elem|
126
+ if elem == 'c' and arr.include?('d')
127
+ arr[-3] == 'd'
128
+ elsif elem == 'd' and arr.include?('c')
129
+ arr[-3] == 'c'
130
+ else
131
+ true
132
+ end
133
+ end
134
+
135
+ # A must always sit to the left of C.
136
+ rules << proc do |arr, elem|
137
+ !(elem == 'c') || arr.include?('a')
138
+ end
139
+
140
+ # Method to apply all the rules to a given |arr, elem|
141
+ def apply_all(rules, arr, elem)
142
+ rules.all? { |p| p.call(arr, elem) }
143
+ end
144
+
145
+ # The names of E and her friends.
146
+ people = ['a', 'b', 'c', 'd', 'e']
147
+
148
+ # Conditions array that implements all the rules
149
+ conditions = people.count.times.map do
150
+ proc { |arr, elem| apply_all(rules, arr, elem) }
151
+ end
152
+
153
+ # Output the permutation that satisfies all rules.
154
+ 20.times do
155
+ result = people.shuffle.conditional_permutation(conditions)
156
+ expect(result).to eq ['b', 'd', 'e', 'a', 'c']
157
+ end
158
+ end
159
+
160
+ ##############################################################################
161
+
162
+ it "should output the correct results: Logic puzzle 2" do
163
+
164
+ # Make sure certain people don't sit next to each other.
165
+ # e.g. "A is not next to B (or C, or D)"
166
+ def person_not_next_to(arr, elem, subject, *people)
167
+ if elem == subject
168
+ people.all? do |person|
169
+ !arr.include?(person) || arr.last != person
170
+ end
171
+ elsif people.include?(elem)
172
+ !arr.include?(subject) || arr.last != subject
173
+ else
174
+ true
175
+ end
176
+ end
177
+
178
+ # Create an array of procs, one for each rule.
179
+ rules = []
180
+
181
+ # Colin is not next to Kate.
182
+ rules << proc do |arr, elem|
183
+ person_not_next_to(arr, elem, 'Colin', 'Kate')
184
+ end
185
+
186
+ # Emily is not next to Fred or Kate.
187
+ rules << proc do |arr, elem|
188
+ person_not_next_to(arr, elem, 'Emily', 'Fred', 'Kate')
189
+ end
190
+
191
+ # Neither Kate or Emily are next to Irene.
192
+ rules << proc do |arr, elem|
193
+ person_not_next_to(arr, elem, 'Irene', 'Kate', 'Emily')
194
+ end
195
+
196
+ # And Fred should sit on Irene's left.
197
+ rules << proc do |arr, elem|
198
+ !(elem == 'Irene') || arr.last == 'Fred'
199
+ end
200
+
201
+ # Method to apply all the rules to a given |arr, elem|
202
+ def apply_all(rules, arr, elem)
203
+ rules.all? { |p| p.call(arr, elem) }
204
+ end
205
+
206
+ # The names of the wedding guests.
207
+ people = ['Colin', 'Emily', 'Kate', 'Fred', 'Irene']
208
+
209
+ # Conditions array that implements all the rules.
210
+ conditions = people.count.times.map do
211
+ proc { |arr, elem| apply_all(rules, arr, elem) }
212
+ end
213
+
214
+ # Output the permutation that satisfies all rules.
215
+ 20.times do
216
+ result = people.shuffle.conditional_permutation(conditions).reverse
217
+ expect(result).to eq ['Emily', 'Colin', 'Irene', 'Fred', 'Kate']
218
+ end
219
+ end
220
+
221
+ end
222
+
223
+ ################################################################################
@@ -0,0 +1,5 @@
1
+ require 'bundler/setup'
2
+ Bundler.setup
3
+
4
+ $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
5
+ require 'conditional_sample'
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env ruby
2
+ # Encoding: UTF-8
3
+
4
+ ################################################################################
5
+
6
+ require File.dirname(__FILE__) + '/spec_helper.rb'
7
+
8
+ ################################################################################
9
+
10
+ # Using the timeout function is recommended when first developing a
11
+ # project, as it may not be transparent how computationally expensive
12
+ # a condition array will be.
13
+
14
+ describe ConditionalSample, "timeout" do
15
+
16
+ # Even if a conditions array can be fulfilled using the corpus array,
17
+ # it may just take too long.
18
+ it "should output [] or valid array if conditions are possible but long" do
19
+
20
+ # Read in the Rakefile, just as an example.
21
+ # Multiply the array, so there's more rows.
22
+ # Add just a few unique needles to this haystack.
23
+ lines = File.read('Rakefile').split("\n")
24
+ lines *= 500000
25
+ lines << 'unique_string_1'
26
+ lines << 'unique_string_2'
27
+ lines << 'unique_string_3'
28
+
29
+ # The first 3 will be easy to fill, as there are multiple valid lines.
30
+ # The rest will be harder, as only one line matches each.
31
+ conditions = [
32
+ proc { |arr, elem| elem.match(/spec/i) },
33
+ proc { |arr, elem| elem.match(/core/i) },
34
+ proc { |arr, elem| elem.match(/rspec/i) },
35
+ proc { |arr, elem| elem.match(/unique_string_1/i) },
36
+ proc { |arr, elem| elem.match(/unique_string_2/i) },
37
+ proc { |arr, elem| elem.match(/unique_string_3/i) }
38
+ ]
39
+
40
+ # Exact timing will depend on the results of the 'Array#shuffle'.
41
+ # So this may or may not succeed.
42
+ results = lines.shuffle.conditional_sample(conditions, 2.5)
43
+
44
+ # Output will either be a 6 element or an empty array.
45
+ expect([0, 6]).to include(results.count)
46
+ end
47
+
48
+ ##############################################################################
49
+
50
+ # Example of using the Timeout module to ensure CPUs aren't locked up
51
+ # until the heat-death of the universe.
52
+ #
53
+ # This will never succeed, because the corpus array and the conditions
54
+ # array are incompatible. Without a timeout, it would calculate all
55
+ # permutations, in factorial time, before telling you it's impossible.
56
+ #
57
+ # Using the timeout function is recommended when first developing a
58
+ # project, as it may not be transparent how computationally expensive
59
+ # a condition array will be.
60
+ it "should output [] if conditions are impossible" do
61
+
62
+ # Read in the Rakefile, just as an example.
63
+ # Multiply the array, so there's more rows.
64
+ # Add just a few unique needles to this haystack.
65
+ lines = File.read('Rakefile').split("\n")
66
+ lines *= 500000
67
+ lines << 'unique_string_1'
68
+ lines << 'unique_string_2'
69
+ lines << 'unique_string_3'
70
+
71
+ # Look at the last condition! It will never return true!
72
+ # So this will take ages, unless we use the timer argument.
73
+ conditions = [
74
+ proc { |arr, elem| elem.match(/core/i) },
75
+ proc { |arr, elem| elem.match(/rspec/i) },
76
+ proc { |arr, elem| elem.match(/unique_string_1/i) },
77
+ proc { |arr, elem| elem.match(/unique_string_2/i) },
78
+ proc { |arr, elem| elem.match(/unique_string_3/i) },
79
+ proc { |arr, elem| elem.match(/unique_string_4/i) }
80
+ ]
81
+
82
+ # Give up after some time.
83
+ results = lines.shuffle.conditional_sample(conditions, 2.5)
84
+
85
+ # Output will always be [], no matter how long we give it.
86
+ expect(results).to eq []
87
+ end
88
+
89
+ end
90
+
91
+ ################################################################################
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: conditional_sample
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Paul Thompson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-06-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.13'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.13'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: ruby_rhymes
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.1'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.1'
69
+ description: Patch the Array with a couple of nice methods for sampling based on the
70
+ results of an array of Boolean procs. Array is sampled using the procs as conditions
71
+ that each specific array index element must conform to.
72
+ email:
73
+ - nossidge@gmail.com
74
+ executables: []
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - ".gitignore"
79
+ - ".rspec"
80
+ - Gemfile
81
+ - LICENSE
82
+ - README.md
83
+ - Rakefile
84
+ - conditional_sample.gemspec
85
+ - lib/conditional_sample.rb
86
+ - lib/conditional_sample/private.rb
87
+ - lib/conditional_sample/public.rb
88
+ - lib/conditional_sample/version.rb
89
+ - spec/conditional_sample_spec.rb
90
+ - spec/examples_spec.rb
91
+ - spec/spec_helper.rb
92
+ - spec/timeout_spec.rb
93
+ homepage: https://github.com/nossidge/conditional_sample
94
+ licenses:
95
+ - GPL-3.0
96
+ metadata: {}
97
+ post_install_message:
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ requirements: []
112
+ rubyforge_project:
113
+ rubygems_version: 2.5.2
114
+ signing_key:
115
+ specification_version: 4
116
+ summary: Array sampling based on an input array of Boolean procs
117
+ test_files: []