cheech has asked for the wisdom of the Perl Monks concerning the following question:

Hello, The code below prompts the user for a start and end date for scraping historical temperature data from a website. I've used a few while loops to make sure the input is in the correct format and range. The problem is that once the range exception is caught and the user is prompted again, the format exception can't be caught. So the user could accidentally input a date before the earliest available date and the program would alert them of this, but then an incorrect format input would be accepted.

Is there a better way for me to handle these exceptions?

Thanks,

Dave

# First, the current date is determined use Date::Simple::D8 (':all'); my $today = Date::Simple::D8->today(); my $end_date = $today; # Prompt user for start date and verify it print "Please enter the collection start date (YYYYMMDD): "; my $start_date = <>; chomp ($start_date); while ($start_date !~ (/\d\d\d\d\d\d\d\d/)) { print "ERROR: Invalid format! Start date (YYYYMMDD): "; $start_date = <>; chomp ($start_date); } while ($start_date < 18960101) { print "ERROR: Data prior to 18960101 is not available! Start date +(YYYYMMDD): "; $start_date = <>; chomp ($start_date); } while ($start_date > "$today") { print "ERROR: Start date is in the future! Start date (YYYYMMDD): +"; $start_date = <>; chomp ($start_date); } print "Thank you, collection will begin with $start_date\n"; # Ask user if most recent available date should be used as end date print "Would you like to collect through the most recent available dat +e? (y/n): "; my $ans = <>; chomp ($ans); if ($ans eq "n" || $ans eq "N") { print "Please enter the collection end date (YYYYMMDD): "; my $end_date = <>; chomp ($end_date); while ($end_date !~ (/\d\d\d\d\d\d\d\d/)) { print "ERROR: Invalid format! End date (YYYYMMDD): "; $end_date = <>; chomp ($end_date); } while ($end_date < 18960101) { print "ERROR: Data prior to 18960101 is not available! End dat +e (YYYYMMDD): "; $end_date = <>; chomp ($end_date); } while ($end_date > "$today") { print "ERROR: End date is in the future! End date (YYYYMMDD): +"; $end_date = <>; chomp ($end_date); } print "Thank you, collection will end with $end_date\n"; }

Replies are listed 'Best First'.
Re: Handling User Input
by almut (Canon) on Nov 25, 2009 at 20:13 UTC
    The problem is that once the range exception is caught and the user is prompted again, the format exception can't be caught.

    Encapsulate getting the user input ($start_date = <>; chomp ($start_date);) and checking its format into one routine (e.g. get_date()) that you then call every time you need new/corrected input.  This way, the format checking would also happen every time...

    (Update) Something like this:

    sub get_date { my $prompt = shift; while (1) { print "$prompt (YYYYMMDD): "; my $date = <>; chomp $date; if ($date !~ (/\d{8}/)) { print "ERROR: Invalid format!\n"; } else { return $date; } } } my $today = ... my $start_date; while (1) { $start_date = get_date("Please enter the collection start date"); if ($start_date < 18960101) { print "ERROR: Data prior to 18960101 is not available!\n"; } elsif ($start_date > $today) { print "ERROR: Start date is in the future!\n"; } else { last; } } # ditto for $end_date
Re: Handling User Input
by jethro (Monsignor) on Nov 25, 2009 at 20:54 UTC
    What you want is to have only one place where input is accepted and then many checks that all return to that one place if they find fault:
    while(1) { $date = <>; ... if (...) { print "was wrong"; next; } if (...) { print "was very wrong"; next; } if (...) { print "totally wrong"; next; } last; }

    Now almuts suggestion is a step further than this. She suggests putting this into a subroutine and use 'return($date)' instead of the 'last'. That would do exactly the same but would be even more readable

Re: Handling User Input
by Marshall (Canon) on Nov 26, 2009 at 05:05 UTC
    I've shown one standard way to set up a command loop below. First a few comments:

    Obviously we need some kind of looping structure to reprompt the user. A while() is the right choice here. When you design a while() loop, you want to put the terminating condition of the loop right there at the beginning of the loop instead of using while(1){...some buried termination condition in body somewhere}. I don't want to go looking down through a bunch of code to figure out how the loop stops! In the code below, typing "q,QUIT,qUIt, etc, stops the loop.

    Now we want to do something else: get the loop initialized and re-initialized for each iteration. I could have put a print above the loop for the first prompting and then put some other print inside the body of the loop for re-prompting, but the easy way to do this is to put the prompting in the while() condition! The while loop only takes a single statement. So the way to do this is with the "comma" operator. This allows combining what normally would be two different statements into one. The loop termination condition that is being tested is only the very last part of the statement, the regex. I have on occasion come across situations where 3 things are in the loop condition, but usually just 2 like below is just fine.

    A "blank" input should not be an error, just a reprompt. I check for that early and use "next" to restart the loop if that is the case. For the other conditions, I like the if (condition){err msg, next;} structure versus some complex if(),elseif(),else() deal. One reason is that this allows me to easily move the order of the conditions around (nothing special about the first one, middle one or last one). Then the last thing we do of course is some actual operation on the data that we've just validated.

    #!/usr/bin/perl -w use strict; while ( (print "Enter Date: "), (my $date =<STDIN>) !~ /^\s*q(uit)?\s*$/i ) { next if $date =~ /^\s*$/; # reprompt on blank line chomp($date); if ($date =~ m|[^\d/]|) { print "Valid date has only digits and /!\n"; next; } #....more valdiation here .... #then actually do something before reprompting }
    Update:

    I presented a more general sort of a thing than perhaps you need for just entering and validating a couple of inputs. Something like below could be setup so that it just keeps re-prompting user for a date and doesn't give up until you get a valid date. You write the "ok" routines so that they output any necessary error messages to the user and return false until you get something that you like. I would imagine that they both call some common format validation for whatever format you need. Now that you see the idea of the technique, you can adapt it to your specific needs.

    my $start_date; my $end_date; while (print "Enter start date: ",!start_date_ok(<STDIN>)); while (print "Enter end date: ", !end_date_ok (<STDIN>));
Re: Handling User Input
by holli (Abbot) on Nov 26, 2009 at 16:21 UTC
    A command line program that prompts for input is (normally) a bad idea, as it makes it impossible (or at least hard) to use it in a shell skript or some kind of batch.

    It's better to use command line arguments, which would not solve your problem but making it obsolete.


    holli

    You can lead your users to water, but alas, you cannot drown them.