Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

I want to generate a random alphanumeric string in ruby, as succinctly and efficiently as possible. The following solution works, but is obviously not very efficient.

Please review the following code, bearing in mind that I am trying to keep it as succinct and readable as possible while improving efficiency. Be sure to include a detailed explanation in your answer.

([nil]*8).map { ((48..57).to_a+(65..90).to_a+(97..122).to_a).sample.chr }.join
share|improve this question

We're looking for long answers that provide some explanation and context. Don't just give a one-line answer; explain why your answer is right, ideally with citations. Answers that don't include explanations may be removed.

3 Answers 3

up vote 8 down vote accepted

You could use

Array.new(8){[*'0'..'9', *'a'..'z', *'A'..'Z'].sample}.join

From the documentation for Array:

new(size=0, obj=nil) new(array) new(size) {|index| block }

Returns a new array. In the first form, the new array is empty. In the second it is created with size copies of obj (that is, size references to the same obj). The third form creates a copy of the array passed as a parameter (the array is generated by calling to_ary on the parameter). In the last form, an array of the given size is created. Each element in this array is calculated by passing the element’s index to the given block and storing the return value.

Perhaps it is better to create the array with the valid characters once in advance:

range = [*'0'..'9', *'a'..'z', *'A'..'Z']
Array.new(8){range.sample}.join

I made a Benchmark for the solutions.

require 'benchmark'
N = 10_000 #Number of Test loops

Benchmark.bmbm(10) {|b|

  b.report('Nat'      ) { N.times { ([nil]*8).map { ((48..57).to_a+(65..90).to_a+(97..122).to_a).sample.chr }.join } }
  b.report('tokland') { N.times { 8.times.map { [*'0'..'9', *'a'..'z', *'A'..'Z'].sample }.join } }
  b.report('knut'   ) { N.times { Array.new(8){[*'0'..'9', *'a'..'z', *'A'..'Z'].sample}.join } }
  b.report('Natinit') { 
    range = ((48..57).to_a+(65..90).to_a+(97..122).to_a)
    N.times { ([nil]*8).map { range.sample.chr }.join }
  }
  b.report('knutinit') { 
      range = ((48..57).to_a+(65..90).to_a+(97..122).to_a)
      N.times { Array.new(8){range.sample}.join }
  }

} #Benchmark

The result:

Rehearsal ---------------------------------------------
Nat         0.765000   0.000000   0.765000 (  0.765625)
tokland     2.172000   0.000000   2.172000 (  2.171875)
knut        1.953000   0.000000   1.953000 (  1.984375)
Natinit     0.063000   0.000000   0.063000 (  0.062500)
knutinit    0.078000   0.000000   0.078000 (  0.078125)
------------------------------------ total: 5.031000sec

                user     system      total        real
Nat         0.781000   0.000000   0.781000 (  0.781250)
tokland     1.953000   0.000000   1.953000 (  1.968750)
knut        1.922000   0.000000   1.922000 (  1.921875)
Natinit     0.063000   0.000000   0.063000 (  0.062500)
knutinit    0.078000   0.000000   0.078000 (  0.078125)

@Nat: Gratulation, your version is the fastest ;)

To analyze the fastest way to build the range I used the following benchmark:

Benchmark.bmbm(10) {|b|

  b.report('Natinit') { 
    N.times { ((48..57).to_a+(65..90).to_a+(97..122).to_a) }
  }
  b.report('knutinit') { 
      N.times { [*'0'..'9', *'a'..'z', *'A'..'Z'] }
  }
} #Benchmark

Result:

Rehearsal ---------------------------------------------
Natinit     0.093000   0.000000   0.093000 (  0.093750)
knutinit    0.250000   0.000000   0.250000 (  0.250000)
------------------------------------ total: 0.343000sec

    user     system      total        real
Natinit     0.094000   0.000000   0.094000 (  0.093750)
knutinit    0.219000   0.000000   0.219000 (  0.218750)
share|improve this answer
    
That's not what I was expecting. Combining our code into range = ('0'..'9').to_a+('a'..'z').to_a+('A'..'Z').to_a; N.times { Array.new(8){ range.sample }.join } makes for even better performance. I guess the main difference is that concatenation is more efficiency that exploding. –  Nat Sep 28 '12 at 10:35
1  
it's always interesting to see benchmarks, but in this case, unless you need to generate millions and millions of strings, I don't see the point. Go for the most clear solution to you. –  tokland Sep 28 '12 at 11:58

I think this is both declarative and concise:

8.times.map { [*'0'..'9', *'a'..'z', *'A'..'Z'].sample }.join

It works because:

  1. You can call map on times since >= 1.8.7 made it return an enumerator when called without block (a lot of methods do this now: each, each_cons, each_slice, ...).
  2. You can build a range with objects of any class as long as they support <=> and succ.
  3. You can explode any enumerable. If you don't like the exploding syntax, use ['0'..'9', 'a'..'z', 'A'..'Z'].flat_map(&:to_a).

The only problem with this being a one-liner (as requested), it's that the array is re-created on every iteration. It would be better to assign it once to a variable outside the block:

cs = [*'0'..'9', *'a'..'z', *'A'..'Z']
8.times.map { cs.sample }.join

Alternatevely, you can write:

cs = [*'0'..'9', *'a'..'z', *'A'..'Z']
Array.new(8) { cs.sample }.join
share|improve this answer
    
ah, didn't know you could call map on times or do ranges of characters, or explode ranges! nice one. –  Nat Sep 26 '12 at 23:40
    
I don't think anyone should use n.times.map{ -- use Array.new(n){ instead. –  Nakilon Jul 1 at 2:50

I've reviewed many means of random string generation of many, but this form is always the one I come back to:

rand(36**7...36**8).to_s(36)

It's a bit rougher to the more pleasing alternative rand(36**8).to_s(36), but it's also more accurate.

I also don't really care for Ruby's benchmarking class (read: I haven't bothered to learn how to utilize it), so I instead just timed how long using your original method takes to generate 100000 iterations versus the code above:

time echo "100000.times {puts ([nil]*8).map { ((48..57).to_a+(65..90).to_a+(97..122).to_a).sample.chr }.join}" | ruby
# ruby  8.37s user 0.55s system 99% cpu 8.926 total

time echo "100000.times {puts rand(36**7..(36**8)-1).to_s(36)}" | ruby
# ruby  0.59s user 0.34s system 97% cpu 0.949 total

In other words, while your original method occurs at a rate of 11,203 iterations per second. The code above occurs at a rate of 105,400 iterations per second. I'm sure this will vary from computer to computer, OS to OS, but anyone is certainly free to try this themselves.

share|improve this answer
    
ooh, thats clever, took me a minute to get how it works though. Can it be made to include capital letters? –  Nat Oct 2 '12 at 13:02
1  
nice! If you use the "..." vs. the ".." in the range can you remove the -1? :-) –  engineerDave Oct 2 '12 at 22:22
    
@engineerDave Good call! Updated the answer. –  Kyle Lacy Oct 3 '12 at 11:40
    
@Nat I don't believe capital letters can be used, unfortunately. –  Kyle Lacy Oct 3 '12 at 11:47

protected by 200_success Feb 19 at 22:53

Thank you for your interest in this question. Because it has attracted low-quality answers, posting an answer now requires 10 reputation on this site.

Would you like to answer one of these unanswered questions instead?

Not the answer you're looking for? Browse other questions tagged or ask your own question.