A command line calculator?

General FreeBASIC programming questions.
Post Reply
badidea
Posts: 2591
Joined: May 24, 2007 22:10
Location: The Netherlands

A command line calculator?

Post by badidea »

Did anyone make ever made a calculator in freebasic that can be called from the command line / terminal?
E.g. if I type fbcalc "((3+4)/2-1)" I get as output 2.5
I did see this parser: mathematical expression parser (operator precedence parser) but I will take me a while to understand it.
In the meantime, I hacked this version.
Warning: creates & deletes two files!

Code: Select all

dim as string fbcPath = "fbc32" '<-- set correct
dim as string execFileName = "tempFbCalc" '<-- set preferred name
dim as string srcFileName = execFileName & ".bas"

dim as string cmsStr = command()
if cmsStr = "" then print "Error: no input" : end

dim as integer fileNum = freefile()
if open(srcFileName for output as fileNum) = 0 then
	print #fileNum, "print " & cmsStr
	close #fileNum
	sleep 100 'lets give the OS a short break
	#ifdef __FB_LINUX__
		shell fbcPath & " " & srcFileName 'call the compiler
		shell "./" & execFileName 'excecute the new program
	#else
		'Not tested!
		shell fbcPath & " " & srcFileName
		shell execFileName
	#endif
	sleep 100 'lets give the OS another short break
	kill srcFileName
	kill execFileName
else
	print "Error creating temporary executable: " & execFileName
end if
It can also do fbcalc "csng(sin(atn(1))^2)" which gives 0.5
caseih
Posts: 2157
Joined: Feb 26, 2007 5:32

Re: A command line calculator?

Post by caseih »

I made such a calculate years ago, back in the PB days. Building such a calculator (with operator precedence and right-to-left, and left-to-right operation), is super educational. I highly recommend it. Turns out I discovered recursive-descent parsing, which I wasn't formally introduced to until years later. I never translated it to FB, and it uses global variables which I wouldn't use if I did it again. But if you want to see it, I can post it here. It's not a very big program. I chose to do ad-hoc parsing rather than formal tokenization. For a calculator, that was simple and worked well. I just did single byte lookahead during parsing.

Of course for every-day command-line math, Linux has plenty of built-in tools for that, of which I mostly use bc and the python interpreter (which works nicely as a calculator). Surprisingly Windows has very little to offer by way of command-line tools that do math.
Ed Davis
Posts: 37
Joined: Jul 28, 2008 23:24

Re: A command line calculator?

Post by Ed Davis »

badidea wrote: Feb 20, 2022 23:07 Did anyone make ever made a calculator in freebasic that can be called from the command line / terminal?
E.g. if I type fbcalc "((3+4)/2-1)" I get as output 2.5
Here is a (hopefully) simple one I wrote a few years ago. It uses precedence climbing to parse the expression.

Code: Select all

'Eval function, supports all QBasic operators and selected numeric functions.
'
'Numeric functions:
'abs atan cos exp fix int len log rnd sgn sin sqr tan val
'
'Simple to add additional single parameter functions
'Just a little busy work to add multiple parameter functions
'
'Compiles with QB64 and FreeBasic (-lang qb).
'
'Precedence - highest to lowest:
' ^
' unary -, +
' *, /
' \
' mod
' +, -
' =, <>, <, >, <=, >=
' unary not
' and
' or
' xor
' eqv
' imp
'
'Example usage:
'
'calc abs(1+2*3+(5-1) + sin(42))
'
'Written by Ed Davis.  Contact: ed_davis2 at that yahoo place.
'Use at your own risk.

option explicit ' drop the "_" for FreeBasic

declare function eval#(userstr as string)
declare function expr#(p as integer, userstr as string, sym as string)

const rightassoc=0, leftassoc=1
dim userstr as string

userstr = command$
if userstr <> "" then print eval#(userstr)

' lexical analyzer functions

function isdigit%(ch as string)
    isdigit% = left$(ch, 1) >= "0" and left$(ch, 1) <= "9"
end function

function isnumeric%(ch as string)
    isnumeric% = isdigit(ch) or left$(ch, 1) = "."
end function

function isalpha%(ch as string)
    isalpha% = lcase$(left$(ch, 1)) >= "a" and lcase$(left$(ch, 1)) <= "z"
end function

sub takechar(userstr as string, sym as string)
    sym = sym + left$(userstr, 1)
    userstr = right$(userstr, len(userstr) - 1)
end sub

sub nextsym(userstr as string, sym as string)
    sym = ""
    userstr = ltrim$(userstr)
    takechar userstr, sym
    select case sym
        case "%", "(", ")", "*", "+", "-", "/", "=", "\", "^"   'all set
        case "0" to "9"
            while isdigit%(userstr)
                takechar userstr, sym
            wend
            if left$(userstr, 1) = "." then
                takechar userstr, sym

                while isdigit%(userstr)
                    takechar userstr, sym
                wend
            end if
        case "."
            while isdigit%(userstr)
                takechar userstr, sym
            wend

        case "<"
            if left$(userstr, 1) = "=" or left$(userstr, 1) = ">" then
                takechar userstr, sym
            end if

        case ">"
            if left$(userstr, 1) = "=" then
                takechar userstr, sym
            end if

        case "a" to "z"
            while isalpha%(userstr) or isdigit%(userstr)
                takechar userstr, sym
            wend

        case ""
        case else
            print "unrecognized character:", sym
            sym = ""
    end select
end sub

' parser starts here

function accept&(s as string, userstr as string, sym as string)
  accept& = 0
  if sym = s then accept& = -1: call nextsym(userstr, sym)
end function

sub expect(s as string, userstr as string, sym as string)
  if not accept&(s, userstr, sym) then print "expecting "; s; " but found "; sym
end sub

function unaryprec%(op as string)
    select case op
        case "+", "-": unaryprec% = 13
        case "not":    unaryprec% =  6
        case else:     unaryprec% =  0  ' not a unary operator
    end select
end function

function binaryprec%(op as string)
  select case op
    case "^":                              binaryprec% = 14
    case "*", "/":                         binaryprec% = 12
    case "\" :                             binaryprec% = 11
    case "mod":                            binaryprec% = 10
    case "+", "-":                         binaryprec% =  9
    case "=", "<>", "<", ">", "<=", ">=":  binaryprec% =  7
    case "and":                            binaryprec% =  5
    case "or":                             binaryprec% =  4
    case "xor":                            binaryprec% =  3
    case "eqv":                            binaryprec% =  2
    case "imp":                            binaryprec% =  1
    case else:                             binaryprec% =  0 ' not a binary operator
  end select
end function

' all QBasic operators are left associative
function associativity%(op as string)
    if op = op then :
    associativity% = leftassoc
end function

' parse a parenthesized numeric expression
function getvalue#(userstr as string, sym as string)
    getvalue# = 1
    call nextsym(userstr, sym)     ' skip fun
    call expect("(", userstr, sym)

    getvalue# = expr#(0, userstr, sym)

    call expect(")", userstr, sym)
end function

' handle numeric operands - numbers, functions - and unary operators
function primary#(userstr as string, sym as string)
    dim op as string, prec as integer, n as double

    primary# = 0                    'prepare for errors
    prec = unaryprec%(sym)
    if prec > 0 then
        op = sym
        call nextsym(userstr, sym)
        select case op
            case "-":   primary# =     -expr#(prec, userstr, sym)
            case "+":   primary# =      expr#(prec, userstr, sym)
            case "not": primary# =  not expr#(prec, userstr, sym)
        end select
    elseif sym = "(" then
        call nextsym(userstr, sym)
        primary# = expr#(0, userstr, sym)
        call expect(")", userstr, sym)
    elseif isnumeric%(sym) then
        primary# = val(sym)
        call nextsym(userstr, sym)
    else
        select case sym
            case "abs":  n = getvalue#(userstr, sym): primary# = abs(n)
            case "atan": n = getvalue#(userstr, sym): primary# = atn(n)
            case "cos":  n = getvalue#(userstr, sym): primary# = cos(n)
            case "exp":  n = getvalue#(userstr, sym): primary# = exp(n)
            case "fix":  n = getvalue#(userstr, sym): primary# = fix(n)
            case "int":  n = getvalue#(userstr, sym): primary# = int(n)
            case "log":  n = getvalue#(userstr, sym): primary# = log(n)
            case "sgn":  n = getvalue#(userstr, sym): primary# = sgn(n)
            case "sin":  n = getvalue#(userstr, sym): primary# = sin(n)
            case "sqr":  n = getvalue#(userstr, sym): primary# = sqr(n)
            case "tan":  n = getvalue#(userstr, sym): primary# = tan(n)
            case else: print "syntax error: expecting a primary, found:", sym
        end select
    end if
end function

' main expression parsing routine
function expr#(p as integer, userstr as string, sym as string)
    dim n as double, n2 as double, op as string, q as integer, prec as integer

    n = primary#(userstr, sym)
    do  ' while binary operator and precedence of sym >= p
        prec = binaryprec%(sym)
        if prec = 0 or prec < p% then exit do
        op = sym

        call nextsym(userstr, sym)
        select case associativity%(op)
            case rightassoc : q = binaryprec%(op)
            case leftassoc  : q = binaryprec%(op) + 1
        end select

        n2 = expr#(q, userstr, sym)
        select case op
            case "^":   n = n ^   n2
            case "*":   n = n *   n2
            case "/":   n = n /   n2
            case "\":   n = n \   n2
            case "mod": n = n mod n2
            case "+":   n = n +   n2
            case "-":   n = n -   n2
            case "=":   n = n =   n2
            case "<>":  n = n <>  n2
            case "<":   n = n <   n2
            case ">":   n = n >   n2
            case "<=":  n = n <=  n2
            case ">=":  n = n >=  n2
            case "and": n = n and n2
            case "or":  n = n or  n2
            case "xor": n = n xor n2
            case "eqv": n = n eqv n2
            case "imp": n = n imp n2
            case else: print "syntax error: expecting a binary operator, found:", sym
        end select
    loop

    expr# = n
end function

function eval#(userstr as string)
    dim sym as string
    call nextsym(userstr, sym)
    eval# = expr#(0, userstr, sym)
    if sym <> "" then print "error: extra input found: "; sym; userstr
end function
RockTheSchock
Posts: 252
Joined: Mar 12, 2006 16:25

Re: A command line calculator?

Post by RockTheSchock »

Some ideas how to make an eval function.

viewtopic.php?t=23524
badidea
Posts: 2591
Joined: May 24, 2007 22:10
Location: The Netherlands

Re: A command line calculator?

Post by badidea »

caseih wrote: Feb 21, 2022 15:15Of course for every-day command-line math, Linux has plenty of built-in tools for that, of which I mostly use bc and the python interpreter (which works nicely as a calculator).
I know bc, but I do not know how to set the scale form the command line (non-interactive mode).
So bc <<< 3/4 results in 0 where in would expect .75
And bc <<< 3^0.5 gives Runtime warning (func=(main), adr=8): non-zero scale in exponent
Maybe I should read the manual, but if someone makes a tool that works like that, then I dislike it from the start and look for something else.
badidea
Posts: 2591
Joined: May 24, 2007 22:10
Location: The Netherlands

Re: A command line calculator?

Post by badidea »

Ed Davis wrote:Here is a (hopefully) simple one I wrote a few years ago. It uses precedence climbing to parse the expression.
Thanks, I'll look into that code. I gives some errors now with the latest compiler.
Ed Davis
Posts: 37
Joined: Jul 28, 2008 23:24

Re: A command line calculator?

Post by Ed Davis »

badidea wrote: Feb 21, 2022 20:38
Ed Davis wrote:Here is a (hopefully) simple one I wrote a few years ago. It uses precedence climbing to parse the expression.
Thanks, I'll look into that code. I gives some errors now with the latest compiler.
How did you compile it?
It requires "-lang qb" in order to compile.

--
dodicat
Posts: 7983
Joined: Jan 10, 2006 20:30
Location: Scotland

Re: A command line calculator?

Post by dodicat »

Here is an eval function:

Code: Select all


'==============  PARSER START  ==================================
namespace evaluate
Dim  e_input    As String 
Dim  e_tok      As String 
Dim  e_spelling As String 
Dim  e_error    As Integer
'Dim Pi As Double = 4 * Atn(1)
end namespace

Function SEC(Byval x As Double) As Double
      SEC = 1 / Cos(x)
End Function

Function COSEC(Byval x As Double) As Double
      COSEC = 1 / Sin(x)
End Function

Function COT(Byval x As Double) As Double
      COT = 1 / Tan(x)
End Function

Function ARCSEC(Byval x As Double) As Double ''''''
      ARCSEC = Atn(x / Sqr(x * x - 1)) + Sgn((x) -1) * (2 * Atn(1))
End Function

Function ARCCOSEC(Byval x As Double) As Double
      ARCCOSEC = Atn(x / Sqr(x * x - 1)) + (Sgn(x) - 1) * (2 * Atn(1))
End Function

Function ARCCOT(Byval x As Double) As Double
      ARCCOT = Atn(x) + 2 * Atn(1)
End Function

Function sin_h(Byval x As Double) As Double
      sin_h = (Exp(x) - Exp(-x)) / 2 
End Function

Function cos_h(Byval x As Double) As Double
      cos_h = (Exp(x) + Exp(-x)) / 2
End Function

Function tan_h(Byval x As Double) As Double
      tan_h = (Exp(x) - Exp(-x)) / (Exp(x) + Exp(-x))
End Function

Function sech(Byval x As Double) As Double
      sech = 2 / (Exp(x) + Exp(-x))
End Function

Function cosech(Byval x As Double) As Double
      cosech = 2 / (Exp(x) - Exp(-x))
End Function

Function coth(Byval x As Double) As Double
      coth = (Exp(x) + Exp(-x)) / (Exp(x) - Exp(-x))
End Function

Function arcsinh(Byval x As Double) As Double
      arcsinh = Log(x + Sqr(x * x + 1))
End Function

Function arccosh(Byval x As Double) As Double
      arccosh = Log(x + Sqr(x * x - 1))
End Function

Function arctanh(Byval x As Double) As Double
      arctanh = Log((1 + x) / (1 - x)) / 2
End Function

Function arcsech(Byval x As Double) As Double
      arcsech = Log((Sqr(-x * x + 1) + 1) / x)
End Function

Function arccosech(Byval x As Double) As Double
      arccosech = Log((Sgn(x) * Sqr(x * x + 1) +1) / x)
End Function

Function arccoth(Byval x As Double) As Double
      arccoth = Log((x + 1) / (x - 1)) / 2
End Function

Function HAVERSINE(Byval x As Double) As Double
      HAVERSINE = (Sin(x/2))^2
End Function

Function pie(Byval x As Double=1) As Double
      Return (4*Atn(1))*x
End Function

Function e_function(Byref fun As String,Byval arg As Double) As Double
      Dim n As Double
      
      Select Case Lcase(fun)
      Case "abs": n = Abs(arg)
      Case "atn": n = Atn(arg)
      Case "cos": n = Cos(arg)
      Case "exp": n = Exp(arg)
      'Case "ezp": n = Exp(arg)
      Case "fix": n = Fix(arg)
      Case "int": n = Int(arg)
      Case "log": n = Log(arg)
      Case "rnd": n = Rnd(arg)
      Case "sgn": n = Sgn(arg)
      Case "sin": n = Sin(arg)
      Case "sqr": n = Sqr(arg)
      Case "tan": n = Tan(arg)
      Case "haversine":n=haversine(arg)
      Case "cosec":n=cosec(arg)
      Case "sec":n=sec(arg)
      Case "cot": n=cot(arg)
      Case "asin":n=Asin(arg)
      Case "acos":n=Acos(arg)
      Case "atn":n=Atn(arg)
      Case "arcsec":n=arcsec(arg)
      Case "arccosec":n=arccosec(arg)
      Case "arccot":n=arccot(arg)
      Case "sinh":n=sin_h(arg)
      Case "cosh":n=cos_h(arg)
      Case "tanh":n=tan_h(arg)
      Case "sech":n=sech(arg)
      Case "cosech":n=cosech(arg)
      Case "coth":n=coth(arg)
      Case "arcsinh":n=arcsinh(arg)
      Case "arccosh":n=arccosh(arg)
      Case "arccoth":n=arccoth(arg)
      Case "arctanh":n=arctanh(arg)
      Case "arcsech":n=arcsech(arg)
      Case "arccosech":n=arccosech(arg)
      Case "pi"      :n=pie(arg)
      Case Else
            If Not evaluate.e_error Then
                  Print "UNDEFINED FUNCTION " + fun
                  Print
                 evaluate.e_error = -1
                  End
            End If
      End Select
      e_function = n
End Function

Sub e_nxt()
      Dim is_keyword As Integer
      Dim c As String
      evaluate.e_tok = ""
      evaluate.e_spelling = ""
      Do
            c = Left(evaluate.e_input, 1)
            evaluate.e_input = Mid(evaluate.e_input, 2)
      Loop While c = " " Or c = Chr(9) Or c = Chr(13) Or c = Chr(10)
      
      Select Case Lcase(c)
      
      Case "0" To "9", "."
            evaluate.e_tok = "num"
            Do
                  evaluate.e_spelling = evaluate.e_spelling + c
                  c = Left(evaluate.e_input, 1)
                  evaluate.e_input = Mid(evaluate.e_input, 2)
            Loop While (c >= "0" And c <= "9") Or c = "."
            evaluate.e_input = c + evaluate.e_input
            
      Case "a" To "z", "_"
            Dim As Integer is_id
            evaluate.e_tok = "id"
            Do
                  evaluate.e_spelling = evaluate.e_spelling + c
                  c = Lcase(Left(evaluate.e_input, 1))
                  evaluate.e_input = Mid(evaluate.e_input, 2)
                  is_id = (c >= "a" And c <= "z")
                  is_id = is_id Or c = "_" Or (c >= "0" And c <= "9")
            Loop While is_id
            evaluate.e_input = c + evaluate.e_input
            is_keyword = -1
            Select Case Lcase(evaluate.e_spelling)
            Case "and"
            Case "eqv"
            Case "imp"
            Case "mod"
            Case "not"
            Case "or"
            Case "xor"
            Case Else: is_keyword = 0
            End Select
            If is_keyword Then
                  evaluate.e_tok = Lcase(evaluate.e_spelling)
            End If
            
      Case "<", ">"
            evaluate.e_tok = c
            c = Left(evaluate.e_input, 1)
            If c = "=" Or c = ">" Then
                  evaluate.e_tok = evaluate.e_tok + c
                  evaluate.e_input = Mid(evaluate.e_input, 2)
            End If
            
      Case Else
            evaluate.e_tok = c
      End Select
      
      If evaluate.e_spelling = "" Then
            evaluate.e_spelling = evaluate.e_tok
      End If
End Sub

Sub e_match (Byref token As String)
      If Not evaluate.e_error And evaluate.e_tok <> token Then
            Print "EXPECTED " + token + ", got '" + evaluate.e_spelling + "'"
            evaluate.e_error = -1:End
      End If
      e_nxt()
End Sub

Function e_prs (Byval p As Integer) As Double
      Dim n   As Double 
      Dim fun As String 
      If evaluate.e_tok = "num" Then
            n = Val(evaluate.e_spelling)
            e_nxt()
      Elseif evaluate.e_tok = "-" Then
            e_nxt()
            n = -e_prs(12)   ''   11 before then 12
      Elseif evaluate.e_tok = "not" Then
            e_nxt()
            n = Not e_prs(6)
      Elseif evaluate.e_tok = "(" Then
            e_nxt()
            n = e_prs(1)
            e_match(")")
      Elseif evaluate.e_tok = "id" Then
            fun = evaluate.e_spelling
            e_nxt()
            e_match("(")
            n = e_prs(1)
            e_match(")")
            n = e_function(fun, n)
      Else
            If Not evaluate.e_error Then
                  Print "syntax error, at '" + evaluate.e_spelling + "'"
                  evaluate.e_error = -1:End
            End If
      End If
      
      Do While Not evaluate.e_error
            If     p <= 11 And evaluate.e_tok = "^"   Then
                  e_nxt(): n = n ^ e_prs(12)
            Elseif p <= 10 And evaluate.e_tok = "*"   Then
                  e_nxt(): n = n *   e_prs(11)
            Elseif p <= 10 And evaluate.e_tok = "/"   Then
                  e_nxt(): n = n /   e_prs(11)
            Elseif p <= 9  And evaluate.e_tok = "\"   Then
                  e_nxt(): n = n \   e_prs(10)
            Elseif p <= 8  And evaluate.e_tok = "mod" Then
                  e_nxt(): n = n Mod e_prs(9)
            Elseif p <= 7  And evaluate.e_tok = "+"   Then
                  e_nxt(): n = n +   e_prs(8)
            Elseif p <= 7  And evaluate.e_tok = "-"   Then
                  e_nxt(): n = n -   e_prs(8)
            Elseif p <= 6  And evaluate.e_tok = "="   Then
                  e_nxt(): n = n =   e_prs(7)
            Elseif p <= 6  And evaluate.e_tok = "<"   Then
                  e_nxt(): n = n <   e_prs(7)
            Elseif p <= 6  And evaluate.e_tok = ">"   Then
                  e_nxt(): n = n >   e_prs(7)
            Elseif p <= 6  And evaluate.e_tok = "<>"  Then
                  e_nxt(): n = n <>  e_prs(7)
            Elseif p <= 6  And evaluate.e_tok = "<="  Then
                  e_nxt(): n = n <=  e_prs(7)
            Elseif p <= 6  And evaluate.e_tok = ">="  Then
                  e_nxt(): n = n >=  e_prs(7)
            Elseif p <= 5  And evaluate.e_tok = "and" Then
                  e_nxt(): n = n And e_prs(6)
            Elseif p <= 4  And evaluate.e_tok = "or"  Then
                  e_nxt(): n = n Or  e_prs(5)
            Elseif p <= 3  And evaluate.e_tok = "xor" Then
                  e_nxt(): n = n Xor e_prs(4)
            Elseif p <= 2  And evaluate.e_tok = "eqv" Then
                  e_nxt(): n = n Eqv e_prs(3)
            Elseif p <= 1  And evaluate.e_tok = "imp" Then
                  e_nxt(): n = n Imp e_prs(2)
            Else
                  Exit Do
            End If
      Loop
      e_prs = n
End Function

Function eval(Byref sp As String ) As Double
      Dim As Double value
      evaluate.e_error = 0
      evaluate.e_input = sp
      e_nxt()
      value = e_prs(1)
      If Not evaluate.e_error Then Return value Else evaluate.e_error=0
End Function

Function FindAndReplace(Byref instring As String,Byref ReplaceThis As String,Byref WithThis As String) As String 'optional not used
      Var lens1=Len(ReplaceThis),lens2=Len(WithThis)
      If lens1=lens2 Then lens1=0
      Dim As String s=instring
      Dim As Integer position=Instr(s,ReplaceThis)
      While position>0
            If lens1 Then   
                  s=Left(s,position-1) & WithThis & Mid(s,position+Lens1)
            Else
                  Mid(s,position) = WithThis
            End If
            position=Instr(position+Lens2,s,ReplaceThis)
      Wend
      Function=s
End Function

#include "crt.bi"
print       sin(6)-cos(6)-2^3.5+exp(1)+log(4.00086)+cosh(.009)+1/7+8+(25 and 3)
print eval("sin(6)-cos(6)-2^3.5+exp(1)+log(4.00086)+cosh(.009)+1/7+8+(25 and 3)")
sleep
 
caseih
Posts: 2157
Joined: Feb 26, 2007 5:32

Re: A command line calculator?

Post by caseih »

badidea wrote: Feb 21, 2022 20:36I know bc, but I do not know how to set the scale form the command line (non-interactive mode).
I don't remember that either. But looking it up, you have to add "scale=#" before your expression. For example "scale=4 ; 1/3"

In practice it's simpler to use the Python interpreter for me. 99% of the time, I use my desktop HP 48G calculator, however.
badidea
Posts: 2591
Joined: May 24, 2007 22:10
Location: The Netherlands

Re: A command line calculator?

Post by badidea »

Ed Davis wrote: Feb 21, 2022 20:51 How did you compile it?
It requires "-lang qb" in order to compile.
Yes, that works. Thanks. I'll play a bit more with it and see if I understand the code.
dodicat wrote: Feb 21, 2022 21:32 Here is an eval function:
...
Also seems to work perfect. Thanks.
But not much comments, so a bit of a puzzle to figure out what e_nxt() and e_prs () do.
StillLearning
Posts: 54
Joined: Aug 27, 2019 22:22

Re: A command line calculator?

Post by StillLearning »

How did you handle number overflow and underflow?
Post Reply