\documentclass{ictlab}

% Copyright (c) 2003 by Nick Urbanik <nicku(at)nicku.org>.
% This material may be distributed only subject to the terms and
% conditions set forth in the Open Publication License, v1.0 or later
% (the latest version is presently available at
% http://www.opencontent.org/openpub/).

\RCS $Revision: 1.9 $

\usepackage{alltt}
\ifx\pdftexversion\undefined \else
  \usepackage[pdfpagemode=None,pdfauthor={Nick Urbanik}]{hyperref}
\fi

\newcommand*{\labTitle}{Perl Data Structures}

\begin{Solutions}%
  \gdef\solution{\item[\textbf{Solution:}]}%
\end{Solutions}

\begin{document}

\section{Background}
\label{sec:background}

\subsection{Scalars}
\label{sec:scalars}

You can specify scalar numerical values in the same ways as in C:

The following are all scalar constant values: \texttt{123},
\texttt{12.4}, \texttt{5E-10}, \texttt{0xff} (a hexadecimal value),
\texttt{0377} (octal).

Single quotes preserve all characters unchanged, except for `\bs\bs'
and `\texttt{\bs'}':
\begin{verbatim}
my $see = "very far";
my $x = "big X";
print 'What you \'$see\' is (almost) what \n you get';
print "\n";
print 'Don\'t Walk';
print "\n";
print "How are you?" . '  ' . "Substitute values of $x and \n in \" quotes.";
print "\n";
\end{verbatim}
The output is:
\begin{verbatim}
What you '$see' is (almost) what \n you get
Don't Walk
How are you?  Substitute values of big X and 
 in " quotes.
\end{verbatim}%$

Back ticks work rather like they do in the shell:
\begin{verbatim}
my $total_file_size = `du -s $directory_name`;
my $date = `date`;
\end{verbatim}%$

Here are three scalar values; the second is an array element, the
third is a hash element:
\begin{verbatim}
$x    $list_of_things[5]     $lookup{key}
\end{verbatim}%$

Single-quotes \texttt{'\ldots'} allow no \emph{interpolation}
(substitution) except for \bs\bs and \texttt{\bs'}.  Double-quotes
\texttt{"\ldots"} allow interpolation of variables like \texttt{\$x}
and control codes like \texttt{\bs n} (newline).  Back-quotes
\texttt{`\ldots`} also allow interpolation, then try to execute the
result as a system command, returning as the final value whatever the
system command sends to standard output.


\subsection{Arrays (or \emph{Lists})}
\label{sec:arrays}

Here are some examples of constant lists:
\begin{verbatim}
( 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday',
     'Saturday' )
( 13, 14, 15, 16, 17, 18, 19 )     is equivalent to  (13..19)
( 13, 14, 15, 16, 17, 18, 19 )[2..4]  equivalent to  (15, 16, 17)
\end{verbatim}


\subsection{Hashes}
\label{sec:hashes}

Here are a few examples of hashes:
\begin{verbatim}
$DaysInMonth{January} = 31;
$enrolled{'Chan Wai-yee'} = 1;
$StudentName{012345678} = 'Chan Wai-yee';  
%whole_hash
\end{verbatim}%$

\subsection{Some More Examples}
\label{sec:more-examples}

\begin{verbatim}
# A list with 12 elements:
@days = ( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
%days = (
          Mon => 'SNM lecture and workshop',
          Tue => 'SNM workshops',
          Wed => 'OSSI lecture and workshops',
          Thu => 'Projects, OSSI workshops',
          Fri => 'OSSI workshops',
          Sat => 'riding bicycle with my son',
          Sun => 'Write more workshops and notes',
        );

$#days              # Last index of @days; 11 for above list @days
$#days = 7;         # shortens or lengthens list @days to 8 elements
@days               # ( $days[0], $days[1],... )
@days[3,4,5]        # = ( 30, 31, 30 )
@days{'Mon', 'Wed'} # same as ($days{Mon}, $days{Wed})
%days               # (key1, value1, key2, value2, ...)
\end{verbatim}

\subsection{Comparisons}
\label{sec:comparisons}

This example program illustrates comparisons, and the ``here
document'' (borrowed from the shell):
\begin{verbatim}
#! /usr/bin/perl

# The following "<<" variation of quoting can help simplify things sometimes
my $x = 'operator';
print <<THATSALL;
A common mistake:  Confusing the assignment $x =
and the numeric comparison $x ==, and the character
comparison $x eq.
THATSALL
$x = 7;
if ($x == 7) { print "x is $x\n"; }
if ($x = 5)  {
    print "x is now $x, ",
    "the assignment is successful.\n";
}
$x = 'stuff';
if ($x eq 'stuff') {
    print "Use eq, ne, lt, gt, le, ge for strings.\n";
}
\end{verbatim}%$
The output looks like this:
\begin{verbatim}
A common mistake:  Confusing the assignment operator =
and the numeric comparison operator ==, and the character
comparison operator eq.
x is 7
x is now 5, the assignment is successful.
Use eq, ne, lt, gt, le, ge for strings.
\end{verbatim}
Note that if the first line has a minus w `\texttt{-w}' like this:
\begin{verbatim}
#! /usr/bin/perl -w
\end{verbatim}
then the compiler also generates a warning:
\begin{verbatim}
Found = in conditional, should be == at ./comparisons line 12.
\end{verbatim}

\subsection{\texttt{split}}
\label{sec:split}

It is very useful to be able to split a string into elements of an
array.  The Perl builtin function for this is called \texttt{split}.
Here is a simple fragment to show how it works:
\begin{verbatim}
my $string = "   What a    lovely piece of    string ";
my @array = split ' ', $string;
\end{verbatim}
Now the elements of \texttt{@array} have been filled with the words of
the string:
\begin{verbatim}
$array[0] contains "What"
$array[1] contains "a"
$array[2] contains "lovely"
$array[3] contains "piece"
$array[4] contains "of"
$array[5] contains "string"
\end{verbatim}

\section{What to do}
\label{sec:what-to-do}

\begin{enumerate}
\item Type in this program and run it:

\begin{verbatim}
#! /usr/bin/perl -w
use strict;
print "Enter numeric:  day  month  year\n";
my ( $day, $month, $year ) = split ' ', <STDIN>;
print "day=$day, month=$month, year=$year\n";
\end{verbatim}
  
  Complete this program. Print an error message if the month is not
  valid. Print an error message if the day is not valid for the given
  month (31 is ok for January but not for February). See if you can
  reduce the use of conditional statements (\texttt{if},
  \texttt{unless}, \texttt{?}, \texttt{and}, \texttt{or},
  \texttt{\&\&}, \verb!||!,\,\ldots) and use data
  structures as far as is practical without making your program hard
  to read.
    
  Approach this incrementally.
  \begin{enumerate}
  \item On the first draft, assume that the user enters 3 numbers
    separated by spaces and that February has 28 days.
    \begin{solution}
\begin{verbatim}
#! /usr/bin/perl -w
use strict;
print "Enter numeric:  day  month  year\n";
my ( $day, $month, $year ) = split ' ', <STDIN>;
print "day=$day, month=$month, year=$year\n";
my @days = ( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
--$month >= 0 or die "bad date\n";
$day > 0 and $days[$month] and $day <= $days[$month] or die "bad date\n";
print "good date\n";
\end{verbatim}%$
    \end{solution}

  \item Modify your program so that you can enter the month with
    \emph{either} an integer 1\ldots12, or a three
    letter value from \texttt{Jan}, \texttt{Feb}, \texttt{Mar},
    \texttt{Apr}, \texttt{May}, \texttt{Jun}, \texttt{Jul},
    \texttt{Aug}, \texttt{Sep}, \texttt{Oct}, \texttt{Nov} and
    \texttt{Dec}.  You should be able to accept these in any case, so
    entering \texttt{jan}, \texttt{jAN}, \texttt{JAN} should all count
    as the month January\@.
    \begin{solution}
\begin{verbatim}
#! /usr/bin/perl -w
use strict;
print "Enter numeric:  day  month  year\n";
my ( $day, $month, $year ) = split ' ', <STDIN>;
print "day=$day, month=$month, year=$year\n";
my %days = (
            Jan => 31,
            Feb => 28,
            Mar => 31,
            Apr => 30,
            May => 31,
            Jun => 30,
            Jul => 31,
            Aug => 31,
            Sep => 30,
            Oct => 31,
            Nov => 30,
            Dec => 31,
            );
$day > 0 and $days{$month} and $day <= $days{$month} or die "bad date\n";
print "good date\n";
\end{verbatim}
    \end{solution}

  \item Subsequent refinements should account for bad input and leap
    year.  You should also be able to provide a month as an
    integer in the range 1--12 or as a three-character string as
    described above.
    \begin{explanation}
      A year is a leap year if it is divisible by 400, or if it is
      divisible by 4 but is not also divisible by 100.
    \end{explanation}
    \begin{solution}
\begin{verbatim}
#! /usr/bin/perl -w
use strict;
print "Enter numeric:  day  month  year\n";
my ( $day, $month, $year ) = split ' ', <STDIN>;
print "day=$day, month=$month, year=$year\n";
my %days = (
            Jan => 31,
            Feb => 28,
            Mar => 31,
            Apr => 30,
            May => 31,
            Jun => 30,
            Jul => 31,
            Aug => 31,
            Sep => 30,
            Oct => 31,
            Nov => 30,
            Dec => 31,
            );
$days{Feb} += ( not $year % 400 or not $year % 4 and $year % 100 ) ? 1 : 0;
$day > 0 and $days{$month} and $day <= $days{$month} or die "bad date\n";
print "good date\n";
\end{verbatim}
      Hmm, this is getting a bit poor with the conditional operator
      \texttt{?\ldots:\ldots}.  Would be nice to use a data structure
      instead, since this is what the tutorial is about. This check is
      not bulletproof, and should allow a number of month input formats.
    \end{solution}
  \item Finally, use the standard Perl module \texttt{Time::Local} to
    perform the verification of your input.  See \texttt{perldoc
      Time::Local}.  Note that there are a number of Perl built-in
    functions that handle time.  Of course, this should also be able
    to handle months in either format, as described previously.
  \end{enumerate}
  \begin{solution}
\begin{verbatim}
#! /usr/bin/perl -w
use strict;
use Time::Local;

print "Enter:  day  month  year in format n mmm y\n";
my ( $day, $month, $year ) = split ' ', <STDIN>;
print "day=$day, month=$month, year=$year\n";
$month = lc $month;
my %months = (
              jan => 0,
              feb => 1,
              mar => 2,
              apr => 3,
              may => 4,
              jun => 5,
              jul => 6,
              aug => 7,
              sep => 8,
              oct => 9,
              nov => 10,
              dec => 11,
              );
if ( $month =~ /^\d+$/ ) { --$month } else { $month = $months{$month} }
eval {
    my $time = timelocal( 0, 0, 0, $day, $month, $year );
};
if ( $@ ) { print "$@: bad" } else { print "good" }
print " date\n";
\end{verbatim}
  \end{solution}
  
\end{enumerate}
\end{document}
What checks to do:

verify that the day is in the allowable range for the appropriate
month

If a leap year, then range for february is 1 .. 29, otherwise it's 1..28.
