Version 10 of A fancier little calculator

Updated 2005-04-28 18:43:09

Derek Peschel 2005-04-27 This started as A little calculator and got transformed to meet my goals:

  • learn Tk by writing it
  • add real scientific notation

This is a feature I've wanted for a long time. With modern software and fonts, I see no reason for displaying e notation to the user in the final result. (Intermediate results, and communications between programs, are another matter.)

  • make the onscreen keys match the positions of the keypad on my Mac keyboard

Calculator keyboard layouts work well on calculators. But on a computer I'd rather use a computer keyboard. Having a calculator layout, and being forced to use the mouse, is doubly inefficient. But getting rid of the screen layout, or making it unresponsive to the mouse, gets rid of the free online help you already have. And forcing the functions to match the keypad wouldn't work well either. You don't need enter and =. So this is a compromise: the positions work with the keypad layout, but the help and some of the functions remain. And the main part of the keyboard needs to work too. Later, a way to hide the buttons or put them in a separate window would be good.

  • split the expression and results

Again, why not use hardware like the nice big screen you almost certainly have? The split may make selection and cursor placement more intuitive too (but I need more evidence). And it makes the scientific-notation feature more reliable. The color feature just sort of happened along the way.


Mysterious bugs:

  • The text widget doesn't always accumulate characters. SMH 20050427 Fixed. Changed by removing $ from set $::estate notDone
  • The presence of the insertion cursor doesn't tell the whole story about focus.
  • Superscripts are being translated back into ASCII. I have tried just an entry widget in wish and not gotten correct results then either.
  • escargo 27 Apr 2005 - Numbers entered through the keyboard are entered twice. (I suspect that perhaps two different bindings are firing.) DHP 28 Apr - It has something to do with focus. I thought the problem might be writing <Key- instead of <KeyPress- but changing to <KeyPress- doesn't fix the bug.

Other changes:

  • DHP 2005-04-28 Multiplying large integers may give a negative integer. Added a second "string map" entry, from * to *1.0*.
  • DHP 2005-04-28 Changed [0-9][0-9]* to [0-9]+ in superscriptRE.

Grand plans for later, from other people's code if it works:

  • Get rid of the bugs.
  • Switch to RPN. Display as much stack (or memory or whatever) as possible.
  • A text widget that fully acts like a calculator display.
  • More accurate math.
  • Some kind of type system, because with an entire programming language at your disposal, using regexes on the results can't possibly work reliably. Also expr may not be predictable enough.
  • Features, always features....
  • Use of the option database or another way to separate preferences from hardcoded behavior.
  • A single and short representation of the grid layout data.
  • Better Macintosh compatibility.
  • Good UNIX and Windows compatibility would also be nice.
  • As much as I like HP calculators, borrowing the shift keys and programming mentality from them (as in Programmable RPN calculator) seems misguided. So some kind of better languge (Tcl? FORTH? LISP?).

 ### Constants and variables.

 set ::n 0                       ;# current button-widget name
 set ::c 4                       ;# number of columns

 set ::upToDateForeground black  ;# for result that has just been computed
                                  #  when expression is shown, and for
                                  #  expression that can be edited
 set ::staleForeground gray      ;# for result that remains from last
                                  #  calculation but expression isn't shown,
                                  #  or for expression that can't be edited
 set ::errorForeground red       ;# for result after a Tcl error
 set ::staleErrorForeground darkred
                                  # for stale erroneous result

 ## As well as representing colors, these change the behavior of procedures.
 set ::estate notDone            ;# notDone (can be edited,
                                  #  upToDateForeground) or done (can't be
                                  #  edited, staleForeground)
 set ::rstate upToDate           ;# upToDate or stale or error or staleError
                                  #  as explained next to colors

 array set superscripts {
     0   \u2070
     1   \u0089
     2   \u0082
     3   \u0083
     4   \u2074
     5   \u2075
     6   \u2076
     7   \u2077
     8   \u2078
     9   \u2079
     +   \u207A
     -   \u207B
 }
 #                   1           2             3        4
 set superscriptRE {^([-+]?[0-9]+(?:\.[0-9]+)?)(?:[DdEe]([-+]?[0-9]+))?$}
 # 1 is sign, integer, fraction if any; integer. with no fraction is not
 #  allowed
 # 2 is fraction if any, an operand to the ? afterward, not returned
 #  by regex because of the ?: modifier
 # 3 is exponent character, exponent sign, exponent, not returned by regex
 #  because of the ?: modifier
 # 4 is the part of 3 that I want to translate to Unicode

 ## Widgets created later: .e is the expression (an entry widget)
 ## and .r is the result (a second entry widget).  .0 through .17
 ## are the buttons.

 ### Set the window title.

 wm title . Calculator

 ### Create buttons but don't do anything with them yet.

 ## keypad     button widget names
 ## layout     and "grid" command args
 ##
 ## c = / *    .0  .1  .2  .3
 ## 7 8 9 -    .4  .5  .6  .7
 ## 4 5 6 +    .8  .9  .10 .11
 ## 1 2 3 e    .12 .13 .14 .15
 ##  0  .      .16  -  .17  ^
 ##
 ## c represents "clear"
 ## e represents "enter"
 ##
 ## button purposes like keypad except enter position is = purpose
 ##  and = position is exponent purpose

 foreach key {
     C e / *
     7 8 9 -
     4 5 6 +
     1 2 3 =
     0   .  
 } {
     ## Set each key's button's text.
     ## "default" applies to C 7 8 9 4 5 6 + 1 2 3 = 0 . keys
     switch -- $key {
         e       {set keytext "\u00d710\u207F"}
         /       {set keytext "\u00f7"}
  • {set keytext "\u00d7"}
         -       {set keytext "\u2212"}
         default {set keytext $key}
     }
     ## Set each key's button's command.  See "procedures" below.
     ## "default" applies to e / * 7 8 9 - 4 5 6 + 1 2 3 0 . keys
     switch -- $key {
         C       {set cmd clearboth}
         =       {set cmd =}
         default {set cmd "hit $key"}
     }
     ## Create a button with the text and command as just set.
     ## The grid manager changes a button's width automatically,
     ## but not its height, so do that now.
     if [expr $::n == 15] {
         button .$::n -text $keytext -command $cmd -width 4 -height 2
     } else {
         button .$::n -text $keytext -command $cmd -width 4
     }
     incr ::n
 }

 ### Lay out the entry widgets and buttons in a grid.

 ## Macintosh system dependency -- This is the only font I've found
 ## with superscripts that are all the same size.
 grid [entry .e -textvar e -font {{Hoefler Text} 24} -just left] \
     -sticky we -columnspan $::c -pady 5
 grid [entry .r -font {{Hoefler Text} 24} -just right] -sticky we \
     -columnspan $::c -pady 5

 grid .0  .1  .2  .3
 grid .4  .5  .6  .7
 grid .8  .9  .10 .11
 grid .12 .13 .14 .15
 grid .16  -  .17  ^
 grid configure .0  -sticky we
 grid configure .1  -sticky we
 grid configure .2  -sticky we
 grid configure .3  -sticky we
 grid configure .4  -sticky we
 grid configure .5  -sticky we
 grid configure .6  -sticky we
 grid configure .7  -sticky we
 grid configure .8  -sticky we
 grid configure .9  -sticky we
 grid configure .10 -sticky we
 grid configure .11 -sticky we
 grid configure .12 -sticky we
 grid configure .13 -sticky we
 grid configure .14 -sticky we
 grid configure .15 -sticky nsew
 grid configure .16 -sticky we
 grid configure .17 -sticky we

 ### Probably Macintosh system dependency -- Bind keyboard keys.
 ### Focus will be set later, and we assume it is never reset.

 bind .e <Key-Num_Lock>    clearboth
 bind .e <Key-c>           clearboth
 bind .e <Key-KP_Equal>    "hit e"
 bind .e <Key-e>           "hit e"
 bind .e <Key-KP_Divide>   "hit /"
 bind .e <Key-/>           "hit /"
 bind .e <Key-KP_Multiply> "hit *"
 bind .e <Key-*>           "hit *"
 bind .e <Key-KP_7>        "hit 7"
 bind .e <Key-7>           "hit 7"
 bind .e <Key-KP_8>        "hit 8"
 bind .e <Key-8>           "hit 8"
 bind .e <Key-KP_9>        "hit 9"
 bind .e <Key-9>           "hit 9"
 bind .e <Key-KP_Subtract> "hit -"
 bind .e <Key-minus>       "hit -"
 bind .e <Key-KP_4>        "hit 4"
 bind .e <Key-4>           "hit 4"
 bind .e <Key-KP_5>        "hit 5"
 bind .e <Key-5>           "hit 5"
 bind .e <Key-KP_6>        "hit 6"
 bind .e <Key-6>           "hit 6"
 bind .e <Key-KP_Add>      "hit +"
 bind .e <Key-plus>        "hit +"
 bind .e <Key-KP_1>        "hit 1"
 bind .e <Key-1>           "hit 1"
 bind .e <Key-KP_2>        "hit 2"
 bind .e <Key-2>           "hit 2"
 bind .e <Key-KP_3>        "hit 3"
 bind .e <Key-3>           "hit 3"
 bind .e <Key-KP_Enter>    =
 bind .e <Key-Return>      =
 bind .e <Key-KP_0>        "hit 0"
 bind .e <Key-0>           "hit 0"
 bind .e <Key-KP_Decimal>  "hit ."
 bind .e <Key-period>      "hit ."

 ### Procedures that widgets call.

 proc clearboth {} {
     focus .e
     set ::e ""
     .e config -foreground $::upToDateForeground
     set ::estate notDone

     set ::r ""
     setr
     .r config -foreground $::upToDateForeground
     set ::rstate upToDate
 }

 proc = {} {
     focus .e
     .e config -foreground $::staleForeground
     set ::estate done

     if {![catch [set ::r [expr [string map {* *1.0* / *1.0/} $::e]]]]} {
         # what value should r show? its previous one? expr's best nuemrical
         #  approximation? text of the error?
         .r config -foreground $::errorForeground
         # is this really a good idea?
         setr
         set ::rstate error
     } else {
         .r config -foreground $::upToDateForeground
         setr
         set ::rstate upToDate
     }
 }

 proc hit {key} {
     focus .e
     switch -- $::estate {
         notDone {
             .e insert end $key
             .e icursor end
         }
         done    {
             if [regexp {[-+*/]} $key] {
                 set ::e $::r
             } else {
                 set ::e ""
             }
             .e insert end $key
             .e icursor end
             set ::estate notDone
             .e config -foreground $::upToDateForeground
         }
         default {
             # ignore for now
         }
     }

     switch -- $::rstate {
         upToDate   {
             .r config -foreground $::staleForeground
             set ::rstate stale
         }
         stale      {
             # remains stale
         }
         error      {
             .r config -foreground $::staleErrorForeground
             set ::rstate staleError
         }
         staleError {
             # remains staleError
         }
         default    {
             # ignore for now
         }
     }
 }

 ### Subroutines.

 proc setr {} {
     # Hopefully end will be rounded, as described in manual
     .r delete 0 end
     set ignored ""
     set mantissa ""
     set exponent ""
     set rdisplay ""
     if {[regexp $::superscriptRE $::r ignored mantissa exponent]} {
         if {[string length $exponent] > 0} {
             set unicodeExponent [string map [array get superscripts] $exponent]
             set rdisplay "$mantissa \u00d710$unicodeExponent"
         } else {
             set rdisplay $mantissa
         }
     }
     .r insert end $rdisplay
 }

 ### Main loop.

 encoding system utf-8
 focus .e
 wm resizable . 0 0