Calculator using the infix package

Summary

Arjen Markus 2010-03-22: Every so often I seem to reinvent a calculator program, largely because none of them really do the job comfortably. The infix package solves the hardest problem out-of-the-box: parsing the arithmetic expressions and turning them into something that can be evaluated. So, using that package, here is a new attempt ... far from complete, but it does do the job.

Description

Lars H: Rather than calling the infix command (which will cache the compiled form of all expressions it evaluates), you may prefer to call the underlying infix::core::compile command. Then instead of

set rc [catch {
    set R [infix $variables $cmd]
} msg]
#I would say something like
set rc [catch {
    set R [namespace eval ::values [infix::core::compile ::values $cmd]]
} msg]

The "infix variables" will then be entries of the ::values::V array.

Implementation

# calc.tcl --
#     Use the infix package as a basis for a desktop calculator program
#     like "bc"
#
#     To do:
#     - Define functions
#     - Introduce bigfloat package
#

package require infix


# values --
#     Namespace to run the commands in, stores the variables
#
namespace eval ::values {
    variable R ""
    ::infix::core::setup base numconst expr::ops expr::fun expr::ternary

    set ::variables "R <-> ::values::R"
}


# runCommand --
#     Run the infix command, keeping track of variables
#
# Arguments:
#     cmd          Command to run
#
# Result:
#     None
#
# Side effects:
#     Prints the answer (stored in R) and sets any variables
#
proc ::values::runCommand {cmd} {
    global variables
    variable R

    set var ""
    if { [regexp {([a-zA-Z_][a-zA-Z_0-9]*) *:?=} $cmd ==> var] } {
        if { [lsearch $variables $var] < 0 } {
            variable $var
            lappend variables $var -> ::values::$var
        }
    }

    set rc [catch {
        set R [infix $variables $cmd]
    } msg]

    if { $rc != 0 } {
        puts [lindex [split $::errorInfo \n] 0]
    } else {
        lset variables end-1 <->
        puts $R
    }
}


# main --
#     Read the command, evaluate and print
#
while {1} {
   puts -nonewline "> "
   flush stdout

   gets stdin line
   set cmd ""
   while { [string index [string trim $line] end] == "\\" } {
       append cmd " [string range [string trim $line] 0 end-1]"
       gets stdin line
   }
   append cmd " $line"

   switch -glob -- $cmd {
       " quit" -
       " exit" {
           exit
       }
       "scale *" {
           set newscale [lindex $cmd 1]
           if { [string is -strict integer $newscale] } {
               set scale newscale
           } else {
               puts "Scale must be an integer"
           }
       }
       default {
           ::values::runCommand $cmd
       }
   }
}