\documentclass{ictlab} % Copyright (c) 2003 by Nick Urbanik . % 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 <; 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 ' ', ; 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 ' ', ; 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 ' ', ; 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 ' ', ; 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.