--- loncom/interface/Attic/lonspreadsheet.pm 2001/10/22 15:03:22 1.72 +++ loncom/interface/Attic/lonspreadsheet.pm 2002/09/27 18:29:15 1.111 @@ -1,13 +1,54 @@ +# +# $Id: lonspreadsheet.pm,v 1.111 2002/09/27 18:29:15 matthew Exp $ +# +# Copyright Michigan State University Board of Trustees +# +# This file is part of the LearningOnline Network with CAPA (LON-CAPA). +# +# LON-CAPA is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# LON-CAPA is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with LON-CAPA; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# /home/httpd/html/adm/gpl.txt +# +# http://www.lon-capa.org/ +# # The LearningOnline Network with CAPA # Spreadsheet/Grades Display Handler # -# 11/11,11/15,11/27,12/04,12/05,12/06,12/07, -# 12/08,12/09,12/11,12/12,12/15,12/16,12/18,12/19,12/30, -# 01/01/01,02/01,03/01,19/01,20/01,22/01, -# 03/05,03/08,03/10,03/12,03/13,03/15,03/17, -# 03/19,03/20,03/21,03/27,04/05,04/09, -# 07/09,07/14,07/21,09/01,09/10,9/11,9/12,9/13,9/14,9/17, -# 10/16,10/17,10/20 Gerd Kortemeyer +# POD required stuff: + +=head1 NAME + +lonspreadsheet + +=head1 SYNOPSIS + +Spreadsheet interface to internal LON-CAPA data + +=head1 DESCRIPTION + +Lonspreadsheet provides course coordinators the ability to manage their +students grades online. The students are able to view their own grades, but +not the grades of their peers. The spreadsheet is highly customizable, +offering the ability to use Perl code to manipulate data, as well as many +built-in functions. + +=head2 Functions available to user of lonspreadsheet + +=over 4 + +=cut package Apache::lonspreadsheet; @@ -19,7 +60,7 @@ use Apache::lonnet; use Apache::Constants qw(:common :http); use GDBM_File; use HTML::TokeParser; - +use Apache::lonhtmlcommon; # # Caches for previously calculated spreadsheets # @@ -54,6 +95,14 @@ my %courseopt; my %useropt; my %parmhash; +# +# Some hashes for stats on timing and performance +# + +my %starttimes; +my %usedtimes; +my %numbertimes; + # Stuff that only the screen handler can know my $includedir; @@ -70,6 +119,7 @@ sub initsheet { $safeeval->permit("sort"); $safeeval->deny(":base_io"); $safehole->wrap(\&Apache::lonnet::EXT,$safeeval,'&EXT'); + $safeeval->share('$@'); my $code=<<'ENDDEFS'; # ---------------------------------------------------- Inside of the safe space @@ -81,37 +131,39 @@ sub initsheet { # rl: row label # os: other spreadsheets (for student spreadsheet only) -undef %v; +undef %sheet_values; undef %t; undef %f; undef %c; -undef %rl; +undef %rowlabel; undef @os; -$maxrow=0; -$sheettype=''; +$maxrow = 0; +$sheettype = ''; # filename/reference of the sheet - -$filename=''; +$filename = ''; # user data -$uname=''; -$uhome=''; -$udom=''; +$uname = ''; +$uhome = ''; +$udom = ''; # course data -$csec=''; -$chome=''; -$cnum=''; -$cdom=''; -$cid=''; -$cfn=''; +$csec = ''; +$chome= ''; +$cnum = ''; +$cdom = ''; +$cid = ''; +$coursefilename = ''; # symb -$usymb=''; +$usymb = ''; + +# error messages +$errormsg = ''; sub mask { my ($lower,$upper)=@_; @@ -142,16 +194,16 @@ sub mask { } else { if (length($ld)!=length($ud)) { $num.='('; - map { + foreach ($ld=~m/\d/g) { $num.='['.$_.'-9]'; - } ($ld=~m/\d/g); + } if (length($ud)-length($ld)>1) { $num.='|\d{'.(length($ld)+1).','.(length($ud)-1).'}'; } $num.='|'; - map { + foreach ($ud=~m/\d/g) { $num.='[0-'.$_.']'; - } ($ud=~m/\d/g); + } $num.=')'; } else { my @lda=($ld=~m/\d/g); @@ -189,12 +241,285 @@ sub mask { return '^'.$alpha.$num."\$"; } +#------------------------------------------------------- + +=item UWCALC(hashname,modules,units,date) + +returns the proportion of the module +weights not previously completed by the student. + +=over 4 + +=item hashname + +name of the hash the module dates have been inserted into + +=item modules + +reference to a cell which contains a comma deliminated list of modules +covered by the assignment. + +=item units + +reference to a cell which contains a comma deliminated list of module +weights with respect to the assignment + +=item date + +reference to a cell which contains the date the assignment was completed. + +=back + +=cut + +#------------------------------------------------------- +sub UWCALC { + my ($hashname,$modules,$units,$date) = @_; + my @Modules = split(/,/,$modules); + my @Units = split(/,/,$units); + my $total_weight; + foreach (@Units) { + $total_weight += $_; + } + my $usum=0; + for (my $i=0; $i<=$#Modules; $i++) { + if (&HASH($hashname,$Modules[$i]) eq $date) { + $usum += $Units[$i]; + } + } + return $usum/$total_weight; +} + +#------------------------------------------------------- + +=item CDLSUM(list) + +returns the sum of the elements in a cell which contains +a Comma Deliminate List of numerical values. +'list' is a reference to a cell which contains a comma deliminated list. + +=cut + +#------------------------------------------------------- +sub CDLSUM { + my ($list)=@_; + my $sum; + foreach (split/,/,$list) { + $sum += $_; + } + return $sum; +} + +#------------------------------------------------------- + +=item CDLITEM(list,index) + +returns the item at 'index' in a Comma Deliminated List. + +=over 4 + +=item list + +reference to a cell which contains a comma deliminated list. + +=item index + +the Perl index of the item requested (first element in list has +an index of 0) + +=back + +=cut + +#------------------------------------------------------- +sub CDLITEM { + my ($list,$index)=@_; + my @Temp = split/,/,$list; + return $Temp[$index]; +} + +#------------------------------------------------------- + +=item CDLHASH(name,key,value) + +loads a comma deliminated list of keys into +the hash 'name', all with a value of 'value'. + +=over 4 + +=item name + +name of the hash. + +=item key + +(a pointer to) a comma deliminated list of keys. + +=item value + +a single value to be entered for each key. + +=back + +=cut + +#------------------------------------------------------- +sub CDLHASH { + my ($name,$key,$value)=@_; + my @Keys; + my @Values; + # Check to see if we have multiple $key values + if ($key =~ /[A-z](\-[A-z])?\d+(\-\d+)?/) { + my $keymask = &mask($key); + # Assume the keys are addresses + my @Temp = grep /$keymask/,keys(%sheet_values); + @Keys = $sheet_values{@Temp}; + } else { + $Keys[0]= $key; + } + my @Temp; + foreach $key (@Keys) { + @Temp = (@Temp, split/,/,$key); + } + @Keys = @Temp; + if ($value =~ /[A-z](\-[A-z])?\d+(\-\d+)?/) { + my $valmask = &mask($value); + my @Temp = grep /$valmask/,keys(%sheet_values); + @Values =$sheet_values{@Temp}; + } else { + $Values[0]= $value; + } + $value = $Values[0]; + # Add values to hash + for (my $i = 0; $i<=$#Keys; $i++) { + my $key = $Keys[$i]; + if (! exists ($hashes{$name}->{$key})) { + $hashes{$name}->{$key}->[0]=$value; + } else { + my @Temp = sort(@{$hashes{$name}->{$key}},$value); + $hashes{$name}->{$key} = \@Temp; + } + } + return "hash '$name' updated"; +} + +#------------------------------------------------------- + +=item GETHASH(name,key,index) + +returns the element in hash 'name' +reference by the key 'key', at index 'index' in the values list. + +=cut + +#------------------------------------------------------- +sub GETHASH { + my ($name,$key,$index)=@_; + if (! defined($index)) { + $index = 0; + } + if ($key =~ /^[A-z]\d+$/) { + $key = $sheet_values{$key}; + } + return $hashes{$name}->{$key}->[$index]; +} + +#------------------------------------------------------- + +=item CLEARHASH(name) + +clears all the values from the hash 'name' + +=item CLEARHASH(name,key) + +clears all the values from the hash 'name' associated with the given key. + +=cut + +#------------------------------------------------------- +sub CLEARHASH { + my ($name,$key)=@_; + if (defined($key)) { + if (exists($hashes{$name}->{$key})) { + $hashes{$name}->{$key}=undef; + return "hash '$name' key '$key' cleared"; + } + } else { + if (exists($hashes{$name})) { + $hashes{$name}=undef; + return "hash '$name' cleared"; + } + } + return "Error in clearing hash"; +} + +#------------------------------------------------------- + +=item HASH(name,key,value) + +loads values into an internal hash. If a key +already has a value associated with it, the values are sorted numerically. + +=item HASH(name,key) + +returns the 0th value in the hash 'name' associated with 'key'. + +=cut + +#------------------------------------------------------- +sub HASH { + my ($name,$key,$value)=@_; + my @Keys; + undef @Keys; + my @Values; + # Check to see if we have multiple $key values + if ($key =~ /[A-z](\-[A-z])?\d+(\-\d+)?/) { + my $keymask = &mask($key); + # Assume the keys are addresses + my @Temp = grep /$keymask/,keys(%sheet_values); + @Keys = $sheet_values{@Temp}; + } else { + $Keys[0]= $key; + } + # If $value is empty, return the first value associated + # with the first key. + if (! $value) { + return $hashes{$name}->{$Keys[0]}->[0]; + } + # Check to see if we have multiple $value(s) + if ($value =~ /[A-z](\-[A-z])?\d+(\-\d+)?/) { + my $valmask = &mask($value); + my @Temp = grep /$valmask/,keys(%sheet_values); + @Values =$sheet_values{@Temp}; + } else { + $Values[0]= $value; + } + # Add values to hash + for (my $i = 0; $i<=$#Keys; $i++) { + my $key = $Keys[$i]; + my $value = ($i<=$#Values ? $Values[$i] : $Values[0]); + if (! exists ($hashes{$name}->{$key})) { + $hashes{$name}->{$key}->[0]=$value; + } else { + my @Temp = sort(@{$hashes{$name}->{$key}},$value); + $hashes{$name}->{$key} = \@Temp; + } + } + return $Values[-1]; +} + +#------------------------------------------------------- + +=item NUM(range) + +returns the number of items in the range. + +=cut + +#------------------------------------------------------- sub NUM { my $mask=mask(@_); - my $num=0; - map { - $num++; - } grep /$mask/,keys %v; + my $num= $#{@{grep(/$mask/,keys(%sheet_values))}}+1; return $num; } @@ -202,31 +527,49 @@ sub BIN { my ($low,$high,$lower,$upper)=@_; my $mask=mask($lower,$upper); my $num=0; - map { - if (($v{$_}>=$low) && ($v{$_}<=$high)) { + foreach (grep /$mask/,keys(%sheet_values)) { + if (($sheet_values{$_}>=$low) && ($sheet_values{$_}<=$high)) { $num++; } - } grep /$mask/,keys %v; + } return $num; } +#------------------------------------------------------- + +=item SUM(range) + +returns the sum of items in the range. + +=cut + +#------------------------------------------------------- sub SUM { my $mask=mask(@_); my $sum=0; - map { - $sum+=$v{$_}; - } grep /$mask/,keys %v; + foreach (grep /$mask/,keys(%sheet_values)) { + $sum+=$sheet_values{$_}; + } return $sum; } +#------------------------------------------------------- + +=item MEAN(range) + +compute the average of the items in the range. + +=cut + +#------------------------------------------------------- sub MEAN { my $mask=mask(@_); my $sum=0; my $num=0; - map { - $sum+=$v{$_}; + foreach (grep /$mask/,keys(%sheet_values)) { + $sum+=$sheet_values{$_}; $num++; - } grep /$mask/,keys %v; + } if ($num) { return $sum/$num; } else { @@ -234,58 +577,106 @@ sub MEAN { } } +#------------------------------------------------------- + +=item STDDEV(range) + +compute the standard deviation of the items in the range. + +=cut + +#------------------------------------------------------- sub STDDEV { my $mask=mask(@_); my $sum=0; my $num=0; - map { - $sum+=$v{$_}; + foreach (grep /$mask/,keys(%sheet_values)) { + $sum+=$sheet_values{$_}; $num++; - } grep /$mask/,keys %v; + } unless ($num>1) { return undef; } my $mean=$sum/$num; $sum=0; - map { - $sum+=($v{$_}-$mean)**2; - } grep /$mask/,keys %v; + foreach (grep /$mask/,keys(%sheet_values)) { + $sum+=($sheet_values{$_}-$mean)**2; + } return sqrt($sum/($num-1)); } +#------------------------------------------------------- + +=item PROD(range) + +compute the product of the items in the range. + +=cut + +#------------------------------------------------------- sub PROD { my $mask=mask(@_); my $prod=1; - map { - $prod*=$v{$_}; - } grep /$mask/,keys %v; + foreach (grep /$mask/,keys(%sheet_values)) { + $prod*=$sheet_values{$_}; + } return $prod; } +#------------------------------------------------------- + +=item MAX(range) + +compute the maximum of the items in the range. + +=cut + +#------------------------------------------------------- sub MAX { my $mask=mask(@_); my $max='-'; - map { - unless ($max) { $max=$v{$_}; } - if (($v{$_}>$max) || ($max eq '-')) { $max=$v{$_}; } - } grep /$mask/,keys %v; + foreach (grep /$mask/,keys(%sheet_values)) { + unless ($max) { $max=$sheet_values{$_}; } + if (($sheet_values{$_}>$max) || ($max eq '-')) { $max=$sheet_values{$_}; } + } return $max; } +#------------------------------------------------------- + +=item MIN(range) + +compute the minimum of the items in the range. + +=cut + +#------------------------------------------------------- sub MIN { my $mask=mask(@_); my $min='-'; - map { - unless ($max) { $max=$v{$_}; } - if (($v{$_}<$min) || ($min eq '-')) { $min=$v{$_}; } - } grep /$mask/,keys %v; + foreach (grep /$mask/,keys(%sheet_values)) { + unless ($max) { $max=$sheet_values{$_}; } + if (($sheet_values{$_}<$min) || ($min eq '-')) { + $min=$sheet_values{$_}; + } + } return $min; } +#------------------------------------------------------- + +=item SUMMAX(num,lower,upper) + +compute the sum of the largest 'num' items in the range from +'lower' to 'upper' + +=cut + +#------------------------------------------------------- sub SUMMAX { my ($num,$lower,$upper)=@_; my $mask=mask($lower,$upper); my @inside=(); - map { - $inside[$#inside+1]=$v{$_}; - } grep /$mask/,keys %v; + foreach (grep /$mask/,keys(%sheet_values)) { + push (@inside,$sheet_values{$_}); + } @inside=sort(@inside); my $sum=0; my $i; for ($i=$#inside;(($i>$#inside-$num) && ($i>=0));$i--) { @@ -294,13 +685,23 @@ sub SUMMAX { return $sum; } +#------------------------------------------------------- + +=item SUMMIN(num,lower,upper) + +compute the sum of the smallest 'num' items in the range from +'lower' to 'upper' + +=cut + +#------------------------------------------------------- sub SUMMIN { my ($num,$lower,$upper)=@_; my $mask=mask($lower,$upper); my @inside=(); - map { - $inside[$#inside+1]=$v{$_}; - } grep /$mask/,keys %v; + foreach (grep /$mask/,keys(%sheet_values)) { + $inside[$#inside+1]=$sheet_values{$_}; + } @inside=sort(@inside); my $sum=0; my $i; for ($i=0;(($i<$num) && ($i<=$#inside));$i++) { @@ -309,6 +710,53 @@ sub SUMMIN { return $sum; } +#------------------------------------------------------- + +=item MINPARM(parametername) + +Returns the minimum value of the parameters matching the parametername. +parametername should be a string such as 'duedate'. + +=cut + +#------------------------------------------------------- +sub MINPARM { + my ($expression) = @_; + my $min = undef; + study($expression); + foreach $parameter (keys(%c)) { + next if ($parameter !~ /$expression/); + if ((! defined($min)) || ($min > $c{$parameter})) { + $min = $c{$parameter} + } + } + return $min; +} + +#------------------------------------------------------- + +=item MAXPARM(parametername) + +Returns the maximum value of the parameters matching the input parameter name. +parametername should be a string such as 'duedate'. + +=cut + +#------------------------------------------------------- +sub MAXPARM { + my ($expression) = @_; + my $max = undef; + study($expression); + foreach $parameter (keys(%c)) { + next if ($parameter !~ /$expression/); + if ((! defined($min)) || ($max < $c{$parameter})) { + $max = $c{$parameter} + } + } + return $max; +} + +#-------------------------------------------------------- sub expandnamed { my $expression=shift; if ($expression=~/^\&/) { @@ -316,32 +764,58 @@ sub expandnamed { my @vars=split(/\W+/,$formula); my %values=(); undef %values; - map { + foreach ( @vars ) { my $varname=$_; if ($varname=~/\D/) { $formula=~s/$varname/'$c{\''.$varname.'\'}'/ge; $varname=~s/$var/\(\\w\+\)/g; - map { + foreach (keys(%c)) { if ($_=~/$varname/) { $values{$1}=1; } - } keys %c; + } } - } @vars; + } if ($func eq 'EXPANDSUM') { my $result=''; - map { + foreach (keys(%values)) { my $thissum=$formula; $thissum=~s/$var/$_/g; $result.=$thissum.'+'; - } keys %values; + } $result=~s/\+$//; return $result; } else { return 0; } } else { - return '$c{\''.$expression.'\'}'; + # it is not a function, so it is a parameter name + # We should do the following: + # 1. Take the list of parameter names + # 2. look through the list for ones that match the parameter we want + # 3. If there are no collisions, return the one that matches + # 4. If there is a collision, return 'bad parameter name error' + my $returnvalue = ''; + my @matches = (); + $#matches = -1; + study $expression; + foreach $parameter (keys(%c)) { + push @matches,$parameter if ($parameter =~ /$expression/); + } + if ($#matches == 0) { + $returnvalue = '$c{\''.$matches[0].'\'}'; + } elsif ($#matches > 0) { + # more than one match. Look for a concise one + $returnvalue = "'non-unique parameter name : $expression'"; + foreach (@matches) { + if (/^$expression$/) { + $returnvalue = '$c{\''.$_.'\'}'; + } + } + } else { + $returnvalue = "'bad parameter name : $expression'"; + } + return $returnvalue; } } @@ -353,27 +827,31 @@ sub sett { } else { $pattern='[A-Z]'; } - map { - if ($_=~/template\_(\w)/) { - my $col=$1; - unless ($col=~/^$pattern/) { - map { - if ($_=~/A(\d+)/) { - my $trow=$1; - if ($trow) { - my $lb=$col.$trow; - $t{$lb}=$f{'template_'.$col}; - $t{$lb}=~s/\#/$trow/g; - $t{$lb}=~s/\.\.+/\,/g; - $t{$lb}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$v\{\'$2\'\}/g; - $t{$lb}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.&expandnamed($2)/ge; - } - } - } keys %f; - } - } - } keys %f; - map { + # Deal with the template row + foreach (keys(%f)) { + next if ($_!~/template\_(\w)/); + my $col=$1; + next if ($col=~/^$pattern/); + foreach (keys(%f)) { + next if ($_!~/A(\d+)/); + my $trow=$1; + next if (! $trow); + # Get the name of this cell + my $lb=$col.$trow; + # Grab the template declaration + $t{$lb}=$f{'template_'.$col}; + # Replace '#' with the row number + $t{$lb}=~s/\#/$trow/g; + # Replace '....' with ',' + $t{$lb}=~s/\.\.+/\,/g; + # Replace 'A0' with the value from 'A0' + $t{$lb}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$sheet_values\{\'$2\'\}/g; + # Replace parameters + $t{$lb}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.&expandnamed($2)/ge; + } + } + # Deal with the normal cells + foreach (keys(%f)) { if (($f{$_}) && ($_!~/template\_/)) { my $matches=($_=~/^$pattern(\d+)/); if (($matches) && ($1)) { @@ -383,37 +861,53 @@ sub sett { } else { $t{$_}=$f{$_}; $t{$_}=~s/\.\.+/\,/g; - $t{$_}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$v\{\'$2\'\}/g; + $t{$_}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$sheet_values\{\'$2\'\}/g; $t{$_}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.&expandnamed($2)/ge; } } - } keys %f; + } + # For inserted lines, [B-Z] is also valid + unless ($sheettype eq 'assesscalc') { + foreach (keys(%f)) { + if ($_=~/[B-Z](\d+)/) { + if ($f{'A'.$1}=~/^[\~\-]/) { + $t{$_}=$f{$_}; + $t{$_}=~s/\.\.+/\,/g; + $t{$_}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$sheet_values\{\'$2\'\}/g; + $t{$_}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.&expandnamed($2)/ge; + } + } + } + } + # For some reason 'A0' gets special treatment... This seems superfluous + # but I imagine it is here for a reason. $t{'A0'}=$f{'A0'}; $t{'A0'}=~s/\.\.+/\,/g; - $t{'A0'}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$v\{\'$2\'\}/g; + $t{'A0'}=~s/(^|[^\"\'])([A-Za-z]\d+)/$1\$sheet_values\{\'$2\'\}/g; $t{'A0'}=~s/(^|[^\"\'])\[([^\]]+)\]/$1.&expandnamed($2)/ge; } sub calc { - %v=(); + undef %sheet_values; &sett(); my $notfinished=1; + my $lastcalc=''; my $depth=0; while ($notfinished) { $notfinished=0; - map { - my $old=$v{$_}; - $v{$_}=eval($t{$_}); + foreach (keys(%t)) { + my $old=$sheet_values{$_}; + $sheet_values{$_}=eval $t{$_}; if ($@) { - %v=(); - return $@; + undef %sheet_values; + return $_.': '.$@; } - if ($v{$_} ne $old) { $notfinished=1; } - } keys %t; + if ($sheet_values{$_} ne $old) { $notfinished=1; $lastcalc=$_; } + } $depth++; if ($depth>100) { - %v=(); - return 'Maximum calculation depth exceeded'; + undef %sheet_values; + return $lastcalc.': Maximum calculation depth exceeded'; } } return ''; @@ -422,44 +916,53 @@ sub calc { sub templaterow { my @cols=(); $cols[0]='Template'; - map { + foreach ('A','B','C','D','E','F','G','H','I','J','K','L','M', + 'N','O','P','Q','R','S','T','U','V','W','X','Y','Z', + 'a','b','c','d','e','f','g','h','i','j','k','l','m', + 'n','o','p','q','r','s','t','u','v','w','x','y','z') { my $fm=$f{'template_'.$_}; $fm=~s/[\'\"]/\&\#34;/g; - $cols[$#cols+1]="'template_$_','$fm'".'___eq___'.$fm; - } ('A','B','C','D','E','F','G','H','I','J','K','L','M', - 'N','O','P','Q','R','S','T','U','V','W','X','Y','Z', - 'a','b','c','d','e','f','g','h','i','j','k','l','m', - 'n','o','p','q','r','s','t','u','v','w','x','y','z'); + push(@cols,"'template_$_','$fm'".'___eq___'.$fm); + } return @cols; } +# +# This is actually used for the student spreadsheet, not the assessment sheet +# Do not be fooled by the name! +# sub outrowassess { + # $n is the current row number my $n=shift; my @cols=(); if ($n) { - my ($usy,$ufn)=split(/\_\_\&\&\&\_\_/,$f{'A'.$n}); - $cols[0]=$rl{$usy}.'
'. - ''. + ''; + } else { + $cols[0]=''; + } + foreach (@os) { + $cols[0].='