Wednesday September 29, 2004 Puzzle #2: cal(1)
(I've been fixing little smf(5) bugs, as well as revising our documentation,
presentations and--most importantly--more block diagrams for this blog. But I bumped into
an annoyance and thought I should share.)
As an young old-school Unix developer, I tend to live in terminal windows. One of my favourite commands is cal(1), which has a great default mode:
$ cal
September 2004
S M Tu W Th F S
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
But if you want to see an October calendar, you might get confused:
$ cal 10
10
Jan Feb Mar
S M Tu W Th F S S M Tu W Th F S S M Tu W Th F S
1 2 3 4 1 1
5 6 7 8 9 10 11 2 3 4 5 6 7 8 2 3 4 5 6 7 8
12 13 14 15 16 17 18 9 10 11 12 13 14 15 9 10 11 12 13 14 15
19 20 21 22 23 24 25 16 17 18 19 20 21 22 16 17 18 19 20 21 22
26 27 28 29 30 31 23 24 25 26 27 28 23 24 25 26 27 28 29
30 31
Apr May Jun
S M Tu W Th F S S M Tu W Th F S S M Tu W Th F S
1 2 3 4 5 1 2 3 1 2 3 4 5 6 7
6 7 8 9 10 11 12 4 5 6 7 8 9 10 8 9 10 11 12 13 14
13 14 15 16 17 18 19 11 12 13 14 15 16 17 15 16 17 18 19 20 21
20 21 22 23 24 25 26 18 19 20 21 22 23 24 22 23 24 25 26 27 28
27 28 29 30 25 26 27 28 29 30 31 29 30
Jul Aug Sep
S M Tu W Th F S S M Tu W Th F S S M Tu W Th F S
1 2 3 4 5 1 2 1 2 3 4 5 6
6 7 8 9 10 11 12 3 4 5 6 7 8 9 7 8 9 10 11 12 13
13 14 15 16 17 18 19 10 11 12 13 14 15 16 14 15 16 17 18 19 20
20 21 22 23 24 25 26 17 18 19 20 21 22 23 21 22 23 24 25 26 27
27 28 29 30 31 24 25 26 27 28 29 30 28 29 30
31
Oct Nov Dec
S M Tu W Th F S S M Tu W Th F S S M Tu W Th F S
1 2 3 4 1 1 2 3 4 5 6
5 6 7 8 9 10 11 2 3 4 5 6 7 8 7 8 9 10 11 12 13
12 13 14 15 16 17 18 9 10 11 12 13 14 15 14 15 16 17 18 19 20
19 20 21 22 23 24 25 16 17 18 19 20 21 22 21 22 23 24 25 26 27
26 27 28 29 30 31 23 24 25 26 27 28 29 28 29 30 31
30
It's an interesting UI choice to assume that anyone would want the calendar for the year 10 C.E. Certainly I never do, and I'm pretty sure someone would have told me if UNIX systems were the professional historian's first choice for computing...
So today's puzzle is to make cal(1) more usable. If I enter cal month_num, give me the current month; If I enter "cal now" give me the 3-month window around the current month, like so:
$ cal now
August 2004 September 2004 October 2004
S M Tu W Th F S S M Tu W Th F S S M Tu W Th F S
1 2 3 4 5 6 7 1 2 3 4 1 2
8 9 10 11 12 13 14 5 6 7 8 9 10 11 3 4 5 6 7 8 9
15 16 17 18 19 20 21 12 13 14 15 16 17 18 10 11 12 13 14 15 16
22 23 24 25 26 27 28 19 20 21 22 23 24 25 17 18 19 20 21 22 23
29 30 31 26 27 28 29 30 24 25 26 27 28 29 30
31
Other than that, all other standard invocations of cal(1) should work as
usual.
My example solution is a couple dozen line ksh(1) shell function, and I'll
post it along with the best submissions. (Perl folks: no non-core modules, please.)
Comments are closed for this entry.
$ cat cal-wrapper #!/bin/sh if [ $# -eq 1 ]; then case $1 in [0-9]|[0-9][0-9]) cal $1 `date '+%Y'` ;; now) month=`date '+%m' | sed 's/^0//'` prev=$(( ( $month -1 - 1 ) % 12 + 1 )) [ $prev -lt 1 ] && prev=$(( $prev + 12 )) next=$(( ( $month -1 + 1 ) % 12 + 1 )) for lc in 1 2 3 4 5 6 7 8 ;do sh $0 $prev | head -$lc | tail -1 | awk '{printf "%-22s",$0}' sh $0 $month | head -$lc | tail -1 | awk '{printf "%-22s",$0}' sh $0 $next | head -$lc | tail -1 | awk '{printf "%-22s",$0}' echo '' done ;; *) cal "$1" ;; esac else cal "$@" fiThe single month trick was easy to do with date(1). I have to admit that the multiple invocations of cal(1) when the "now" mode is invoked though are ugly. I might think of something better tomorrow, after I get some more sleep :-)Posted by gkeramidas on September 29, 2004 at 05:13 PM PDT #
Here's my entry. I started doing this in the Bourne shell, but halfway through I felt just too dirty writing it. So, I whipped this up in Perl, instead. Enjoy (and if you think this is too verbose, be happy that I didn't "use strict;".... at least I didn't write it in lisp).
#!/usr/bin/perl -w $real_cal = "/usr/bin/cal"; if (@ARGV != 1 || $ARGV[0] ne "now") { exec($real_cal, @ARGV) || die "could not exec $real_cal: $!\n"; } # Set $months to be our current year * 12 + the current month of the year. # Remember, $t[5] contains the number of years since 1900. Also, $t[4] is # the month of the year starting at zero (Jan == 0, Dec == 11). @t = localtime(time()); $months = (($t[5] + 1900) * 12) + $t[4]; # @cal will store the merged output of three months' output from cal(1). @cal = (); # Iterate over the three months for which we want data. for $month ($months - 1, $months, $months + 1) { # append_cal wants the first argument to be an array reference, # the second argument to be the month, *numbering from one, not zero*, # and the last to be the year. append_cal(\@cal, ($month % 12) + 1, int($month / 12)); } # Print out our data, appending newlines. foreach $line (@cal) { $line =~ s/\s+$//; print $line, "\n"; } # # append_cal - Append the calendar for the specified month and year to # an array of lines. # Parameters: # $cal: an array reference into which to store the output from cal(1). # $month: month of the year, suitable for passing directly to cal(1). # Note that this starts at one (Jan == 1, Dec == 12). # $year: year, numbering from zero (2004 == 2004). # sub append_cal { my ($cal) = shift(@_); my ($month) = shift(@_); my ($year) = shift(@_); my ($fh); my ($index); my ($line); open($fh, "$real_cal $month $year|") || die "opening pipe from cal: $!\n"; $index = 0; while (defined($line = <$fh>)) { chomp($line); if (!defined($$cal[$index])) { $$cal[$index] = ""; } $$cal[$index] .= sprintf("%-20s ", $line); $index++; } close($fh); }Posted by DJ Gregor on September 29, 2004 at 09:41 PM PDT #
Ooops.. change that first <code>if</code> block to:
if (@ARGV == 1 && $ARGV[0] =~ m/^\d{1,2}$/) { exec($real_cal, $ARGV[0], (localtime(time()))[5] + 1900) || die "could not exec $real_cal: $!\n"; } elsif (@ARGV != 1 || $ARGV[0] ne "now") { exec($real_cal, @ARGV) || die "could not exec $real_cal: $!\n"; }Posted by DJ Gregor on September 29, 2004 at 09:48 PM PDT #
Here's a version in zsh, modeled after gkeramidas' solution, but gets the years wrapped properly, and uses two different constructs to get things side by side. I use zsh specifically for the =() construct, which sticks the contents of the output of the command inside in a temporary file, though once I'm using zsh, I take advantage of a few other features.
For the "now" clause, I've two ways to print the calendars side by side. The first, using sdiff, forks fewer processes, but I believe is likely to be more fragile, given the column cutting. The second, using join, is probably more robust, but forks eleven processes to do the job. Choose your poison.
#!/bin/zsh -f PATH=/usr/bin if [[ $# -eq 1 ]]; then case $1 in (<0-12>) cal $1 $(date +%Y) ;; ("now") this=( $(date +%m) $(date +%Y) ) next=( $(( this[1] % 12 + 1)) $this[2] ) prev=( $(( (this[1] - 2) % 12 + 1)) $this[2] ) (( next[1] == 1 )) && (( ++next[2] )) (( prev[1] == 0 )) && (( --prev[2], prev[1] = 12 )) sdiff =(cal $=prev) =(sdiff -w 60 =(cal $=this) =(cal $=next)) | \ cut -c 1-22,67-88,98- echo join -t$'\t' -o 1.2,2.2 =(join -t$'\t' -o 1.2,2.2 \ =(cal $=prev | cat -n) =(cal $=this | cat -n) | expand -t 23 | cat -n) \ =(cal $=next | cat -n) | expand -t 46 ;; (*) exec cal $0 ;; esac else exec cal $@ fiPosted by Danek Duvall on September 29, 2004 at 11:14 PM PDT #
Posted by Giorgos Keramidas on September 30, 2004 at 02:32 AM PDT #
#!/bin/sh if [ $# -eq 1 ]; then case $1 in [0-9]|[0-9][0-9]) cal $1 `date '+%Y'` ;; now) year=$(date '+%Y') mon=$(date '+%m') pmon=$(( ( $mon - 2 ) % 12 + 1 )) [ $pmon -eq 0 ] && pmon=12 pyear=$year [ $pmon -gt $mon ] && pyear=$(( $pyear - 1 )) nmon=$(( $mon % 12 + 1 )) nyear=$year [ $nmon -lt $mon ] && nyear=$(( $nyear + 1 )) { cal "$pmon" "$pyear" | sed -e 's/^/a:/' ; \ cal "$mon" "$year" | sed -e 's/^/b:/' ; \ cal "$nmon" "$nyear" | sed -e 's/^/c:/' ; } | \ awk 'BEGIN { ja = jb = jc = 0;} /^a:/ {av[ja] = substr($0,3); ja++;} /^b:/ {bv[jb] = substr($0,3); jb++;} /^c:/ {cv[jc] = substr($0,3); jc++;} END { jmax = ja; if (jb > jmax) {jmax = jb;} if (jc > jmax) {jmax = jc;} for (ja = 0; ja < jmax; ja++) { printf "%-22s%-22s%-22s\n", av[ja],bv[ja],cv[ja]; } }' ;; *) cal "$1" ;; esac else cal "$@" fiThis one uses three string arrays in awk(1) to store the output of cal(1) as it comes in and saves a hell of a lot of process invocations from my first attempt. That's probably why it is a lot faster too. That was fun playing with :-)Posted by Giorgos Keramidas on September 30, 2004 at 02:52 AM PDT #
Posted by Alan Burlison on October 01, 2004 at 06:51 AM PDT #
#!/bin/perl # # You are not supposed to understand this, just be in awe of my perl-foo. # use strict; use warnings; my ($m, $y, @my) = (localtime())[4, 5]; $m += 1; $y += 1900; if (@ARGV == 1 && $ARGV[0] eq 'now') { $my[1] = [ $m, $y ]; } elsif (@ARGV == 1 && $ARGV[0] =~ /^\d+$/ && $ARGV[0] >= 1 && $ARGV[0] <= 12) { $my[1] = [ $ARGV[0], $y ]; } else { exec('/usr/bin/cal', @ARGV) || die("Can't exec cal: $!\n"); } $_ = $my[1][0] - 1; @{$my[0]} = $_ == 0 ? ( 12, $my[1][1] - 1 ) : ( $_, $my[1][1] ); $_ = $my[1][0] + 1; @{$my[2]} = $_ == 13 ? ( 1, $my[1][1] + 1 ) : ( $_, $my[1][1] ); my @cal = map([ map({ chomp($_); $_ } qx{/usr/bin/cal $_->[0] $_->[1]}) ], @my); for ($_ = 0; $_ < 7; $_++) { printf("%-20s %-20s %-20s\n", map(shift(@{$_}) || '', @cal)); } exit(0);Posted by Alan Burlison on October 01, 2004 at 06:53 AM PDT #
my @cal = map([ map({ chomp($_); $_ } qx{/usr/bin/cal $_->[0] $_->[1]}) ], @my);can be replaced with:my @cal = map([ map({ chomp($_); $_ } qx{/usr/bin/cal $_->[0] @{$_}, @my);I've been able to reduce this to is 460 bytes (448 if you discount the #! line), any less and I feel I might risk compromising readibility:#!/bin/perl ($m,$y)=(localtime())[4,5];$m+=1;$y+=1900;$n=@ARGV;$_=$ARGV[0];if($n==1&&$_ eq'now'){$a[1]=[$m,$y];}elsif($n==1&&$_=~/^\d+$/&&$_>=1&&$_<=12){$a[1]=[$_,$y];}else{exec('/usr/bin/cal',@ARGV)||die($!);}$_=$a[1][0]-1;@{$a[0]}=$_==0?(12,$a[1][1]-1):($_,$a[1][1]);$_=$a[1][0]+1;@{$a[2]}=$_==13?(1,$a[1][1]+1):($_,$a[1][1]);@c=map([map({chomp($_);$_}qx{/usr/bin/cal @$_})],@a);for($_=0;$_<7;$_++){printf("%-20s %-20s %-20s\n",map(shift(@$_)||'',@c));}Note in particular how it takes care to still produce the correct output in a leap year when 1st Febuary falls on a Sunday - see that bit there on the second line?Posted by Alan Burlison on October 01, 2004 at 07:24 AM PDT #
Posted by Alan Burlison on October 01, 2004 at 07:33 AM PDT #
Posted by gkeramidas on October 01, 2004 at 03:50 PM PDT #
Posted by Alan Burlison on October 01, 2004 at 05:01 PM PDT #
#!/bin/ksh if [ $# == 2 ] then /usr/bin/cal $1 $2 exit 0 elif [ $# == 1 ] then if [ $1 != now ] then /usr/bin/cal $1 $(date '+%Y') exit 0 fi else /usr/bin/cal exit 0 fi integer year=$(date '+%Y') integer now=$(date '+%m') integer prev=$now-1 integer next=$now+1 integer i=0 integer j=0 store_cal() { export IFS="\n" cal $1 $2 | while read line do out[$i]=$line i=$i+3 j=$j+1 done } store_cal $prev $year i=1 store_cal $now $year i=2 store_cal $next $year i=0 while (( i < j )) do printf "%-20s %-20s %-20s\n" \ "${out[$i]}" "${out[$i+1]}" "${out[$i+2]}" i=$i+3 donePosted by Gary on October 07, 2004 at 02:14 AM PDT #
#!/usr/bin/env perl
my $arg = shift @ARGV;
my @years = ( );
my @months = ( );
my %output = ( );
my @lt = localtime; # see manual for localtime in perl but:
# index 5 is year minus 1900,
# index 4 is month minus 1
if (not defined $arg or $arg eq '') {
system ('/usr/bin/env cal');
exit (0);
} elsif ($arg =~ m/^now$/i) {
foreach (($lt [4] - 1)..($lt [4] + 1)) {
if ($_ < 0) {
push @years, $lt [5] + 1900 - 1;
push @months, $_ + 11;
} elsif ($_ > 11) {
push @years, $lt [5] + 1900 + 1;
push @months, $_ - 11;
} else {
push @years, $lt [5] + 1900;
push @months, $_ + 1;
}
}
} elsif ($arg =~ m/^[0-9]{0,2}$/ and $arg+0 >= 1 and $arg+0 <= 12) {
push @years, $lt [5] + 1900;
push @months, $arg+0;
} else {
die 'usage: ' . $0 . ' [<month> | now]';
}
foreach my $i (0..$#years) {
$output{$i} = [split ("\n", `/usr/bin/env cal $months[$i] $years[$i]`)];
}
# -- join output
foreach my $row (0..7) { # max of each cal output are 8 rows
print join (' 'x3, map { $_ . ' 'x(20-length ($_)) } map { $output{$_}->[$row] } (0..$#years) ) . "\n";
}
# my email: Antonio.Santos at pt.jazztel.com
Posted by Ant�nio Santos on October 07, 2004 at 07:46 AM PDT #
my $arg = shift @ARGV;
my @years = ( );
my @months = ( );
my %output = ( );
my @lt = localtime; # see manual for localtime in perl but:
# index 5 is year minus 1900,
# index 4 is month minus 1
if (not defined $arg or $arg eq '') {
system ('/usr/bin/env cal');
exit (0);
} elsif ($arg =~ m/^now$/i) {
foreach (($lt [4] - 1)..($lt [4] + 1)) {
if ($_ < 0) {
push @years, $lt [5] + 1900 - 1;
push @months, $_ + 11;
} elsif ($_ > 11) {
push @years, $lt [5] + 1900 + 1;
push @months, $_ - 11;
} else {
push @years, $lt [5] + 1900;
push @months, $_ + 1;
}
}
} elsif ($arg =~ m/^[0-9]{0,2}$/ and $arg+0 >= 1 and $arg+0 <= 12) {
push @years, $lt [5] + 1900;
push @months, $arg+0;
} else {
die 'usage: ' . $0 . ' [<month> | now]';
}
foreach my $i (0..$#years) {
$output{$i} = [split ("\n", `/usr/bin/env cal $months[$i] $years[$i]`)];
}
# -- join output
foreach my $row (0..7) { # max of each cal output are 8 rows
print join (' 'x3, map { $_ . ' 'x(20-length ($_)+1) } map { $output{$_}->[$row] } (0..$#years) ) . "\n";
}
# Here it goes new version, was a +1 missing on the padding of spaces on output.
Posted by Antonio Santos on October 07, 2004 at 07:52 AM PDT #
#!/bin/ksh -p z=/usr/bin/cal c(){ $z $*|sed 's/$/ /'|expand -t 23 } date "+%Y %m"|read y m d=0${1%%now} [[ $d -lt 13 && $d -gt 0 ]]&&exec $z $d ${2:-$y} [[ $1 != "now" ]]&&exec $z $* set -A a $y $y $y set -A b -- $((m-1)) $m $((m+1)) f[1]='a[0]=$((y-1));b[0]=12' f[12]='a[2]=$((y+1));b[2]=1' eval ${f[$m]} eval paste `for i in 0 1 2; do echo '<(c '${b[$i]} ${a[$i]}')' done`(note: the whitespace in the sed substitution is a single tab)Posted by Dave Powell on October 08, 2004 at 07:54 PM PDT #
Posted by Dave Powell on October 09, 2004 at 01:56 AM PDT #
#!/bin/ksh -p z=/usr/bin/cal c(){ sed 's/$/ /'|expand -t23 } date +%Y\ %m|read y m d=0${1%%now} [[ $d -lt 13 && $d -gt 0 ]]&&exec $z $d ${2:-$y} [[ $1 = now ]]||exec $z $* set -A a $y $y $y set -A b -- $((m-1)) $m $((m+1)) f[1]='a[0]=$((y-1));b[0]=12' f[12]='a[2]=$((y+1));b[2]=1' eval ${f[$m]} eval paste `for i in 0 1 2; do echo "<($z ${b[$i]} ${a[$i]}|c)" done`Posted by Dave Powell on October 09, 2004 at 02:37 AM PDT #
#!/bin/ksh -p z=/usr/bin/cal\ c(){ sed 's/$/ /'|expand -t23 } date +%Y\ %m|read y m d=0${1%%now} [[ $d -lt 13&&$d -gt 0 ]]&&exec $z$d ${2:-$y} [[ $1 = now ]]||exec $z$* set -A a 0 0 0 f[1]='a[0]=-1' f[12]='a[2]=1' eval ${f[m]} eval paste `for i in 0 1 2; do echo "<($z$(((m+i+10)%12+1)) $((y+a[i]))|c)" done`Posted by Dave Powell on October 13, 2004 at 05:02 PM PDT #
Posted by Jaime Cardoso on October 15, 2004 at 07:21 AM PDT #
Posted by Anonymous on October 21, 2004 at 02:49 PM PDT #
#!/bin/ksh -p c() { cal $(($1%12+1)) $(($1/12)) | sed -e 's/$/ /' | expand -21 } [[ $# -ne 1 ]] && exec cal "$@" integer y=$(date +%Y) [[ $1 != now ]] && { [[ $1 -lt 13 ]] && exec cal $1 $y || exec cal $1 } integer m=$(date +%m) integer d=$((y*12+(m-1))) exec paste <(c $((d-1))) <(c $((d))) <(c $((d+1)))Posted by Anonymous on October 21, 2004 at 02:51 PM PDT #