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 +7 -0
- data/.gitignore +68 -0
- data/.rspec +1 -0
- data/Gemfile +1 -0
- data/LICENSE +13 -0
- data/README.md +348 -0
- data/Rakefile +8 -0
- data/conditional_sample.gemspec +29 -0
- data/lib/conditional_sample/private.rb +138 -0
- data/lib/conditional_sample/public.rb +78 -0
- data/lib/conditional_sample/version.rb +24 -0
- data/lib/conditional_sample.rb +18 -0
- data/spec/conditional_sample_spec.rb +96 -0
- data/spec/examples_spec.rb +223 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/timeout_spec.rb +91 -0
- metadata +117 -0
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,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
|
+
################################################################################
|
data/spec/spec_helper.rb
ADDED
@@ -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: []
|