TekSel

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

ID;PWXL;N;E

B;Y16;X3;D0 0 15 2

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

C;Y1;X1;K"X"

C;X2;K"Y"

C;X3;K"atan(xy)"

C;Y2;X1;K10

C;X2;K1

C;X3;K0.099668652491162;EATAN2(RC[-2],RC[-1])

C;Y3;X1;K9;ER[-1]C-1

C;X2;K2;ER[-1]C+1

C;X3;K0.218668945873942;EATAN2(RC[-2],RC[-1])

C;Y4;X1;K8;ER[-1]C-1

C;X2;K3;ER[-1]C+1

C;X3;K0.358770670270572;EATAN2(RC[-2],RC[-1])

C;Y5;X1;K7;ER[-1]C-1

C;X2;K4;ER[-1]C+1

C;X3;K0.519146114246523;EATAN2(RC[-2],RC[-1])

C;Y6;X1;K6;ER[-1]C-1

C;X2;K5;ER[-1]C+1

C;X3;K0.694738276196703;EATAN2(RC[-2],RC[-1])

C;Y7;X1;K5;ER[-1]C-1

C;X2;K6;ER[-1]C+1

C;X3;K0.876058050598193;EATAN2(RC[-2],RC[-1])

C;Y8;X1;K4;ER[-1]C-1

C;X2;K7;ER[-1]C+1

C;X3;K1.05165021254837;EATAN2(RC[-2],RC[-1])

C;Y9;X1;K3;ER[-1]C-1

C;X2;K8;ER[-1]C+1

C;X3;K1.21202565652432;EATAN2(RC[-2],RC[-1])

C;Y10;X1;K2;ER[-1]C-1

C;X2;K9;ER[-1]C+1

C;X3;K1.35212738092095;EATAN2(RC[-2],RC[-1])

C;Y11;X1;K1;ER[-1]C-1

C;X2;K10;ER[-1]C+1

C;X3;K1.47112767430373;EATAN2(RC[-2],RC[-1])

C;Y12;X1;K0;ER[-1]C-1

C;X2;K11;ER[-1]C+1

C;X3;K1.5707963267949;EATAN2(RC[-2],RC[-1])

C;Y13;X1;K-1;ER[-1]C-1

C;X2;K12;ER[-1]C+1

C;X3;K1.65393755868334;EATAN2(RC[-2],RC[-1])

C;Y14;X1;K-2;ER[-1]C-1

C;X2;K13;ER[-1]C+1

C;X3;K1.72344565519016;EATAN2(RC[-2],RC[-1])

C;Y15;X1;K-3;ER[-1]C-1

C;X2;K14;ER[-1]C+1

C;X3;K1.78188966001764;EATAN2(RC[-2],RC[-1])

C;Y16;X1;K-4;ER[-1]C-1

C;X2;K15;ER[-1]C+1

C;X3;K1.83139871854224;EATAN2(RC[-2],RC[-1])

E