From: Daniel Brockman Date: 2005-07-20T09:48:38+09:00 Subject: Re: Regarding rand 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 So really, we all have to ask ourselves: Am I waiting for RMS to do this? --TTN.