diff options
Diffstat (limited to 'tcllib/modules/report/report.tcl')
-rw-r--r-- | tcllib/modules/report/report.tcl | 1386 |
1 files changed, 1386 insertions, 0 deletions
diff --git a/tcllib/modules/report/report.tcl b/tcllib/modules/report/report.tcl new file mode 100644 index 0000000..87a8f49 --- /dev/null +++ b/tcllib/modules/report/report.tcl @@ -0,0 +1,1386 @@ +# report.tcl -- +# +# Implementation of report objects for Tcl. +# +# Copyright (c) 2001-2014 by Andreas Kupries <andreas_kupries@users.sourceforge.net> +# +# See the file "license.terms" for information on usage and redistribution +# of this file, and for a DISCLAIMER OF ALL WARRANTIES. +# +# RCS: @(#) $Id: report.tcl,v 1.8 2004/01/15 06:36:13 andreas_kupries Exp $ + +package require Tcl 8.2 +package provide report 0.3.2 + +namespace eval ::report { + # Data storage in the report module + # ------------------------------- + # + # One namespace per object, containing + # 1) An array mapping from template codes to templates + # 2) An array mapping from template codes and columns to horizontal template items + # 3) An array mapping from template codes and columns to vertical template items + # 4) ... deleted, local to formatting + # 5) An array mapping from columns to left padding + # 6) An array mapping from columns to right padding + # 7) An array mapping from columns to column size + # 8) An array mapping from columns to justification + # 9) A scalar containing the number of columns in the report. + # 10) An array mapping from template codes to enabledness + # 11) A scalar containing the size of the top caption + # 12) A scalar containing the size of the bottom caption + # + # 1 - template 5 - lpad 9 - columns + # 2 - hTemplate 6 - rpad 10 - enabled + # 3 - vTemplate 7 - csize 11 - tcaption + # 4 - fullHTemplate 8 - cjust 12 - bcaption + + # commands is the list of subcommands recognized by the report + variable commands [list \ + "bcaption" \ + "botcapsep" \ + "botdata" \ + "botdatasep" \ + "bottom" \ + "columns" \ + "data" \ + "datasep" \ + "justify" \ + "pad" \ + "printmatrix" \ + "printmatrix2channel" \ + "size" \ + "sizes" \ + "tcaption" \ + "top" \ + "topcapsep" \ + "topdata" \ + "topdatasep" + ] + + # Only export the toplevel commands + namespace export report defstyle rmstyle stylearguments stylebody + + # Global data, style definitions + + variable styles [list plain] + variable styleargs + variable stylebody + + array set styleargs {plain {}} + array set stylebody {plain {}} + + # Global data, template codes, for easy checking + + variable tcode + array set tcode { + topdata 0 data 0 + botdata 0 top 1 + topdatasep 1 topcapsep 1 + datasep 1 botcapsep 1 + botdatasep 1 bottom 1 + } +} + +# ::report::report -- +# +# Create a new report with a given name +# +# Arguments: +# name Optional name of the report; if null or not given, generate one. +# +# Results: +# name Name of the report created + +proc ::report::report {name columns args} { + variable styleargs + + if { [llength [info commands ::$name]] } { + error "command \"$name\" already exists, unable to create report" + } + if {![string is integer $columns]} { + return -code error "columns: expected integer greater than zero, got \"$columns\"" + } elseif {$columns <= 0} { + return -code error "columns: expected integer greater than zero, got \"$columns\"" + } + + set styleName "" + switch -exact -- [llength $args] { + 0 {# No style was specied. This is OK} + 1 { + # We possibly got the "style" keyword, but everything behind is missing + return -code error "wrong # args: report name columns ?\"style\" styleName ?arg...??" + } + default { + # Break tail apart, check for correct keyword, ensure that style is known too. + # Don't forget to check the actual against the formal arguments. + + foreach {dummy styleName} $args break + set args [lrange $args 2 end] + + if {![string equal $dummy style]} { + return -code error "wrong # args: report name columns ?\"style\" styleName ?arg...??" + } + if {![info exists styleargs($styleName)]} { + return -code error "style \"$styleName\" is not known" + } + CheckStyleArguments $styleName $args + } + } + + # The arguments seem to be ok, setup the namespace for the object + # and configure it to style "plain". + + namespace eval ::report::report$name "variable columns $columns" + namespace eval ::report::report$name { + variable tcaption 0 + variable bcaption 0 + variable template + variable enabled + variable hTemplate + variable vTemplate + variable lpad + variable rpad + variable csize + variable cjust + + variable t + variable i + variable dt [list] + variable st [list] + for {set i 0} {$i < $columns} {incr i} { + set lpad($i) "" + set rpad($i) "" + set csize($i) dyn + set cjust($i) left + lappend dt {} + lappend st {} {} + } + lappend dt {} + lappend st {} + + foreach t { + topdata data botdata + } { + set enabled($t) 1 + set template($t) $dt + for {set i 0} {$i <= $columns} {incr i} { + set vTemplate($t,$i) {} + } + } + foreach t { + top topdatasep topcapsep + datasep + botcapsep botdatasep bottom + } { + set enabled($t) 0 + set template($t) $st + for {set i 0} {$i < $columns} {incr i} { + set hTemplate($t,$i) {} + } + for {set i 0} {$i <= $columns} {incr i} { + set vTemplate($t,$i) {} + } + } + + unset t i dt st + } + + # Create the command to manipulate the report + # $name -> ::report::ReportProc $name + interp alias {} ::$name {} ::report::ReportProc $name + + # If a style was specified execute it now, before the oobject is + # handed back to the user. + + if {$styleName != {}} { + ExecuteStyle $name $styleName $args + } + + return $name +} + +# ::report::defstyle -- +# +# Defines a new named style, with arguments and defining script. +# +# Arguments: +# styleName Name of the new style. +# arguments Formal arguments of the style, some format as for proc. +# body The script actually defining the style. +# +# Results: +# None. + +proc ::report::defstyle {styleName arguments body} { + variable styleargs + variable stylebody + variable styles + + if {[info exists styleargs($styleName)]} { + return -code error "Cannot create style \"$styleName\", already exists" + } + + # Check the formal arguments + # 1. Arguments without default may not follow an argument with a + # default. The special "args" is no exception! + # 2. Compute the minimal number of arguments required by the proc. + + set min 0 + set def 0 + set ca 0 + + foreach v $arguments { + switch -- [llength $v] { + 1 { + if {$def} { + return -code error \ + "Found argument without default after arguments having defaults" + } + incr min + } + 2 { + set def 1 + } + default { + error "Illegal length of value \"$v\"" + } + } + } + if {[string equal args [lindex $arguments end]]} { + # Correct requirements if we have a catch-all at the end. + incr min -1 + set ca 1 + } + + # Now we are allowed to extend the internal database + + set styleargs($styleName) [list $min $ca $arguments] + set stylebody($styleName) $body + lappend styles $styleName + return +} + +# ::report::rmstyle -- +# +# Deletes the specified style. +# +# Arguments: +# styleName Name of the style to destroy. +# +# Results: +# None. + +proc ::report::rmstyle {styleName} { + variable styleargs + variable stylebody + variable styles + + if {![info exists styleargs($styleName)]} { + return -code error "cannot delete unknown style \"$styleName\"" + } + if {[string equal $styleName plain]} { + return -code error {cannot delete builtin style "plain"} + } + + unset styleargs($styleName) + unset stylebody($styleName) + + set pos [lsearch -exact $styles $styleName] + set styles [lreplace $styles $pos $pos] + return +} + +# ::report::_stylearguments -- +# +# Introspection, returns the list of formal arguments of the +# specified style. +# +# Arguments: +# styleName Name of the style to query. +# +# Results: +# A list containing the formal argument of the style + +proc ::report::stylearguments {styleName} { + variable styleargs + if {![info exists styleargs($styleName)]} { + return -code error "style \"$styleName\" is not known" + } + return [lindex $styleargs($styleName) 2] +} + +# ::report::_stylebody -- +# +# Introspection, returns the body/script of the +# specified style. +# +# Arguments: +# styleName Name of the style to query. +# +# Results: +# A script, the body of the style. + +proc ::report::stylebody {styleName} { + variable stylebody + if {![info exists stylebody($styleName)]} { + return -code error "style \"$styleName\" is not known" + } + return $stylebody($styleName) +} + +# ::report::_styles -- +# +# Returns alist containing the names of all known styles. +# +# Arguments: +# None. +# +# Results: +# A list containing the names of all known styles + +proc ::report::styles {} { + variable styles + return $styles +} + +########################## +# Private functions follow + +# ::report::CheckStyleArguments -- +# +# Internal helper. Used to check actual arguments of a style against the formal ones. +# +# Arguments: +# styleName Name of the style in question +# arguments Actual arguments for the style. +# +# Results: +# None, or an error in case of problems. + +proc ::report::CheckStyleArguments {styleName arguments} { + variable styleargs + + # Match formal and actual arguments, error out in case of problems. + foreach {min catchall formal} $styleargs($styleName) break + + if {[llength $arguments] < $min} { + # Determine the name of the first formal parameter which did not get a value. + set firstmissing [lindex $formal [llength $arguments]] + return -code error "no value given for parameter \"$firstmissing\" to style \"$styleName\"" + } elseif {[llength $arguments] > $min} { + if {!$catchall && ([llength $arguments] > [llength $formal])} { + # More actual arguments than formals, without catch-all argument, error + return -code error "called style \"$styleName\" with too many arguments" + } + } +} + +# ::report::ExecuteStyle -- +# +# Internal helper. Applies a named style to the specified report object. +# +# Arguments: +# name Name of the report the style is applied to. +# styleName Name of the style to apply +# arguments Actual arguments for the style. +# +# Results: +# None. + +proc ::report::ExecuteStyle {name styleName arguments} { + variable styleargs + variable stylebody + variable styles + variable commands + + CheckStyleArguments $styleName $arguments + foreach {min catchall formal} $styleargs($styleName) break + + array set a {} + + if {([llength $arguments] > $min) && $catchall} { + # #min = number of formal arguments - 1 + set a(args) [lrange $arguments $min end] + set formal [lrange $formal 0 end-1] + incr min -1 + set arguments [lrange $arguments 0 $min] + + # arguments and formal are now of equal length and we also + # know that there are no arguments having a default value. + foreach v $formal aval $arguments { + set a($v) $aval + } + } + + # More arguments than minimally required, but no more than formal + # arguments! Proceed to standard matching: Go through the actual + # values and associate them with a formal argument. Then fill the + # remaining formal arguments with their default values. + + foreach aval $arguments { + set v [lindex $formal 0] + set formal [lrange $formal 1 end] + if {[llength $v] > 1} {set v [lindex $v 0]} + set a($v) $aval + } + + foreach vd $formal { + foreach {var default} $vd { + set a($var) $default + } + } + + # Create and initialize a safe interpreter, execute the style and + # then break everything down again. + + set ip [interp create -safe] + + # -- Report methods -- + + foreach m $commands { + # safe-ip method --> here report method + interp alias $ip $m {} $name $m + } + + # -- Styles defined before this one -- + + foreach s $styles { + if {[string equal $s $styleName]} {break} + interp alias $ip $s {} ::report::LinkExec $name $s + } + + # -- Arguments as variables -- + + foreach {var val} [array get a] { + $ip eval [list set $var $val] + } + + # Finally execute / apply the style. + + $ip eval $stylebody($styleName) + interp delete $ip + return +} + +# ::report::_LinkExec -- +# +# Internal helper. Used for application of styles from within +# another style script. Collects the formal arguments into the +# one list which is expected by "ExecuteStyle". +# +# Arguments: +# name Name of the report the style is applied to. +# styleName Name of the style to apply +# args Actual arguments for the style. +# +# Results: +# None. + +proc ::report::LinkExec {name styleName args} { + ExecuteStyle $name $styleName $args +} + +# ::report::ReportProc -- +# +# Command that processes all report object commands. +# +# Arguments: +# name Name of the report object to manipulate. +# cmd Subcommand to invoke. +# args Arguments for subcommand. +# +# Results: +# Varies based on command to perform + +proc ::report::ReportProc {name {cmd ""} args} { + variable tcode + + # Do minimal args checks here + if { [llength [info level 0]] == 2 } { + error "wrong # args: should be \"$name option ?arg arg ...?\"" + } + + # Split the args into command and args components + + if {[info exists tcode($cmd)]} { + # Template codes are a bit special + eval [list ::report::_tAction $name $cmd] $args + } else { + if { [llength [info commands ::report::_$cmd]] == 0 } { + variable commands + set optlist [join $commands ", "] + set optlist [linsert $optlist "end-1" "or"] + error "bad option \"$cmd\": must be $optlist" + } + eval [list ::report::_$cmd $name] $args + } +} + +# ::report::CheckColumn -- +# +# Helper to check and transform column indices. Returns the +# absolute index number belonging to the specified +# index. Rejects indices out of the valid range of columns. +# +# Arguments: +# columns Number of columns +# column The incoming index to check and transform +# +# Results: +# The absolute index to the column + +proc ::report::CheckColumn {columns column} { + switch -regex -- $column { + {end-[0-9]+} { + regsub -- {end-} $column {} column + set cc [expr {$columns - 1 - $column}] + if {($cc < 0) || ($cc >= $columns)} { + return -code error "column: index \"end-$column\" out of range" + } + return $cc + } + end { + if {$columns <= 0} { + return -code error "column: index \"$column\" out of range" + } + return [expr {$columns - 1}] + } + {[0-9]+} { + if {($column < 0) || ($column >= $columns)} { + return -code error "column: index \"$column\" out of range" + } + return $column + } + default { + return -code error "column: syntax error in index \"$column\"" + } + } +} + +# ::report::CheckVerticals -- +# +# Internal helper. Used to check the consistency of all active +# templates with respect to the generated vertical separators +# (Same length). +# +# Arguments: +# name Name of the report object to check. +# +# Results: +# None. + +proc ::report::CheckVerticals {name} { + upvar ::report::report${name}::vTemplate vTemplate + upvar ::report::report${name}::enabled enabled + upvar ::report::report${name}::columns columns + upvar ::report::report${name}::tcaption tcaption + upvar ::report::report${name}::bcaption bcaption + + for {set c 0} {$c <= $columns} {incr c} { + # Collect all lengths for a column in a list, sort that and + # compare first against last element. If they are not equal we + # have found an inconsistent definition. + + set res [list] + lappend res [string length $vTemplate(data,$c)] + + if {$tcaption > 0} { + lappend res [string length $vTemplate(topdata,$c)] + if {($tcaption > 1) && $enabled(topdatasep)} { + lappend res [string length $vTemplate(topdatasep,$c)] + } + if {$enabled(topcapsep)} { + lappend res [string length $vTemplate(topcapsep,$c)] + } + } + if {$bcaption > 0} { + lappend res [string length $vTemplate(botdata,$c)] + if {($bcaption > 1) && $enabled(botdatasep)} { + lappend res [string length $vTemplate(botdatasep,$c)] + } + if {$enabled(botcapsep)} { + lappend res [string length $vTemplate(botcapsep,$c)] + } + } + foreach t {top datasep bottom} { + if {$enabled($t)} { + lappend res [string length $vTemplate($t,$c)] + } + } + + set res [lsort $res] + + if {[lindex $res 0] != [lindex $res end]} { + return -code error "inconsistent verticals in report" + } + } +} + +# ::report::_tAction -- +# +# Implements the actions on templates (set, get, enable, disable, enabled) +# +# Arguments: +# name Name of the report object. +# template Name of the template to query or manipulate. +# cmd The action applied to the template +# args Additional arguments per action, see documentation. +# +# Results: +# None. + +proc ::report::_tAction {name template cmd args} { + # When coming in here we know that $template contains a legal + # template code. No need to check again. We need 'tcode' + # nevertheless to distinguish between separator (1) and data + # templates (0). + + variable tcode + + switch -exact -- $cmd { + set { + if {[llength $args] != 1} { + return -code error "Wrong # args: $name $template $cmd template" + } + set templval [lindex $args 0] + + upvar ::report::report${name}::columns columns + upvar ::report::report${name}::template tpl + upvar ::report::report${name}::hTemplate hTemplate + upvar ::report::report${name}::vTemplate vTemplate + upvar ::report::report${name}::enabled enabled + + if {$tcode($template)} { + # Separator template, expected size = 2*colums+1 + if {[llength $templval] > (2*$columns+1)} { + return -code error {template to long for number of columns in report} + } elseif {[llength $templval] < (2*$columns+1)} { + return -code error {template to short for number of columns in report} + } + + set tpl($template) $templval + + set even 1 + set c1 0 + set c2 0 + foreach item $templval { + if {$even} { + set vTemplate($template,$c1) $item + incr c1 + set even 0 + } else { + set hTemplate($template,$c2) $item + incr c2 + set even 1 + } + } + } else { + # Data template, expected size = columns+1 + if {[llength $templval] > ($columns+1)} { + return -code error {template to long for number of columns in report} + } elseif {[llength $templval] < ($columns+1)} { + return -code error {template to short for number of columns in report} + } + + set tpl($template) $templval + + set c 0 + foreach item $templval { + set vTemplate($template,$c) $item + incr c + } + } + if {$enabled($template)} { + # Perform checks for active separator templates and + # all data templates. + CheckVerticals $name + } + } + get - + enable - + disable - + enabled { + if {[llength $args] > 0} { + return -code error "Wrong # args: $name $template $cmd" + } + switch -exact -- $cmd { + get { + upvar ::report::report${name}::template tpl + return $tpl($template) + } + enable { + if {!$tcode($template)} { + # Data template, can't be enabled. + return -code error "Cannot enable data template \"$template\"" + } + + upvar ::report::report${name}::enabled enabled + + if {!$enabled($template)} { + set enabled($template) 1 + CheckVerticals $name + } + + } + disable { + if {!$tcode($template)} { + # Data template, can't be disabled. + return -code error "Cannot disable data template \"$template\"" + } + + upvar ::report::report${name}::enabled enabled + if {$enabled($template)} { + set enabled($template) 0 + } + } + enabled { + if {!$tcode($template)} { + # Data template, can't be disabled. + return -code error "Cannot query state of data template \"$template\"" + } + + upvar ::report::report${name}::enabled enabled + return $enabled($template) + } + default {error "Can't happen, panic, run, shout"} + } + } + default { + return -code error "Unknown template command \"$cmd\"" + } + } + return "" +} + +# ::report::_tcaption -- +# +# Sets or queries the size of the top caption region of the report. +# +# Arguments: +# name Name of the report object. +# size The new size, if not empty. Emptiness indicates that a +# query was requested +# +# Results: +# None, or the current size of the top caption region + +proc ::report::_tcaption {name {size {}}} { + upvar ::report::report${name}::tcaption tcaption + + if {$size == {}} { + return $tcaption + } + if {![string is integer $size]} { + return -code error "size: expected integer greater than or equal to zero, got \"$size\"" + } + if {$size < 0} { + return -code error "size: expected integer greater than or equal to zero, got \"$size\"" + } + if {$size == $tcaption} { + # No change, nothing to do + return "" + } + if {($size > 0) && ($tcaption == 0)} { + # Perform a consistency check after the assignment, the + # template might have been changed. + set tcaption $size + CheckVerticals $name + } else { + set tcaption $size + } + return "" +} + +# ::report::_bcaption -- +# +# Sets or queries the size of the bottom caption region of the report. +# +# Arguments: +# name Name of the report object. +# size The new size, if not empty. Emptiness indicates that a +# query was requested +# +# Results: +# None, or the current size of the bottom caption region + +proc ::report::_bcaption {name {size {}}} { + upvar ::report::report${name}::bcaption bcaption + + if {$size == {}} { + return $bcaption + } + if {![string is integer $size]} { + return -code error "size: expected integer greater than or equal to zero, got \"$size\"" + } + if {$size < 0} { + return -code error "size: expected integer greater than or equal to zero, got \"$size\"" + } + if {$size == $bcaption} { + # No change, nothing to do + return "" + } + if {($size > 0) && ($bcaption == 0)} { + # Perform a consistency check after the assignment, the + # template might have been changed. + set bcaption $size + CheckVerticals $name + } else { + set bcaption $size + } + return "" +} + +# ::report::_size -- +# +# Sets or queries the size of the specified column. +# +# Arguments: +# name Name of the report object. +# column Index of the column to manipulate or query +# size The new size, if not empty. Emptiness indicates that a +# query was requested +# +# Results: +# None, or the current size of the column + +proc ::report::_size {name column {size {}}} { + upvar ::report::report${name}::columns columns + upvar ::report::report${name}::csize csize + + set column [CheckColumn $columns $column] + + if {$size == {}} { + return $csize($column) + } + if {[string equal $size dyn]} { + set csize($column) $size + return "" + } + if {![string is integer $size]} { + return -code error "expected integer greater than zero, got \"$size\"" + } + if {$size <= 0} { + return -code error "expected integer greater than zero, got \"$size\"" + } + set csize($column) $size + return "" +} + +# ::report::_sizes -- +# +# Sets or queries the sizes of all columns. +# +# Arguments: +# name Name of the report object. +# sizes The new sizes, if not empty. Emptiness indicates that a +# query was requested +# +# Results: +# None, or a list containing the sizes of all columns. + +proc ::report::_sizes {name {sizes {}}} { + upvar ::report::report${name}::columns columns + upvar ::report::report${name}::csize csize + + if {$sizes == {}} { + set res [list] + foreach k [lsort -integer [array names csize]] { + lappend res $csize($k) + } + return $res + } + if {[llength $sizes] != $columns} { + return -code error "Wrong # number of column sizes" + } + foreach size $sizes { + if {[string equal $size dyn]} { + continue + } + if {![string is integer $size]} { + return -code error "expected integer greater than zero, got \"$size\"" + } + if {$size <= 0} { + return -code error "expected integer greater than zero, got \"$size\"" + } + } + + set i 0 + foreach s $sizes { + set csize($i) $s + incr i + } + return "" +} + +# ::report::_pad -- +# +# Sets or queries the padding for the specified column. +# +# Arguments: +# name Name of the report object. +# column Index of the column to manipulate or query +# where Where to place the padding. Emptiness indicates +# that a query was requested. +# +# Results: +# None, or the padding for the specified column. + +proc ::report::_pad {name column {where {}} {string { }}} { + upvar ::report::report${name}::columns columns + upvar ::report::report${name}::lpad lpad + upvar ::report::report${name}::rpad rpad + + set column [CheckColumn $columns $column] + + if {$where == {}} { + return [list $lpad($column) $rpad($column)] + } + + switch -exact -- $where { + left { + set lpad($column) $string + } + right { + set rpad($column) $string + } + both { + set lpad($column) $string + set rpad($column) $string + } + default { + return -code error "where: expected left, right, or both, got \"$where\"" + } + } + return "" +} + +# ::report::_justify -- +# +# Sets or queries the justification for the specified column. +# +# Arguments: +# name Name of the report object. +# column Index of the column to manipulate or query +# jvalue Justification to set. Emptiness indicates +# that a query was requested +# +# Results: +# None, or the current justication for the specified column + +proc ::report::_justify {name column {jvalue {}}} { + upvar ::report::report${name}::columns columns + upvar ::report::report${name}::cjust cjust + + set column [CheckColumn $columns $column] + + if {$jvalue == {}} { + return $cjust($column) + } + switch -exact -- $jvalue { + left - right - center { + set cjust($column) $jvalue + return "" + } + default { + return -code error "justification: expected, left, right, or center, got \"$jvalue\"" + } + } +} + +# ::report::_printmatrix -- +# +# Format the specified matrix according to the configuration of +# the report. +# +# Arguments: +# name Name of the report object. +# matrix Name of the matrix object to format. +# +# Results: +# A string containing the formatted matrix. + +proc ::report::_printmatrix {name matrix} { + CheckMatrix $name $matrix + ColumnSizes $name $matrix state + + upvar ::report::report${name}::tcaption tcaption + upvar ::report::report${name}::bcaption bcaption + + set row 0 + set out "" + append out [Separator top $name $matrix state] + if {$tcaption > 0} { + set n $tcaption + while {$n > 0} { + append out [FormatData topdata $name state [$matrix get row $row] [$matrix rowheight $row]] + if {$n > 1} { + append out [Separator topdatasep $name $matrix state] + } + incr n -1 + incr row + } + append out [Separator topcapsep $name $matrix state] + } + + set n [expr {[$matrix rows] - $bcaption}] + + while {$row < $n} { + append out [FormatData data $name state [$matrix get row $row] [$matrix rowheight $row]] + incr row + if {$row < $n} { + append out [Separator datasep $name $matrix state] + } + } + + if {$bcaption > 0} { + append out [Separator botcapsep $name $matrix state] + set n $bcaption + while {$n > 0} { + append out [FormatData botdata $name state [$matrix get row $row] [$matrix rowheight $row]] + if {$n > 1} { + append out [Separator botdatasep $name $matrix state] + } + incr n -1 + incr row + } + } + + append out [Separator bottom $name $matrix state] + + #parray state + return $out +} + +# ::report::_printmatrix2channel -- +# +# Format the specified matrix according to the configuration of +# the report. +# +# Arguments: +# name Name of the report. +# matrix Name of the matrix object to format. +# chan Handle of the channel to write the formatting result into. +# +# Results: +# None. + +proc ::report::_printmatrix2channel {name matrix chan} { + CheckMatrix $name $matrix + ColumnSizes $name $matrix state + + upvar ::report::report${name}::tcaption tcaption + upvar ::report::report${name}::bcaption bcaption + + set row 0 + puts -nonewline $chan [Separator top $name $matrix state] + if {$tcaption > 0} { + set n $tcaption + while {$n > 0} { + puts -nonewline $chan \ + [FormatData topdata $name state [$matrix get row $row] [$matrix rowheight $row]] + if {$n > 1} { + puts -nonewline $chan [Separator topdatasep $name $matrix state] + } + incr n -1 + incr row + } + puts -nonewline $chan [Separator topcapsep $name $matrix state] + } + + set n [expr {[$matrix rows] - $bcaption}] + + while {$row < $n} { + puts -nonewline $chan \ + [FormatData data $name state [$matrix get row $row] [$matrix rowheight $row]] + incr row + if {$row < $n} { + puts -nonewline $chan [Separator datasep $name $matrix state] + } + } + + if {$bcaption > 0} { + puts -nonewline $chan [Separator botcapsep $name $matrix state] + set n $bcaption + while {$n > 0} { + puts -nonewline $chan \ + [FormatData botdata $name state [$matrix get row $row] [$matrix rowheight $row]] + if {$n > 1} { + puts -nonewline $chan [Separator botdatasep $name $matrix state] + } + incr n -1 + incr row + } + } + + puts -nonewline $chan [Separator bottom $name $matrix state] + return +} + +# ::report::_columns -- +# +# Retrieves the number of columns in the report. +# +# Arguments: +# name Name of the report queried +# +# Results: +# A number + +proc ::report::_columns {name} { + upvar ::report::report${name}::columns columns + return $columns +} + +# ::report::_destroy -- +# +# Destroy a report, including its associated command and data storage. +# +# Arguments: +# name Name of the report to destroy. +# +# Results: +# None. + +proc ::report::_destroy {name} { + namespace delete ::report::report$name + interp alias {} ::$name {} + return +} + +# ::report::CheckMatrix -- +# +# Internal helper for the "print" methods. Checks that the +# supplied matrix can be formatted by the specified report. +# +# Arguments: +# name Name of the report to use for the formatting +# matrix Name of the matrix to format. +# +# Results: +# None, or an error in case of problems. + +proc ::report::CheckMatrix {name matrix} { + upvar ::report::report${name}::columns columns + upvar ::report::report${name}::tcaption tcaption + upvar ::report::report${name}::bcaption bcaption + + if {$columns != [$matrix columns]} { + return -code error "report/matrix mismatch in number of columns" + } + if {($tcaption + $bcaption) > [$matrix rows]} { + return -code error "matrix too small, top and bottom captions overlap" + } +} + +# ::report::ColumnSizes -- +# +# Internal helper for the "print" methods. Computes the final +# column sizes (with and without padding) and stores them in +# the print-state +# +# Arguments: +# name Name of the report used for the formatting +# matrix Name of the matrix to format. +# statevar Name of the array variable holding the state +# of the formatter. +# +# Results: +# None. + +proc ::report::ColumnSizes {name matrix statevar} { + # Calculate the final column sizes with and without padding and + # store them in the local state. + + upvar $statevar state + + upvar ::report::report${name}::columns columns + upvar ::report::report${name}::csize csize + upvar ::report::report${name}::lpad lpad + upvar ::report::report${name}::rpad rpad + + for {set c 0} {$c < $columns} {incr c} { + if {[string equal dyn $csize($c)]} { + set size [$matrix columnwidth $c] + } else { + set size $csize($c) + } + + set state(s,$c) $size + + incr size [string length $lpad($c)] + incr size [string length $rpad($c)] + + set state(s/pad,$c) $size + } + + return +} + +# ::report::Separator -- +# +# Internal helper for the "print" methods. Computes the final +# shape of the various separators using the column sizes with +# padding found in the print state. Uses also the print state as +# a cache to avoid costly recomputation for the separators which +# are used multiple times. +# +# Arguments: +# tcode Code of the separator to compute / template to use +# name Name of the report used for the formatting +# matrix Name of the matrix to format. +# statevar Name of the array variable holding the state +# of the formatter. +# +# Results: +# The final separator string. Empty for disabled separators. + +proc ::report::Separator {tcode name matrix statevar} { + upvar ::report::report${name}::enabled e + if {!$e($tcode)} {return ""} + upvar $statevar state + if {![info exists state($tcode)]} { + upvar ::report::report${name}::vTemplate vt + upvar ::report::report${name}::hTemplate ht + upvar ::report::report${name}::columns cs + set str "" + for {set c 0} {$c < $cs} {incr c} { + append str $vt($tcode,$c) + set fill $ht($tcode,$c) + set flen [string length $fill] + set rep [expr {($state(s/pad,$c)/$flen)+1}] + append str [string range [string repeat $fill $rep] 0 [expr {$state(s/pad,$c)-1}]] + } + append str $vt($tcode,$cs) + set state($tcode) $str + } + return $state($tcode)\n +} + +# ::report::FormatData -- +# +# Internal helper for the "print" methods. Computes the output +# for one row in the matrix, given its values, the rowheight, +# padding and justification. +# +# Arguments: +# tcode Code of the data template to use +# name Name of the report used for the formatting +# statevar Name of the array variable holding the state +# of the formatter. +# line List containing the values to format +# rh Height of the row (line) in lines. +# +# Results: +# The formatted string for the supplied row. + +proc ::report::FormatData {tcode name statevar line rh} { + upvar $statevar state + upvar ::report::report${name}::vTemplate vt + upvar ::report::report${name}::columns cs + upvar ::report::report${name}::lpad lpad + upvar ::report::report${name}::rpad rpad + upvar ::report::report${name}::cjust cjust + + if {$rh == 1} { + set str "" + set c 0 + foreach cell $line { + # prefix, cell (pad-l, value, pad-r) + append str $vt($tcode,$c)$lpad($c)[FormatCell $cell $state(s,$c) $cjust($c)]$rpad($c) + incr c + } + append str $vt($tcode,$cs)\n + return $str + } else { + array set str {} + for {set l 1} {$l <= $rh} {incr l} {set str($l) ""} + + # - Future - Vertical justification of cells less tall than rowheight + # - Future - Vertical cutff aftert n lines, auto-repeat of captions + # - Future - => Higher level, not here, use virtual matrices for this + # - Future - and count the generated lines + + set c 0 + foreach fcell $line { + set fcell [split $fcell \n] + for {set l 1; set lo 0} {$l <= $rh} {incr l; incr lo} { + append str($l) $vt($tcode,$c)$lpad($c)[FormatCell \ + [lindex $fcell $lo] $state(s,$c) $cjust($c)]$rpad($c) + } + incr c + } + set strout "" + for {set l 1} {$l <= $rh} {incr l} { + append strout $str($l)$vt($tcode,$cs)\n + } + return $strout + } +} + +# ::report::FormatCell -- +# +# Internal helper for the "print" methods. Formats the value of +# a single cell according to column size and justification. +# +# Arguments: +# value The value to format +# size The size of the column, without padding +# just The justification for the current cell/column +# +# Results: +# The formatted string for the supplied cell. + +proc ::report::FormatCell {value size just} { + set vlen [string length [StripAnsiColor $value]] + + if {$vlen == $size} { + # Value fits exactly, justification is irrelevant + return $value + } + + # - Future - Other fill characters ... + # - Future - Different fill characters per class of value => regex/glob pattern|functions + # - Future - Wraparound - interacts with rowheight! + + switch -exact -- $just { + left { + if {$vlen < $size} { + return $value[string repeat " " [expr {$size - $vlen}]] + } + return [string range $value [expr {$vlen - $size}] end] + } + right { + if {$vlen < $size} { + return [string repeat " " [expr {$size - $vlen}]]$value + } + incr size -1 + return [string range $value 0 $size] + } + center { + if {$vlen < $size} { + set fill [expr {$size - $vlen}] + set rfill [expr {$fill / 2}] + set lfill [expr {$fill - $rfill}] + return [string repeat " " $lfill]$value[string repeat " " $rfill] + } + + set cut [expr {$vlen - $size}] + set lcut [expr {$cut / 2}] + set rcut [expr {$cut - $lcut}] + + return [string range $value $lcut end-$rcut] + } + default { + error "Can't happen, panic, run, shout" + } + } +} + +proc ::report::StripAnsiColor {string} { + # Look for ANSI color control sequences and remove them. Avoid + # counting their characters as such sequences as a whole represent + # a state change, and are logically of zero/no width. + regsub -all "\033\\\[\[0-9;\]*m" $string {} string + return $string +}
\ No newline at end of file |