#!/usr/bin/env ruby # version 0.1.0 # # Jakub Travnik's tui example # this example uses widgets from jttuistd.rb # and localization stuff from jtloc.rb # # # It uses idea of separated back-end (class CalcNumber and Calculator) and # front-end (user interface). I recomend this coding style for any project # using JTTui or user interface generally. It is easily maintainable. # Calculator class can be tested without UI just from irb. # require 'addlocalpath' require 'jttui/jttui' require 'jttui/jttuistd' require 'jttui/jtloc' class CalcNumber # we use Float numbers, but some may need/want to change it # to something with better precision (there is discontinued arbitrary # floating point number class in misc/calcnum.rb) # # CalcNumber can read numbers from string in bases of 2 to 16 all including # exponentation (in that base also) # i.e. CalcNumber.new('ab.cE-d',16) # have value of 3.81361609e-14 (in dec., approx.) # # CalcNumber also eliminate need for using Math module directly include Comparable attr_accessor :value def initialize(v,base=10) case v when Float,Integer @value=v.to_f when String if v=='' @value=0.0 return end pointbase=nil; number=nil; exponent=nil; sign=nil; expsign=nil haveexponent=false v.each_byte{|b| if b==?+ if exponent raise -'too many signs in exponent' if expsign raise -'exponent have embedded sign' if haveexponent expsign=1 else raise -'too many signs in mantisa' if sign raise -'mantisa have embedded sign' if number sign=1 end elsif b==?- if exponent raise -'too many signs in exponent' if expsign raise -'exponent have embedded sign' if haveexponent expsign=-1 else raise -'too many signs in mantisa' if sign raise -'mantisa have embedded sign' if number sign=-1 end elsif b==?. raise -'decimal point cannot be in exponent' if exponent raise -'too many decimal points' if pointbase pointbase=1 number=0 unless number elsif b==?E raise -'too many exponent signs' if exponent exponent=0 elsif exponent d=getdigit b,base raise -'invalid digit' unless d haveexponent=true exponent=base*exponent+d else number=0 unless number d=getdigit b,base raise -'invalid digit' unless d number=base*number+d if pointbase pointbase*=base end end } raise -'invalid number' unless number or exponent raise -'empty exponent' if exponent and not haveexponent sign=1 unless sign number=1 unless number exponent=0 unless exponent expsign=1 unless expsign pointbase=1 unless pointbase @value=sign*(number.to_f/pointbase)*(base**(expsign*exponent)) else raise "value #{v} cannot be converted to CalcNumber" end end def getdigit(code,base) # FIXME: index is slow, use inverse of digits @@digits='0123456789abcdef' unless defined? @@digits d=@@digits.index code d ? (d=base8 m*=baser8; e+=8 end while m=1 m*=baser; e+=1 end ms='' # we need to know how many digits to display, this is base dependent # table below have them precomputed. This computation was based on # eb=11 # bits in exponent (including exponent sign) # mb=52-4 # bits in mantisa (excluding sign, it is independent), # # I consider last 4 to be inexact so I'm ignoring them # maxwidth=45 # maximum available width for displaing value # internal_wodth=5 # other characters in numbers: 'E', '.', '-', '+' # # values above are valid for 8byte IEEE double floating point # def log(x,base); Math.log(x)/Math.log(base) end # logarithm of some base # def width_of_mantisa(eb,base) # # number of character for longest exponent without a sign # (log(2**eb*log(2,base),base)).ceil # end # def maximum_precision_in_digits(mb,base) # (mb*log(2,base)).floor # end # (2..16).collect{|x| # [ maximum_precision_in_digits(mb,x), # maxwidth-internal_width-width_of_mantisa(eb,x)].min # } # # this can be found in source distribution in ./misc/numdigist.rb numdigits=[29, 30, 24, 20, 18, 17, 16, 15, 14, 13, 13, 12, 12, 12, 12][base-2] while numdigits>0 m*=base digit=m.truncate ms+=@@digits[digit,1] m-=digit numdigits-=1 end if getdigit(ms[-1],base)==base-1 # round to up if something like 119999999999 is generated idx=ms.length-1 begin ms[idx]=?0 idx-=1 d=getdigit(ms[idx],base) end while d==base-1 if d ms[idx]=@@digits[d+1,1] else ms='1'+ms; e+=1 break end end if e>=0 and e<14 len=ms.length ms << '0'*(e-len+1) unless len>e ms[e,0]='.' ms.gsub!(/(\..*?)0+$/,'\1') # clear tail zeroes ms.gsub!(/\.$/,'') # remove tail point if it is there ms=s+ms else ms[1,0]='.' ms.gsub!(/(\..*?)0+$/,'\1') # clear tail zeroes exp=''; e-=1 if e<0 exps='-' e=-e else exps='' end while e>0 e,ed=e.divmod base exp=@@digits[ed,1]+exp end ms=s+ms+'E'+exps+exp end return ms end def +(other) CalcNumber.new(self.value+other.value) end def -(other) CalcNumber.new(self.value-other.value) end def *(other) CalcNumber.new(self.value*other.value) end def /(other) CalcNumber.new(self.value/other.value) end def **(other) CalcNumber.new(self.value**other.value) end def -@ CalcNumber.new(-self.value) end def <=>(other) @value<=>other end def sin CalcNumber.new(Math.sin(@value)) end def cos CalcNumber.new(Math.cos(@value)) end def tan CalcNumber.new(Math.sin(@value)/Math.cos(@value)) end def asin CalcNumber.new(@value/Math.sqrt(1-@value*@value)).atan rescue return CalcNumber.nan end def acos return CalcNumber.pi/CalcNumber.two if @value==0 CalcNumber.new(Math.sqrt(1-@value*@value)/@value).atan rescue return CalcNumber.nan end def atan CalcNumber.new(Math.atan2(@value,1)) end def n! if (@value % 1)==0.0 and @value>=0.0 arg=@value.to_i return CalcNumber.nan if arg>1000 CalcNumber.new(_n!(arg)) else CalcNumber.nan end end def _n!(n) res=1 while n>1 res*=n; n-=1 end res end def sqrt CalcNumber.new(Math.sqrt(@value)) rescue return CalcNumber.nan end def exp CalcNumber.new(Math.exp(@value)) end def ln CalcNumber.new(Math.log(@value)) rescue return CalcNumber.nan end def log10 CalcNumber.new(Math.log10(@value)) rescue return CalcNumber.nan end end def CalcNumber.pi CalcNumber.new(Math::PI) end def CalcNumber.zero CalcNumber.new(0.0) end def CalcNumber.one CalcNumber.new(1.0) end def CalcNumber.minusone CalcNumber.new(-1.0) end def CalcNumber.two CalcNumber.new(2.0) end def CalcNumber.posinf (CalcNumber.one/CalcNumber.zero) end def CalcNumber.neginf (CalcNumber.minusone/CalcNumber.zero) end def CalcNumber.nan CalcNumber.posinf*CalcNumber.zero end class Calculator attr_reader :stack,:editing,:base,:error,:angleunit def initialize @prihash={'='=>0,')'=>1,'('=>1,'+'=>2,'-'=>2,'*'=>3,'/'=>3,'^'=>4} @base=10 # the number is multiplied by this before processing if it is angle input # or divided by this if result is angle @angleunits={'RAD'=>CalcNumber.one, 'DEG'=>CalcNumber.pi/CalcNumber.new(180), 'GRD'=>CalcNumber.pi/CalcNumber.new(200)} @angleunit='RAD' reset end def reset @stack=[CalcNumber.zero] @typednum='' @editing=true @memory=CalcNumber.zero @error=nil end def base=(v) case base when 2,8,10,16 endeditmode if @editing @base=v else raise -'unsupported base' end end def setangleunit(v,convert=false) raise "unknown angle unit '#{v}'" unless @angleunits[v] if convert i=getlastvalueindex @stack[i]=@stack[i]*@angleunits[@angleunit]/@angleunits[v] if i end @angleunit=v end def angletorad(x) x*@angleunits[@angleunit] end def radtoangle(x) x/@angleunits[@angleunit] end def typenum(s) return if @error case s when '.' # only one decimal point and only in mantisa unless @typednum.index 'E' @typednum+=s unless @typednum.index '.' end when 'E' # only first exponentation makes sense @typednum+=s unless @typednum.index 'E' when '+/-' # sign change key if @typednum.index 'E' # change sign of exponent signindex=1+@typednum.index('E') case @typednum[signindex,1] when nil @typednum+='-' when '-' @typednum[signindex,1]='' else @typednum[signindex,0]='-' end else # without exp notation, change leading sign if @typednum[0,1]=='-' @typednum=@typednum[1..-1] else @typednum='-'+@typednum end end when 'backspace' @typednum=@typednum[0..-2] if @typednum.length>0 when '0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f' # numbers are appended @typednum+=s if '0123456789abcdef'.index(s)<@base end end def function?(k) ['MR','MC','MS','M+', 'SIN','COS','TAN', 'ASIN','ACOS','ATAN', 'SQR','SQRT','1/x','n!', 'LN','EXP','10^x','LOG', 'const_pi','const_e'].include? k end def endeditmode if @editing begin value=CalcNumber.new(@typednum,base) rescue @error=$!.message value=CalcNumber.nan end if stack[-1].kind_of? CalcNumber stack[-1]=value else stack << value end @editing=false @typednum='' end end def enterconst(value) if @typednum.length==0 if @stack.length>1 and @stack[-1].kind_of?(CalcNumber) @error=-'missing operator' else if @stack[-1].kind_of?(CalcNumber) @stack[-1]=value else @stack << value end end else @error=-'missing operator' end @editing=false @typednum='' end def starteditmode if @stack.length>1 and @stack[-1].kind_of?(CalcNumber) @error=-'operation was expected' end @typednum='' @editing=true end def processkey(k) return if @error and (k != 'AC') if @editing if ['=',')','+','-','*','/','^','AC'].include?(k) or function?(k) endeditmode processkey k elsif k=='(' if @typednum.length==0 if @stack.length>1 and @stack[-1].kind_of?(CalcNumber) @error=-'missing operator' else if @stack[-1].kind_of?(CalcNumber) @stack[-1]=k else @stack << k end end else @error=-'missing operator' end elsif k=='+/-' if @typednum.length>0 typenum k else endeditmode processkey k end else typenum k end else if function? k processfunction k else if k=='AC' reset else case k when '=',')','+','-','*','/','^' if @stack[-1].kind_of? CalcNumber @stack << k compute else @error=-'no number' end when '+/-' @stack[-1]=-@stack[-1] else starteditmode processkey k end end # if k=='AC' end end end def processfunction(k) case k when 'MR' # memory functions are not yet used in calculator enterconst(@memory) when 'MC' @memory=CalcNumber.zero when 'const_pi' enterconst(CalcNumber.pi) when 'const_e' enterconst(CalcNumber.one.exp) else unless @stack[-1].kind_of?(CalcNumber) @error=-'value required' return end case k when 'MS' @memory=@stack[-1] when 'M+' @memory+=@stack[-1] when 'SIN' @stack[-1]=angletorad(@stack[-1]).sin when 'COS' @stack[-1]=angletorad(@stack[-1]).cos when 'TAN' @stack[-1]=angletorad(@stack[-1]).tan when 'ASIN' @stack[-1]=radtoangle(@stack[-1].asin) when 'ACOS' @stack[-1]=radtoangle(@stack[-1].acos) when 'ATAN' @stack[-1]=radtoangle(@stack[-1].atan) when 'SQR' @stack[-1]*=@stack[-1] when 'SQRT' @stack[-1]=(@stack[-1]).sqrt when '1/x' @stack[-1]=CalcNumber.one/@stack[-1] when 'n!' @stack[-1]=@stack[-1].n! when 'LN' @stack[-1]=@stack[-1].ln when 'EXP' @stack[-1]=@stack[-1].exp when 'LOG' @stack[-1]=@stack[-1].log10 when '10^x' @stack[-1]=CalcNumber.new(10)**@stack[-1] else raise "bug: function '#{k}' is unknown to calculator" end end end def compute begin changed=false pritop=@prihash[@stack[-1]] if pritop (@stack.length-2).downto(0){|pos| if @stack[pos].kind_of? String pricur=@prihash[@stack[pos]] if pricur0 @typednum else lv=getlastvalue lv ? lv.to_s(@base) : '0' end end end class LCDLabel < JTTWindow attr_reader :caption def initialize(*params) @caption=params.pop super(*params) end def paintself(pc) super pc.windowframe self,@color pc.move 2,1 pc.addstra @caption.rjust(self.w-4),JTTui.color_lcd end def caption=(v) @caption=v addmessage self,:paint end end class CalcButton < JTTWButton attr_reader :help attr_accessor :helphandler, :postaction def initialize(parent,label,other_hotkeys,help, &block) super(parent, 'CalcButton', 0, 0, 7, 1, label, &block) other_hotkeys.each{|keyname| self.hl_addone keyname} if other_hotkeys @help=help @helphandler=nil @postaction=nil end def gotfocus super @helphandler.call(self) if @helphandler end def action super @postaction.call if @postaction end def keypress(k) case k when 'C-j','C-m' # ignore enter key addmessage @parent, :keypress, k else super end end end JTLanguage.load_locale('./example-jttuistd-2-calculator-lang') # JTLanguage.write_missing JTTui.run do |root| c=Calculator.new JTTui.colors << JTTColor.new('color_lcd', JTCur.color_black,JTCur.color_green,0, JTCur.attr_bold).recompute JTTui.colors << JTTColor.new('color_calc', JTCur.color_white,JTCur.color_blue,0, 0).recompute JTTui.colors << JTTColor.new('color_calc_hi', JTCur.color_yellow,JTCur.color_blue, JTCur.attr_bold, JTCur.attr_bold).recompute cw=JTTDialog.new(root, 'Calculator Window', 0, 0, 56, 19, -'Ruby/JTTui Calculator') cw.align=JTTWindow::ALIGN_CENTER bq=JTTWButton.new(cw, 'Quit Button', 0, 0, 8, 1, -'_Quit') { JTTui.addmessage nil,:close} lcd=LCDLabel.new(cw, 'LC Display Label', 2, 2, 49, 3, '0') helplabel=JTTWLabel.new(cw, 'Help Label', 2, 14, 52, 3, '') helpcheck=JTTWCheckbox.new(cw,'Help check',26,0,13,1,-'Show _help') { helplabel.caption='' } baselabel=JTTWLabel.new(cw, 'Base Label', 2, 1, 8, 1, '') anglelabel=JTTWLabel.new(cw, 'Angle Label', 13, 1, 9, 1, '') stacklabel=JTTWLabel.new(cw, 'Stack Label', 2, 5, 49, 1, '') stackcheck=JTTWCheckbox.new(cw,'Stack check',40,0,14,1,-'Show stack') { stacklabel.caption='' } helpcheck.state=1 g=JTTWGrid.new(cw,'Button Grid',3,6,47,8) [g,cw,lcd,helplabel,helpcheck,baselabel,anglelabel, stacklabel,stackcheck].each{ |x| x.color=JTTui.color_calc x.color_hi=JTTui.color_calc_hi if defined? x.color_hi } m_yes_no=JTTWMessagebox.new('',0,-1,-'_Yes',-'_No') m_angle=JTTWMessagebox.new(-'Choose angle units',0,-1,'_RAD','_DEG','_GRD') m_base=JTTWMessagebox.new(-'Choose base of numbers',2,-1, '2 (_BIN)','8 (_OCT)','10 (_DEC)','16 (_HEX)') # mapping of calculator keys # [ f ] [sqrt ] [ e^x ] [ ln ] [10^x ] [ log ] # [ e ] [ x^2 ] [asin ] [acos ] [atan ] [ y^x ] # [ d ] [ n! ] [ sin ] [ cos ] [ tan ] [ 1/x ] # [ c ] [ Exp ] [ +/- ] [ ( ] [ ) ] [ <- ] # [ b ] [BASE ] [ 7 ] [ 8 ] [ 9 ] [ / ] # [ a ] [ANGLE] [ 4 ] [ 5 ] [ 6 ] [ * ] # [ Pi ] [ ALT ] [ 1 ] [ 2 ] [ 3 ] [ - ] # [ e ] [ C ] [ 0 ] [ . ] [ = ] [ + ] # top left button is b11, bottom right is b86 # digits in button identifier indicate its coordinates hexbuttonhelp=-"Inserts &1 digit in hexadecimal (HEX) base.\nkey: &1" b11=CalcButton.new(g, '_f', nil, hexbuttonhelp & 'f') { c.processkey 'f' } b12=CalcButton.new(g, 'sqrt', ['s'], -("Square root of value,"+ " fails for negative argument.\nkey: s")) { c.processkey 'SQRT' } b13=CalcButton.new(g, 'e^x', ['['], -"Euler number powered to value.\nkey: [") { c.processkey 'EXP' } b14=CalcButton.new(g, 'ln', [']'], -("Natural logarithm of value,"+ " fails for negative argument.\nkey: ]")) { c.processkey 'LN' } b15=CalcButton.new(g, '10^x', ['{'], -"10 powered to value.\nkey: {") { c.processkey '10^x' } b16=CalcButton.new(g, 'log', ['}'], -("base 10 logarithm,"+ " fails for negative argument.\nkey: }")) { c.processkey 'LOG' } b21=CalcButton.new(g, '_e', nil, hexbuttonhelp & 'e') { c.processkey 'e' } b22=CalcButton.new(g, 'x^2', nil, -"Square of number.") { c.processkey 'SQR' } b23=CalcButton.new(g, 'asin', nil, -("Inverse operation for sin,"+ " fails for argument outside -1 to 1.\n")) { c.processkey 'ASIN' } b24=CalcButton.new(g, 'acos', nil, -("Inverse operation for cos,"+ " fails for argument outside -1 to 1.")) { c.processkey 'ACOS' } b25=CalcButton.new(g, 'atan', nil, -"Inverse operation for tangent.") { c.processkey 'ATAN' } b26=CalcButton.new(g, 'y_^x', nil, -("y powered to x, fails for negative y.\n"+ "keys: ^")) { c.processkey '^' } b31=CalcButton.new(g, '_d', nil, hexbuttonhelp & 'd') { c.processkey 'd' } b32=CalcButton.new(g, 'n_!', nil, -("Factorial. Only for integer argument.\n"+ "keys: !")) { c.processkey 'n!' } b33=CalcButton.new(g, 'sin', nil, -"Sinus.") { c.processkey 'SIN' } b34=CalcButton.new(g, 'cos', nil, -"Cosinus.") { c.processkey 'COS' } b35=CalcButton.new(g, 'tan', nil, -"Tangent, fails for singular points.") { c.processkey 'TAN' } b36=CalcButton.new(g, '1/x',['r'], -("One divided by argument. Fails for zero.\n"+ "keys: r")) { c.processkey '1/x' } b41=CalcButton.new(g, '_c', nil, hexbuttonhelp & 'c') { c.processkey 'c' } b42=CalcButton.new(g, 'E_xp', nil, -("Use to enter exponent as usually"+ " in engineering notation.\nkeys: x")) { c.processkey 'E' } b43=CalcButton.new(g, '+/-',['i'], -"Swap sign of number or its exponent.\nkeys: i") { c.processkey '+/-' } b44=CalcButton.new(g, '_(', nil, -"Opening parenthesis for new grouping.\nkeys: (") { c.processkey '(' } b45=CalcButton.new(g, '_)', nil, -"Closing parenthesis to finish grouping.\nkeys: )") { c.processkey ')' } b46=CalcButton.new(g, '<-',['backspace','del','C-h'], -"Erase last digit.\nkeys: backspace, del, C-h") { c.processkey 'backspace' } b51=CalcButton.new(g, '_b', nil, hexbuttonhelp & 'b') { c.processkey 'b' } b52=CalcButton.new(g, 'BASE', nil, -("Change base for displaying and entering numbers. "+ "Current number is converted.")) { mres=m_base.execute case mres when 0 then c.base=2 when 1 then c.base=8 when 2 then c.base=10 when 3 then c.base=16 end m_base.defaultnr=mres unless mres==-1 } b53=CalcButton.new(g, '_7', nil, "") { c.processkey '7' } b54=CalcButton.new(g, '_8', nil, "") { c.processkey '8' } b55=CalcButton.new(g, '_9', nil, "") { c.processkey '9' } b56=CalcButton.new(g, '_/', nil, -"Division. Fails for zero denominator.\nkeys: /") { c.processkey '/' } b61=CalcButton.new(g, '_a', nil, hexbuttonhelp & 'a') { c.processkey 'a' } b62=CalcButton.new(g, 'ANGLE', nil, -("Change angle units. Current number is converted "+ "or not depending on user choose.")) { c.endeditmode mres=m_angle.execute if mres>=0 m_angle.defaultnr=mres myn=m_yes_no.execute -'Convert current value?' if myn>=0 c.setangleunit(['RAD','DEG','GRD'][mres], myn==0) end end } b63=CalcButton.new(g, '_4', nil, "") { c.processkey '4' } b64=CalcButton.new(g, '_5', nil, "") { c.processkey '5' } b65=CalcButton.new(g, '_6', nil, "") { c.processkey '6' } b66=CalcButton.new(g, '_*', nil, -"Multiply operator.\nkeys: *") { c.processkey '*' } b71=CalcButton.new(g, 'Pi', nil, -"Circumference ratio constant.") { c.processkey 'const_pi' } b72=CalcButton.new(g, 'AL_T', nil, "Alternate functions.\n"+ "Not yet implemented") { } b73=CalcButton.new(g, '_1', nil, "") { c.processkey '1' } b74=CalcButton.new(g, '_2', nil, "") { c.processkey '2' } b75=CalcButton.new(g, '_3', nil, "") { c.processkey '3' } b76=CalcButton.new(g, '_-', nil, -"Substraction operator.\nkeys: -") { c.processkey '-'} b81=CalcButton.new(g, 'e', nil, -"Euler constant.") { c.processkey 'const_e' } b82=CalcButton.new(g, 'C', ['esc','C-c'], -"Clear all.\nkeys: C-c, esc (two times)") { c.processkey 'AC' } b83=CalcButton.new(g, '_0', nil, "") { c.processkey '0' } b84=CalcButton.new(g, '_.', nil, -"Decimal dot.\nkeys: .") { c.processkey '.' } b85=CalcButton.new(g, '_=', ['C-j','C-m'], -"Finish computation.\nkeys: =, enter, C-m, C-j") { c.processkey '=' } b86=CalcButton.new(g, '_+', nil, -"Addition operator.\nkeys: +") { c.processkey '+' } helphandler=Proc.new{ |obj| if helpcheck.state==1 text=-("Use arrows, tab, M-tab, C-f, C-b, C-n, C-p to move.\n"+ "Use space to activate buttons.\nEnter key act as = key.") text=obj.help if obj.help.length>0 helplabel.caption=text end } showhandler=Proc.new{ lcd.caption=c.getvalue if stackcheck.state==1 st='Stack: '; stlw=stacklabel.w-st.length st=st+c.stack.join(' ').ljust(stlw)[-stlw .. -1] stacklabel.caption=st end baselabel.caption=-"Base: &1" & c.base.to_s anglelabel.caption=-"Mode: &1" & c.angleunit } showhandler.call g.subwindows.each{|b| b.color=JTTui.color_calc b.color_hi=JTTui.color_calc_hi b.helphandler=helphandler b.postaction=showhandler } g.setcontent(1,0, [b11,b12,b13,b14,b15,b16], [b21,b22,b23,b24,b25,b26], [b31,b32,b33,b34,b35,b36], [b41,b42,b43,b44,b45,b46], [b51,b52,b53,b54,b55,b56], [b61,b62,b63,b64,b65,b66], [b71,b72,b73,b74,b75,b76], [b81,b82,b83,b84,b85,b86]) cw.addtabstop bq cw.addtabstop helpcheck cw.addtabstop stackcheck cw.cancelbutton=b82 cw.settab b64 lcd.down end