Hi Ben,
Daniel, I started out with something similar to that,
although yours is alot better, but then thought it would
be nice to have something like random 'a'..'z' aswell
That could be useful, at least for single-character strings.
Luckily, there was a just quiz about random choosing, so we
can steal the winning algorithm from there.
Here's a more extensive implementation:
def random(object)
if object.kind_of? Numeric
if object.integer?
rand(object)
else
rand * object
end
else
object.random
end
end
# Inefficient fallback implementation.
module Enumerable
def random(n=nil) to_a.random(n) end
end
class Range
def bounds ; [self.begin, self.end] end
def lower_bound ; bounds.sort.first end
def upper_bound ; bounds.sort.last end
def numeric? ; bounds.all? { |x| x.kind_of? Numeric } end
def integral? ; numeric? and bounds.all? { |x| x.integer? } end
def include_end? ; not exclude_end? end
def size
d = upper_bound - lower_bound
if include_end? and d.respond_to? :succ
then d.succ else d end
end
def random(n=nil)
if integral?
if n.nil?
rand(size) + lower_bound
else
n.integer? or raise ArgumentError,
"can't choose a non-integral (#{n}) number of elements"
n <= size or raise ArgumentError,
"can't choose #{n} out of #{size} elements"
n >= 0 or raise ArgumentError,
"can't choose a negative (#{n}) number of elements"
hash = {}
hash[random] = true while hash.size < n
hash.keys.sort_by { rand }
end
elsif numeric?
if n.nil?
rand * size + lower_bound
else
values =
n.times { values << random }
values
end
else
super
end
end
end
class Array
def random(n=nil)
if n.nil?
self[rand(size)]
else
values_at(*(0...size).random(n)).sort_by { rand }
end
end
end
There's some overkill randomization going on in Array#random
where we first grab n random numbers in random order, and
then select the values at those indices and randomize
*their* order. It would be sufficient to grab n random
numbers in whatever order (not necessarily random), select
the values at those indices, and then randomize the result.
If you populate a hash with random float keys, I'm not sure
in what order the keys come out when you call Hash#keys.
Could the order be random? I assume not, but if that's so,
we could get rid of the ‘sort_by { rand }’ in Range#random.
Actually, I'm not even sure whether Array#random needs to
return the elements in random order. Maybe it could return
them in whatever order is most convenient, and leave it to
the user to randomize or sort the result according to need.
Oh, and we could add this convenience method:
module Enumerable
def shuffle
sort_by { rand }
end
end
So you'd do one of these:
* foo.random(4) # Order doesn't matter.
* foo.random(4).sort # Increasing order.
* foo.random(4).shuffle # Random order.
Okay, I'm convinced — let's change Array#random accordingly:
class Array
def random(n=nil)
if n.nil?
self[rand(size)]
else
values_at(*(0...size).random(n))
end
end
end
I think my implementation of Hash#random is pretty much as
good as it gets, but if anyone can think of a better way,
please share it. I don't particularly like the way I'm
first building an array, then flattening it, splatting it,
and finally turning it back into a hash.
Here it is, anyway:
class Hash
def pair_at(key) [key, self[key]] end
def pairs_at(*keys) keys.collect { |k| pair_at k } end
def random(n=nil)
if n.nil?
pair_at(keys.random)
else
Hash[*pairs_at(*keys.random(n)).flatten]
end
end
end
I think returning a subhash (except when n is nil) seems the
most useful, since if you just want some random values or
keys from the hash, that's easy:
hash.values.random(123) hash.keys.random(123)
It's easy to adapt other collections to this API:
class Set
def random(n=nil) Set[*super] end
end
Which i'll probably not need anyway so i'll 'borrow' yours
if I may.
Of course, help yourself. You may want to put all this in a
separate file — say, ‘random.rb’.
The above code will let you do all these:
random(42) #=> 13 random(2.0) #=> 1.9656...
random(1..9) #=> 5 random(2.3..5.8) #=> 3.1246...
random("a".."z") #=> "f" random("a".."zz") #=> "ge"
[1, 1, 2, 3, 5, 8].random #=> 2
[1, 1, 2, 3, 5, 8].random(3) #=> [1, 1, 5]
{:a=>1, :b=>2, :c=>3}.random #=> [:b, 2]
{:a=>1, :b=>2, :c=>3}.random(2) #=> {:a=>1, :c=>3}
Set[1, 2, 3, "x", "y", "z"].random(3) #=> Set[2, 3, "y"]
···
--
Daniel Brockman <daniel@brockman.se>
So really, we all have to ask ourselves:
Am I waiting for RMS to do this? --TTN.