1

If I have a range of numbers from 0 - 70, I want to pick five random numbers and the sum of random numbers to be higher then 140 and lower then 220. I want to do that until the sum of five random numbers is in the range 140..220, and then display those five random numbers.*

This is what I did so far:

5.times {r=rand (0..70); puts "#{r}"}

or:

5.times {puts "#{[*0...70].sample}"}
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Kanu Kabal
  • 13
  • 4

5 Answers5

3

If you need the actual digits, rather than the sum, use this one liner:

nums = [*0..70].sample(5) until (140..220) === (nums || []).inject(:+)

If the digits can be repeated, use this instead:

nums = 5.times.collect {rand(0..70)} until (140..220) === (nums || []).inject(:+)

Multiline version of the second one, with no stat error (hat tip samuil):

digits = []
valid = 140..220
until valid === digits.inject(:+)
  digits = []
  5.times { digits << rand(0..70) }
end
Denis de Bernardy
  • 75,850
  • 13
  • 131
  • 154
  • What's the purpose of `|| []` in this context? – Peter Alfvin Jun 22 '13 at 11:57
  • @PeterAlfvin: to avoid a no method error on the first pass because `nums` is then nil. – Denis de Bernardy Jun 22 '13 at 11:58
  • @samuil: it reads like english to me, but I suppose you might have a point... It could always be rewritten in multiple lines if needed. :-) – Denis de Bernardy Jun 22 '13 at 12:03
  • 1
    @PeterAlfvin: It's exactly this `nil` on the first pass that sours many usecases of onliner `if`, `unless`, `until` etc. for me. – Boris Stitnicky Jun 22 '13 at 12:04
  • Your [*0..70] * 5 trick is not equivalent of taking random number every time, as it yields different probability of repetitions. – samuil Jun 22 '13 at 12:11
  • @BorisStitnicky: It makes me want a straightforward way of expressing a loop where you want to execute it once before evaluating the terminating condition. – Peter Alfvin Jun 22 '13 at 12:14
  • @samuil: My stats are rusty. Are you sure that picking two random digits from `[1,2,3,1,2,3]` is not equivalent to picking a random number from `[1,2,3]` twice? – Denis de Bernardy Jun 22 '13 at 12:14
  • @Denis: after selecting first digit, you are left with a set, that contains every digit twice, and the one you have chosen once -- chance of getting two identical numbers is different from picking number twice from the same set. – samuil Jun 22 '13 at 12:16
  • @PeterAlfvin: Me too. I'm already looking forward to the `ergo` method proposed in Ruby core that will simply `yield self`, but something should be done about those `nil`s, too. Maybe Matz considers it a feature or something. – Boris Stitnicky Jun 22 '13 at 12:21
  • I think `rand(71)` would be preferable to sampling a haysack, for the case where numbers can be reused. – Peter Alfvin Jun 22 '13 at 12:25
  • :-) Should really fix the one line version as well - for both issues. – Peter Alfvin Jun 22 '13 at 12:29
  • @PeterAlfvin: I'm leaning towards keeping the initial reply around for posterity. The line break and subsequent edit hopefully makes it clear enough that the initial answer had issues. If you disagree, you're welcome to edit it accordingly. – Denis de Bernardy Jun 22 '13 at 12:31
  • @Denis: I edited it (subject to peer review) before I read your comment. Hadn't thought about the posterity issue, but I think showing `collect` is helpful as well. – Peter Alfvin Jun 22 '13 at 12:37
  • I would use `rand(0..70)` instead `rand(71)`. More universal and more readable. – Hauleth Jun 22 '13 at 13:28
  • rand(71) has its basis in languages since early 80's, though. So not sure about the universality argument. – vgoff Jun 22 '13 at 16:02
  • @ŁukaszNiemier: Thanks for the `rand(0..70)` suggestion. I didn't know `rand` took a range, let alone that it was more universal, as I'd only seen it with no argument or an integer argument. I agree it's more readable. – Peter Alfvin Jun 22 '13 at 16:04
2

Not very short answer, but seems to be very clear, and ruby-style.

e = Enumerator.new do |y| 
  loop do 
    y << 5.times.map { rand(0..70) }
  end
end

e.find { |ary| (140..220).include? ary.inject(&:+) }

First, we define enumerator, that returns batches of numbers, randomized from 0..70 range. Later, using #find method we're looking for first entry, that will satisfy condition of sum in (140..220) range.

samuil
  • 5,001
  • 1
  • 37
  • 44
1

In pure Ruby

sum = [*0..70].sample(5).inject(&:+) until (140..220) === sum

if you use ActiveSupport then you can use #sum instead of inject(&:+).

Hauleth
  • 22,873
  • 4
  • 61
  • 112
  • Your code will return unique elements, while question samples allow repetition. – samuil Jun 22 '13 at 11:28
  • 1
    Ampersand before `:+` no longer necessary today. – Boris Stitnicky Jun 22 '13 at 11:32
  • I keep it for compatibility, but yes. It isn't needed now (in `#inject`). I use pretzel-colon syntax in `#map`, where it is still needed, so it is "more natural" for me. – Hauleth Jun 22 '13 at 11:35
  • 1
    I believe that "no longer necessary" is not true. Few methods were implementing `to_proc`-like behavior, which in ruby 1.9 was promoted to language feature with `&:` notation. Pretzel-colon is language feature, which will work for every method, so it is preferable to use it. – samuil Jun 22 '13 at 11:37
  • 1
    @samuil: `inject` accepts a symbol from the ancient 1.8.7, I see no reason not to use it. – tokland Jun 22 '13 at 11:57
  • 1.8.7 had backported 1.9 feature of `&:`, so I believe that it accepted symbol in 1.8.6 at least. The reason not to use it is consistency. – samuil Jun 22 '13 at 12:04
  • @ŁukaszNiemier please note that this solution returns sum, instead of actual numbers that yielded the matching result. – samuil Jun 22 '13 at 12:12
  • @samuil: You could also point out that 140..220 also includes the boundary elements, but nitpicking is not the point. The point is to see and learn from various solutions by various people :-) – Boris Stitnicky Jun 22 '13 at 12:15
1

Just for fun, that's the most declarative code I could come up with:

repeat { (0..70).sample(5) }.detect { |xs| xs.reduce(:+).in?(140...220) }

The implementation of the non-existing abstractions are left as an exercise for the reader :-)

tokland
  • 66,169
  • 13
  • 144
  • 170
  • That's a good one, too. May I add spaces around your parenthesis, or it's your style? – Boris Stitnicky Jun 22 '13 at 12:07
  • Is there `repeat` function in ruby, that works like what you have proposed? I can't find it. – samuil Jun 22 '13 at 12:14
  • Well, your abstractions are translating directly to my answer :) – samuil Jun 22 '13 at 12:17
  • samuil: No, you'd have to write it (same for `sample` for ranges, and `in?`). For example: `def repeat; Enumerator.new { |y| loop { y.yield(yield) } }; end` – tokland Jun 22 '13 at 12:17
  • @samuil: It's `loop`, do not nitpick :-) – Boris Stitnicky Jun 22 '13 at 12:19
  • Now that I see it, yes, it's exactly the same idea, just abstracting things (I specially dislike to use `include?`, nobody thinks in those terms, Ruby should have an `in?` like Python. IMHO, that's it). – tokland Jun 22 '13 at 12:19
-1
boundary = 140..220
numbers = *0..70
begin
  numbers.sample( 5 ).reduce( :+ ).tap do |sum|
    fail unless boundary === sum
  end
rescue
  retry
end

Following the discussion, I would like to throw in a catch and throw example, which I have advocated myself recently, but this somehow does not seem to be the right opportunity. So let me throw in another way of doing this without exceptions:

class Object
  def ergo; yield self end
end

numbers.ergo do |ary|
  rslt = ary.sample( 5 ).reduce( :+ )
  boundary === rslt ? rslt : redo
end
Community
  • 1
  • 1
Boris Stitnicky
  • 12,444
  • 5
  • 57
  • 74
  • Interestingly, I thought the same as you not so long ago. But compare using `StopIteration` exception to control built-in `Enumerator` class. – Boris Stitnicky Jun 22 '13 at 11:34
  • 1
    If you really want to use non-loop solution, try `catch`-`throw` notation – samuil Jun 22 '13 at 11:35
  • 1
    +1 from me myself. [I asked this question on SO just a few days ago](http://stackoverflow.com/questions/16972757). But I'm not sure this very case is sutiable for catch and throw. – Boris Stitnicky Jun 22 '13 at 11:38
  • It is excellent place to use `until` loop: `sum = numbers.sample(5).reduce(:+) until boundary === sum`. `catch`-`throw` is bad example also. – Hauleth Jun 22 '13 at 11:41
  • @ŁukaszNiemier: You have edited your answer accordingly already; I have added another underused keyword, `redo`. – Boris Stitnicky Jun 22 '13 at 11:48