An Excel emulator written in Tcl, expanded from A bigger spreadsheet.

Requires the code in SYLK to be included as a separate tcl file (as in this code - you could import that code directly into this).

  # TekSel A spreadsheet with options to set/change formulae.
  # The basic data structure of a cell is an array:
  # a Tk label with a formula, a value, a list of cells referred to by this cell.
  # An entry area for editing formula.
  # The list of traces causes referring cells to reevaluate.
  package require BWidget ;# for scrilledwindow
    proc changeformula {vn newv dbg} { ;# change cell variable vn to new formula
        upvar #0 $vn vloc
        set vloc(formula) [subst -nocommands $newv]
        foreach trac $vloc(links) { # unset traces on cell.
                eval trace remove $trac
        set vloc(links) {} ;# clear record of cell links
        foreach var [regsub -all {[-+/*(), \]\[]} $vloc(formula) { }] { ;# set watch on each variable
                # if variable in formula changes then change this result (& so on)
                if {[uplevel #0 info vars $var]!=""} { 
                   set cmd "variable ::$var {write} \"evaluate $vn\""
                   eval trace add $cmd
                    lappend vloc(links)  $cmd
        subvariables $vn $dbg;# convert names of cells/vars to tcl form
        evaluate $vn
   proc txlchangeform {v {dbg 0}} { ;# testing for newer txlchangeformula
        set bracks [matchbrackets $v] ;# get list of bracket/close pairs
        if {[llength $bracks]>-1} { ;# may need to analyse the brackets if preceded by function
                set in2 ""
                set cpos 0
                set comit -1
                set prevtoken ""
                set fn ""
                set in2 {}
         #       puts "bracks in $v are $bracks"
                foreach part [regsub -all {([\^+\-*/\(\) ,])} $v { \1 }] {
                        # each part is a token in the string - a cell name OR an operator
                        incr cpos [string length $part]
                        if {$cpos>$comit} {
                                global slkdata
                                if {$part!="*" && [array names slkdata "name,$part"]!=""} {
                                        regsub -all {R(\d*)C(\d*)} $slkdata(name,$part) {\1 \2} f1
                                        set cellref [eval getreference $f1]
                                        set part $cellref
                                switch -- $part {
                                        "(" {
                                                set lpos [lsearch $bracks "$cpos *"]
                                                if {$lpos>=0} {
                                                        set range [lindex $bracks $lpos]
                                                        set cs [txlchangeform [eval string range "$v" $range] $dbg]
                                                        set part "$part[join $cs " "])"
                                                        #puts "Lbracket $lpos >>$range<< $v becomes >>$part<<"
                                                        set comit [expr {[lindex $range 1]+2}]
                                                        if {[string match $fn "power"]} {
                                                                append in2 "pow($prevtoken,$part)"
                                                                set fn ""
                                                                set part ""
                                                        } elseif {[string match $fn "date"]} { ;# split arguments at unused comma
                                                                # allows complex dates
                                                                set ags [split [string range $part 1 end-1] {@}]
                                                                append in2 "$prevtoken\[$fn [join $ags]\]"
                                                                set fn ""
                                                                set part ""
                                                        } elseif {[string match $fn "atan2"]} { ;# swap arguments
                                                                set ags [split [string range $part 1 end-1] {@}]
                                                                # split at @ as these are unused args.
                                                                # eg function atan2(atan2(3,2),1) would
                                                                # split at the comma in the first arg if split at comma.
                                                                # this algorithm returns atan2(atan2(3,2)@1)
                                                                append in2 "${prevtoken}$fn\([lindex $ags 1],[lindex $ags 0]\)" 
                                                                set fn ""
                                                                set part ""
                                                        } elseif {[string match $fn "pi"]} {
                                                        } elseif {![string match $fn ""]} {
                                                                append in2 "$prevtoken[eval $fn [split [string range $part 1 end-1]  ":"]]"
                                                                set fn ""
                                                                set part ""
                                                        } else {
                                                                append in2 $prevtoken
                                                } else {
                                                append in2 $prevtoken
                                        "," {# replace commas with character marking 'unchanged comma'
                                                append in2 "$prevtoken"
                                                set part "@" ;# make clear the commas at this level.
                                        "^" {  set fn "power" }
                                        atan2 -
                                        date -
                                        degrees -
                                        radians -
                                        laplacian -
                                        prod -
                                        sum -
                                        mean {
                                                set fn "$part"
                                        pi {
                                                append in2 "$prevtoken[$part]"
                                                set comit [expr {$cpos+2}]
                                                set part ""
                                        default {
                                                if {[string match $fn "power"]} {
                                                        append in2 "pow($prevtoken,$part)"
                                                        set fn ""
                                                        set part ""
                                                } elseif {[string match $fn "date"]} { ;# split arguments at comma
                                                        set ags [split [string range $part 1 end-1] ","]
                                                        append in2 "$prevtoken\[$fn [join $ags]\]"
                                                } elseif {[string match $fn "atan2"]} { ;# swap arguments
                                                        set ags [split [string range $part 1 end-1] ","]
                                                        append in2 "${prevtoken}$fn\([lindex $ags 1],[lindex $ags 0]\)" 
                                                        puts "Atan2 == $in2"
                                                } elseif {[string match $fn "pi"]} {
                                                } elseif {![string match $fn ""]} {
                                                        append in2 "$prevtoken[$fn $part]"
                                                        set fn ""
                                                        set part ""
                                                } else {
                                                        append in2 $prevtoken
                                if {[string match $fn ""]} {set prevtoken $part} ;#else { puts "Not part $v at $part; $prevtoken"}
                        } ;# cpos<comit region to be omitted from parsing
                append in2 $prevtoken
        return [subst $in2]

  proc txlchangeformula {cell v {dbg 0}} { ;# converts an excel-like formula to tcl-> spreadsheet
        upvar #0 $cell vloc
        set vloc(rawformula) $v
        set in2 [txlchangeform $v $dbg] ;# append [expr {$cell=="e8"}] to set dbg flag for cell e8
        changeformula $cell $in2 $dbg
        return $in2
    proc subvariables {vn {dbg 0}} { ;# change variables into Tcl variables, eg d12->$d12(value).
        upvar #0 $vn vloc
        upvar #0 vlist clist
        set form [regsub -all {([+\-*/\(\) ,\[\]])} $vloc(formula) { {\1} }] ;# the formula
        set i 0
        foreach part $form {
                if {[lsearch -exact $clist $part]>=0} {
                set form [lreplace $form $i $i "\$${part}(value)"]
                } ;#else { puts "No good $part"}
                incr i
        #this string map ensures division uses floating point arithmetic
        set vloc(tclformula) [string map {/ *1.0/} [join $form {}]]
    proc evaluate {vn args} { ;# reevaluate the formula - args are sent by trace callbacks
        upvar #0 $vn vloc
        # evaluate the formula in top level (as accesses other cell values)
        # replace value for display            
        if {[catch {set vloc(value) [uplevel #0 expr $vloc(tclformula)]}]} {
                set vloc(value) $vloc(rawformula)
        return $vloc(value)
  proc showformula {where tick cell} {  ;# where is the input entry area. cell is where to copy formula from
    # copy formula from cell to change formula area.
    upvar #0 $cell vloc
    global slkdata
    $where delete 0 end; $where insert 0 $vloc(rawformula)
    # Change effect of Update button to send "where" to cell
    $tick config -command "txlchangeformula $cell \[$where get\]" -text "Update $cell"
  proc matchbrackets {tsb {depth 0}} { ;# returns list of matching brace to Nth lh brace.
  # Or any other pair of letters, eg ( ); [ ]
        incr depth
        set rddepth 0
        set cind 0
        set cclos -1
        set res ""
        set words [regsub -all {([\(\)\[\]])} $tsb { \1 }]
        ;# check each block for brackets
        catch {
          foreach block $words {
                if {$cind>$cclos} {
                if {$block== "\[" || $block== "\("} {
                        set substring [string range $tsb [expr {$cind+1}] end]
                        set match [matchbrackets $substring $depth]
                        foreach p $match {
                                set p [list [expr {[lindex $p 0]+$cind+1}] [expr {[lindex $p 1]+$cind+1}]]
                                lappend res $p
                        set cclos [lindex [lindex $res end] end] ;# last character in bracket - already tested
                        incr rddepth 
                } elseif {$block== "\]" || $block== "\)"} {
                        incr rddepth -1
                        if {$rddepth<0} {
                                lappend res [list 0 [expr $cind-1]]
                                return $res
                if {$depth<=1 && $rddepth<0} {return $rddepth}
                incr cind [string length $block]
        if {$depth<=1 && $rddepth>0} {return $rddepth}
        if {![info exists res]} { return ""}
        return $res;#### try new method - below this works
  # create the cells. Lets call them A1, A2... b1,b2 etc like many other spreadsheets
  set vlist {} ;# list of cell names
  pack [frame .enterform]
  pack [button .enterform.tick -text "Update" -relief raised -width 8] -side left
  pack [entry .enterform.input -width 72] -side left
  pack [frame .rowtitle]
  pack [label .rowtitle.about -text "TXl" -width 4] -side left
  proc getcell {cell dc dr} { ;# cell relative to cell by dc, dr cols & rows
        set col [string index $cell 0]
        set newcol [format %c [expr [scan $col %c]+$dc]]
        return $newcol[expr {[string range $cell 1 end]+$dr}]
  proc laplacian {cell} { # laplacian - mean of 4 surrounding cells.
        set row [string range $cell 1 end]
        set col [string index $cell 0]
        set sum "[getcell $cell -1 0]+[getcell $cell 1 0]"
        append sum "+[getcell $cell 0 -1]+[getcell $cell 0 1]"
        return "($sum)*.25"
  proc debug {{cell b6}} {
        upvar #0 $cell cl
        puts "Cell $cell - $cl(rawformula)"
        puts " l1  $cell - $cl(formula)"
        puts " tcl $cell - $cl(tclformula)"
if 0 {
Tcl math functions can be used and new functions such as pi.
  proc pi {} { return  3.1415926535897}
if 0 { Examples of extending the functions available:
  range is used internally to produce a list of cells in a rectangular region
  sum adds a range of cells
  mean the mean value of the range
  prod the product of all the values in the range.

Note how these are inserted into cells, eg:
txlchangeformula a7 "[mean e1 e4]"
OR using the conversion procs above:
txlchangeformula a7 "mean(e1:e4)"
  proc range {cstart cend} { ;# return list of variables in range start to end
        set col [string index $cstart 0]
        while {$col <= [string index $cend 0]} {
                set row [string range $cstart 1 end]
                while {$row <=[string range $cend 1 end]} {
                        lappend range $col$row
                        incr row
                set col [format %c [expr [scan $col %c]+1]]
        return $range
  proc sum {cstart cend} { # sum converts to sum of cell names.
        foreach vn [range $cstart $cend] { append res "+$vn"}
        return "(${res})"
  proc mean {cstart cend} { # mean= sum of cell names divide by Ncells.
        return "([sum $cstart $cend]/[expr double([llength [range $cstart $cend]])])"
  proc prod {cstart cend} { # prod converts to multiplication of cell names.
        set res "1"
        foreach vn [range $cstart $cend] { append res "*$vn"}
        return "(${res})"
  proc radians {degs} { # converts cell or formula to radians.
        return "(($degs)/57.295779513082320876798154814105)"
  proc degrees {rads} { # converts to degrees.
        return "(57.295779513082320876798154814105*($rads))"
  proc date {year month day} { # converts to days since 1 Jan 1970.
        return "round(\[clock scan $year-$month-$day\]/86400.)"
  pack [ScrolledWindow  .sw] -expand t -fill both
  set grd [ScrollableFrame  .sw.gridd]
  .sw setwidget $grd
  set grd [$grd getframe]

  catch {
  # auto_mkindex [pwd]  
  # pkg_mkIndex [pwd]  
  #  package require Sylk 0.2
  source rdslk.tcl
    proc getreference {irow icol} { ;# convert int I,J to B12 format
        return "[format %c [expr {96+$icol}]]$irow"
  proc cellsub {form col row} { ;# sub RNNCMM to AB for SLK files only
        set fou ""
        foreach part [regsub -all {([\^+\-*/\(\) ,])} $form { \1 }] {
                regsub -all {R(\d*)C(\d*)} $part {\1 \2} rowcol
                if {$rowcol!=$part} { ;# format RnnCmm found
                if {[llength $rowcol]<2} { lappend rowcol $col  }
                        #puts "$part->$rowcol"
                        append fou [eval getreference $rowcol]
                } else {append fou $part}
    #    set fou [regsub -all {R(\d*)C(\d*)} $form {[getreference \1 \2]}]
        return [string tolower $fou]

  set slkfile sample.slk
  readSYLK $slkfile slkdata

  for {set ir 1} {$ir<=$slkdata(yMax)} {incr ir} { ;#  for all the rows of data defined
        set rowlabel [label $grd.rlab$ir -text $ir -width 4]
        grid $rowlabel -column 1 -row [expr {$ir+1}]
        for {set jr 1} {$jr<=$slkdata(xMax)} {incr jr} { ;# for each cell that has been defined
        if {$ir==1} { ;#  label the columns
                set colnam [format "%c" [expr [scan "a" %c]+$jr-1]]
                grid [label $grd.$colnam -text $colnam] -column [expr $jr+1] -row 1
                set column [format "%c" [expr [scan "a" %c]+$jr-1]]
                set vname $column$ir
                variable $vname
                lappend vlist $vname
                set ${vname}(rawformula) 0.0
                set ${vname}(tclformula) 0.0
                set ${vname}(formula) 0.0
                set ${vname}(value) ""
                set ${vname}(links) {}
                set element [label $grd.$ir$column -textvar ${vname}(value) -relief raised -width 16]
                grid $element -column [expr {$jr+1}] -row [expr {$ir+1}]
                bind $element <ButtonRelease> "showformula .enterform.input \
                        .enterform.tick $vname"
  for {set ir 1} {$ir<=$slkdata(yMax)} {incr ir} { ;#  or <$slkdata(yMax)
    foreach name [array names slkdata *,$ir,value] { ;# for each cell that has been defined
      set formnam [string map {value formula} $name]
        set cellparts [split $name ","]
        set cellref [getreference [lindex $cellparts 1] [lindex $cellparts 0]]
        if {[array names slkdata $formnam]!=""} { ;# use the formula
                if {[catch {
                txlchangeformula $cellref [cellsub $slkdata($formnam) [lindex $cellparts 1] [lindex $cellparts 0]]
                } errmsg]} {puts "$cellref Error $errmsg in formula $slkdata($formnam)"
                        catch {  txlchangeformula $cellref [cellsub $slkdata($formnam) [lindex $cellparts 1] [lindex $cellparts 0]] 1}
        } else { ;# constant value- no formula  puts "$cellref Row $ir constant $slkdata($name) "
                txlchangeformula $cellref [cellsub $slkdata($name) [lindex $cellparts 1] [lindex $cellparts 0]]
        set sep 0
        foreach type {"format" "numformat" "pformindex"} {
        if {[array names slkdata [string map "value $type" $name]]!=""} {
             if {!$sep} { puts "=== cell $cellref"; set sep 1}
                puts "Format $type $slkdata([string map "value $type" $name])"}                
  wm title . "TXl $slkfile"

  if {[array size slkdata]<2} { ;# could not find/read the file. Load some test formulae
    puts "File $slkfile was not loaded correctly."

Here is a small file to save as sample.slk


B;Y16;X3;D0 0 15 2

O;L;D;V0;K47;G100 0.001