# copyright (C) 1997-2006 Jean-Luc Fontaine (mailto:jfontain@free.fr)
# this program is free software: please read the COPYRIGHT file enclosed in this package or use the Help Copyright menu

# $Id: formutab.tcl,v 1.46 2006/04/08 13:31:54 jfontain Exp $


namespace eval formulas {

if {$global::withGUI} { ;# >8
    set (existingMessage) [mc {identical expression found in existing formula "%s"}]
} ;# >8

    class table {

if {$global::withGUI} { ;# >8

        proc table {this parentPath args} composite {[new frame $parentPath] $args} viewer {} {
            composite::complete $this
            constructor $this $composite::($this,-object) $composite::($this,-category)
            # wait till after completion before creating table since some options are not dynamically settable
            set table [new dataTable $widget::($this,path)\
                -data $($this,dataName) -draggable $composite::($this,-draggable) -background $viewer::(background)\
            ]
            pack $widget::($table,path) -fill both -expand 1
            set tablePath $dataTable::($table,tablePath)
            if {!$global::readOnly} {
                set menu [menu $tablePath.menu -tearoff 0]
                set ($this,help) [new menuContextHelp $menu]
                $menu add command -label [mc Edit]... -command "formulasDialog $this \$($this,pointed)"
                menuContextHelp::set $($this,help) 0 [mc {edit formulas in this table}]
                bindtags $tablePath [concat [bindtags $tablePath] PopupMenu$this]
                bind PopupMenu$this <ButtonPress-3> "
                    if {\$::tk::Priv(popup) eq \"\"} {
                        set ($this,pointed) \[formulas::table::pointed $this %x %y\]
                        tk_popup $menu %X %Y
                    }
                "
            }
            set ($this,drop) [new dropSite -path $tablePath -formats {FORMULAS KILL} -command "formulas::table::handleDrop $this"]
            set bindings [new bindings $tablePath end]
            bindings::set $bindings <Enter> "formulas::table::enter $this %x %y"
            bindings::set $bindings <Leave> "formulas::table::leave $this"
            set ($this,bindings) $bindings
            set ($this,tablePath) $tablePath
            set ($this,table) $table
            set configurations $composite::($this,-configurations); set rows $composite::($this,-rows)
            if {[llength $configurations] != [llength $rows]} {
                error "fatal dashboard file error: [llength $configurations] configurations but [llength $rows] rows"
            }
            set formulas [createFormulas $this $configurations $rows]
            if {[llength $formulas] == 0} {
                set label [centerMessage $tablePath\
                    [mc "formulas table:\ndrop or edit formulas"] $viewer::(background) $global::viewerMessageColor\
                ]
                if {!$global::readOnly} {
                    bindtags $label [concat [bindtags $label] PopupMenu$this]
                }
            } else {
                manage $this $formulas
            }
            set ($this,constructed) {}
            set-state $this $composite::($this,-state)            ;# force state initialization now that drag and drop objects exist
        }

} else { ;# >8                       remove Tk dependant code since this class can be used by monitoring daemon in a Tcl environment

        proc table {this args} switched {$args} viewer {} {
            switched::complete $this
            constructor $this $switched::($this,-object) $switched::($this,-category)
            set formulas [createFormulas $this $switched::($this,-configurations) $switched::($this,-rows)]
            if {[llength $formulas] > 0} {
                manage $this $formulas
            }
        }

} ;# >8

        proc constructor {this object category} {
            set ($this,nextRow) 0
            set dataName ::formulas::table::$(nextDataIndex)data          ;# unique name based on index (available after completion)
            unset -nocomplain $dataName
            array set $dataName [list\
                updates 0\
                0,label [mc formula] 0,type ascii\
                1,label [mc value] 1,type real\
                indexColumns 0\
                sort {0 increasing}\
            ]
            set ($this,dataName) $dataName
            set instance [modules::loadFormulasModule $(nextDataIndex) $object $category]
            set ($this,namespace) $modules::instance::($instance,namespace)
            foreach [list ${dataName}(0,message) ${dataName}(1,message)] [$($this,namespace)::messages] {}
            set ($this,instance) $instance
            incr (nextDataIndex)
        }

        proc createFormulas {this configurations rows} {
            set formulas {}
            foreach options $configurations row $rows {                                       ;# create formulas from dashboard file
                set formula [eval new formulas::formula $options]
                set ($this,row,$formula) $row
                if {$row >= $($this,nextRow)} {set ($this,nextRow) [expr {$row + 1}]}             ;# make sure rows are never reused
                lappend formulas $formula
            }
            return $formulas
        }

        proc ~table {this} {
            variable ${this}count

            foreach formula [formulas $this] {delete $formula}
            unset -nocomplain ${this}count
if {$global::withGUI} { ;# >8
            catch {delete $($this,tip)}
            delete $($this,drop) $($this,bindings) $($this,table)
            if {[info exists ($this,help)]} {delete $($this,help)}
} ;# >8
            set dataName $($this,dataName)
            incr ${dataName}(updates)                                          ;# so related viewers can show cells have disappeared
            unset $dataName
            modules::unload $($this,instance)
if {$global::withGUI} { ;# >8
            if {$composite::($this,-deletecommand) ne ""} {
                uplevel #0 $composite::($this,-deletecommand)                               ;# always invoke command at global level
            }
} else { ;# >8
            if {$switched::($this,-deletecommand) ne ""} {
                uplevel #0 $switched::($this,-deletecommand)
            }
} ;# >8
        }

        proc options {this} {                                       ;# data index must be forced so that initialization always occur
            return [list\
                [list -category {} {}]\
                [list -configurations {} {}]\
                [list -dataindex {}]\
                [list -deletecommand {} {}]\
                [list -draggable 0 0]\
                [list -object {} {}]\
                [list -rows {} {}]\
                [list -state normal]\
            ]
        }

        set (nextDataIndex) 0                        ;# used when data array index is not specified as an option when creating table
        proc reset {} {                              ;# reset generated counter (invoker must insure that there are no viewers left)
            set (nextDataIndex) 0
        }
        # data array name index must be specifiable so that data viewers depending on formulas table data array name (through their
        # monitored cells) do not fail accessing that data (required when generating viewers from save file)
        proc set-dataindex {this value} {                                                     ;# always invoked at construction time
if {$global::withGUI} { ;# >8
            set complete $composite::($this,complete)
} else { ;# >8
            set complete $switched::($this,complete)
} ;# >8
            if {$complete} {
                error {option -dataindex cannot be set dynamically}
            }
            if {$value ne ""} {                                     ;# specified, else use internally generated next available index
                if {$value < $(nextDataIndex)} {
                    error "specified data index ($value) is lower than internal formulas table index"
                }
                set (nextDataIndex) $value
            }
        }

        proc set-deletecommand {this value} {}

        proc set-draggable {this value} {
if {$global::withGUI} { ;# >8
            set complete $composite::($this,complete)
} else { ;# >8
            set complete $switched::($this,complete)
} ;# >8
            if {$complete} {
                error {option -draggable cannot be set dynamically}
            }
        }

        proc set-configurations {this value} {}                                                               ;# from dashboard file

        proc set-rows {this value} {}                                                                         ;# from dashboard file

        proc set-category {this value} {
if {$global::withGUI} { ;# >8
            set complete $composite::($this,complete)
} else { ;# >8
            set complete $switched::($this,complete)
} ;# >8
            if {$complete} {
                error {option -category cannot be set dynamically}
            }
        }

        proc set-object {this value} {
if {$global::withGUI} { ;# >8
            set complete $composite::($this,complete)
} else { ;# >8
            set complete $switched::($this,complete)
} ;# >8
            if {$complete} {
                error {option -object cannot be set dynamically}
            }
        }

if {$global::withGUI} { ;# >8

        proc set-state {this value} {
            if {![info exists ($this,constructed)]} return                           ;# delayed until the end of object construction
            switch $value {
                disabled {
                    if {$composite::($this,-draggable)} {
                        set drag $dataTable::($($this,table),drag)
                        dragSite::provide $drag FORMULAS {}
                        dragSite::provide $drag OBJECTS {}
                        dragSite::provide $drag DATACELLS {}
                    }
                    switched::configure $($this,drop) -state disabled
                }
                normal {
                    ### hack: drag  code should be separated from dataTable which should provide a selected member procedure ###
                    if {$composite::($this,-draggable)} {
                        set drag $dataTable::($($this,table),drag)
                        # extend data table drag capabilities ### hack ### eventually allow row selection only instead of cells ###
                        dragSite::provide $drag FORMULAS "formulas::table::dragData $this"
                        dragSite::provide $drag OBJECTS "formulas::table::dragData $this"   ;# to be able to drop formulas in eraser
                        dragSite::provide $drag DATACELLS "formulas::table::dragData $this"    ;# intercept original data cells drag
                    }
                    switched::configure $($this,drop) -state normal
                }
                default {
                    error "bad state value \"$value\": must be normal or disabled"
                }
            }
        }

} else { ;# >8

        proc set-state {this value} {}

} ;# >8

        proc cells {this} {
            return {}                                                                           ;# contains formulas, not data cells
        }

        # note: also returns whether any existing formulas names were changed
        proc manage {this formulas {update 0}} {                                      ;# note: formulas to be managed by this object
            variable ${this}count             ;# cells data array usage count per formula (empty array if formula contains no cells)

if {$global::withGUI} { ;# >8
            if {[llength $formulas] > 0} {
                centerMessage $($this,tablePath) {}                             ;# keep displaying help message only in empty viewer
            }
} ;# >8
            set dataName $($this,dataName)
            set anyConstant 0
            set anyNameChange 0
            foreach formula $formulas {
                # always make sure formula deletion is detected, as formulas delete command may be reset in dialog box
                switched::configure $formula -deletecommand "formulas::table::deleted $this $formula"
                set name [switched::cget $formula -name]
                if {[info exists ($this,row,$formula)]} {                              ;# created from constructor or edited by user
                    set row $($this,row,$formula)
                    if {[info exists ${dataName}($row,0)] && ([set ${dataName}($row,0)] ne $name)} {
                        set anyNameChange 1
                    }
                } else {                                                                  ;# new formula created dynamically by user
                    $($this,namespace)::new [set row [set ($this,row,$formula) $($this,nextRow)]]
                    incr ($this,nextRow)
                }
                set ${dataName}($row,0) $name
                set ${dataName}($row,1) ?
                set cells [switched::cget $formula -cells]
                if {[llength $cells] == 0} {                                                                       ;# no moving data
                    set ${this}count($formula,) 1
                    catch {set ${dataName}($row,1) [formulas::formula::value $formula]}                  ;# update now and once only
                    set anyConstant 1
                } else {
                    array unset ${this}count $formula,*
                    foreach cell $cells {
                        viewer::parse $cell array ignore ignore ignore
                        if {![catch {set asynchronous [modules::asynchronous $array]}] && $asynchronous} {
                            # notes: may not be a module (statistics table); for synchronous modules, update{} is directly invoked
                            viewer::registerTrace $this $array                   ;# otherwise formula not updated when value changes
                        }
                        if {![info exists ${this}count($formula,$array)]} {set ${this}count($formula,$array) 0}
                        incr ${this}count($formula,$array)
                        set arrays($array) {}
                    }
                }
                $($this,namespace)::name $row [set ${dataName}($row,0)]
                $($this,namespace)::value $row [set ${dataName}($row,1)]
                set managed($formula) {}
            }
            if {$update} {                                                                                    ;# remove old formulas
                foreach formula [formulas $this] {
                    if {![info exists managed($formula)]} {
                        delete $formula                                                    ;# note: automatically updates data table
                    }
                }
            }
            if {$anyConstant} {
                $($this,namespace)::update
if {$global::withGUI} { ;# >8
                dataTable::update $($this,table)                  ;# since there is no associated array that could trigger an update
} ;# >8
            }
            foreach array [array names arrays] {                   ;# update immediately (as done in viewer layer for other viewers)
                update $this $array
            }
            return $anyNameChange
        }

        proc update {this {array *}} {      ;# update all formulas or formulas with cells of the specified asynchronous module array
            variable ${this}count

            foreach name [array names ${this}count "\[0-9\]*,$array"] {
                set update([lindex [split $name ,] 0]) {} ;# update a formula once only even if containing cells from the same array
            }
            set dataName $($this,dataName)
            set updated 0
            foreach name [array names update] {
                set formula [lindex [split $name ,] 0]
                set value ?; catch {set value [formulas::formula::value $formula]}
                set row $($this,row,$formula)
                $($this,namespace)::value $row [set ${dataName}($row,1) $value]
                set updated 1
            }
            if {$updated} {
                incr ${dataName}(updates)                                                            ;# let data table update itself
                $($this,namespace)::update
            }
        }

        proc deleted {this formula} {
            variable ${this}count

            set cells [switched::cget $formula -cells]
            if {[llength $cells] == 0} {
                if {[incr ${this}count($formula,) -1] == 0} {unset ${this}count($formula,)}
            } else {
                foreach cell [switched::cget $formula -cells] {
                    viewer::parse $cell array ignore ignore ignore
                    if {![catch {set asynchronous [modules::asynchronous $array]}] && $asynchronous}  { ;# note: may not be a module
                        viewer::unregisterTrace $this $array
                    }
                    if {[incr ${this}count($formula,$array) -1] == 0} {unset ${this}count($formula,$array)}
                }
            }
            set row $($this,row,$formula)
            set dataName $($this,dataName)
            unset ${dataName}($row,0) ${dataName}($row,1) ($this,row,$formula)
            $($this,namespace)::delete $row
            $($this,namespace)::update                                              ;# so that viewers immediately display void data
if {$global::withGUI} { ;# >8
            dataTable::update $($this,table)
} ;# >8
        }

        proc formulas {this} {
            set list {}
            foreach name [array names {} $this,row,*] {
                lappend list [lindex [split $name ,] end]
            }
            return [lsort -integer $list]
        }

if {$global::withGUI} { ;# >8

        proc initializationConfiguration {this} {
            scan [namespace tail $($this,dataName)] %u index                                        ;# retrieve index from data name
            set arguments {}
            set rows {}
            foreach formula [formulas $this] {                               ;# note: always return configurations in the same order
                set list {}
                foreach option {cellindexes cells commenttext name text} {
                    lappend list -$option [switched::cget $formula -$option]
                }
                lappend rows $($this,row,$formula)
                lappend arguments $list
            }
            return [list\
                -dataindex $index -rows $rows -object $composite::($this,-object) -category $composite::($this,-category)\
                -configurations $arguments\
            ]
        }

        proc handleDrop {this} {
            if {![catch {set formulas $dragSite::data(FORMULAS)}]} {
                foreach formula $formulas {
                    set identical 0
                    foreach existing [formulas $this] {
                        if {[formulas::formula::equal $existing $formula]} {set identical $existing; break}
                    }
                    if {$identical > 0} {
                        lifoLabel::flash $global::messenger [format $formulas::(existingMessage) [switched::cget $identical -name]]
                        continue
                    }
                    manage $this [new $formula]
                }
            } elseif {[info exists dragSite::data(KILL)]} {
                delete $this                                                                                       ;# self destructs
                updateViewObjectsMenu
            }
        }

        proc dragData {this format} {
            set cells [dataTable::dragData $($this,table) DATACELLS]
            switch $format {
                FORMULAS - OBJECTS {
                    foreach cell $cells {
                        regexp {\(([^,]+)} $cell dummy row
                        foreach {name value} [array get {} $this,row,*] {
                            if {$value == $row} {
                                set formulas([lindex [split $name ,] end]) {}
                                break
                            }
                        }
                    }
                    set objects [array names formulas]
                    if {([llength $objects] == 0) && ($format eq "OBJECTS")} {
                        return $this                                                 ;# return formulas table object itself if empty
                    } else {
                        return $objects
                    }
                }
                DATACELLS {                               ;# return data cells from the formulas module, not the internal data array
                    set namespace $($this,namespace)
                    set list {}
                    foreach cell $cells {
                        regexp {\((.+)\)$} $cell dummy coordinates
                        # note: works before columns are identically numbered between the local and formulas module data arrays
                        lappend list ${namespace}::data($coordinates)
                    }
                    return $list
                }
            }
        }

        proc pointed {this x y} {                                                      ;# return formula under specified coordinates
            set row [dataTable::dataRow $($this,table) [$($this,tablePath) index @$x,$y row]]
            if {$row ne ""} {
                foreach {name value} [array get {} $this,row,*] {
                    if {$value == $row} {return [lindex [split $name ,] end]}
                }
            }
            return 0
        }

        proc enter {this x y} {
            raiseExistingFormulasDialog                                 ;# so that user stays aware a formulas table is being edited
            bindings::set $($this,bindings) <Motion> "formulas::table::motion $this %x %y"
            set ($this,cell) [$($this,tablePath) index @$x,$y]
            in $this $($this,cell) $x $y
        }

        proc leave {this} {
            bindings::set $($this,bindings) <Motion> {}
            unset -nocomplain ($this,cell)                                                   ;# cell should exist but better be safe
        }

        proc motion {this x y} {
            set cell [$($this,tablePath) index @$x,$y]
            if {![info exists ($this,cell)]} {set ($this,cell) cell}                       ;# should never happen but better be safe
            if {$cell eq $($this,cell)} return                                                                          ;# no change
            in $this [set ($this,cell) $cell] $x $y
        }

        proc in {this cell x y} {
            scan $cell %d,%d row column
            if {($row < 0) || ($column != 0)} return                                 ;# ignore title areas and check for name column
            set formula [pointed $this $x $y]
            if {$formula == 0} return
            foreach {left top width height} [$($this,tablePath) bbox $cell] {}
            if {![info exists height]} return                                                 ;# check for invalid box, just in case
            catch {delete $($this,tip)}                                  ;# ephemeral tip has a good chance of being deleted already
            set ($this,tip) [new widgetTip\
                -path $($this,tablePath) -text [switched::cget $formula -commenttext]\
                -rectangle [list $left $top [expr {$left + $width}] [expr {$top + $height}]] -ephemeral 1\
            ]
        }

        proc monitored {this cell} {                                                     ;# implementation of viewer layer interface
            set namespace $($this,namespace)           ;# note: cell is not part of a formula but rather a formulas module data cell
            if {([modules::moduleFromNamespace $namespace] == 0) || ([namespace qualifiers $cell] ne $namespace)} {
                return 0                           ;# note: companion module may have been unloaded (happens when viewer is deleted)
            }
            scan $cell {%*[^(](%lu,%u)} row column
            return [${namespace}::exists $row $column]                                    ;# whether cell exists in companion module
        }

        proc setCellColor {this source color} { ;# source cell always comes from formulas module data: formulas<N>::data(row,column)
            if {[namespace qualifiers $source] ne $($this,namespace)} return                                   ;# not for this table
            scan $source {%*[^(](%lu,%u)} row column
            # note: works because rows and  columns are identically numbered between the local and formulas module data arrays
            dataTable::setCellColor $($this,table) $row $column $color
        }

        proc title {this} {
            regsub {<0>$} $modules::instance::($($this,instance),identifier) {} title
            return $title                                      ;# with removed trailing namespace index for first instance of module
        }

} ;# >8

    }


    class formula {

        proc formula {this args} switched {$args} {
            if {![info exists (interpreter)]} {set (interpreter) [interpreter $this]}
            switched::complete $this
        }

        proc formula {this copy} switched {} {                                                                   ;# copy constructor
            if {![info exists (interpreter)]} {set (interpreter) [interpreter $this]}
            switched::complete $this
            copyOptions $this $copy
        }

        proc ~formula {this} {
            variable ${this}cell
            variable ${this}last

            unset -nocomplain ${this}cell ${this}last
            if {$switched::($this,-deletecommand) ne ""} {
                uplevel #0 $switched::($this,-deletecommand)                                ;# always invoke command at global level
            }
        }

        proc interpreter {this} {
            set interpreter [interp create -safe]           ;# safe interpreter with only expr{} command and no variables whatsoever
            foreach variable [$interpreter eval {info globals}] {
                $interpreter eval "unset $variable"
            }
            foreach command [$interpreter eval {info commands}] {
                switch $command {expr - rename continue}
                $interpreter eval "rename $command {}"
            }
            $interpreter eval {rename rename {}}
            interp recursionlimit $interpreter 1
            return $interpreter
        }

        proc copyOptions {to from} {
            switched::configure $to -cellindexes [switched::cget $from -cellindexes] -cells [switched::cget $from -cells]\
                -commenttext [switched::cget $from -commenttext] -name [switched::cget $from -name]\
                -text [switched::cget $from -text] -deletecommand [switched::cget $from -deletecommand]
        }

        proc options {this} {
            return [list\
                [list -cellindexes {} {}]\
                [list -cells {} {}]\
                [list -commenttext {} {}]\
                [list -deletecommand {} {}]\
                [list -name {} {}]\
                [list -text {} {}]\
            ]
        }

        proc set-cellindexes {this value} {
            unset -nocomplain ($this,expression)                                                    ;# force expression regeneration
        }
        proc set-cells {this value} {
            unset -nocomplain ($this,expression)
        }
        proc set-text {this value} {
            unset -nocomplain ($this,expression)
        }

        proc set-commenttext {this value} {}                  ;# note: with a "text" ending so that it is properly in dashboard file

        proc set-deletecommand {this value} {}

        proc set-name {this value} {}

        proc expression {this} {
            variable ${this}cell
            variable ${this}last

            unset -nocomplain ${this}cell ${this}last                                              ;# regenerate included cells list
            set text $switched::($this,-text)
            set offset(0) 0
            set length 0; set index 0
            foreach line [split $text \n] {
                incr length [string length $line]
                set offset([incr index]) [incr length]                                              ;# also count new line character
            }
            set indexes {}
            foreach value $switched::($this,-cellindexes) {
                foreach {line index} [split $value .] {}
                incr line -1                                                 ;# normalize line numbers (start from 1 in text widget)
                lappend indexes [expr {$offset($line) + $index}]
            }
            set expression {}
            set first 0
            foreach index $indexes cell $switched::($this,-cells) {
                viewer::parse $cell array row column ignore
                set key $array,$row,$column
                set ${this}cell($key) $cell                                                 ;# remember cell inclusion in expression
                set string [string range $text $first [expr {$index - 1}]]
                switch -regexp -- $string {
                    {delta\s*\(\s*$} {
                        regsub {delta\s*\(\s*$} $string {} string                    ;# remove function name and opening parentheses
                        # force 64 bit integers subtraction (valid on 32 and 64 bit Tcl platforms):
                        append expression $string \( wide(\${$cell}-\${formulas::formula::${this}last($key)})
                    }
                    {delta32\s*\(\s*$} {
                        regsub {delta32\s*\(\s*$} $string {} string
                        # force 32 bit integers subtraction, with an always positive result (valid on 32 and 64 bit Tcl platforms):
                        append expression $string \( ((\${$cell}-\${formulas::formula::${this}last($key)})&0xFFFFFFFF)
                        set last($key) {}
                    }
                    {diff\s*\(\s*$} {
                        regsub {diff\s*\(\s*$} $string {} string
                        # force double calculations:
                        append expression $string \( double(\${$cell})-\${formulas::formula::${this}last($key)}
                        set last($key) {}
                    }
                    {last\s*\(\s*$} {
                        regsub {last\s*\(\s*$} $string {} string
                        append expression $string \( \${formulas::formula::${this}last($key)}
                        set last($key) {}
                    }
                    default {
                        append expression $string \${$cell}
                    }
                }
                set first [expr {$index + 1}]                                              ;# account for cell replacement character
            }
            append expression [string range $text $first end]
            set ($this,last) [array names last]                                                       ;# cells that need last values
            return $expression
        }

        proc value {this} {
            variable ${this}cell
            variable ${this}last

            check $this
            if {![info exists ($this,expression)]} {
                set ($this,expression) [expression $this]
            }
            set error 0
            set now [expr {[clock clicks -milliseconds] / 1000.0}]
            set seconds ?; catch {set seconds [expr {$now - $($this,seconds)}]}
            set ($this,seconds) $now
            set pattern {diff\s*\(\s*time\s*\)}                 ;# use expression variable for better performance by pre-compilation
            if {([regsub -all $pattern $($this,expression) $seconds expression] > 0) && ($seconds eq "?")} {
                set result {diff(time) not yet available}
                set error 1
            } else {
                foreach key $($this,last) {                                   ;# check cells that require last value to be available
                    if {![info exists ${this}last($key)]} {
                        foreach {array row column} [split $key ,] break
                        set result "[lindex [viewer::label $array $row $column] 0] data not yet available"
                        set error 1
                        break
                    }
                }
            }
            if {!$error} {
                set error [catch {$(interpreter) eval expr [list [subst -nobackslashes -nocommands $expression]]} result]
            }
            foreach {key cell} [array get ${this}cell] {
                catch {set ${this}last($key) [set $cell]}
            }
            foreach key [array names ${this}last] {                                             ;# delete no longer used last values
                if {![info exists ${this}cell($key)]} {unset ${this}last($key)}
            }
            if {$error} {
                error $result                                                                                  ;# throw error if any
            } else {
                return $result                                                            ;# implies value is correct if we get here
            }
        }

        proc check {this} {
            foreach function [list delta delta32 diff last] {                                  ;# check arguments of added functions
                set text $switched::($this,-text)
                while {$text ne ""} {
                    set expression "$function\\s*\\((\[^\)\]*)\\)"
                    if {![regexp $expression $text ignore argument]} break
                    switch [string trim $argument] {
                        \$ {}
                        time {
                            if {$function ne "diff"} {
                                error "${function}(time) should be diff(time)"
                            }
                        }
                        default {
                            regsub -all {\$} $argument value argument                            ;### replace with actual values ###
                            if {$function eq "diff"} {
                                set message "${function}() takes one argument (data cell or \"time\")"
                            } else {
                                set message "${function}() takes one data cell argument"
                            }
                            if {[string trim $argument] ne ""} {append message ", not: $argument"}
                            error $message
                        }
                    }
                    regexp -indices $expression $text indexes
                    set text [string range $text [expr {[lindex $indexes end] + 1}] end]                     ;# check remaining text
                }
            }
        }

        proc equal {formula1 formula2} {
            regsub -all {\s} [expression $formula1] {} expression1                                              ;# ignore whitespace
            regsub -all {\s} [expression $formula2] {} expression2
            return [string equal $expression1 $expression2]                              ;# note: name and comments are not compared
        }

    }


}
