crosstab again

Richard Suchenwirth 2007-02-05 - The cross-tabulation code at crosstab works like it should, but I very soon didn't like it - the main proc is too big and has puts sprinkled over the place. So I rewrote it in more functional programming terms by adding some clean functions operating on matrixes (implemented as lists of row lists), lists, resp. strings. The code hasn't become shorter from that, but better organized, and the functions could be re-used in other situations

 set usage {
    usage: crosstab.tcl infile ?-c x,y? ?-sep '\t'? -w 12
    Reads the infile (or stdin if infile is "-") for cases
    Prints on stdout a cross-tabulation for the specified columns.
    -c x,y : columns to use (1...), default is 1,2
    -sep x : column separator, default is ";"
    -w   n : output column width, default is 12
 if {[llength $argv] == 0} {puts stderr $usage; exit 1}

 proc main argv {
     getopt argv -sep sep  ";" 
     set sep [subst $sep] ;#-- e.g. for \t
     getopt argv -c   cols 1,2
     getopt argv -w   w    12
     set infile [lindex $argv 0]
     if {$infile eq "-"} {
         set f stdin
     } elseif {[file exists $infile]} {
         set f [open $infile]
     } else {puts stderr "no such file $infile\n$::usage"; exit 1}
     foreach {x y z} [split $cols ,] break
     incr x -1; incr y -1
     if {$z ne ""} {incr z -1}
     while {[gets $f line] >= 0} {
         set fields [split $line $sep]
         set key [lindex $fields $x],[lindex $fields $y],
         if {$z ne ""} {append key [lindex $fields $z]}
         inc N($key)
     if {$f ne "stdout"} {close $f}
     set xs [get_values N 0]
     set ys [get_values N 1]
     if {$z ne ""} {
         set zs [get_values N 2]
     } else {set zs {{}}}
     set totals [lrepeat [llength $ys] [lrepeat [llength $xs] 0]]
     set ylabels [map [list format %-${w}s] [concat $ys Total]]
     foreach zv $zs {
         array unset a
         foreach i [array names N *,$zv] {
             foreach {p1 p2} [split $i ,] break
             set a($p1,$p2) $N($i)
         set m [marray a $xs $ys]
         set totals [mexpr $totals + $m]
         set m [mlabel [msums $m] [concat $xs Total] $ylabels]
         puts $zv\n[mformat $m $w 1]\n
     if {$zs ne "{}"} {
         set m [mlabel [msums $totals] [concat $xs Total] $ylabels]
         puts "Grand Total:\n[mformat $m $w 1]\n"
#------------------------ additional list functions
 proc lrepeat {n args} { #-- built-in from 8.5
    set res {}
    for {set i 0} {$i<$n} {incr i} {eval lappend res $args}
    set res
#-- [Map] a function to a list, returning the results
 proc map {script list} {
    set res {}
    foreach i $list {lappend res [eval [linsert $script end $i]]}
    set res
#-- Sum of a list
 proc lsum list {
    set res 0
    foreach i $list {set res [expr {$res+$i}]}
    set res
#-- Apply a binary operator element-wise to two matrixes, giving a third
 proc mexpr {mat1 op mat2} {
    set res {}
    foreach row1 $mat1 row2 $mat2 row "" {
        foreach col1 $row1 col2 $row2 {
            lappend row [expr {$col1} $op {$col2}]
        lappend res $row
    set res
#-- Create a matrix from an array with (x,y) keys
 proc marray {_arr cols rows} {
    upvar 1 $_arr arr
    set res {}
    foreach row $rows {
        set outrow {}
        foreach col $cols {lappend outrow [get arr($col,$row)]}
        lappend res $outrow
    set res

#-- Compute row and columns sums, and add them to a matrix (at right resp. bottom
 proc msums matrix {
    set ncol -1
    set ncols {}
    foreach i [lindex $matrix 0] {
        set [incr ncol] 0
        lappend ncols $ncol
    set res {}
    foreach row $matrix {
        foreach cell $row ncol $ncols {
            set $ncol  [expr {[set $ncol]+$cell}]
        lappend res [lappend row [lsum $row]]
    set colsums {}
    foreach i [lindex $matrix 0] ncol $ncols {
        lappend colsums [set $ncol]
    lappend res [lappend colsums [lsum $colsums]]

#-- turn a matrix into a formatted multiline string
 proc mformat {matrix {w 12} {underline 0}} {
    set res ""
    foreach row $matrix line "" {
        foreach cell $row {append line [format %${w}s $cell]}
        lappend res $line
    if $underline {
        set length [string length [lindex $res 0]]
        set res [linsert $res 1 [string repeat - $length]]
    join $res \n

#-- Add column and row labels to a matrix
 proc mlabel {matrix collabels {rowlabels {}}} {
    #-- Add column and row labels to a matrix
    set res [list [linsert $collabels 0 {}]]
    foreach row $matrix label $rowlabels {
        lappend res [linsert $row 0 $label]
    set res
#-- Retrieve a variable's value, if present, else 0
 proc get _var {
     upvar 1 $_var var
     if {[info exists var]} {set var} else {return 0}
#-- Get the values of (x,y,...) array keys by position
 proc get_values {_arr pos} {
     upvar 1 $_arr arr
     set values {}
     foreach i [array names arr] {
         lappend values [lindex [split $i ,] $pos]
     lsort -unique $values
#-- See [getopt] for discussion
 proc getopt {_argv name {_var ""} {default ""}} {
     upvar 1 $_argv argv $_var var
     set pos [lsearch -regexp $argv ^$name]
     if {$pos>=0} {
         set to $pos
         if {$_var ne ""} {
             set var [lindex $argv [incr to]]
         set argv [lreplace $argv $pos $to]
         return 1
     } else {
         if {[llength [info level 0]] == 5} {set var $default}
         return 0
#-- auto-initializing increment (standard in 8.5)
 proc inc {_var {amount 1}} {
     upvar 1 $_var var
     if ![info exists var] {set var 0}
     incr var $amount

 main $argv