Version 12 of Tiny Excel-like app in plain Tcl/Tk

Updated 2015-03-28 09:16:53 by dbohdan

dbohdan 2015-03-25: The following code is a Tk spreadsheet inspired, title included, by http://jsfiddle.net/ondras/hYfN3/ . Halfway through writing it I found the tiny spreadsheet and lifted the idea of tracing variable reads to trigger recalculation from it. One important difference from the tiny spreadsheet is that TEAIPTT does not require a Tcl expr patch.

Feature comparison with the JavaScript version

✓ Under 35 lines of plain Tcl/Tk (rather than 30 lines of vanilla JS*)
✓ Libraries used: none
Excel-like syntax (formulas start with "=")
✓ Support for arbitrary expressions (=A1+B2*C3)
✓ Circular reference prevention
✗ Automatic persistence

* Lines are assumed to be up to 100 characters long (because the under-30-line JavaScript spreadsheet uses lines longer than 80 characters, 100 is the next common limit and using lines of unlimited length would feel like cheating).

The smallest JS spreadsheet is currently this one: http://xem.github.io/sheet/ (227 bytes for the minimal version, 267b if we add automatic persistence).

Screenshot

under-35-line-spreadsheet-screenshot

Code

#!/usr/bin/wish
foreach row {0 1 2 3 4 5 6} {
    foreach {column columnName} {0 "" 1 A 2 B 3 C 4 D 5 E 6 F} {
        set widget [if {$column == 0 || $row == 0} {
            ::ttk::label .label$columnName$row -text [expr { $row == 0 ? $columnName : $row }]
        } else {
            lassign {"" ""} ::formula($columnName$row) ::$columnName$row
            trace add variable ::$columnName$row read recalc
            ::ttk::entry .cell$columnName$row -textvar ::$columnName$row -width 10 -validate focus \
                    -validatecommand [list ::reveal-formula $columnName$row %V %s]
        }]
        grid $widget -row $row -column $column
    }
}
proc set-cell {cell value} {
    .cell$cell delete 0 end
    .cell$cell insert 0 $value
}
proc recalc {cell args} {
    if {$::formula($cell) ne ""} {
        catch {set-cell $cell [uplevel #0 [list \
                expr [regsub -all {([A-F][1-6])} $::formula($cell) {$\1}]]]}
    }
}
proc reveal-formula {cell event value} {
    if {$event eq "focusin"} {
        if {$::formula($cell) ne ""} { set-cell $cell =$::formula($cell) }
    } else { ;# focusout
        if {[string match "=*" $value]} { set ::formula($cell) [string range $value 1 end] }
        foreach otherCell [array names ::formula] { recalc $otherCell }
    }
    return 1
}

Discussion

AMG: Exchanged the row and column foreach lines so that the tab key moves across rather than down.

dbohdan 2015-03-28: xem, it is neat just how much functionality your spreadsheet packs in 0x00-0xE2 but it looks like it only recalculates formulas one step at a time. I.e., if you have A1 set to =B1 and B1 set to =C1 and then put the number 5 into C1 it will take two onblur events before the value propagates back to A1. This also means you can create stable infinite loops that will cycle values and NaNs between cells.

I wonder how much you could golf the Tcl/Tk version if you try to optimize for character count rather than line count and relax the requirement on recalc. Any takers? A page like Golfed spreadsheet would be appropriate for that. I also wonder what would be the overhead of adding full recalculation with circular reference prevention to xem's JS version.