Here is my solution. I convert the expression into RPN (using the algorithm
described in the Wikipedia article) and then calculate it (I have added a
'd' method to Fixnum so that I can use it like the standard arithmetic
operators). My solution is not very strict, so it allows '%' as an alias for
100 anywhere in the expression (not just after a 'd'), but I think that
should not be a big problem. It also ignores other characters, so whitespace
is allowed anywhere.


Pablo

---

#!/usr/bin/ruby

class Fixnum
  def d(b)
    (1..self).inject(0) {|s,x| s + rand(b) + 1}
  end
end

class Dice

  def initialize(exp)
    @expr = to_rpn(exp)
  end
  
  def roll
    stack = []
    @expr.each do |token|
      case token
        when /\d+/
          stack << token.to_i
        when /[-+*\/d]/
          b = stack.pop
          a = stack.pop
          stack << a.send(token.to_sym, b)
      end
    end
    stack.pop
  end
  
  private
  
  def to_rpn(infix)
    stack, rpn, last = [], [], nil
    infix.scan(/\d+|[-+*\/()d%]/) do |token|
      case token
        when /\d+/
          rpn << token
        when '%'
          rpn << "100"
        when /[-+*\/d]/
          while stack.any? && stronger(stack.last, token)
            rpn << stack.pop
          end
          rpn << "1" unless last =~ /\d+|\)|%/
          stack << token
        when '('
          stack << token
        when ')'
          while (op = stack.pop) && (op != '(')
            rpn << op
          end
      end
      last = token
    end
    while op = stack.pop
      rpn << op
    end
    rpn
  end
  
  def stronger(op1, op2)
    (op1 == 'd' && op2 != 'd') || (op1 =~ /[*\/]/ && op2 =~ /[-+]/)
  end
  
end

if $0 == __FILE__
  d = Dice.new(ARGV[0])
  (ARGV[1] || 1).to_i.times { print "#{d.roll} " }
end