#!/usr/bin/ruby #require 'fox14' #include Fox require 'readline' class Calculator def initialize(view) @operands = [] @operators = [] @vars = Hash.new @view = view @pi = 0 @e = 0 end def hex2dec(num) end def addOperand(operand) operand.gsub!(/ /, '') case operand when "pi" then @operands.push(pi) when "phi" then @operands.push(phi) when "e" then @operands.push(e) when /0x(.*)/ then @operands.push($~[1].to_i(base=16)) when /[A-Za-z]/ then @operands.push(operand) else @operands.push(operand.to_f) end end def addOperator(operator) if(operator.symbol == ")") while(operator = @operators.pop) break if operator.symbol == "(" operate(operator) end else while(operator.symbol != "(" && !@operators.empty? && @operators.last >= operator) operate(@operators.pop) end @operators.push(operator) end end def operate(operator) case operator.symbol when '=' then equals when '+' then @operands.push(add()) when '-' then @operands.push(subtract()) when '*' then @operands.push(multiply()) when '/' then @operands.push(divide()) when '!' then @operands.push(factorial()) when '^' then @operands.push(exp()) when 'log' then @operands.push(log()) when 'logx' then @operands.push(logx()) when 'ln' then @operands.push(ln()) when 'sqrt' then @operands.push(sqrt()) when 'sin' then @operands.push(sin()) when 'cos' then @operands.push(cos()) when 'tan' then @operands.push(tan()) when 'atan' then @operands.push(atan()) when '%' then @operands.push(percent()) when 'b' then @operands.push(binary()) end end def done while(operator = @operators.pop) operate(operator) end #showResult end def getOperandAsNumber res = @operands.pop if(res =~ /[A-Za-z]/) res = @vars[res] end if(!res.nil?) res else 0 end end def equals operand2 = getOperandAsNumber operand1 = @operands.pop @vars[operand1] = operand2 @operands.push(operand2) end def add operand2 = getOperandAsNumber operand1 = getOperandAsNumber operand1 += operand2 end def subtract operand2 = getOperandAsNumber operand1 = getOperandAsNumber operand1 -= operand2 end def multiply operand2 = getOperandAsNumber operand1 = getOperandAsNumber operand1 *= operand2 end def divide operand2 = getOperandAsNumber operand1 = getOperandAsNumber operand1 /= operand2 end def factorial operand1 = getOperandAsNumber fact(operand1) end def fact(num) num = num.to_i return 1.0 if num == 0 (num-1).downto(2){|k| num *= k} num end def e return @e if (@e != 0) res = 0.0 0.upto(17){|num| res += 1.0/(fact(num).to_f) } @e = res end def log operand1 = getOperandAsNumber operand1 = Math.log10(operand1) end def logx operand2 = getOperandAsNumber operand1 = getOperandAsNumber operand1 = Math.log10(operand2) / Math.log10(operand1) end def ln operand1 = getOperandAsNumber operand1 = Math.log(operand1) end def exp operand2 = getOperandAsNumber operand1 = getOperandAsNumber operand1 **= operand2 end def sqrt_n(num) guess = 0.0; guess2 = 10.0; while(guess != guess2) guess = guess2; guess2 = 0.5 * (guess + num/guess); end return guess end def sqrt operand1 = getOperandAsNumber operand1 = sqrt_n(operand1) end def sin operand1 = getOperandAsNumber operand1 = Math.sin(operand1) end def cos operand1 = getOperandAsNumber operand1 = Math.cos(operand1) end def tan operand1 = getOperandAsNumber operand1 = Math.tan(operand1) end def atan operand1 = getOperandAsNumber operand1 = arctan(operand1) end def percent operand1 = getOperandAsNumber operand1 /= 100 end def binary operand1 = getOperandAsNumber.to_i multiplier = 1 res = 0 while (operand1 > 0) if((operand1 % 2) == 1) res += multiplier end multiplier *= 2 operand1 /= 10 end res end def arctan(num) if(num > 0.8) # will converge slowly, so use alternate method res = 2*arctan((sqrt_n(1+num**2)-1)/num) else res = num neg = -1.0 nextStep = 1 3.step(101, 2){|val| nextStep = (num**val)/val if(nextStep < 1e-20) # this will make it accurate out to 20 decimal places # that's well beyond what we're displaying break end res += neg*nextStep neg *= -1 } res end end def pi return @pi if @pi != 0 res = 0.0 # we will use every other fibonacci number, starting with 2 val0 = 2 val1 = 3 35.times{ res += arctan(1.0/val0) val0 = val0 + val1 val1 = val0 + val1 } # the result is pi/4 @pi = res*4 end def phi (Math.sqrt(5)+1)/2 end def showResult res = getOperandAsNumber # we want to leave this one on the stack @operands.push(res) res = res.to_i if res % 1 == 0.0 @view.show(res) $stdout.flush end def getResult res = getOperandAsNumber @operands.push(res) res end end class CalcView def show(value) puts value end end class Operator include Comparable attr_reader :symbol, :priority def initialize(symbol) @symbol = symbol case symbol when '=' then @priority = 0 when '(' then @priority = 0 when '+' then @priority = 1 when '-' then @priority = 1 when '*' then @priority = 2 when '/' then @priority = 2 when 'logx' then @priority = 2 # I make this priority 2, because it is binary rather than unary. made sense to me :) when '^' then @priority = 3 when '!' then @priority = 3 when 'log' then @priority = 3 when 'ln' then @priority = 3 when 'sqrt' then @priority = 3 when 'sin' then @priority = 3 when 'cos' then @priority = 3 when 'tan' then @priority = 3 when 'atan' then @priority = 3 when '%' then @priority = 3 when 'b' then @priority = 3 else @priority = 3 end end def <=>(other) self.priority <=> other.priority end end def toShow(num) res = num res = res.to_i if res % 1 == 0.0 return res end def stats puts "Enter list of values, one per line, blank line to finish." puts "You can put multiple values on the same line, comma separated." list = Array.new print ": " val = gets while(val != "" and val != nil) if(val =~ /,/) list += val.split(/, */).map{|x|x.to_f} else list.push(val.to_f) end print ": " val = gets.chomp end list.sort! n = list.size if(n < 2) return end numDecimals = 0 hasFloat = false list.each{|item| if(item%1 != 0) itemS = item.to_s decCount = itemS.slice(itemS.index('.'),itemS.length).length - 1 if(decCount > numDecimals) numDecimals = decCount hasFloat = true end end } # calculate mode mode = 0 # make it different from the start prev = list[0]+1 count = 0 modeCount = 0 list.each{|item| if(item != prev) if(count > modeCount) mode = prev modeCount = count end prev = item count = 0 end count += 1 } if(count > modeCount) mode = prev modeCount = count end range = toShow(list[n-1] - list[0]) sum = list.inject(0){|result,item| result+item} sumSquares = list.inject(0){|result,item| result+item**2} median = list[n/2] if(n%2 == 0) median = (list[n/2-1] + list[n/2])/2 end variance = (n*sumSquares - sum**2)/(n*(n-1)) avgAbsDev = list.inject(0){|result,item| result + (item-median).abs}/n puts "n = #{n}" puts "median = #{toShow(median)}" puts "mean = #{toShow(sum/n)}" puts "mode = #{toShow(mode)}" puts "min = #{list[0]}" puts "max = #{list[list.size-1]}" puts "range = #{range}" puts "variance = #{variance}" puts "standard deviation = #{Math.sqrt(variance)}" puts "average absolute deviation = #{avgAbsDev}" # # draw histogram # puts if(!hasFloat and range <= 10) prevVal = list[0]-1 list.each{|item| if(item != prevVal) puts prevVal = item print "#{item.to_i} |" end print "*" } else buckets = (Math.log10(n) / Math.log10(2)).to_i + 1 if(buckets < 2) buckets = 2.0 elsif (buckets > 20) buckets = 20.0 end bucketWidth = range / buckets # get bucket width - rounded to smallest decimal of the data numDecimals.times{ bucketWidth = bucketWidth*10 } bucketWidth = bucketWidth.ceil.to_f numDecimals.times{ bucketWidth = bucketWidth/10 } offset = 0.5 numDecimals.times{ offset /= 10 } bucketsStart = list[0]-offset bucketEnd = bucketsStart list.each{|item| while(item > bucketEnd) bucketEnd += bucketWidth puts print "#{bucketEnd-bucketWidth} - #{bucketEnd} |" end print "*" } end puts end #class GraphWindow < FXMainWindow # def initialize(app, equation) # @mandScale = 0.02 # @mandBaseX = 0.0 # @mandBaseY = 0.0 # @height = 600 # @width = 700 # super(app, "Graph Window", nil, nil, DECOR_ALL, 0, 0, @width, @height) # # y is negated, because in the graphics window, y=0 is at the top # @basex = 1 # @basey = -10 # @rangex = 20 # @rangey = 20 # @parser = StatementParser.new() # @equation = equation # # @contents = FXHorizontalFrame.new(self, LAYOUT_SIDE_TOP|LAYOUT_FILL_X|LAYOUT_FILL_Y, 0, 0, 0, 0, 0, 0, 0, 0) # @canvasFrame = FXVerticalFrame.new(@contents, FRAME_SUNKEN|LAYOUT_FILL_X|LAYOUT_FILL_Y|LAYOUT_TOP|LAYOUT_LEFT, # 0,0,0,0,10,10,10,10) # @graphCanvas = FXCanvas.new(@canvasFrame, nil, 0, LAYOUT_FILL_X|LAYOUT_FILL_Y|LAYOUT_TOP|LAYOUT_LEFT) # # @equations = FXVerticalFrame.new(@contents, FRAME_SUNKEN|LAYOUT_FILL_Y|LAYOUT_TOP|LAYOUT_LEFT, 0, 0, 0, 0, 10, 10, 10, 10) # @graph1 = FXTextField.new(@equations, 10, nil, 0, TEXTFIELD_NORMAL|JUSTIFY_CENTER_X|LAYOUT_FILL_X) # @graph2 = FXTextField.new(@equations, 10) # @graphButton = FXButton.new(@equations, "Graph!") # @graphButton.connect(SEL_COMMAND) do |sender, sel, event| # @graphCanvas.update # end # # # default to -10 to 10 x, -10 to 10 y # @basexText = FXTextField.new(@equations, 10) # @basexText.text = "-100" # @baseyText = FXTextField.new(@equations, 10) # @baseyText.text = "-100" # @rangexText = FXTextField.new(@equations, 10) # @rangexText.text = "200" # @rangeyText = FXTextField.new(@equations, 10) # @rangeyText.text = "200" # # @graphCanvas.connect(SEL_LEFTBUTTONPRESS) do |sender, sel, event| # #@mandBaseX += @mandScale*(event.win_x-300) # #@mandBaseY += @mandScale*(event.win_y-300) # @mandScale /= 2 # @graphCanvas.update # end # # @graphCanvas.connect(SEL_PAINT) do |sender, sel, event| # FXDCWindow.new(@graphCanvas, event) do |dc| # canvasWidth = @graphCanvas.width # @basex = @basexText.text.to_i # @basey = @baseyText.text.to_i # @rangex = @rangexText.text.to_i # @rangey = @rangeyText.text.to_i # @stepx = @rangex/canvasWidth.to_f # @stepy = @rangey/@height.to_f # @gridStepX = canvasWidth/20 # @gridStepY = @height/20 # # dc.foreground = @graphCanvas.backColor # dc.fillRectangle(event.rect.x, event.rect.y, event.rect.w, event.rect.h) # dc.foreground = "black" # # xAxis = 0 - @basex # xAxis /= @stepx # if(event.rect.x < xAxis && (event.rect.x + event.rect.w) > xAxis && # xAxis > 0 && xAxis < canvasWidth) # dc.drawLine(xAxis, event.rect.y, xAxis, event.rect.y+event.rect.h) # end # # yAxis = 0 - @basey # yAxis /= @stepy # yAxis = @height-yAxis # if(event.rect.y < yAxis && (event.rect.y+event.rect.h) > yAxis && # yAxis > 0 && yAxis < @height) # dc.drawLine(event.rect.x, yAxis, event.rect.x+event.rect.w, yAxis) # end # # dc.foreground = "grey" # 1.upto(10){|offset| # dc.drawLine(xAxis+offset*@gridStepX, event.rect.y, xAxis+offset*@gridStepX, event.rect.y+event.rect.h) # dc.drawLine(xAxis-offset*@gridStepX, event.rect.y, xAxis-offset*@gridStepX, event.rect.y+event.rect.h) # # dc.drawLine(event.rect.x, yAxis+offset*@gridStepY, event.rect.x+event.rect.w, yAxis+offset*@gridStepY) # dc.drawLine(event.rect.x, yAxis-offset*@gridStepY, event.rect.x+event.rect.w, yAxis-offset*@gridStepY) # } # # dc.foreground = "blue" # 1.upto(2){|num| # if(num == 1) # @equation = @graph1.text # end # if(num == 2) # @equation = @graph2.text # end # break if(@equation == "") # if(@equation == "mand") # dc.foreground = "black" # -100.upto(100){|x| # -100.upto(100){|y| # if(mandelbrot(x, y, @mandBaseX, @mandBaseY, @mandScale) == 255) # dc.drawPoint((x-@basex)/@stepx, @height-((y-@basey)/@stepy)) # end # } # } # break # end # prevx = NIL # prevy = NIL # event.rect.x.upto(event.rect.x+event.rect.w){|x| # y = @parser.parse(@equation.gsub(/x/, (@basex + x*@stepx).to_s)) # if(y < @basey || y > (@basey+@rangey)) # prevx = NIL # next # end # y-=@basey # y/=@stepy # if(prevx == NIL) # prevx = x # prevy = y # end # dc.drawLine(x, @height-y, prevx, @height-prevy) # prevx = x # prevy = y # } # } # end # end # end # def create # super # show(PLACEMENT_SCREEN) # end # # def mandelbrot(x, y, cx, cy, scale) # ax = cx+x*scale # ay = cy+y*scale # a1 = ax # b1 = ay # 0.upto(255){|i| # a2 = a1*a1 - b1*b1 + ax # b2 = 2*a1*b1 + ay # a1 = a2 # b1 = b2 # if(((a1*a1)+(b1*b1)) > 4) # return i # end # } # return 255 # end #end class StatementParser def initialize @calc = Calculator.new(CalcView.new) end def parse(exp) while(exp =~ /sin|cos|tan|atan|sqrt|ln|logx|log|b|[^0-9. A-Za-z]/) rand1 = $` rand2 = $' op = $& if(rand1 == "" && op == "-") # handle negative numbers break unless (rand2 =~ /sin|cos|tan|atan|sqrt|ln|logx|log|b|[^0-9. A-Za-z]/) rand1 = "-" + $` rand2 = $' op = $& end @calc.addOperand(rand1) if rand1 != "" @calc.addOperator(Operator.new(op)) if op != "" exp = rand2.strip end @calc.addOperand(exp) if exp != "" @calc.done @calc.getResult end def show @calc.showResult end end parser = StatementParser.new() puts "Enter your expressions:" while(true) exp = Readline::readline('> ', true) # first, check for ^d, which we can't chomp break if exp == nil # OK, now we can chomp off the terminating carriage return exp.chomp! break if exp == "q" or exp == "quit" if(exp =~ /graph/) application = FXApp.new('Graph Window', 'Calculator') graph = GraphWindow.new(application, $') application.create application.run break elsif(exp =~ /^stats/i) stats else parser.parse(exp) parser.show end end puts "Thank you for trying this product. If you enjoyed it, please consider other products by this author, such as MyChess and Wutris."